"Chord Drill Sergeant" -> "Learn Piano Chords"
In the spirit of "keep it simple, stupid", I am naming this application as closely to the functionality as I can imagine.
This commit is contained in:
parent
39d084e493
commit
f0803547e4
20 changed files with 16 additions and 15 deletions
3
website/sandbox/learnpianochords/.gitignore
vendored
Normal file
3
website/sandbox/learnpianochords/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/elm-stuff
|
||||
/elm.js
|
||||
/output.css
|
58
website/sandbox/learnpianochords/README.md
Normal file
58
website/sandbox/learnpianochords/README.md
Normal file
|
@ -0,0 +1,58 @@
|
|||
# Learn Piano Chords (LPC)
|
||||
|
||||
Are you a musician looking for a more effective way to improve your craft? Maybe
|
||||
you're a music teacher looking to create useful exercises to give your students.
|
||||
|
||||
Studying music theory can be a fruitful undertaking, but it can often overwhelm
|
||||
or bore students. I think that if practicing is enjoyable, students will
|
||||
practice more. Practice doesn't make perfect; *perfect* practice makes perfect.
|
||||
Learn Piano Chords is a web app that lowers the barrier to practicing and
|
||||
internalizing music theory.
|
||||
|
||||
## How does it work?
|
||||
|
||||
1. Grab a cell phone or a laptop and your instrument.
|
||||
2. Open a web browser and visit the Learn Piano Chords app (URL and app
|
||||
forthcoming).
|
||||
3. Set the tempo at which you would like to practice.
|
||||
4. Set the target duration of your session.
|
||||
5. Select the key(s) and chord(s) you would like to practice.
|
||||
6. Set the tempo (i.e. pace) at which you would like to practice.
|
||||
7. LPC will display chords at various rhythmic intervals during your practice
|
||||
session. It is your job to play these chords in time before the next chord
|
||||
appears.
|
||||
|
||||
## Highlights
|
||||
|
||||
Here are some useful features of LPC:
|
||||
- Tempo: Set the rate at which LPC displays chords.
|
||||
- Predefined practice sessions: LPC offers users a few practice sessions to get
|
||||
users started. The goal, however, is to teach users to create their own
|
||||
bespoke practice sessions. LPC aims to foster a community of practitioners who
|
||||
curate and share their practice sessions.
|
||||
- Whitelist / blacklist: Construct the set of chords you would like to
|
||||
practice. Let's say you only want to practice triads in the keys of F, C, and
|
||||
G. Would you also like to avoid diminished chords? Or maybe you *only* want to
|
||||
practice major-7th chords for *all* keys. LPC supports all of these scenarios
|
||||
and many others. You can save these chord configurations to reuse them at any
|
||||
time. You can also share chord configurations with other LPC users if you find
|
||||
the practice useful.
|
||||
- Inversions: Every chord has inversions. For instance, every triad (i.e. chord
|
||||
composed of three notes) has three inversions: root, second, and third
|
||||
positions. LPC acknowledges all of the positions in which chords may appear
|
||||
and helps you study all, some, or none of these inversions.
|
||||
- Harmony: LPC understands basic harmony and can sort the chords you would like
|
||||
to train in various harmonious permutations.
|
||||
- Chaos-mode: Feeling confident? Throw the classical notions of harmony to the
|
||||
wayside and use LPC in "chaos-mode" where LPC samples randomly from the Circle
|
||||
of Fifths.
|
||||
|
||||
## Developing
|
||||
|
||||
If you're interested in contributing, the following will create an environment
|
||||
in which you can develop:
|
||||
|
||||
```shell
|
||||
$ nix-shell
|
||||
$ elm-live -- src/Main.elm --output=elm.js
|
||||
```
|
60
website/sandbox/learnpianochords/default.nix
Normal file
60
website/sandbox/learnpianochords/default.nix
Normal file
|
@ -0,0 +1,60 @@
|
|||
{ pkgs ? <nixpkgs>, ... }:
|
||||
|
||||
with pkgs;
|
||||
|
||||
let
|
||||
mkDerivation =
|
||||
{ srcs ? ./elm-srcs.nix
|
||||
, src
|
||||
, name
|
||||
, srcdir ? "./src"
|
||||
, targets ? []
|
||||
, registryDat ? ./registry.dat
|
||||
, outputJavaScript ? false
|
||||
}:
|
||||
stdenv.mkDerivation {
|
||||
inherit name src;
|
||||
|
||||
buildInputs = [ elmPackages.elm ]
|
||||
++ lib.optional outputJavaScript nodePackages_10_x.uglify-js;
|
||||
|
||||
buildPhase = pkgs.elmPackages.fetchElmDeps {
|
||||
elmPackages = import srcs;
|
||||
elmVersion = "0.19.1";
|
||||
inherit registryDat;
|
||||
};
|
||||
|
||||
installPhase = let
|
||||
elmfile = module: "${srcdir}/${builtins.replaceStrings ["."] ["/"] module}.elm";
|
||||
extension = if outputJavaScript then "js" else "html";
|
||||
in ''
|
||||
mkdir -p $out/share/doc
|
||||
${lib.concatStrings (map (module: ''
|
||||
echo "compiling ${elmfile module}"
|
||||
elm make ${elmfile module} --output $out/${module}.${extension} --docs $out/share/doc/${module}.json
|
||||
${lib.optionalString outputJavaScript ''
|
||||
echo "minifying ${elmfile module}"
|
||||
uglifyjs $out/${module}.${extension} --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' \
|
||||
| uglifyjs --mangle --output=$out/${module}.min.${extension}
|
||||
''}
|
||||
'') targets)}
|
||||
'';
|
||||
};
|
||||
mainDotElm = mkDerivation {
|
||||
name = "elm-app-0.1.0";
|
||||
srcs = ./elm-srcs.nix;
|
||||
src = ./.;
|
||||
targets = ["Main"];
|
||||
srcdir = "./src";
|
||||
outputJavaScript = true;
|
||||
};
|
||||
in stdenv.mkDerivation {
|
||||
name = "learn-piano-chords";
|
||||
buildInputs = [];
|
||||
src = ./.;
|
||||
buildPhase = ''
|
||||
mkdir -p $out
|
||||
cp index.html output.css ${mainDotElm}/Main.min.js $out
|
||||
'';
|
||||
dontInstall = true;
|
||||
}
|
3
website/sandbox/learnpianochords/dir-locals.nix
Normal file
3
website/sandbox/learnpianochords/dir-locals.nix
Normal file
|
@ -0,0 +1,3 @@
|
|||
let
|
||||
briefcase = import <briefcase> {};
|
||||
in briefcase.utils.nixBufferFromShell ./shell.nix
|
67
website/sandbox/learnpianochords/elm-srcs.nix
Normal file
67
website/sandbox/learnpianochords/elm-srcs.nix
Normal file
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
|
||||
"elm-community/maybe-extra" = {
|
||||
sha256 = "0qslmgswa625d218djd3p62pnqcrz38f5p558mbjl6kc1ss0kzv3";
|
||||
version = "5.2.0";
|
||||
};
|
||||
|
||||
"elm/html" = {
|
||||
sha256 = "1n3gpzmpqqdsldys4ipgyl1zacn0kbpc3g4v3hdpiyfjlgh8bf3k";
|
||||
version = "1.0.0";
|
||||
};
|
||||
|
||||
"elm-community/random-extra" = {
|
||||
sha256 = "1dg2nz77w2cvp16xazbdsxkkw0xc9ycqpkd032faqdyky6gmz9g6";
|
||||
version = "3.1.0";
|
||||
};
|
||||
|
||||
"elm/svg" = {
|
||||
sha256 = "1cwcj73p61q45wqwgqvrvz3aypjyy3fw732xyxdyj6s256hwkn0k";
|
||||
version = "1.0.1";
|
||||
};
|
||||
|
||||
"elm/browser" = {
|
||||
sha256 = "0nagb9ajacxbbg985r4k9h0jadqpp0gp84nm94kcgbr5sf8i9x13";
|
||||
version = "1.0.2";
|
||||
};
|
||||
|
||||
"elm/core" = {
|
||||
sha256 = "19w0iisdd66ywjayyga4kv2p1v9rxzqjaxhckp8ni6n8i0fb2dvf";
|
||||
version = "1.0.5";
|
||||
};
|
||||
|
||||
"elm-community/list-extra" = {
|
||||
sha256 = "1ayv3148drynqnxdfwpjxal8vwzgsjqanjg7yxp6lhdcbkxgd3vd";
|
||||
version = "8.2.3";
|
||||
};
|
||||
|
||||
"elm/random" = {
|
||||
sha256 = "138n2455wdjwa657w6sjq18wx2r0k60ibpc4frhbqr50sncxrfdl";
|
||||
version = "1.0.0";
|
||||
};
|
||||
|
||||
"elm/time" = {
|
||||
sha256 = "0vch7i86vn0x8b850w1p69vplll1bnbkp8s383z7pinyg94cm2z1";
|
||||
version = "1.0.0";
|
||||
};
|
||||
|
||||
"elm/json" = {
|
||||
sha256 = "0kjwrz195z84kwywaxhhlnpl3p251qlbm5iz6byd6jky2crmyqyh";
|
||||
version = "1.1.3";
|
||||
};
|
||||
|
||||
"owanturist/elm-union-find" = {
|
||||
sha256 = "13gm7msnp0gr1lqia5m7m4lhy3m6kvjg37d304whb3psn88wqhj5";
|
||||
version = "1.0.0";
|
||||
};
|
||||
|
||||
"elm/url" = {
|
||||
sha256 = "0av8x5syid40sgpl5vd7pry2rq0q4pga28b4yykn9gd9v12rs3l4";
|
||||
version = "1.0.0";
|
||||
};
|
||||
|
||||
"elm/virtual-dom" = {
|
||||
sha256 = "0q1v5gi4g336bzz1lgwpn5b1639lrn63d8y6k6pimcyismp2i1yg";
|
||||
version = "1.0.2";
|
||||
};
|
||||
}
|
30
website/sandbox/learnpianochords/elm.json
Normal file
30
website/sandbox/learnpianochords/elm.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"type": "application",
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"elm/browser": "1.0.2",
|
||||
"elm/core": "1.0.5",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/random": "1.0.0",
|
||||
"elm/svg": "1.0.1",
|
||||
"elm/time": "1.0.0",
|
||||
"elm-community/list-extra": "8.2.3",
|
||||
"elm-community/maybe-extra": "5.2.0",
|
||||
"elm-community/random-extra": "3.1.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/json": "1.1.3",
|
||||
"elm/url": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.2",
|
||||
"owanturist/elm-union-find": "1.0.0"
|
||||
}
|
||||
},
|
||||
"test-dependencies": {
|
||||
"direct": {},
|
||||
"indirect": {}
|
||||
}
|
||||
}
|
3
website/sandbox/learnpianochords/ideas.org
Normal file
3
website/sandbox/learnpianochords/ideas.org
Normal file
|
@ -0,0 +1,3 @@
|
|||
* Support a frequency table of all of the chords
|
||||
* Support using spaced-repetition to help populate the frequency table of chords
|
||||
* If doing a frequency table, support left and right hands
|
3
website/sandbox/learnpianochords/index.css
Normal file
3
website/sandbox/learnpianochords/index.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
15
website/sandbox/learnpianochords/index.html
Normal file
15
website/sandbox/learnpianochords/index.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Learn Piano Chords</title>
|
||||
<link rel="stylesheet" href="./output.css" />
|
||||
<script src="./Main.min.js"></script>
|
||||
</head>
|
||||
<body class="font-serif">
|
||||
<div id="mount"></div>
|
||||
<script>
|
||||
Elm.Main.init({node: document.getElementById("mount")});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
BIN
website/sandbox/learnpianochords/registry.dat
Normal file
BIN
website/sandbox/learnpianochords/registry.dat
Normal file
Binary file not shown.
9
website/sandbox/learnpianochords/shell.nix
Normal file
9
website/sandbox/learnpianochords/shell.nix
Normal file
|
@ -0,0 +1,9 @@
|
|||
let
|
||||
pkgs = import <nixpkgs> {};
|
||||
in pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
elmPackages.elm
|
||||
elmPackages.elm-format
|
||||
elmPackages.elm-live
|
||||
];
|
||||
}
|
15
website/sandbox/learnpianochords/src/ChordInspector.elm
Normal file
15
website/sandbox/learnpianochords/src/ChordInspector.elm
Normal file
|
@ -0,0 +1,15 @@
|
|||
module ChordInspector exposing (render)
|
||||
|
||||
import Html exposing (..)
|
||||
import NoteInspector
|
||||
import Theory
|
||||
|
||||
|
||||
render : Theory.Chord -> Html a
|
||||
render chord =
|
||||
case Theory.notesForChord chord of
|
||||
Nothing ->
|
||||
p [] [ text "Cannot retrieve the notes for the chord." ]
|
||||
|
||||
Just notes ->
|
||||
NoteInspector.render notes
|
44
website/sandbox/learnpianochords/src/Icon.elm
Normal file
44
website/sandbox/learnpianochords/src/Icon.elm
Normal file
|
@ -0,0 +1,44 @@
|
|||
module Icon exposing (..)
|
||||
|
||||
import Svg exposing (node, svg)
|
||||
import Svg.Attributes exposing (..)
|
||||
import UI
|
||||
|
||||
|
||||
svgColor color =
|
||||
let
|
||||
classes =
|
||||
case color of
|
||||
UI.Primary ->
|
||||
[ "text-gray-500", "fill-current" ]
|
||||
|
||||
UI.Secondary ->
|
||||
[ "text-gray-300", "fill-current" ]
|
||||
in
|
||||
class <| String.join " " classes
|
||||
|
||||
|
||||
cog =
|
||||
svg [ class "icon-cog", viewBox "0 0 24 24", xmlLang "http://www.w3.org/2000/svg" ]
|
||||
[ Svg.path
|
||||
[ svgColor UI.Primary
|
||||
, d "M6.8 3.45c.87-.52 1.82-.92 2.83-1.17a2.5 2.5 0 0 0 4.74 0c1.01.25 1.96.65 2.82 1.17a2.5 2.5 0 0 0 3.36 3.36c.52.86.92 1.8 1.17 2.82a2.5 2.5 0 0 0 0 4.74c-.25 1.01-.65 1.96-1.17 2.82a2.5 2.5 0 0 0-3.36 3.36c-.86.52-1.8.92-2.82 1.17a2.5 2.5 0 0 0-4.74 0c-1.01-.25-1.96-.65-2.82-1.17a2.5 2.5 0 0 0-3.36-3.36 9.94 9.94 0 0 1-1.17-2.82 2.5 2.5 0 0 0 0-4.74c.25-1.01.65-1.96 1.17-2.82a2.5 2.5 0 0 0 3.36-3.36zM12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"
|
||||
, fill "red"
|
||||
]
|
||||
[]
|
||||
, node "circle"
|
||||
[ svgColor UI.Secondary, cx "12", cy "12", r "2" ]
|
||||
[]
|
||||
]
|
||||
|
||||
|
||||
close =
|
||||
svg [ class "icon-close", viewBox "0 0 24 24", xmlLang "http://www.w3.org/2000/svg" ]
|
||||
[ Svg.path
|
||||
[ svgColor UI.Primary
|
||||
, d "M15.78 14.36a1 1 0 0 1-1.42 1.42l-2.82-2.83-2.83 2.83a1 1 0 1 1-1.42-1.42l2.83-2.82L7.3 8.7a1 1 0 0 1 1.42-1.42l2.83 2.83 2.82-2.83a1 1 0 0 1 1.42 1.42l-2.83 2.83 2.83 2.82z"
|
||||
, fill "red"
|
||||
, fillRule "evenodd"
|
||||
]
|
||||
[]
|
||||
]
|
555
website/sandbox/learnpianochords/src/Main.elm
Normal file
555
website/sandbox/learnpianochords/src/Main.elm
Normal file
|
@ -0,0 +1,555 @@
|
|||
module Main exposing (main)
|
||||
|
||||
import Browser
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (..)
|
||||
import Icon
|
||||
import Piano
|
||||
import Random
|
||||
import Random.List
|
||||
import Tempo
|
||||
import Theory
|
||||
import Time exposing (..)
|
||||
import UI
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ whitelistedChords : List Theory.Chord
|
||||
, whitelistedChordTypes : List Theory.ChordType
|
||||
, whitelistedInversions : List Theory.ChordInversion
|
||||
, whitelistedPitchClasses : List Theory.PitchClass
|
||||
, whitelistedKeys : List Theory.Key
|
||||
, selectedChord : Maybe Theory.Chord
|
||||
, isPaused : Bool
|
||||
, tempo : Int
|
||||
, firstNote : Theory.Note
|
||||
, lastNote : Theory.Note
|
||||
, practiceMode : PracticeMode
|
||||
, view : View
|
||||
}
|
||||
|
||||
|
||||
type View
|
||||
= Preferences
|
||||
| Practice
|
||||
|
||||
|
||||
{-| Control the type of practice you'd like.
|
||||
-}
|
||||
type PracticeMode
|
||||
= KeyMode
|
||||
| FineTuneMode
|
||||
|
||||
|
||||
type Msg
|
||||
= NextChord
|
||||
| NewChord Theory.Chord
|
||||
| Play
|
||||
| Pause
|
||||
| IncreaseTempo
|
||||
| DecreaseTempo
|
||||
| SetTempo String
|
||||
| ToggleInversion Theory.ChordInversion
|
||||
| ToggleChordType Theory.ChordType
|
||||
| TogglePitchClass Theory.PitchClass
|
||||
| ToggleKey Theory.Key
|
||||
| DoNothing
|
||||
| SetPracticeMode PracticeMode
|
||||
| SelectAllKeys
|
||||
| DeselectAllKeys
|
||||
| SetView View
|
||||
|
||||
|
||||
{-| The amount by which we increase or decrease tempo.
|
||||
-}
|
||||
tempoStep : Int
|
||||
tempoStep =
|
||||
5
|
||||
|
||||
|
||||
{-| Return the number of milliseconds that elapse during an interval in a
|
||||
`target` bpm.
|
||||
-}
|
||||
bpmToMilliseconds : Int -> Int
|
||||
bpmToMilliseconds target =
|
||||
let
|
||||
msPerMinute =
|
||||
1000 * 60
|
||||
in
|
||||
round (toFloat msPerMinute / toFloat target)
|
||||
|
||||
|
||||
{-| The initial state for the application.
|
||||
-}
|
||||
init : Model
|
||||
init =
|
||||
let
|
||||
( firstNote, lastNote ) =
|
||||
( Theory.C3, Theory.C6 )
|
||||
|
||||
inversions =
|
||||
Theory.allInversions
|
||||
|
||||
chordTypes =
|
||||
Theory.allChordTypes
|
||||
|
||||
pitchClasses =
|
||||
Theory.allPitchClasses
|
||||
|
||||
keys =
|
||||
[]
|
||||
|
||||
practiceMode =
|
||||
KeyMode
|
||||
in
|
||||
{ practiceMode = practiceMode
|
||||
, whitelistedChords =
|
||||
case practiceMode of
|
||||
KeyMode ->
|
||||
keys |> List.concatMap Theory.chordsForKey
|
||||
|
||||
FineTuneMode ->
|
||||
Theory.allChords
|
||||
{ start = firstNote
|
||||
, end = lastNote
|
||||
, inversions = inversions
|
||||
, chordTypes = chordTypes
|
||||
, pitchClasses = pitchClasses
|
||||
}
|
||||
, whitelistedChordTypes = chordTypes
|
||||
, whitelistedInversions = inversions
|
||||
, whitelistedPitchClasses = pitchClasses
|
||||
, whitelistedKeys = keys
|
||||
, selectedChord = Nothing
|
||||
, isPaused = True
|
||||
, tempo = 20
|
||||
, firstNote = firstNote
|
||||
, lastNote = lastNote
|
||||
, view = Preferences
|
||||
}
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions { isPaused, tempo } =
|
||||
if isPaused then
|
||||
Sub.none
|
||||
|
||||
else
|
||||
Time.every (tempo |> bpmToMilliseconds |> toFloat) (\_ -> NextChord)
|
||||
|
||||
|
||||
{-| Now that we have state, we need a function to change the state.
|
||||
-}
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
DoNothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SetPracticeMode practiceMode ->
|
||||
( { model
|
||||
| practiceMode = practiceMode
|
||||
, isPaused = True
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
SetView x ->
|
||||
( { model
|
||||
| view = x
|
||||
, isPaused = True
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
SelectAllKeys ->
|
||||
( { model
|
||||
| whitelistedKeys = Theory.allKeys
|
||||
, whitelistedChords =
|
||||
Theory.allKeys |> List.concatMap Theory.chordsForKey
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
DeselectAllKeys ->
|
||||
( { model
|
||||
| whitelistedKeys = []
|
||||
, whitelistedChords = []
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
NewChord chord ->
|
||||
( { model | selectedChord = Just chord }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
NextChord ->
|
||||
( model
|
||||
, Random.generate
|
||||
(\x ->
|
||||
case x of
|
||||
( Just chord, _ ) ->
|
||||
NewChord chord
|
||||
|
||||
( Nothing, _ ) ->
|
||||
DoNothing
|
||||
)
|
||||
(Random.List.choose model.whitelistedChords)
|
||||
)
|
||||
|
||||
Play ->
|
||||
( { model | isPaused = False }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Pause ->
|
||||
( { model | isPaused = True }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
IncreaseTempo ->
|
||||
( { model | tempo = model.tempo + tempoStep }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
DecreaseTempo ->
|
||||
( { model | tempo = model.tempo - tempoStep }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
ToggleChordType chordType ->
|
||||
let
|
||||
chordTypes =
|
||||
if List.member chordType model.whitelistedChordTypes then
|
||||
List.filter ((/=) chordType) model.whitelistedChordTypes
|
||||
|
||||
else
|
||||
chordType :: model.whitelistedChordTypes
|
||||
in
|
||||
( { model
|
||||
| whitelistedChordTypes = chordTypes
|
||||
, whitelistedChords =
|
||||
Theory.allChords
|
||||
{ start = model.firstNote
|
||||
, end = model.lastNote
|
||||
, inversions = model.whitelistedInversions
|
||||
, chordTypes = chordTypes
|
||||
, pitchClasses = model.whitelistedPitchClasses
|
||||
}
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
ToggleInversion inversion ->
|
||||
let
|
||||
inversions =
|
||||
if List.member inversion model.whitelistedInversions then
|
||||
List.filter ((/=) inversion) model.whitelistedInversions
|
||||
|
||||
else
|
||||
inversion :: model.whitelistedInversions
|
||||
in
|
||||
( { model
|
||||
| whitelistedInversions = inversions
|
||||
, whitelistedChords =
|
||||
Theory.allChords
|
||||
{ start = model.firstNote
|
||||
, end = model.lastNote
|
||||
, inversions = inversions
|
||||
, chordTypes = model.whitelistedChordTypes
|
||||
, pitchClasses = model.whitelistedPitchClasses
|
||||
}
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
TogglePitchClass pitchClass ->
|
||||
let
|
||||
pitchClasses =
|
||||
if List.member pitchClass model.whitelistedPitchClasses then
|
||||
List.filter ((/=) pitchClass) model.whitelistedPitchClasses
|
||||
|
||||
else
|
||||
pitchClass :: model.whitelistedPitchClasses
|
||||
in
|
||||
( { model
|
||||
| whitelistedPitchClasses = pitchClasses
|
||||
, whitelistedChords =
|
||||
Theory.allChords
|
||||
{ start = model.firstNote
|
||||
, end = model.lastNote
|
||||
, inversions = model.whitelistedInversions
|
||||
, chordTypes = model.whitelistedChordTypes
|
||||
, pitchClasses = pitchClasses
|
||||
}
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
ToggleKey key ->
|
||||
let
|
||||
keys =
|
||||
if List.member key model.whitelistedKeys then
|
||||
List.filter ((/=) key) model.whitelistedKeys
|
||||
|
||||
else
|
||||
key :: model.whitelistedKeys
|
||||
in
|
||||
( { model
|
||||
| whitelistedKeys = keys
|
||||
, whitelistedChords =
|
||||
keys |> List.concatMap Theory.chordsForKey
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
SetTempo tempo ->
|
||||
( { model
|
||||
| tempo =
|
||||
case String.toInt tempo of
|
||||
Just x ->
|
||||
x
|
||||
|
||||
Nothing ->
|
||||
model.tempo
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
playPause : Model -> Html Msg
|
||||
playPause { isPaused } =
|
||||
if isPaused then
|
||||
button [ onClick Play ] [ text "Play" ]
|
||||
|
||||
else
|
||||
button [ onClick Pause ] [ text "Pause" ]
|
||||
|
||||
|
||||
chordTypeCheckboxes : List Theory.ChordType -> Html Msg
|
||||
chordTypeCheckboxes chordTypes =
|
||||
ul []
|
||||
(Theory.allChordTypes
|
||||
|> List.map
|
||||
(\chordType ->
|
||||
li []
|
||||
[ label [] [ text (Theory.chordTypeName chordType) ]
|
||||
, input
|
||||
[ type_ "checkbox"
|
||||
, onClick (ToggleChordType chordType)
|
||||
, checked (List.member chordType chordTypes)
|
||||
]
|
||||
[]
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
inversionCheckboxes : List Theory.ChordInversion -> Html Msg
|
||||
inversionCheckboxes inversions =
|
||||
ul []
|
||||
(Theory.allInversions
|
||||
|> List.map
|
||||
(\inversion ->
|
||||
li []
|
||||
[ label [] [ text (Theory.inversionName inversion) ]
|
||||
, input
|
||||
[ type_ "checkbox"
|
||||
, onClick (ToggleInversion inversion)
|
||||
, checked (List.member inversion inversions)
|
||||
]
|
||||
[]
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
selectKey :
|
||||
Model
|
||||
->
|
||||
{ relativeMajor : Theory.Key
|
||||
, relativeMinor : Theory.Key
|
||||
}
|
||||
-> Html Msg
|
||||
selectKey model { relativeMajor, relativeMinor } =
|
||||
let
|
||||
active key =
|
||||
List.member key model.whitelistedKeys
|
||||
|
||||
buttonLabel major minor =
|
||||
Theory.viewKey major ++ ", " ++ Theory.viewKey minor
|
||||
in
|
||||
div [ class "flex pt-0" ]
|
||||
[ UI.textToggleButton
|
||||
{ label = buttonLabel relativeMajor relativeMinor
|
||||
, handleClick = ToggleKey relativeMinor
|
||||
, classes = [ "flex-1" ]
|
||||
, toggled = active relativeMinor
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
keyCheckboxes : Model -> Html Msg
|
||||
keyCheckboxes model =
|
||||
let
|
||||
majorKey pitchClass =
|
||||
{ pitchClass = pitchClass, mode = Theory.MajorMode }
|
||||
|
||||
minorKey pitchClass =
|
||||
{ pitchClass = pitchClass, mode = Theory.MinorMode }
|
||||
|
||||
circleOfFifths =
|
||||
[ ( Theory.C, Theory.A )
|
||||
, ( Theory.G, Theory.E )
|
||||
, ( Theory.D, Theory.B )
|
||||
, ( Theory.A, Theory.F_sharp )
|
||||
, ( Theory.E, Theory.C_sharp )
|
||||
, ( Theory.B, Theory.G_sharp )
|
||||
, ( Theory.F_sharp, Theory.D_sharp )
|
||||
, ( Theory.C_sharp, Theory.A_sharp )
|
||||
, ( Theory.G_sharp, Theory.F )
|
||||
, ( Theory.D_sharp, Theory.C )
|
||||
, ( Theory.A_sharp, Theory.G )
|
||||
, ( Theory.F, Theory.D )
|
||||
]
|
||||
in
|
||||
div []
|
||||
[ h2 [ class "text-gray-500 text-center pt-10 text-5xl" ] [ text "Select keys" ]
|
||||
, ul []
|
||||
(circleOfFifths
|
||||
|> List.map
|
||||
(\( major, minor ) ->
|
||||
selectKey model
|
||||
{ relativeMajor = majorKey major
|
||||
, relativeMinor = minorKey minor
|
||||
}
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
practiceModeButtons : Model -> Html Msg
|
||||
practiceModeButtons model =
|
||||
div [ class "text-center" ]
|
||||
[ h2 [ class "py-10 text-5xl" ] [ text "Practice Mode" ]
|
||||
, div [ class "flex pb-6" ]
|
||||
[ UI.simpleButton
|
||||
{ label = "Key"
|
||||
, classes = [ "flex-1", "rounded-r-none" ]
|
||||
, handleClick = SetPracticeMode KeyMode
|
||||
, color =
|
||||
if model.practiceMode == KeyMode then
|
||||
UI.Primary
|
||||
|
||||
else
|
||||
UI.Secondary
|
||||
}
|
||||
, UI.simpleButton
|
||||
{ label = "Fine Tune"
|
||||
, handleClick = SetPracticeMode FineTuneMode
|
||||
, classes = [ "flex-1", "rounded-l-none" ]
|
||||
, color =
|
||||
if model.practiceMode == FineTuneMode then
|
||||
UI.Primary
|
||||
|
||||
else
|
||||
UI.Secondary
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
openPreferences : Html Msg
|
||||
openPreferences =
|
||||
button
|
||||
[ class "w-48 h-48 absolute left-0 top-0 z-20"
|
||||
, onClick (SetView Preferences)
|
||||
]
|
||||
[ Icon.cog ]
|
||||
|
||||
|
||||
closePreferences : Html Msg
|
||||
closePreferences =
|
||||
button
|
||||
[ class "w-48 h-48 absolute right-0 top-0 z-10"
|
||||
, onClick (SetView Practice)
|
||||
]
|
||||
[ Icon.close ]
|
||||
|
||||
|
||||
preferences : Model -> Html Msg
|
||||
preferences model =
|
||||
div [ class "pt-10 pb-20 px-10" ]
|
||||
[ closePreferences
|
||||
, Tempo.render
|
||||
{ tempo = model.tempo
|
||||
, handleInput = SetTempo
|
||||
}
|
||||
, case model.practiceMode of
|
||||
KeyMode ->
|
||||
keyCheckboxes model
|
||||
|
||||
FineTuneMode ->
|
||||
div []
|
||||
[ inversionCheckboxes model.whitelistedInversions
|
||||
, chordTypeCheckboxes model.whitelistedChordTypes
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
practice : Model -> Html Msg
|
||||
practice model =
|
||||
let
|
||||
classes =
|
||||
[ "bg-gray-600"
|
||||
, "h-screen"
|
||||
, "w-full"
|
||||
, "absolute"
|
||||
, "z-10"
|
||||
, "text-6xl"
|
||||
]
|
||||
|
||||
( handleClick, extraClasses, buttonText ) =
|
||||
if model.isPaused then
|
||||
( Play, [ "opacity-50" ], "Press to practice" )
|
||||
|
||||
else
|
||||
( Pause, [ "opacity-0" ], "" )
|
||||
in
|
||||
div []
|
||||
[ button
|
||||
[ [ classes, extraClasses ] |> List.concat |> UI.tw |> class
|
||||
, onClick handleClick
|
||||
]
|
||||
[ text buttonText
|
||||
]
|
||||
, openPreferences
|
||||
, Piano.render
|
||||
{ highlight = model.selectedChord |> Maybe.andThen Theory.notesForChord |> Maybe.withDefault []
|
||||
, start = model.firstNote
|
||||
, end = model.lastNote
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
case model.view of
|
||||
Preferences ->
|
||||
preferences model
|
||||
|
||||
Practice ->
|
||||
practice model
|
||||
|
||||
|
||||
{-| For now, I'm just dumping things onto the page to sketch ideas.
|
||||
-}
|
||||
main =
|
||||
Browser.element
|
||||
{ init = \() -> ( init, Cmd.none )
|
||||
, subscriptions = subscriptions
|
||||
, update = update
|
||||
, view = view
|
||||
}
|
47
website/sandbox/learnpianochords/src/Misc.elm
Normal file
47
website/sandbox/learnpianochords/src/Misc.elm
Normal file
|
@ -0,0 +1,47 @@
|
|||
module Misc exposing (..)
|
||||
|
||||
import Array exposing (Array)
|
||||
|
||||
|
||||
comesAfter : a -> List a -> Maybe a
|
||||
comesAfter x xs =
|
||||
case xs of
|
||||
[] ->
|
||||
Nothing
|
||||
|
||||
_ :: [] ->
|
||||
Nothing
|
||||
|
||||
y :: z :: rest ->
|
||||
if y == x then
|
||||
Just z
|
||||
|
||||
else
|
||||
comesAfter x (z :: rest)
|
||||
|
||||
|
||||
comesBefore : a -> List a -> Maybe a
|
||||
comesBefore x xs =
|
||||
case xs of
|
||||
[] ->
|
||||
Nothing
|
||||
|
||||
_ :: [] ->
|
||||
Nothing
|
||||
|
||||
y :: z :: rest ->
|
||||
if z == x then
|
||||
Just y
|
||||
|
||||
else
|
||||
comesBefore x (z :: rest)
|
||||
|
||||
|
||||
find : (a -> Bool) -> List a -> Maybe a
|
||||
find pred xs =
|
||||
case xs |> List.filter pred of
|
||||
[] ->
|
||||
Nothing
|
||||
|
||||
x :: _ ->
|
||||
Just x
|
238
website/sandbox/learnpianochords/src/Piano.elm
Normal file
238
website/sandbox/learnpianochords/src/Piano.elm
Normal file
|
@ -0,0 +1,238 @@
|
|||
module Piano exposing (render)
|
||||
|
||||
import Browser
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (..)
|
||||
import List.Extra
|
||||
import Theory
|
||||
|
||||
|
||||
{-| On mobile phones, the keyboard displays vertically.
|
||||
-}
|
||||
type Direction
|
||||
= Horizontal
|
||||
| Vertical
|
||||
|
||||
|
||||
type alias KeyMarkup a =
|
||||
{ offset : Int
|
||||
, isHighlit : Bool
|
||||
, note : Theory.Note
|
||||
, direction : Direction
|
||||
}
|
||||
-> Html a
|
||||
|
||||
|
||||
type alias Props =
|
||||
{ highlight : List Theory.Note
|
||||
, start : Theory.Note
|
||||
, end : Theory.Note
|
||||
}
|
||||
|
||||
|
||||
{-| Convert an integer into its pixel representation for CSS.
|
||||
-}
|
||||
pixelate : Int -> String
|
||||
pixelate x =
|
||||
String.fromInt x ++ "px"
|
||||
|
||||
|
||||
{-| Pixel width of the white keys.
|
||||
-}
|
||||
naturalWidth : Direction -> Int
|
||||
naturalWidth direction =
|
||||
case direction of
|
||||
Vertical ->
|
||||
-- Right now, I'm designing this specifically for my Google Pixel 4
|
||||
-- phone, which has a screen width of 1080px.
|
||||
1080
|
||||
|
||||
Horizontal ->
|
||||
45
|
||||
|
||||
|
||||
{-| Pixel height of the white keys.
|
||||
-}
|
||||
naturalHeight : Direction -> Int
|
||||
naturalHeight direction =
|
||||
case direction of
|
||||
Vertical ->
|
||||
-- Right now, I'm designing this specifically for my Google Pixel 4
|
||||
-- phone, which has a screen height of 2280px. 2280 / 21
|
||||
-- (i.e. no. natural keys) ~= 108
|
||||
108
|
||||
|
||||
Horizontal ->
|
||||
250
|
||||
|
||||
|
||||
{-| Pixel width of the black keys.
|
||||
-}
|
||||
accidentalWidth : Direction -> Int
|
||||
accidentalWidth direction =
|
||||
case direction of
|
||||
Vertical ->
|
||||
round (toFloat (naturalWidth direction) * 0.6)
|
||||
|
||||
Horizontal ->
|
||||
round (toFloat (naturalWidth direction) * 0.4)
|
||||
|
||||
|
||||
{-| Pixel height of the black keys.
|
||||
-}
|
||||
accidentalHeight : Direction -> Int
|
||||
accidentalHeight direction =
|
||||
case direction of
|
||||
Vertical ->
|
||||
round (toFloat (naturalHeight direction) * 0.63)
|
||||
|
||||
Horizontal ->
|
||||
round (toFloat (naturalHeight direction) * 0.63)
|
||||
|
||||
|
||||
{-| Return the markup for either a white or a black key.
|
||||
-}
|
||||
pianoKey : KeyMarkup a
|
||||
pianoKey { offset, isHighlit, note, direction } =
|
||||
let
|
||||
sharedClasses =
|
||||
[ "box-border" ]
|
||||
|
||||
{ keyWidth, keyHeight, keyColor, offsetEdge, extraClasses } =
|
||||
case ( Theory.keyClass note, direction ) of
|
||||
( Theory.Natural, Vertical ) ->
|
||||
{ keyWidth = naturalWidth Vertical
|
||||
, keyHeight = naturalHeight Vertical
|
||||
, keyColor = "white"
|
||||
, offsetEdge = "top"
|
||||
, extraClasses = []
|
||||
}
|
||||
|
||||
( Theory.Natural, Horizontal ) ->
|
||||
{ keyWidth = naturalWidth Horizontal
|
||||
, keyHeight = naturalHeight Horizontal
|
||||
, keyColor = "white"
|
||||
, offsetEdge = "left"
|
||||
, extraClasses = []
|
||||
}
|
||||
|
||||
( Theory.Accidental, Vertical ) ->
|
||||
{ keyWidth = accidentalWidth Vertical
|
||||
, keyHeight = accidentalHeight Vertical
|
||||
, keyColor = "black"
|
||||
, offsetEdge = "top"
|
||||
, extraClasses = [ "z-10" ]
|
||||
}
|
||||
|
||||
( Theory.Accidental, Horizontal ) ->
|
||||
{ keyWidth = accidentalWidth Horizontal
|
||||
, keyHeight = accidentalHeight Horizontal
|
||||
, keyColor = "black"
|
||||
, offsetEdge = "left"
|
||||
, extraClasses = [ "z-10" ]
|
||||
}
|
||||
in
|
||||
div
|
||||
[ style "background-color"
|
||||
(if isHighlit then
|
||||
"red"
|
||||
|
||||
else
|
||||
keyColor
|
||||
)
|
||||
, style "border-top" "1px solid black"
|
||||
, style "border-bottom" "1px solid black"
|
||||
, style "border-left" "1px solid black"
|
||||
, style "border-right" "1px solid black"
|
||||
, style "width" (pixelate keyWidth)
|
||||
, style "height" (pixelate keyHeight)
|
||||
, style "position" "absolute"
|
||||
, style offsetEdge (String.fromInt offset ++ "px")
|
||||
, class <| String.join " " (List.concat [ sharedClasses, extraClasses ])
|
||||
]
|
||||
[]
|
||||
|
||||
|
||||
{-| A section of the piano consisting of all twelve notes.
|
||||
-}
|
||||
keys : Direction -> Theory.Note -> Theory.Note -> List Theory.Note -> List (Html a)
|
||||
keys direction start end highlight =
|
||||
let
|
||||
isHighlit note =
|
||||
List.member note highlight
|
||||
|
||||
spacing prevOffset prev curr =
|
||||
case ( Theory.keyClass prev, Theory.keyClass curr, direction ) of
|
||||
-- Horizontal
|
||||
( Theory.Natural, Theory.Accidental, Horizontal ) ->
|
||||
prevOffset + naturalWidth direction - round (toFloat (accidentalWidth direction) / 2)
|
||||
|
||||
( Theory.Accidental, Theory.Natural, Horizontal ) ->
|
||||
prevOffset + round (toFloat (accidentalWidth direction) / 2)
|
||||
|
||||
( Theory.Natural, Theory.Natural, Horizontal ) ->
|
||||
prevOffset + naturalWidth direction
|
||||
|
||||
-- Vertical
|
||||
( Theory.Natural, Theory.Accidental, Vertical ) ->
|
||||
prevOffset + naturalHeight direction - round (toFloat (accidentalHeight direction) / 2)
|
||||
|
||||
( Theory.Accidental, Theory.Natural, Vertical ) ->
|
||||
prevOffset + round (toFloat (accidentalHeight direction) / 2)
|
||||
|
||||
( Theory.Natural, Theory.Natural, Vertical ) ->
|
||||
prevOffset + naturalHeight direction
|
||||
|
||||
-- This pattern should never hit.
|
||||
_ ->
|
||||
prevOffset
|
||||
|
||||
( _, _, notes ) =
|
||||
Theory.notesFromRange start end
|
||||
|> List.foldl
|
||||
(\curr ( prevOffset, prev, result ) ->
|
||||
case ( prevOffset, prev ) of
|
||||
( Nothing, Nothing ) ->
|
||||
( Just 0
|
||||
, Just curr
|
||||
, pianoKey
|
||||
{ offset = 0
|
||||
, isHighlit = List.member curr highlight
|
||||
, note = curr
|
||||
, direction = direction
|
||||
}
|
||||
:: result
|
||||
)
|
||||
|
||||
( Just po, Just p ) ->
|
||||
let
|
||||
offset =
|
||||
spacing po p curr
|
||||
in
|
||||
( Just offset
|
||||
, Just curr
|
||||
, pianoKey
|
||||
{ offset = offset
|
||||
, isHighlit = List.member curr highlight
|
||||
, note = curr
|
||||
, direction = direction
|
||||
}
|
||||
:: result
|
||||
)
|
||||
|
||||
-- This pattern should never hit.
|
||||
_ ->
|
||||
( Nothing, Nothing, [] )
|
||||
)
|
||||
( Nothing, Nothing, [] )
|
||||
in
|
||||
List.reverse notes
|
||||
|
||||
|
||||
{-| Return the HTML that renders a piano representation.
|
||||
-}
|
||||
render : Props -> Html a
|
||||
render { highlight, start, end } =
|
||||
div [ style "display" "flex" ]
|
||||
(keys Vertical start end highlight |> List.reverse |> List.repeat 1 |> List.concat)
|
24
website/sandbox/learnpianochords/src/Tempo.elm
Normal file
24
website/sandbox/learnpianochords/src/Tempo.elm
Normal file
|
@ -0,0 +1,24 @@
|
|||
module Tempo exposing (render)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (..)
|
||||
import UI
|
||||
|
||||
|
||||
type alias Props msg =
|
||||
{ tempo : Int
|
||||
, handleInput : String -> msg
|
||||
}
|
||||
|
||||
|
||||
render : Props msg -> Html msg
|
||||
render { tempo, handleInput } =
|
||||
div [ class "text-center" ]
|
||||
[ p [ class "text-5xl py-10" ] [ text (String.fromInt tempo ++ " BPM") ]
|
||||
, UI.textField
|
||||
{ placeholderText = "Set tempo..."
|
||||
, handleInput = handleInput
|
||||
, classes = []
|
||||
}
|
||||
]
|
1100
website/sandbox/learnpianochords/src/Theory.elm
Normal file
1100
website/sandbox/learnpianochords/src/Theory.elm
Normal file
File diff suppressed because it is too large
Load diff
116
website/sandbox/learnpianochords/src/UI.elm
Normal file
116
website/sandbox/learnpianochords/src/UI.elm
Normal file
|
@ -0,0 +1,116 @@
|
|||
module UI exposing (..)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (..)
|
||||
|
||||
|
||||
type Color
|
||||
= Primary
|
||||
| Secondary
|
||||
|
||||
|
||||
bgForColor : Color -> String
|
||||
bgForColor color =
|
||||
case color of
|
||||
Primary ->
|
||||
"bg-gray-600"
|
||||
|
||||
Secondary ->
|
||||
"bg-gray-300"
|
||||
|
||||
|
||||
textForColor : Color -> String
|
||||
textForColor color =
|
||||
case color of
|
||||
Primary ->
|
||||
"text-white"
|
||||
|
||||
Secondary ->
|
||||
"text-black"
|
||||
|
||||
|
||||
tw : List String -> String
|
||||
tw styles =
|
||||
String.join " " styles
|
||||
|
||||
|
||||
simpleButton :
|
||||
{ label : String
|
||||
, handleClick : msg
|
||||
, color : Color
|
||||
, classes : List String
|
||||
}
|
||||
-> Html msg
|
||||
simpleButton { label, handleClick, color, classes } =
|
||||
let
|
||||
buttonClasses =
|
||||
[ bgForColor color
|
||||
, textForColor color
|
||||
, "py-10"
|
||||
, "px-20"
|
||||
, "text-5xl"
|
||||
, "rounded-lg"
|
||||
]
|
||||
in
|
||||
button
|
||||
[ class (tw <| List.concat [ buttonClasses, classes ])
|
||||
, onClick handleClick
|
||||
]
|
||||
[ text label ]
|
||||
|
||||
|
||||
textToggleButton :
|
||||
{ label : String
|
||||
, handleClick : msg
|
||||
, classes : List String
|
||||
, toggled : Bool
|
||||
}
|
||||
-> Html msg
|
||||
textToggleButton { label, toggled, handleClick, classes } =
|
||||
let
|
||||
( textColor, textTreatment ) =
|
||||
if toggled then
|
||||
( "text-red-600", "underline" )
|
||||
|
||||
else
|
||||
( "text-black", "no-underline" )
|
||||
|
||||
buttonClasses =
|
||||
[ textColor
|
||||
, textTreatment
|
||||
, "py-8"
|
||||
, "px-10"
|
||||
, "text-5xl"
|
||||
]
|
||||
in
|
||||
button
|
||||
[ class (tw <| List.concat [ buttonClasses, classes ])
|
||||
, onClick handleClick
|
||||
]
|
||||
[ text label ]
|
||||
|
||||
|
||||
textField :
|
||||
{ placeholderText : String
|
||||
, handleInput : String -> msg
|
||||
, classes : List String
|
||||
}
|
||||
-> Html msg
|
||||
textField { placeholderText, handleInput, classes } =
|
||||
let
|
||||
inputClasses =
|
||||
[ "text-5xl"
|
||||
, "w-full"
|
||||
, "py-10"
|
||||
, "px-16"
|
||||
, "border"
|
||||
, "rounded-lg"
|
||||
]
|
||||
in
|
||||
input
|
||||
[ class (tw <| List.concat [ inputClasses, classes ])
|
||||
, onInput handleInput
|
||||
, placeholder placeholderText
|
||||
]
|
||||
[]
|
Loading…
Add table
Add a link
Reference in a new issue