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.
Files changed (2) hide show
  1. package/lib/scaffold.mjs +397 -217
  2. 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, shell)
51
+ const sharedTemplate = (namespace, auth) => `module ${namespace}.View.Shared exposing (head, layout)
52
52
 
53
- import ElmSsr.Html exposing (Node, div, h1, text)
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
- shell : String -> List (Node msg) -> Node msg
67
- shell heading body =
68
- div [ class "shell" ] (h1 [] [ text heading ] :: body)
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.env "GREETING"
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 : String -> Document Never
100
- view greeting =
110
+ view : Document Never
111
+ view =
101
112
  Page.page
102
- { title = "Starter | elm-ssr"
113
+ { title = "elm-ssr"
103
114
  , head = Shared.head
104
115
  , body =
105
- [ Shared.shell "elm-ssr starter"
106
- [ p [] [ text ("Greeting from environment: " ++ greeting) ]
107
- , p [] [ text "This page is stateless and renders on the edge with no client runtime." ]
108
- , p [] [ a [ class "link", href "/counter" ] [ text "Open the interactive counter" ] ]
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.shell "Counter"
147
- [ p [] [ text "This page is static. Only the counter below is an interactive island." ]
148
- , Counter.embed { start = 0 }
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 "button", type_ "button", onClick Decrement ] [ text "-" ]
242
- , span [ class "value" ] [ text (String.fromInt model.count) ]
243
- , button [ class "button primary", type_ "button", onClick Increment ] [ text "+" ]
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 = [ Shared.shell "404" [ p [] [ text "This route does not exist." ] ] ]
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 as Attr
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 = "Login | elm-ssr"
347
+ { title = "Sign in | elm-ssr"
302
348
  , head = Shared.head
303
349
  , body =
304
- [ Shared.shell "Sign In"
305
- [ div [ Attr.class "login-container" ]
306
- [ p [] [ text "Access user authentication example (${authProvider})." ]
307
- , div [ Attr.style "margin-top" "20px" ]
308
- [ a
309
- [ Attr.class "button primary"
310
- , Attr.href "/api/auth/login"
311
- ]
312
- [ text "Sign In with Auth Provider" ]
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, h2, p, text)
325
- import ElmSsr.Html.Attributes as Attr
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 as Route exposing (Request)
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.shell "User Profile"
360
- [ div []
361
- [ h2 [] [ text ("Welcome, " ++ (Maybe.withDefault user.email user.name)) ]
362
- , p [] [ text ("Email: " ++ user.email) ]
363
- , p [ Attr.style "margin-top" "20px" ]
364
- [ a [ Attr.class "button", Attr.href "/api/auth/logout" ] [ text "Sign Out" ] ]
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
- color-scheme: light;
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: #18222f;
717
+ color: var(--text);
718
+ background: var(--bg);
647
719
  }
648
720
 
649
- body {
650
- margin: 0;
651
- min-height: 100vh;
652
- background:
653
- radial-gradient(circle at top, rgba(255, 255, 255, 0.85), transparent 45%),
654
- linear-gradient(180deg, #f8f3ea 0%, #efe7dc 100%);
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
- .shell {
658
- max-width: 44rem;
736
+ .header-inner {
737
+ max-width: 1100px;
659
738
  margin: 0 auto;
660
- padding: 4rem 1.5rem;
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
- h1 {
664
- font-size: 2rem;
746
+ .brand {
747
+ display: flex;
748
+ align-items: center;
749
+ gap: 0.5rem;
665
750
  font-weight: 700;
666
- letter-spacing: -0.02em;
667
- margin: 0 0 1rem;
668
- color: #18222f;
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
- p {
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
- a.link {
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
- a.link:hover {
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
- .button {
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
- border-radius: 999px;
699
- border: 1.5px solid #18222f;
700
- padding: 0.6rem 1.2rem;
787
+ padding: 0.55rem 1.1rem;
788
+ border-radius: 8px;
789
+ border: 1.5px solid transparent;
701
790
  font: inherit;
702
- font-size: 0.9rem;
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.15s, color 0.15s;
795
+ transition: background 0.12s, color 0.12s, border-color 0.12s;
796
+ white-space: nowrap;
709
797
  }
710
798
 
711
- .button:hover {
712
- background: #f0ebe3;
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
- .button.primary {
716
- background: #18222f;
717
- color: white;
718
- border-color: #18222f;
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
- .button.primary:hover {
722
- background: #2d3748;
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
- margin: 2rem 0;
891
+ gap: 1rem;
731
892
  }
732
893
 
733
- .value {
894
+ .counter-value {
734
895
  text-align: center;
735
- font-size: 2.5rem;
896
+ font-size: 3rem;
736
897
  font-weight: 700;
737
- color: #18222f;
898
+ letter-spacing: -0.03em;
738
899
  }
739
900
 
740
- .form {
901
+ /* ── Auth pages ──────────────────────────────────────── */
902
+ .auth-page {
741
903
  display: flex;
742
- flex-direction: column;
743
- gap: 1rem;
744
- margin-top: 1.5rem;
904
+ align-items: flex-start;
905
+ justify-content: center;
906
+ padding-top: 3rem;
745
907
  }
746
908
 
747
- .field {
748
- display: flex;
749
- flex-direction: column;
750
- gap: 0.35rem;
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
- .field label,
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
- .input {
761
- width: 100%;
762
- border-radius: 8px;
763
- border: 1.5px solid #d1cdc7;
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
- .input:focus {
773
- outline: none;
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
- .login-container {
778
- margin-top: 2rem;
779
- padding: 2rem;
780
- background: white;
781
- border-radius: 12px;
782
- border: 1px solid rgba(0, 0, 0, 0.08);
783
- max-width: 22rem;
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
- .card {
787
- padding: 1.5rem;
788
- background: white;
789
- border-radius: 12px;
790
- border: 1px solid rgba(0, 0, 0, 0.08);
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
- .muted {
795
- color: #718096;
796
- font-size: 0.875rem;
952
+ /* ── Error page ──────────────────────────────────────── */
953
+ .error-page {
954
+ text-align: center;
955
+ padding: 5rem 0;
797
956
  }
798
957
 
799
- .error {
800
- color: #c53030;
801
- font-size: 0.875rem;
802
- margin-top: 0.25rem;
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
- .shell {
876
- @apply max-w-2xl mx-auto px-6 py-16;
877
- }
878
-
879
- .button {
880
- @apply inline-flex items-center gap-2 rounded-full border border-gray-900
881
- px-5 py-2.5 text-sm font-medium bg-white text-gray-900 no-underline
882
- cursor-pointer transition-colors hover:bg-gray-50;
883
- }
884
-
885
- .button.primary {
886
- @apply bg-gray-900 text-white hover:bg-gray-700;
887
- }
888
-
889
- .counter {
890
- @apply my-8 grid items-center gap-3;
891
- grid-template-columns: auto 1fr auto;
892
- }
893
-
894
- .value {
895
- @apply text-center text-5xl font-bold text-gray-900;
896
- }
897
-
898
- .form {
899
- @apply flex flex-col gap-4 mt-6;
900
- }
901
-
902
- .field {
903
- @apply flex flex-col gap-1.5;
904
- }
905
-
906
- .input {
907
- @apply w-full rounded-lg border border-gray-300 px-3.5 py-2.5 text-sm
908
- bg-white text-gray-900 focus:outline-none focus:border-gray-900
909
- transition-colors;
910
- }
911
-
912
- .login-container {
913
- @apply mt-8 p-8 bg-white rounded-xl border border-gray-100 max-w-sm;
914
- }
915
-
916
- .card {
917
- @apply p-6 bg-white rounded-xl border border-gray-100 mb-4;
918
- }
919
-
920
- .muted {
921
- @apply text-gray-500 text-sm;
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.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>",