feat: improved elm template

This commit is contained in:
Nick 2025-06-21 14:20:54 -05:00
parent 8545d7c1e8
commit 6b2a601776
22 changed files with 2139 additions and 0 deletions

7
templates/elm/frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
/dist
/.elm-land
/.env
/elm-stuff
/node_modules
.DS_Store
*.pem

View file

@ -0,0 +1,16 @@
# frontend
> Built with [Elm Land](https://elm.land) 🌈
## Local development
```bash
# Requires Node.js v18+ (https://nodejs.org)
npx elm-land server
```
## Deploying to production
Elm Land projects are most commonly deployed as static websites.
Please visit [the "Deployment" guide](https://elm.land/guide/deploying) to learn more
about deploying your app for free using Netlify or Vercel.

View file

@ -0,0 +1,26 @@
{
"app": {
"elm": {
"development": { "debugger": true },
"production": { "debugger": false }
},
"env": [],
"html": {
"attributes": {
"html": { "lang": "en" },
"head": {}
},
"title": "Elm Land",
"meta": [
{ "charset": "UTF-8" },
{ "http-equiv": "X-UA-Compatible", "content": "IE=edge" },
{ "name": "viewport", "content": "width=device-width, initial-scale=1.0" }
],
"link": [],
"script": []
},
"router": {
"useHashRouting": false
}
}
}

View file

@ -0,0 +1,53 @@
{
"type": "application",
"source-directories": [
"src",
".elm-land/src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"dillonkearns/elm-markdown": "7.0.1",
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/json": "1.1.3",
"elm/svg": "1.0.1",
"elm/url": "1.0.0",
"elm-community/list-extra": "8.7.0",
"elm-community/maybe-extra": "5.3.0",
"gampleman/elm-visualization": "2.4.2",
"hecrj/html-parser": "2.4.0",
"mdgriffith/elm-ui": "1.1.8"
},
"indirect": {
"avh4/elm-color": "1.0.0",
"elm/parser": "1.1.0",
"elm/random": "1.0.0",
"elm/regex": "1.0.0",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.3",
"elmcraft/core-extra": "2.2.0",
"folkertdev/elm-deque": "3.0.1",
"folkertdev/one-true-path-experiment": "6.0.1",
"folkertdev/svg-path-lowlevel": "4.0.1",
"gampleman/elm-rosetree": "1.1.0",
"ianmackenzie/elm-1d-parameter": "1.0.1",
"ianmackenzie/elm-float-extra": "1.1.0",
"ianmackenzie/elm-geometry": "3.11.0",
"ianmackenzie/elm-interval": "3.1.0",
"ianmackenzie/elm-triangular-mesh": "1.1.0",
"ianmackenzie/elm-units": "2.10.0",
"ianmackenzie/elm-units-interval": "3.2.0",
"ianmackenzie/elm-units-prefixed": "2.8.0",
"justinmimbs/date": "4.1.0",
"justinmimbs/time-extra": "1.2.0",
"rtfeldman/elm-hex": "1.0.0",
"ryan-haskell/date-format": "1.0.0"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View file

@ -0,0 +1,94 @@
module Config.Helpers.Response exposing
( contentContainer
, pageList
, pageListCenter
, pageListFormat
, topLevelContainer
)
import Config.Style.Colour.Helpers exposing (colourTheme)
import Element as E
exposing
( Attribute
, Device
, DeviceClass(..)
, Element
, Orientation(..)
, alignTop
, centerX
, centerY
, el
, fill
, height
, maximum
, minimum
, padding
, paddingXY
, scrollbarY
, spacing
, width
)
import Element.Background as B exposing (color)
import Html.Attributes exposing (style)
topLevelContainer : Element msg -> Element msg
topLevelContainer =
el
[ width fill
, height fill
, B.color colourTheme.backgroundLightGrey
]
pageListCenter : Device -> List (Attribute msg)
pageListCenter device =
[ centerY
]
++ pageListFormat device
pageList : Device -> List (Attribute msg)
pageList device =
[ alignTop
]
++ pageListFormat device
pageListFormat : Device -> List (Attribute msg)
pageListFormat device =
let
pageListAttr =
[ centerX
, width fill
, height fill
, scrollbarY
]
in
pageListAttr
++ (case ( device.class, device.orientation ) of
( Phone, Portrait ) ->
[ spacing 0
, paddingXY 10 30
]
( Tablet, Portrait ) ->
[ spacing 0
, paddingXY 10 30
]
_ ->
[ spacing 20
, paddingXY 30 30
]
)
contentContainer : Element msg -> Element msg
contentContainer =
el
[ width (fill |> minimum 100)
, width (fill |> maximum 875)
, padding 10
, centerX
]

View file

@ -0,0 +1,16 @@
module Config.Helpers.Viewport exposing
( Msg
, resetViewport
)
import Browser.Dom as Dom exposing (setViewport)
import Task exposing (attempt)
type Msg
= NoOp
resetViewport : Cmd Msg
resetViewport =
Task.attempt (\_ -> NoOp) (Dom.setViewportOf "scroll-container" 0 0)

View file

@ -0,0 +1,118 @@
module Config.Style.Colour.Helpers exposing
( ThemeColor(..)
, colourTheme
, getThemeColor
, syntaxTheme
)
import Config.Style.Colour.Types
exposing
( SyntaxColors
, Theme
)
import Element as E
exposing
( Color
, rgb255
, rgba
)
import Element.Font as F exposing (color)
colourTheme : Theme
colourTheme =
{ textLightGrey = rgb255 212 212 212
, textDarkGrey = rgb255 126 126 126
, textLightOrange = rgb255 204 102 0
, textDarkOrange = rgb255 120 60 0
, textDeepDarkOrange = rgb255 60 30 0
, backgroundLightGrey = rgb255 40 40 40
, backgroundDarkGrey = rgb255 30 30 30
, backgroundDeepDarkGrey = rgb255 20 20 20
, backgroundSpreadsheet = rgb255 36 36 36
, backgroundSpreadsheetDark = rgb255 26 26 26
, shadow = rgb255 10 10 10
, barGreen = rgb255 0 102 0
, barRed = rgb255 102 0 0
, debugColour = rgb255 227 28 121
, transparent = rgba 1 1 1 0
}
syntaxTheme : SyntaxColors
syntaxTheme =
{ punctuation = rgb255 202 158 230
, key = rgb255 138 173 244
, string = rgb255 166 218 149
, keyword = rgb255 245 169 127
, operator = rgb255 178 185 194
, background = rgb255 36 39 58
, text = rgb255 202 211 245
}
type ThemeColor
= TextLightGrey
| TextDarkGrey
| TextLightOrange
| TextDarkOrange
| TextDeepDarkOrange
| BackgroundLightGrey
| BackgroundDarkGrey
| BackgroundDeepDarkGrey
| BackgroundSpreadsheet
| BackgroundSpreadsheetDark
| Shadow
| BarGreen
| BarRed
| DebugColour
| Transparent
getThemeColor : ThemeColor -> Color
getThemeColor color =
case color of
TextLightGrey ->
colourTheme.textLightGrey
TextDarkGrey ->
colourTheme.textDarkGrey
TextLightOrange ->
colourTheme.textLightOrange
TextDarkOrange ->
colourTheme.textDarkOrange
TextDeepDarkOrange ->
colourTheme.textDeepDarkOrange
BackgroundLightGrey ->
colourTheme.backgroundLightGrey
BackgroundDarkGrey ->
colourTheme.backgroundDarkGrey
BackgroundDeepDarkGrey ->
colourTheme.backgroundDeepDarkGrey
BackgroundSpreadsheet ->
colourTheme.backgroundSpreadsheet
BackgroundSpreadsheetDark ->
colourTheme.backgroundSpreadsheetDark
Shadow ->
colourTheme.shadow
BarGreen ->
colourTheme.barGreen
BarRed ->
colourTheme.barRed
DebugColour ->
colourTheme.debugColour
Transparent ->
colourTheme.transparent

View file

@ -0,0 +1,36 @@
module Config.Style.Colour.Types exposing
( SyntaxColors
, Theme
)
import Element exposing (Color)
type alias Theme =
{ textLightGrey : Color
, textDarkGrey : Color
, textLightOrange : Color
, textDarkOrange : Color
, textDeepDarkOrange : Color
, backgroundLightGrey : Color
, backgroundDarkGrey : Color
, backgroundDeepDarkGrey : Color
, backgroundSpreadsheet : Color
, backgroundSpreadsheetDark : Color
, shadow : Color
, barGreen : Color
, barRed : Color
, debugColour : Color
, transparent : Color
}
type alias SyntaxColors =
{ punctuation : Color
, key : Color
, string : Color
, keyword : Color
, operator : Color
, background : Color
, text : Color
}

View file

@ -0,0 +1,50 @@
module Config.Style.Fonts exposing
( defaultFontSize
, headerFontSizeBig
, headerFontSizeMedium
, paragraphSpacing
, smallTextFontSize
, spartanFont
)
import Element
exposing
( Attr
, Attribute
, spacing
)
import Element.Font as F
exposing
( size
, typeface
)
spartanFont : F.Font
spartanFont =
F.typeface "League Spartan"
paragraphSpacing : Attribute msg
paragraphSpacing =
spacing 0
headerFontSizeBig : Attr decorative msg
headerFontSizeBig =
F.size 23
headerFontSizeMedium : Attr decorative msg
headerFontSizeMedium =
F.size 20
defaultFontSize : Attr decorative msg
defaultFontSize =
F.size 18
smallTextFontSize : Attr decorative msg
smallTextFontSize =
F.size 16

View file

@ -0,0 +1,24 @@
module Config.Style.Glow exposing
( glowDeepDarkGrey
, glowDeepDarkGreyNavbar
, glowDeepDarkOrange
)
import Config.Style.Colour.Helpers exposing (ThemeColor(..), colourTheme, getThemeColor)
import Element exposing (Attr)
import Element.Border as D exposing (glow)
glowDeepDarkGrey : Attr decorative msg
glowDeepDarkGrey =
D.glow (getThemeColor Shadow) 4
glowDeepDarkOrange : Attr decorative msg
glowDeepDarkOrange =
D.glow (getThemeColor TextDeepDarkOrange) 4
glowDeepDarkGreyNavbar : Attr decorative msg
glowDeepDarkGreyNavbar =
D.glow (getThemeColor Shadow) 10

View file

@ -0,0 +1,32 @@
module Config.Style.Icons.Helpers exposing (buildSvg)
import Config.Style.Icons.Types as SvgTypes
exposing
( InnerPart
, OuterPart
)
import Element as E
exposing
( Element
, el
, html
)
import Svg exposing (svg)
{- buildSvg consumes an inner record to construct most of an SVG, and an outer record to supply
any potentially varying TypedSvg.Core.Attribute msgs and wrap it in an Element.el so it can be
used by elm-ui. It provides a consistent interface for inserting SVGs into elm-ui code.
-}
buildSvg : SvgTypes.OuterPart msg -> SvgTypes.InnerPart msg -> Element msg
buildSvg outer inner =
el
outer.elementAttributes
<|
html <|
Svg.svg
(outer.svgAttributes ++ inner.svgAttributes)
inner.svg

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
module Config.Style.Icons.Types exposing
( InnerPart
, OuterPart
)
{-| The types used for SVG management.
-}
import Element exposing (Attribute)
import Shared exposing (Model)
import Svg exposing (svg)
{-| The outer record for the SVG builder. This is explained in ../Helpers/Svg.elm.
-}
type alias OuterPart msg =
{ elementAttributes : List (Element.Attribute msg)
, sharedModel : Shared.Model
, svgAttributes : List (Svg.Attribute msg)
}
{-| The inner record for the SVG builder. This is explained in ../Helpers/Svg.elm.
-}
type alias InnerPart msg =
{ svgAttributes : List (Svg.Attribute msg)
, svg : List (Svg.Svg msg)
}

View file

@ -0,0 +1,69 @@
module Config.Style.Transitions exposing (..)
import Config.Style.Colour.Helpers exposing (colourTheme)
import Config.Style.Glow
exposing
( glowDeepDarkGrey
, glowDeepDarkOrange
)
import Element
exposing
( Attribute
, htmlAttribute
, mouseOver
)
import Element.Background as B exposing (color)
import Element.Border as D exposing (color)
import Element.Font as F exposing (color)
import Html.Attributes as H exposing (style)
transitionStyleSlow : Attribute msg
transitionStyleSlow =
htmlAttribute <| style "transition" "all 0.4s ease-in-out"
transitionStyleMedium : Attribute msg
transitionStyleMedium =
htmlAttribute <| style "transition" "all 0.2s ease-in-out"
transitionStyleFast : Attribute msg
transitionStyleFast =
htmlAttribute <| style "transition" "all 0.1s ease-in-out"
specialNavbarTransition : Attribute msg
specialNavbarTransition =
htmlAttribute <| style "transition" "opacity .4s"
-- This special transition is needed to avoid weird animation sequencing rather in Chrome-based browsers.
hoverFontLightOrange : Attribute msg
hoverFontLightOrange =
mouseOver [ F.color colourTheme.textLightOrange ]
hoverFontDarkOrange : Attribute msg
hoverFontDarkOrange =
mouseOver [ F.color colourTheme.textDarkOrange ]
hoverCircleButtonDarkOrange : Attribute msg
hoverCircleButtonDarkOrange =
mouseOver
[ D.color colourTheme.textDarkOrange
, B.color colourTheme.textDarkOrange
, glowDeepDarkOrange
]
hoverPageButtonDeepDarkOrange : Attribute msg
hoverPageButtonDeepDarkOrange =
mouseOver
[ B.color colourTheme.textDeepDarkOrange
, F.color colourTheme.textLightOrange
]

View file

@ -0,0 +1,194 @@
module Effect exposing
( Effect
, none, batch
, sendCmd, sendMsg
, pushRoute, replaceRoute, loadExternalUrl
, map, toCmd
)
{-|
@docs Effect
@docs none, batch
@docs sendCmd, sendMsg
@docs pushRoute, replaceRoute, loadExternalUrl
@docs map, toCmd
-}
import Browser.Navigation
import Dict exposing (Dict)
import Route exposing (Route)
import Route.Path
import Shared.Model
import Shared.Msg
import Task
import Url exposing (Url)
type Effect msg
= -- BASICS
None
| Batch (List (Effect msg))
| SendCmd (Cmd msg)
-- ROUTING
| PushUrl String
| ReplaceUrl String
| LoadExternalUrl String
-- SHARED
| SendSharedMsg Shared.Msg.Msg
-- BASICS
{-| Don't send any effect.
-}
none : Effect msg
none =
None
{-| Send multiple effects at once.
-}
batch : List (Effect msg) -> Effect msg
batch =
Batch
{-| Send a normal `Cmd msg` as an effect, something like `Http.get` or `Random.generate`.
-}
sendCmd : Cmd msg -> Effect msg
sendCmd =
SendCmd
{-| Send a message as an effect. Useful when emitting events from UI components.
-}
sendMsg : msg -> Effect msg
sendMsg msg =
Task.succeed msg
|> Task.perform identity
|> SendCmd
-- ROUTING
{-| Set the new route, and make the back button go back to the current route.
-}
pushRoute :
{ path : Route.Path.Path
, query : Dict String String
, hash : Maybe String
}
-> Effect msg
pushRoute route =
PushUrl (Route.toString route)
{-| Set given path as route (without any query params or hash), and make the back button go back to the current route.
-}
pushPath :
Route.Path.Path
-> Effect msg
pushPath path =
PushUrl (Route.toString { path = path, query = Dict.empty, hash = Nothing })
{-| Set the new route, but replace the previous one, so clicking the back
button **won't** go back to the previous route.
-}
replaceRoute :
{ path : Route.Path.Path
, query : Dict String String
, hash : Maybe String
}
-> Effect msg
replaceRoute route =
ReplaceUrl (Route.toString route)
{-| Set given path as route (without any query params or hash), but replace the previous route,
so clicking the back button **won't** go back to the previous route
-}
replacePath :
Route.Path.Path
-> Effect msg
replacePath path =
ReplaceUrl (Route.toString { path = path, query = Dict.empty, hash = Nothing })
{-| Redirect users to a new URL, somewhere external your web application.
-}
loadExternalUrl : String -> Effect msg
loadExternalUrl =
LoadExternalUrl
-- INTERNALS
{-| Elm Land depends on this function to connect pages and layouts
together into the overall app.
-}
map : (msg1 -> msg2) -> Effect msg1 -> Effect msg2
map fn effect =
case effect of
None ->
None
Batch list ->
Batch (List.map (map fn) list)
SendCmd cmd ->
SendCmd (Cmd.map fn cmd)
PushUrl url ->
PushUrl url
ReplaceUrl url ->
ReplaceUrl url
LoadExternalUrl url ->
LoadExternalUrl url
SendSharedMsg sharedMsg ->
SendSharedMsg sharedMsg
{-| Elm Land depends on this function to perform your effects.
-}
toCmd :
{ key : Browser.Navigation.Key
, url : Url
, shared : Shared.Model.Model
, fromSharedMsg : Shared.Msg.Msg -> msg
, batch : List msg -> msg
, toCmd : msg -> Cmd msg
}
-> Effect msg
-> Cmd msg
toCmd options effect =
case effect of
None ->
Cmd.none
Batch list ->
Cmd.batch (List.map (toCmd options) list)
SendCmd cmd ->
cmd
PushUrl url ->
Browser.Navigation.pushUrl options.key url
ReplaceUrl url ->
Browser.Navigation.replaceUrl options.key url
LoadExternalUrl url ->
Browser.Navigation.load url
SendSharedMsg sharedMsg ->
Task.succeed sharedMsg
|> Task.perform options.fromSharedMsg

View file

@ -0,0 +1,70 @@
module Pages.Home_ exposing (Model, Msg, page)
import Effect exposing (Effect)
import Route exposing (Route)
import Html
import Page exposing (Page)
import Shared
import View exposing (View)
page : Shared.Model -> Route () -> Page Model Msg
page shared route =
Page.new
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
-- INIT
type alias Model =
{}
init : () -> ( Model, Effect Msg )
init () =
( {}
, Effect.none
)
-- UPDATE
type Msg
= NoOp
update : Msg -> Model -> ( Model, Effect Msg )
update msg model =
case msg of
NoOp ->
( model
, Effect.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- VIEW
view : Model -> View Msg
view model =
{ title = "Pages.Home_"
, body = [ Html.text "/" ]
}

View file

@ -0,0 +1,69 @@
module Pages.NotFound_ exposing (Model, Msg, page)
import Effect exposing (Effect)
import Html exposing (..)
import Page exposing (Page)
import Route exposing (Route)
import Route.Path
import Shared
import View exposing (View)
page : Shared.Model -> Route () -> Page Model Msg
page shared route =
Page.new
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
-- INIT
type alias Model =
{}
init : () -> ( Model, Effect Msg )
init () =
( {}
, Effect.none
)
-- UPDATE
type Msg
= NoOp
update : Msg -> Model -> ( Model, Effect Msg )
update msg model =
case msg of
NoOp ->
( model
, Effect.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- VIEW
view : Model -> View Msg
view model =
View.fromString "Page not found"

View file

@ -0,0 +1,74 @@
module Shared exposing
( Flags, decoder
, Model, Msg
, init, update, subscriptions
)
{-|
@docs Flags, decoder
@docs Model, Msg
@docs init, update, subscriptions
-}
import Effect exposing (Effect)
import Json.Decode
import Route exposing (Route)
import Route.Path
import Shared.Model
import Shared.Msg
-- FLAGS
type alias Flags =
{}
decoder : Json.Decode.Decoder Flags
decoder =
Json.Decode.succeed {}
-- INIT
type alias Model =
Shared.Model.Model
init : Result Json.Decode.Error Flags -> Route () -> ( Model, Effect Msg )
init flagsResult route =
( {}
, Effect.none
)
-- UPDATE
type alias Msg =
Shared.Msg.Msg
update : Route () -> Msg -> Model -> ( Model, Effect Msg )
update route msg model =
case msg of
Shared.Msg.NoOp ->
( model
, Effect.none
)
-- SUBSCRIPTIONS
subscriptions : Route () -> Model -> Sub Msg
subscriptions route model =
Sub.none

View file

@ -0,0 +1,14 @@
module Shared.Model exposing (Model)
{-| -}
{-| Normally, this value would live in "Shared.elm"
but that would lead to a circular dependency import cycle.
For that reason, both `Shared.Model` and `Shared.Msg` are in their
own file, so they can be imported by `Effect.elm`
-}
type alias Model =
{}

View file

@ -0,0 +1,14 @@
module Shared.Msg exposing (Msg(..))
{-| -}
{-| Normally, this value would live in "Shared.elm"
but that would lead to a circular dependency import cycle.
For that reason, both `Shared.Model` and `Shared.Msg` are in their
own file, so they can be imported by `Effect.elm`
-}
type Msg
= NoOp

View file

@ -0,0 +1,72 @@
module View exposing
( View, map
, none, fromString
, toBrowserDocument
)
{-|
@docs View, map
@docs none, fromString
@docs toBrowserDocument
-}
import Browser
import Html exposing (Html)
import Route exposing (Route)
import Shared.Model
type alias View msg =
{ title : String
, body : List (Html msg)
}
{-| Used internally by Elm Land to create your application
so it works with Elm's expected `Browser.Document msg` type.
-}
toBrowserDocument :
{ shared : Shared.Model.Model
, route : Route ()
, view : View msg
}
-> Browser.Document msg
toBrowserDocument { view } =
{ title = view.title
, body = view.body
}
{-| Used internally by Elm Land to connect your pages together.
-}
map : (msg1 -> msg2) -> View msg1 -> View msg2
map fn view =
{ title = view.title
, body = List.map (Html.map fn) view.body
}
{-| Used internally by Elm Land whenever transitioning between
authenticated pages.
-}
none : View msg
none =
{ title = ""
, body = []
}
{-| If you customize the `View` module, anytime you run `elm-land add page`,
the generated page will use this when adding your `view` function.
That way your app will compile after adding new pages, and you can see
the new page working in the web browser!
-}
fromString : String -> View msg
fromString moduleName =
{ title = moduleName
, body = [ Html.text moduleName ]
}

View file

@ -0,0 +1,25 @@
// This returns the flags passed into your Elm application
export const flags = async ({ env } : ElmLand.FlagsArgs) => {
return {}
}
// This function is called after your Elm app starts
export const onReady = ({ app, env } : ElmLand.OnReadyArgs) => {
console.log('Elm is ready', app)
}
// Type definitions for Elm Land
namespace ElmLand {
export type FlagsArgs = {
env: Record<string, string>
}
export type OnReadyArgs = {
env: Record<string, string>
app: { ports?: Record<string, Port> }
}
export type Port = {
send?: (data: unknown) => void
subscribe?: (callback: (data: unknown) => unknown) => void
}
}