diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Tony Day <tonyday567@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/example.hs b/app/example.hs
new file mode 100644
--- /dev/null
+++ b/app/example.hs
@@ -0,0 +1,236 @@
+{-# LANGUAGE ApplicativeDo #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wall #-}
+
+import Control.Category (id)
+import Control.Lens hiding (Wrapped, Unwrapped)
+import Data.Attoparsec.Text
+import Lucid hiding (b_)
+import Network.Wai
+import Network.Wai.Middleware.RequestLogger
+import Network.Wai.Middleware.Static (addBase, noDots, staticPolicy, (>->))
+import Options.Generic
+import Protolude hiding (replace, Rep, log)
+import Web.Page
+import Web.Page.Examples
+import Web.Scotty
+import qualified Box
+import qualified Data.Text as Text
+
+testPage :: Text -> Text -> [(Text, Html ())] -> Page
+testPage title mid sections =
+  bootstrapPage <>
+  bridgePage &
+  #htmlHeader .~ title_ "iroTestPage" &
+  #htmlBody .~ b_ "container" (mconcat
+    [ b_ "row" (h1_ (toHtml title))
+    , b_ "row" (h2_ ("middleware: " <> toHtml mid))
+    , b_ "row" $ mconcat $ (\(t,h) -> b_ "col" (h2_ (toHtml t) <> with div_ [id_ t] h)) <$> sections
+    ])
+
+-- | bridge testing without the SharedRep method
+rangeTest :: Input Int
+rangeTest =
+  Input
+  3
+  (Just "range example")
+  "rangeid"
+  (Slider
+  [ style_ "max-width:15rem;"
+  , min_ "0"
+  , max_ "5"
+  , step_ "1"
+  ])
+
+textTest :: Input Text
+textTest =
+  Input
+  "abc"
+  (Just "label")
+  "textid"
+  TextBox
+
+initBridgeTest :: (Int, Text)
+initBridgeTest = (rangeTest ^. #inputVal, textTest ^. #inputVal)
+
+stepBridgeTest :: Element -> (Int, Text) -> (Int,Text)
+stepBridgeTest e s =
+  case step e s of
+    Left _ -> s
+    Right x -> x
+  where
+    step (Element "rangeid" v) (_, t) = either
+      (Left . Text.pack)
+      (\x -> Right (x,t))
+      (parseOnly decimal v)
+    step (Element "textid" v) (n, _) = Right (n,v)
+    step e' _ = Left $ "unknown id: " <> show e'
+
+sendBridgeTest :: (Show a) => Engine -> Either Text a -> IO ()
+sendBridgeTest e (Left err) = append e "log" err
+sendBridgeTest e (Right a) =
+  replace e "output"
+  (toText $ cardify (mempty, []) (Just "output was:")
+    (toHtml (show a :: Text), []))
+
+consumeBridgeTest :: Engine -> IO (Int, Text)
+consumeBridgeTest e =
+  valueConsume initBridgeTest stepBridgeTest
+  ( (Box.liftC <$> Box.showStdout) <>
+    pure (Box.Committer (\v -> sendBridgeTest e v >> pure True))
+  ) (bridge e)
+
+midBridgeTest :: (Show a) => Html () -> (Engine -> IO a) -> Application -> Application
+midBridgeTest init eeio = start $ \ e -> do
+  appendWithScript e "input" (toText init)
+  final <- eeio e `finally` putStrLn ("midBridgeTest finalled" :: Text)
+  putStrLn $ ("final value was: " :: Text) <> show final
+
+-- * SharedRep testing
+
+-- | Middleware that shows the current shared values
+midShow :: (Show a) => SharedRep IO a -> Application -> Application
+midShow sr = midShared sr initShowRep (logResults show)
+
+initShowRep
+  :: (Show a)
+  => Engine
+  -> Rep a
+  -> StateT (HashMap Text Text) IO ()
+initShowRep e r =
+  void $ oneRep r
+  (\(Rep h fa) m -> do
+      appendWithScript e "input" (toText h)
+      replace e "output" (show (fa m)))
+
+results :: (a -> Text) -> Engine -> a -> IO ()
+results r e x = replace e "output" (r x)
+
+logResults :: (a -> Text) -> Engine -> Either Text a -> IO ()
+logResults _ e (Left err) = append e "log" (err <> "<br>")
+logResults r e (Right x) = results r e x
+
+-- | evaluate a Fiddle, without attempting to downstream bridging
+midFiddle :: Concerns Text -> Application -> Application
+midFiddle cs = midShared (fiddle cs) initFiddleRep (\e -> logFiddle e . second snd)
+
+initFiddleRep
+  :: Engine
+  -> Rep a
+  -> StateT (HashMap Text Text) IO ()
+initFiddleRep e r =
+  void $ oneRep r
+  (\(Rep h _) _ ->
+      appendWithScript e "input" (toText h))
+
+logFiddle :: Engine -> Either Text (Either Text (Concerns Text, Bool)) -> IO ()
+logFiddle e (Left err) = append e "log" ("map error: " <> err)
+logFiddle e (Right (Left err)) = append e "log" ("parse error: " <> err)
+logFiddle e (Right (Right (c,u))) = bool (pure ()) (sendConcerns e "output" c) u
+
+-- | evaluate a Fiddle, and any downstream bridging representation
+
+midViaFiddle
+  :: Show a
+  => SharedRep IO a
+  -> Application -> Application
+midViaFiddle sr =
+  midShared (viaFiddle sr) (initViaFiddleRep show) (\e -> logViaFiddle e show . second snd)
+
+initViaFiddleRep
+  :: (a -> Text)
+  -> Engine
+  -> Rep (Bool, Concerns Text, a)
+  -> StateT (HashMap Text Text) IO ()
+initViaFiddleRep rend e r =
+  void $ oneRep r
+  (\(Rep h fa) m -> do
+      appendWithScript e "input" (toText h)
+      case (snd $ fa m) of
+        Left err -> append e "log" ("map error: " <> err)
+        Right (_,c,a) -> do
+          sendConcerns e "representation" c
+          replace e "output" (rend a))
+
+logViaFiddle :: Engine -> (a -> Text) -> Either Text (Either Text (Bool, Concerns Text, a)) -> IO ()
+logViaFiddle e _ (Left err) = append e "log" ("map error: " <> err)
+logViaFiddle e _ (Right (Left err)) = append e "log" ("parse error: " <> err)
+logViaFiddle e r (Right (Right (True,c,a))) = do
+  sendConcerns e "representation" c
+  replace e "output" (r a)
+logViaFiddle e r (Right (Right (False,_,a))) = replace e "output" (r a)
+
+data MidType = Dev | Prod | ChooseFileExample | DataListExample | SumTypeExample | SumType2Example | Bridge | Listify | ListifyMaybe | Fiddle | ViaFiddle | NoMid deriving (Eq, Read, Show, Generic)
+
+instance ParseField MidType
+instance ParseRecord MidType
+instance ParseFields MidType
+
+data Opts w = Opts
+  { midtype :: w ::: MidType <?> "type of middleware processing"
+  , log :: w ::: Maybe Bool <?> "server log to stdout"
+  , logPath :: w ::: Maybe Bool <?> "log raw path"
+  } deriving (Generic)
+
+instance ParseRecord (Opts Wrapped)
+
+main :: IO ()
+main = do
+  o :: Opts Unwrapped <- unwrapRecord "examples for web-page"
+  let tr = fromMaybe False
+  scotty 3000 $ do
+    middleware $ staticPolicy (noDots >-> addBase "other")
+    middleware $ staticPolicy (noDots >-> addBase "saves")
+    when (tr $ log o) $
+      middleware logStdoutDev
+    when (tr $ logPath o) $
+      middleware $ \app req res ->
+        putStrLn ("raw path:" :: Text) >>
+        print (rawPathInfo req) >> app req res
+  -- Only one middleware servicing the web socket can be run at a time.  Simply switching on based on paths doesn't work because socket comms comes through "/"
+  -- so that the first bridge middleware consumes all the elements
+    middleware $ case midtype o of
+      NoMid -> id
+      -- WebSocket connection to 'ws://localhost:3000/' failed: Error during WebSocket handshake: Unexpected response code: 200
+      Prod -> midShow 
+        (maybeRep (Just "maybe") True repExamples)
+      Dev -> midShow
+        (repSumTypeExample 2 "default text" SumOnly)
+      ChooseFileExample -> midShow
+        (chooseFile (Just "ChooseFile Label") "")
+      DataListExample -> midShow
+        (datalist (Just "label") ["first", "2", "3"] "2" "idlist")
+      SumTypeExample -> midShow
+        (repSumTypeExample 2 "default text" SumOnly)
+      SumType2Example -> midShow
+        (repSumType2Example 2 "default text" SumOnly (SumOutside 2))
+      Listify -> midShow (listifyExample 5)
+      ListifyMaybe -> midShow (listifyMaybeExample 10)
+      Bridge -> midBridgeTest (toHtml rangeTest <> toHtml textTest)
+        consumeBridgeTest
+      Fiddle -> midFiddle fiddleExample
+      ViaFiddle -> midViaFiddle
+        (slider Nothing 0 10 0.01 4)
+    servePageWith "/simple" defaultPageConfig page1
+    servePageWith "/iro" defaultPageConfig
+      (testPage "iro" (show $ midtype o)
+        [ ("input", mempty)
+        , ("representation", mempty)
+        , ("output", mempty)
+        ])
+    servePageWith "/" defaultPageConfig
+      (testPage "prod" (show $ midtype o)
+       [ ("input", mempty)
+       , ("output",
+          (bool mempty
+            (toHtml (show initBridgeTest :: Text))
+            (midtype o == Bridge)))
+       ])
diff --git a/src/Web/Page.hs b/src/Web/Page.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Page.hs
@@ -0,0 +1,42 @@
+module Web.Page
+  ( renderJs
+  , renderCss
+  , renderHtml
+  , module X
+  , Value(..)
+  , finally
+  , PixelRGB8(..)
+  , HashMap.HashMap(..)
+  , fromList
+  ) where
+
+import Protolude hiding (Selector)
+import Web.Page.Css as X
+import Web.Page.Js as X
+import Web.Page.Render as X
+import Web.Page.Server as X
+import Web.Page.Types as X
+import Web.Page.Bridge as X
+import Web.Page.Rep as X
+import Web.Page.Rep.Input as X
+import Web.Page.Html as X
+import Web.Page.Html.Input as X
+import Web.Page.Bootstrap as X
+import Text.InterpolatedString.Perl6 as X
+import Control.Exception (finally)
+
+import GHC.Exts (fromList)
+import qualified Data.HashMap.Strict as HashMap
+import qualified Web.Page.Css as Css (render)
+import qualified Web.Page.Js as Js
+import Data.Aeson (Value(..))
+import Codec.Picture.Types (PixelRGB8(..))
+
+renderJs :: JS -> Text
+renderJs = toStrict . Js.renderToText . Js.unJS
+
+renderCss :: Css -> Text
+renderCss = toStrict . Css.render
+
+renderHtml :: Html a -> Text
+renderHtml = toText
diff --git a/src/Web/Page/Bootstrap.hs b/src/Web/Page/Bootstrap.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Page/Bootstrap.hs
@@ -0,0 +1,128 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -Wall #-}
+
+module Web.Page.Bootstrap
+  ( bootstrapPage
+  , cardify
+  , b_
+  , accordion
+  , accordionChecked
+  , accordionCardChecked
+  , accordion_
+  ) where
+
+import Lucid hiding (b_)
+import Lucid.Base
+import Protolude
+import Web.Page.Html
+import Web.Page.Types
+
+bootstrapCss :: [Html ()]
+bootstrapCss =
+  [link_
+  [ rel_ "stylesheet"
+  , href_ "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
+  , integrity_ "sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
+  , crossorigin_ "anonymous"
+  ]]
+
+bootstrapJs :: [Html ()]
+bootstrapJs =
+  [ with (script_ mempty)
+    [ src_ "https://code.jquery.com/jquery-3.3.1.slim.min.js"
+    , integrity_ "sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
+    , crossorigin_ "anonymous"
+    ]
+  , with (script_ mempty)
+    [ src_ "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
+    , integrity_ "sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
+    , crossorigin_ "anonymous"
+    ]
+  , with (script_ mempty)
+    [ src_ "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
+    , integrity_ "sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
+    , crossorigin_ "anonymous"
+    ]
+  ]
+
+bootstrapMeta :: [Html ()]
+bootstrapMeta =
+  [ meta_ [charset_ "utf-8"]
+  , meta_ [ name_ "viewport"
+          , content_ "width=device-width, initial-scale=1, shrink-to-fit=no"]
+  ]
+
+bootstrapPage :: Page
+bootstrapPage =
+  Page
+  bootstrapCss
+  bootstrapJs
+  mempty
+  mempty
+  mempty
+  (mconcat bootstrapMeta)
+  mempty
+
+-- | wrap some Html with the bootstrap card class
+cardify :: (Html (), [Attribute]) -> Maybe Text -> (Html (), [Attribute]) -> Html ()
+cardify (h, hatts) t (b, batts) =
+  with div_ ([class__ "card"] <> hatts) $
+   h <>
+   with
+   div_ ([class__ "card-body"] <> batts)
+   (maybe mempty (with h5_ [class__ "card-title"] . toHtml) t <>
+    b)
+
+-- | wrap some html with a classed div
+b_ :: Text -> Html () -> Html ()
+b_ t = with div_ [class__ t]
+
+accordionCard :: Bool -> [Attribute] -> Text -> Text -> Text -> Text -> Html () -> Html ()
+accordionCard collapse atts idp idh idb t0 b =
+  with div_ ([class__ "card"] <> atts) $
+    with div_ [class__ "card-header", id_ idh]
+      (with h2_ [class__ "mb-0"]
+        (with button_ [class__ ("btn btn-link" <> bool "" " collapsed" collapse), type_ "button", data_ "toggle" "collapse", data_ "target" ("#" <> idb), makeAttribute "aria-expanded" (bool "true" "false" collapse), makeAttribute "aria-controls" idb ] (toHtml t0))) <>
+    with div_ [id_ idb, class__ ("collapse" <> bool " show" "" collapse), makeAttribute "aria-labelledby" idh, data_ "parent" ("#" <> idp)]
+    (with div_ [class__ "card-body"] b)
+
+accordionCardChecked :: Bool -> Text -> Text -> Text -> Text -> Html () -> Html () -> Html ()
+accordionCardChecked collapse idp idh idb label bodyhtml checkhtml =
+  with div_ ([class__ "card"]) $
+    with div_ ([class__ "card-header", id_ idh])
+      (checkhtml <>
+       with h2_ [class__ "mb-0"]
+        (with button_ [class__ ("btn btn-link" <> bool "" " collapsed" collapse), type_ "button", data_ "toggle" "collapse", data_ "target" ("#" <> idb), makeAttribute "aria-expanded" (bool "true" "false" collapse), makeAttribute "aria-controls" idb ] (toHtml label))) <>
+    with div_ [id_ idb, class__ ("collapse" <> bool " show" "" collapse), makeAttribute "aria-labelledby" idh, data_ "parent" ("#" <> idp)]
+    (with div_ ([class__ "card-body"]) bodyhtml)
+
+-- | create a bootstrapped accordian class
+accordion :: (MonadState Int m, Monad m) => Text -> Maybe Text -> [(Text, Html ())] -> m (Html ())
+accordion pre x hs = do
+  idp' <- genNamePre pre
+  with div_ [class__ "accordion", id_ idp'] <$> aCards idp'
+    where
+      aCards par = mconcat <$> sequence (aCard par <$> hs)
+      aCard par (t,b) = do
+        idh <- genNamePre pre
+        idb <- genNamePre pre
+        pure $ accordionCard (x /= Just t) [] par idh idb t b
+
+-- | create a bootstrapped accordian class
+accordionChecked :: (MonadState Int m, Monad m) => Text -> [(Text, Html (), Html ())] -> m (Html ())
+accordionChecked pre hs = do
+  idp' <- genNamePre pre
+  with div_ [class__ "accordion", id_ idp'] <$> aCards idp'
+    where
+      aCards par = mconcat <$> sequence (aCard par <$> hs)
+      aCard par (l,bodyhtml,checkhtml) = do
+        idh <- genNamePre pre
+        idb <- genNamePre pre
+        pure $ accordionCardChecked True par idh idb l bodyhtml checkhtml
+
+accordion_ :: Text -> Maybe Text -> [(Text, Html ())] -> Html ()
+accordion_ pre x hs = runIdentity $ evalStateT (accordion pre x hs) 0
+
diff --git a/src/Web/Page/Bridge.hs b/src/Web/Page/Bridge.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Page/Bridge.hs
@@ -0,0 +1,157 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# OPTIONS_GHC -Wall #-}
+
+module Web.Page.Bridge
+  ( bridgePage
+  , sendc
+  , append
+  , replace
+  , appendWithScript
+  , replaceWithScript
+  , bridge
+  , sendConcerns
+  , Engine
+  , start
+  , Application
+  , midShared
+  ) where
+
+import Box.Cont
+import Control.Lens
+import Data.Aeson (Value)
+import Data.HashMap.Strict as HashMap
+import Lucid
+import Network.JavaScript (Engine, start, send, command, addListener, JavaScript(..), Application)
+import Protolude hiding (replace, Rep)
+import Text.InterpolatedString.Perl6
+import Web.Page.Html
+import Web.Page.Js
+import Web.Page.Rep
+import Web.Page.Types
+import qualified Data.Text as Text
+
+preventEnter :: PageJs
+preventEnter = PageJs $ fromText [q|
+window.addEventListener('keydown',function(e) {
+  if(e.keyIdentifier=='U+000A' || e.keyIdentifier=='Enter' || e.keyCode==13) {
+    if(e.target.nodeName=='INPUT' && e.target.type !== 'textarea') {
+      e.preventDefault();
+      return false;
+    }
+  }
+}, true);
+|]
+
+webSocket :: PageJs
+webSocket = PageJsText [q|
+window.jsb = {ws: new WebSocket('ws://' + location.host + '/')};
+jsb.ws.onmessage = (evt) => eval(evt.data);
+|]
+
+-- see https://ghinda.net/article/script-tags/
+runScriptJs :: PageJs
+runScriptJs = PageJsText [q|
+
+function insertScript ($script) {
+  var s = document.createElement('script')
+  s.type = 'text/javascript'
+  if ($script.src) {
+    s.onload = callback
+    s.onerror = callback
+    s.src = $script.src
+  } else {
+    s.textContent = $script.innerText
+  }
+
+  // re-insert the script tag so it executes.
+  document.head.appendChild(s)
+
+  // clean-up
+  $script.parentNode.removeChild($script)
+}
+
+function runScripts ($container) {
+  // get scripts tags from a node
+  var $scripts = $container.querySelectorAll('script')
+  $scripts.forEach(function ($script) {
+    insertScript($script)
+  })
+}
+|]
+
+bridgePage :: Page
+bridgePage =
+  mempty &
+  #jsGlobal .~ (preventEnter <> runScriptJs) &
+  #jsOnLoad .~ webSocket
+
+sendc :: Engine -> Text -> IO ()
+sendc e = send e . command . JavaScript . fromStrict
+
+replace :: Engine -> Text -> Text -> IO ()
+replace e d t = send e $ command
+  [qc|
+     var $container = document.getElementById('{d}')
+     $container.innerHTML = '{clean t}'
+     runScripts($container)
+     |]
+
+append :: Engine -> Text -> Text -> IO ()
+append e d t = send e $ command
+    [qc|
+     var $container = document.getElementById('{d}')
+     $container.innerHTML += '{clean t}'
+     runScripts($container)
+     |]
+
+replaceWithScript :: Engine -> Text -> Text -> IO ()
+replaceWithScript e d t = send e $ command
+  [qc|
+     var $container = document.getElementById('{d}')
+     $container.innerHTML = '{clean t}'
+     runScripts($container)
+     |]
+
+appendWithScript :: Engine -> Text -> Text -> IO ()
+appendWithScript e d t = send e $ command
+    [qc|
+     var $container = document.getElementById('{d}')
+     $container.innerHTML += '{clean t}'
+     runScripts($container)
+     |]
+
+sendConcerns :: Engine -> Text -> Concerns Text -> IO ()
+sendConcerns e t (Concerns c j h) = do
+  replaceWithScript e t h
+  append e t (toText $ style_ c)
+  sendc e j
+
+bridge :: Engine -> Cont_ IO Value
+bridge e = Cont_ $ \vio -> void $ addListener e vio
+
+clean :: Text -> Text
+clean =
+  Text.intercalate "\\'" . Text.split (=='\'') .
+  Text.intercalate "\\n" . Text.lines
+
+
+-- | create Wai Middleware for a SharedRep providing an initialiser and action on events
+midShared ::
+  (Show a) =>
+  SharedRep IO a ->
+  (Engine -> Rep a -> StateT (HashMap Text Text) IO ()) ->
+  (Engine -> Either Text (HashMap Text Text, Either Text a) -> IO ()) ->
+  Application -> Application
+midShared sr init action = start $ \e ->
+  void $ runOnEvent
+  sr
+  (zoom _2 . init e)
+  (action e)
+  (bridge e)
+
+
+
+
diff --git a/src/Web/Page/Css.hs b/src/Web/Page/Css.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Page/Css.hs
@@ -0,0 +1,68 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Web.Page.Css
+  ( fill
+  , stroke
+  , strokeWidth
+  , crispEdges
+  , optimizeSpeed
+  , geometricPrecision
+  , shapeRendering
+  , Css
+  , render
+  , renderWith
+  , compact
+  , PageCss(..)
+  ) where
+
+import Clay hiding (optimizeSpeed, geometricPrecision, PlayState)
+import Clay.Stylesheet (key)
+import Data.Text
+import Data.Text.Lazy (unpack)
+import Protolude
+import qualified GHC.Show
+
+instance Show Css where
+  show = Data.Text.Lazy.unpack . render
+
+instance Eq Css where
+  (==) a' b' = (Protolude.show a' :: Text) == Protolude.show b'
+
+data PageCss = PageCss Css | PageCssText Text deriving (Eq, Show, Generic)
+
+instance Semigroup PageCss where
+  (<>) (PageCss css) (PageCss css') = PageCss (css <> css')
+  (<>) (PageCssText css) (PageCssText css') = PageCssText (css <> css')
+  (<>) (PageCss css) (PageCssText css') =
+    PageCssText (show css <> css')
+  (<>) (PageCssText css) (PageCss css') =
+    PageCssText (css <> show css')
+
+instance Monoid PageCss where
+  mempty = PageCssText mempty
+  mappend = (<>)
+
+-- a few SVG css
+fill :: Color -> Css
+fill = key "fill"
+
+stroke :: Color -> Css
+stroke = key "stroke"
+
+strokeWidth :: Size Position -> Css
+strokeWidth = key "stroke-width"
+
+newtype ShapeRendering = ShapeRendering Value
+  deriving (Inherit, Auto, Val)
+
+crispEdges, optimizeSpeed, geometricPrecision :: ShapeRendering
+crispEdges = ShapeRendering "crispEdges"
+optimizeSpeed = ShapeRendering "optimizeSpeed"
+geometricPrecision = ShapeRendering "geometricPrecision"
+
+shapeRendering :: ShapeRendering -> Css
+shapeRendering = key "shape-rendering"
diff --git a/src/Web/Page/Examples.hs b/src/Web/Page/Examples.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Page/Examples.hs
@@ -0,0 +1,246 @@
+{-# LANGUAGE ApplicativeDo #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# OPTIONS_GHC -Wall #-}
+
+module Web.Page.Examples
+  ( page1
+  , page2
+  , cfg2
+  , RepExamples(..)
+  , repExamples
+  , Shape(..)
+  , fromShape
+  , toShape
+  , SumTypeExample(..)
+  , repSumTypeExample
+  , SumType2Example(..)
+  , repSumType2Example
+  , listifyExample
+  , listifyMaybeExample
+  , fiddleExample
+  ) where
+
+import Control.Category (id)
+import Control.Lens hiding ((.=))
+import Data.Attoparsec.Text
+import Data.Biapplicative
+import Lucid
+import Protolude hiding ((<<*>>))
+import Web.Page
+import qualified Clay
+
+-- | simple page examples
+page1 :: Page
+page1 =
+  #htmlBody .~ button1 $
+  #cssBody .~ PageCss css1 $
+  #jsGlobal .~ mempty $
+  #jsOnLoad .~ click $
+  #libsCss .~ (libCss <$> cssLibs) $
+  #libsJs .~ (libJs <$> jsLibs) $
+  mempty
+
+page2 :: Page
+page2 =
+  #libsCss .~ (libCss <$> cssLibsLocal) $
+  #libsJs .~ (libJs <$> jsLibsLocal) $
+  page1
+
+cfg2 :: PageConfig
+cfg2 =
+  #concerns .~ Separated $
+  #pageRender .~ Pretty $
+  #structure .~ Headless $
+  #localdirs .~ ["test/static"] $
+  #filenames .~ (("other/cfg2" <>) <$> suffixes) $
+  defaultPageConfig
+
+cssLibs :: [Text]
+cssLibs =
+  ["http://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"]
+
+cssLibsLocal :: [Text]
+cssLibsLocal = ["css/font-awesome.min.css"]
+
+jsLibs :: [Text]
+jsLibs = ["http://code.jquery.com/jquery-1.6.3.min.js"]
+
+jsLibsLocal :: [Text]
+jsLibsLocal = ["jquery-2.1.3.min.js"]
+
+css1 :: Css
+css1 = do
+  Clay.fontSize (Clay.px 10)
+  Clay.fontFamily ["Arial", "Helvetica"] [Clay.sansSerif]
+  "#btnGo" Clay.? do
+    Clay.marginTop (Clay.px 20)
+    Clay.marginBottom (Clay.px 20)
+  "#btnGo.on" Clay.? Clay.color Clay.green
+
+-- js
+click :: PageJs
+click = PageJsText [q|
+$('#btnGo').click( function() {
+  $('#btnGo').toggleClass('on');
+  alert('bada bing!');
+});
+|]
+
+button1 :: Html ()
+button1 =
+  with
+    button_
+    [id_ "btnGo", Lucid.type_ "button"]
+    ("Go " <> with i_ [class__ "fa fa-play"] mempty)
+
+data RepExamples =
+  RepExamples
+  { repTextbox :: Text
+  , repTextarea :: Text
+  , repSliderI :: Int
+  , repSlider :: Double
+  , repCheckbox :: Bool
+  , repToggle :: Bool
+  , repDropdown :: Int
+  , repShape :: Shape
+  , repColor :: PixelRGB8
+  } deriving (Show, Eq, Generic)
+
+data Shape = SquareShape | CircleShape deriving (Eq, Show, Generic)
+
+toShape :: Text -> Shape
+toShape t = case t of
+  "Circle" -> CircleShape
+  "Square" -> SquareShape
+  _ -> CircleShape
+
+fromShape :: Shape -> Text
+fromShape CircleShape = "Circle"
+fromShape SquareShape = "Square"
+
+repExamples :: (Monad m) => SharedRep m RepExamples
+repExamples = do
+  t <- textbox (Just "textbox") "sometext"
+  ta <- textarea 3 (Just "textarea") "no initial value & multi-line text\\nrenders is not ok?/"
+  n <- sliderI (Just "int slider") 0 5 1 3
+  ds' <- slider (Just "double slider") 0 1 0.1 0.5
+  c <- checkbox (Just "checkbox") True
+  tog <- toggle (Just "toggle") False
+  dr <- dropdown decimal show (Just "dropdown") (show <$> [1..5::Int]) 3
+  drt <- toShape <$> dropdown takeText id (Just "shape") (["Circle", "Square"]) (fromShape SquareShape)
+  col <- colorPicker (Just "color") (PixelRGB8 56 128 200)
+  pure (RepExamples t ta n ds' c tog dr drt col)
+
+-- encodeFile "saves/rep2.json" $ RepExamples "text1" "text2" 1 1.0 True True 2 (PixelRGB8 0 100 0)
+-- decodeFileStrict "saves/rep2.json" :: IO (Maybe RepExamples)
+
+listifyExample :: (Monad m) => Int -> SharedRep m [Int]
+listifyExample n =
+  accordionListify (Just "accordianListify") "al" Nothing
+  (\l a -> sliderI (Just l) (0::Int) n 1 a) ((\x -> "[" <> show x <> "]") <$> [0..n] :: [Text]) [0..n]
+
+listifyMaybeExample :: (Monad m) => Int -> SharedRep m [Int]
+listifyMaybeExample n =
+  listifyMaybe' (Just "listifyMaybe") "alm" (checkbox Nothing)
+    (sliderI Nothing (0::Int) n 1) n 3 [0..4]
+
+fiddleExample :: Concerns Text
+fiddleExample = Concerns mempty mempty
+  [q|
+<div class=" form-group-sm "><label for="1">fiddle example</label><input max="10.0" value="3.0" oninput="jsb.event({ &#39;element&#39;: this.id, &#39;value&#39;: this.value});" step="1.0" min="0.0" id="1" type="range" class=" custom-range  form-control-range "></div>
+|]
+
+data SumTypeExample = SumInt Int | SumOnly | SumText Text deriving (Eq, Show, Generic)
+
+sumTypeText :: SumTypeExample -> Text
+sumTypeText (SumInt _) = "SumInt"
+sumTypeText SumOnly = "SumOnly"
+sumTypeText (SumText _) = "SumText"
+
+repSumTypeExample :: (Monad m) => Int -> Text -> SumTypeExample -> SharedRep m SumTypeExample
+repSumTypeExample defi deft defst =
+  bimap hmap mmap repst <<*>> repi <<*>> rept
+  where
+    repi = sliderI Nothing 0 20 1 defInt
+    rept = textbox Nothing defText
+    repst = dropdownSum takeText id (Just "SumTypeExample")
+      ["SumInt", "SumOnly", "SumText"]
+      (sumTypeText defst)
+    hmap repst' repi' rept' =
+      div_
+      (repst' <>
+      with repi'
+        [ class__ "subtype "
+        , data_ "sumtype" "SumInt"
+        , style_
+          ("display:" <>
+           bool "block" "none" (sumTypeText defst /= "SumInt"))
+        ] <>
+      with rept'
+        [ class__ "subtype "
+        , data_ "sumtype" "SumText"
+        , style_
+          ("display:" <> bool "block" "none" (sumTypeText defst /= "SumText"))
+        ])
+
+    mmap repst' repi' rept' =
+      case repst' of
+        "SumInt" -> SumInt repi'
+        "SumOnly" -> SumOnly
+        "SumText" -> SumText rept'
+        _ -> SumOnly
+
+    defInt = case defst of
+      SumInt i -> i
+      _ -> defi
+    defText = case defst of
+      SumText t -> t
+      _ -> deft
+
+data SumType2Example = SumOutside Int | SumInside SumTypeExample deriving (Eq, Show, Generic)
+
+sumType2Text :: SumType2Example -> Text
+sumType2Text (SumOutside _) = "SumOutside"
+sumType2Text (SumInside _) = "SumInside"
+
+repSumType2Example :: (Monad m) => Int -> Text -> SumTypeExample -> SumType2Example -> SharedRep m SumType2Example
+repSumType2Example defi deft defst defst2 =
+  bimap hmap mmap repst2 <<*>> repst <<*>> repoi
+  where
+    repoi = sliderI Nothing 0 20 1 defInt
+    repst = repSumTypeExample defi deft SumOnly
+    repst2 = dropdownSum takeText id (Just "SumType2Example")
+      ["SumOutside", "SumInside"]
+      (sumType2Text defst2)
+    hmap repst2' repst' repoi' =
+      div_
+      (repst2' <>
+      with repst'
+        [ class__ "subtype "
+        , data_ "sumtype" "SumInside"
+        , style_
+          ("display:" <>
+           bool "block" "none" (sumType2Text defst2 /= "SumInside"))
+        ] <>
+      with repoi'
+        [ class__ "subtype "
+        , data_ "sumtype" "SumOutside"
+        , style_
+          ("display:" <>
+           bool "block" "none" (sumType2Text defst2 /= "SumOutside"))
+        ])
+
+    mmap repst2' repst' repoi' =
+      case repst2' of
+        "SumOutside" -> SumOutside repoi'
+        "SumInside" -> SumInside repst'
+        _ -> SumOutside repoi'
+
+    defInt = case defst of
+      SumInt i -> i
+      _ -> defi
diff --git a/src/Web/Page/Html.hs b/src/Web/Page/Html.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Page/Html.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Web.Page.Html
+  ( class__
+  , toText
+  , genName
+  , genNamePre
+  , libCss
+  , libJs
+  , fromHex
+  , toHex
+  , Html
+ ) where
+
+import Data.Text
+import Lucid
+import Protolude hiding ((%))
+import qualified Data.Text.Lazy as Lazy
+-- import qualified GHC.Show
+import Codec.Picture.Types (PixelRGB8(..))
+import Data.Attoparsec.Text
+import Numeric
+import Formatting hiding (string)
+
+class__ :: Text -> Attribute
+class__ t = class_ (" " <> t <> " ")
+
+toText :: Html a -> Text
+toText = Lazy.toStrict . renderText
+
+-- | name supply for html elements
+genName :: (MonadState Int m, Monad m) => m Text
+genName = do
+  modify (+1)
+  show <$> get
+
+-- | sometimes a number doesn't work properly in html (or js???), and an alpha prefix seems to help
+genNamePre :: (MonadState Int m, Monad m) => Text -> m Text
+genNamePre pre = (pre <>) <$> genName
+
+libCss :: Text -> Html ()
+libCss url = link_
+  [ rel_ "stylesheet"
+  , href_ url
+  ]
+
+libJs :: Text -> Html ()
+libJs url = with (script_ mempty) [src_ url]
+
+fromHex :: Parser PixelRGB8
+fromHex =
+  (\((r,g),b) ->
+       PixelRGB8 (fromIntegral r) (fromIntegral g) (fromIntegral b)) .
+    (\(f,b) -> (f `divMod` (256 :: Int), b)) .
+    (`divMod` 256) <$>
+    (string "#" *> hexadecimal)
+
+toHex :: PixelRGB8 -> Text
+toHex (PixelRGB8 r g b) = sformat ("#" % ((left 2 '0') %. hex) % ((left 2 '0') %. hex) % ((left 2 '0') %. hex)) r g b
+
+-- `ToHtml a` is used throughout because `Show a` gives "\"text\"" for show "text", and hilarity ensues when rendering to the web page.
+-- hence orphans
+instance ToHtml Double where
+  toHtml = toHtml . (show :: Double -> Text)
+  toHtmlRaw = toHtmlRaw . (show :: Double -> Text)
+
+{-
+instance (RealFrac a, Floating a) => ToHtml (Colour a) where
+  toHtml = toHtml . sRGB24show
+  toHtmlRaw = toHtmlRaw . sRGB24show
+
+instance (RealFrac a, Floating a) => Show (Colour a) where
+  show = sRGB24show
+
+-}
+
+instance ToHtml Bool where
+  toHtml = toHtml . bool ("false" :: Text) "true"
+  toHtmlRaw = toHtmlRaw . bool ("false" :: Text) "true"
+
+instance ToHtml Int where
+  toHtml = toHtml . (show :: Int -> Text)
+  toHtmlRaw = toHtmlRaw . (show :: Int -> Text)
+
+instance ToHtml PixelRGB8 where
+  toHtml = toHtml . toHex
+  toHtmlRaw = toHtmlRaw . toHex
+
+instance ToHtml () where
+  toHtml = const "()"
+  toHtmlRaw = const "()"
+
diff --git a/src/Web/Page/Html/Input.hs b/src/Web/Page/Html/Input.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Page/Html/Input.hs
@@ -0,0 +1,232 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# OPTIONS_GHC -Wall #-}
+
+module Web.Page.Html.Input
+  ( Input(Input)
+  , InputType(..)
+  , scriptToggleShow
+  ) where
+
+import Data.Text
+import Lucid
+import Lucid.Base
+import Protolude hiding (for_)
+import Text.InterpolatedString.Perl6
+import Web.Page.Html
+
+-- | something that might exist on a web page and be a front-end input to computations.
+data Input a =
+  Input
+  { inputVal :: a
+  , inputLabel :: Maybe Text
+  , inputId :: Text
+  , inputType :: InputType
+  } deriving (Eq, Show, Generic)
+
+-- | various types of Inputs, that encapsulate practical bootstrap class functionality
+data InputType =
+  Slider [Attribute] |
+  TextBox |
+  TextArea Int |
+  ColorPicker |
+  ChooseFile |
+  Dropdown [Text] |
+  DropdownSum [Text] |
+  Datalist [Text] Text |
+  Checkbox Bool |
+  Toggle Bool (Maybe Text) |
+  Button
+  deriving (Eq, Show, Generic)
+
+instance (ToHtml a) => ToHtml (Input a) where
+  toHtml (Input v l i (Slider satts)) =
+    with div_ [class__ "form-group"]
+    ((maybe mempty (with label_ [for_ i] . toHtml) l) <>
+     input_
+      ([ type_ "range"
+       , class__ " form-control-range custom-range"
+       , id_ i
+       , value_ (show $ toHtml v)
+       ] <> satts) <>
+      scriptJsbEvent i "change")
+  toHtml (Input v l i TextBox) =
+    with div_ [class__ "form-group"]
+    ((maybe mempty (with label_ [for_ i] . toHtml) l) <>
+    input_
+      ([ type_ "text"
+       , class__ "form-control"
+       , id_ i
+       , value_ (show $ toHtml v)
+       ]) <>
+      scriptJsbEvent i "input")
+  toHtml (Input v l i (TextArea rows)) =
+    with div_ [class__ "form-group"]
+    (maybe mempty (with label_ [for_ i] . toHtml) l <>
+     (with textarea_
+     [ rows_ (show rows)
+     , class__ "form-control"
+     , id_ i
+     ] (toHtmlRaw v)
+    ) <>
+     scriptJsbEvent i "input")
+  toHtml (Input v l i ColorPicker) =
+    with div_ [class__ "form-group"]
+    ((maybe mempty (with label_ [for_ i] . toHtml) l) <>
+    input_
+      ([ type_ "color"
+       , class__ "form-control"
+       , id_ i
+       , value_ (show $ toHtml v)
+       ]) <>
+     scriptJsbEvent i "input")
+  toHtml (Input _ l i ChooseFile) =
+    with div_ [class__ "form-group"]
+    (maybe mempty (with label_ [for_ i] . toHtml) l) <>
+     input_
+      ([ type_ "file"
+       , class__ "form-control-file"
+       , id_ i
+       ]) <>
+    scriptJsbChooseFile i
+  toHtml (Input v l i (Dropdown opts)) =
+    with div_ [class__ "form-group"]
+    ((maybe mempty (with label_ [for_ i] . toHtml) l) <>
+    (with select_
+      [ class__ "form-control"
+      , id_ i
+      ] opts') <>
+     scriptJsbEvent i "input")
+    where
+      opts' = mconcat $
+        (\o -> with option_ (bool [] [selected_ "selected"]
+                            (toText (toHtml o) == toText (toHtml v)))
+               (toHtml o)) <$> opts
+  toHtml (Input v l i (DropdownSum opts)) =
+    with div_ [class__ "form-group sumtype-group"]
+    ((maybe mempty (with label_ [for_ i] . toHtml) l) <>
+    (with select_
+      [ class__ "form-control"
+      , id_ i
+      ] opts') <>
+    scriptShowSum i <>
+     scriptJsbEvent i "input")
+    where
+      opts' = mconcat $
+        (\o -> with option_
+          (bool [] [selected_ "selected"] (toText (toHtml o) == toText (toHtml v)))
+               (toHtml o)) <$> opts
+  toHtml (Input v l i (Datalist opts listId)) =
+    with div_ [class__ "form-group"]
+    ((maybe mempty (with label_ [for_ i] . toHtml) l) <>
+    input_
+      [ type_ "text"
+       , class__ "form-control"
+       , id_ i
+       , list_ listId
+       -- the datalist concept in html assumes initial state is a null
+       -- and doesn't present the list if it has a value alreadyx
+       -- , value_ (show $ toHtml v)
+       ] <>
+     with datalist_ [id_ listId] (mconcat $ (\o ->
+       with option_ (bool [] [selected_ "selected"]
+                     (toText (toHtml o) == toText (toHtml v)))
+       (toHtml o)) <$> opts) <>
+     scriptJsbEvent i "input")
+  -- FIXME: How can you refactor to eliminate this polymorphic wart?
+  toHtml (Input _ l i (Checkbox checked)) =
+    with div_ [class__ "form-check"]
+    (input_
+      ([ type_ "checkbox"
+       , class__ "form-check-input"
+       , id_ i
+       ] <>
+       bool [] [checked_] checked) <>
+      (maybe mempty (with label_ [for_ i, class__ "form-check-label"] . toHtml) l) <>
+    scriptJsbCheckbox i)
+  toHtml (Input _ l i (Toggle pushed lab)) =
+    with div_ [class__ "form-group"]
+    (( maybe mempty (with label_ [for_ i] . toHtml) l) <>
+    input_
+     ([ type_ "button"
+      , class__ "btn btn-primary btn-sm"
+      , data_ "toggle" "button"
+      , id_ i
+      , makeAttribute "aria-pressed" (bool "false" "true" pushed)
+      ] <>
+      (maybe [] (\l' -> [value_ l']) lab) <>
+      bool [] [checked_] pushed) <>
+      scriptJsbToggle i)
+  toHtml (Input _ l i Button) =
+    with div_ [class__ "form-group"]
+    (input_
+     ( [ type_ "button"
+       , id_ i
+       , class__ "btn btn-primary btn-sm"
+       , value_ (fromMaybe "button" l)
+       ]) <>
+    scriptJsbButton i)
+
+  toHtmlRaw = toHtml
+
+-- scripts attached to Inputs
+-- https://eager.io/blog/everything-I-know-about-the-script-tag/
+
+scriptJsbEvent :: (Monad m) => Text -> Text -> HtmlT m ()
+scriptJsbEvent name event = script_ [qq|
+$('#{name}').on('{event}', (function()\{
+  jsb.event(\{ 'element': this.id, 'value': this.value\});
+\}));
+|]
+
+scriptJsbButton :: (Monad m) => Text -> HtmlT m ()
+scriptJsbButton name = script_ [qq|
+$('#{name}').on('click', (function()\{
+  jsb.event(\{ 'element': this.id, 'value': this.value\});
+\}));
+|]
+
+scriptJsbToggle :: (Monad m) => Text -> HtmlT m ()
+scriptJsbToggle name = script_ [qq|
+$('#{name}').on('click', (function()\{
+  jsb.event(\{ 'element': this.id, 'value': (\"true\" !== this.getAttribute(\"aria-pressed\")).toString()\});
+\}));
+|]
+
+scriptJsbCheckbox :: (Monad m) => Text -> HtmlT m ()
+scriptJsbCheckbox name = script_ [qq|
+$('#{name}').on('click', (function()\{
+  jsb.event(\{ 'element': this.id, 'value': this.checked.toString()\});
+\}));
+|]
+
+scriptJsbChooseFile :: (Monad m) => Text -> HtmlT m ()
+scriptJsbChooseFile name = script_ [qq|
+$('#{name}').on('input', (function()\{
+  jsb.event(\{ 'element': this.id, 'value': this.files[0].name\});
+\}));
+|]
+
+scriptShowSum :: (Monad m) => Text -> HtmlT m ()
+scriptShowSum name = script_ [qq|
+$('#{name}').on('change', (function()\{
+  var v = this.value;
+  $(this).parent('.sumtype-group').siblings('.subtype').each(function(i) \{
+    if (this.dataset.sumtype === v) \{
+      this.style.display = 'block';
+      \} else \{
+      this.style.display = 'none';
+      \}\})
+  \}));
+|]
+
+scriptToggleShow :: (Monad m) => Text -> Text -> HtmlT m ()
+scriptToggleShow checkName toggleClass = script_ [qq|
+$('#{checkName}').on('change', (function()\{
+  var vis = this.checked ? "block" : "none";
+  Array.from(document.getElementsByClassName({toggleClass})).forEach(x => x.style.display = vis);
+\}));
+|]
diff --git a/src/Web/Page/Js.hs b/src/Web/Page/Js.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Page/Js.hs
@@ -0,0 +1,111 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE QuasiQuotes #-}
+
+module Web.Page.Js
+  ( JS(..)
+  , JSStatement(..)
+  , PageJs(..)
+  , minifyJS
+  , onLoad
+  , onLoadStatements
+  , toStatements
+  , toStatement
+  , renderToStatement
+  , renderToText
+  , readJs
+  , fromText
+  ) where
+
+import Language.JavaScript.Parser
+import Language.JavaScript.Parser.AST
+import Language.JavaScript.Process.Minify
+import Protolude hiding ((<>))
+import qualified Data.Text as Text
+import Data.Semigroup ((<>))
+import Text.InterpolatedString.Perl6
+
+newtype JS = JS {unJS :: JSAST} deriving (Show, Eq, Generic)
+
+instance Semigroup JS where
+  (<>) (JS (JSAstProgram ss ann)) (JS (JSAstProgram ss' _)) =
+    JS $ JSAstProgram (ss <> ss') ann
+  (<>) (JS (JSAstProgram ss ann)) (JS (JSAstStatement s _)) =
+    JS $ JSAstProgram (ss <> [s]) ann
+  (<>) (JS (JSAstProgram ss ann)) (JS (JSAstExpression e ann')) =
+    JS $ JSAstProgram (ss <> [JSExpressionStatement e (JSSemi ann')]) ann
+  (<>) (JS (JSAstProgram ss ann)) (JS (JSAstLiteral e ann')) =
+    JS $ JSAstProgram (ss <> [JSExpressionStatement e (JSSemi ann')]) ann
+  (<>) (JS (JSAstStatement s ann)) (JS (JSAstProgram ss _)) =
+    JS $ JSAstProgram (s : ss) ann
+  (<>) (JS (JSAstStatement s ann)) (JS (JSAstStatement s' _)) =
+    JS $ JSAstProgram [s,s'] ann
+  (<>) (JS (JSAstStatement s ann)) (JS (JSAstExpression e ann')) =
+    JS $ JSAstProgram [s,JSExpressionStatement e (JSSemi ann')] ann
+  (<>) (JS (JSAstStatement s ann)) (JS (JSAstLiteral e ann')) =
+    JS $ JSAstProgram [s,JSExpressionStatement e (JSSemi ann')] ann
+  (<>) (JS (JSAstExpression e ann)) (JS (JSAstProgram ss _)) =
+    JS $ JSAstProgram (JSExpressionStatement e (JSSemi ann) : ss) ann
+  (<>) (JS (JSAstExpression e ann)) (JS (JSAstStatement s' _)) =
+    JS $ JSAstProgram [JSExpressionStatement e (JSSemi ann),s'] ann
+  (<>) (JS (JSAstExpression e ann)) (JS (JSAstExpression e' ann')) =
+    JS $ JSAstProgram [JSExpressionStatement e (JSSemi ann),JSExpressionStatement e' (JSSemi ann')] ann
+  (<>) (JS (JSAstExpression e ann)) (JS (JSAstLiteral e' ann')) =
+    JS $ JSAstProgram [JSExpressionStatement e (JSSemi ann),JSExpressionStatement e' (JSSemi ann')] ann
+  (<>) (JS (JSAstLiteral e ann)) (JS (JSAstProgram ss _)) =
+    JS $ JSAstProgram (JSExpressionStatement e (JSSemi ann) : ss) ann
+  (<>) (JS (JSAstLiteral e ann)) (JS (JSAstStatement s' _)) =
+    JS $ JSAstProgram [JSExpressionStatement e (JSSemi ann),s'] ann
+  (<>) (JS (JSAstLiteral e ann)) (JS (JSAstExpression e' ann')) =
+    JS $ JSAstProgram [JSExpressionStatement e (JSSemi ann),JSExpressionStatement e' (JSSemi ann')] ann
+  (<>) (JS (JSAstLiteral e ann)) (JS (JSAstLiteral e' ann')) =
+    JS $ JSAstProgram [JSExpressionStatement e (JSSemi ann),JSExpressionStatement e' (JSSemi ann')] ann
+
+
+instance Monoid JS where
+  mempty = JS $ JSAstProgram [] (JSAnnot (TokenPn 0 0 0) [])
+  mappend = (<>)
+
+data PageJs = PageJs JS | PageJsText Text deriving (Eq, Show, Generic)
+
+instance Semigroup PageJs where
+  (<>) (PageJs js) (PageJs js') = PageJs (js <> js')
+  (<>) (PageJsText js) (PageJsText js') = PageJsText (js <> js')
+  (<>) (PageJs js) (PageJsText js') =
+    PageJsText (toStrict (renderToText $ unJS js) <> js')
+  (<>) (PageJsText js) (PageJs js') =
+    PageJsText (js <> toStrict (renderToText $ unJS js'))
+
+instance Monoid PageJs where
+  mempty = PageJs mempty
+  mappend = (<>)
+
+toStatements :: JS -> [JSStatement]
+toStatements (JS (JSAstProgram ss _)) = ss
+toStatements (JS (JSAstStatement s _)) = [s]
+toStatements (JS (JSAstExpression e ann')) = [JSExpressionStatement e (JSSemi ann')]
+toStatements (JS (JSAstLiteral e ann')) = [JSExpressionStatement e (JSSemi ann')]
+
+toStatement :: JS -> JSStatement
+toStatement (JS (JSAstProgram ss ann)) = JSStatementBlock JSNoAnnot ss JSNoAnnot (JSSemi ann)
+toStatement (JS (JSAstStatement s _)) = s
+toStatement (JS (JSAstExpression e ann')) = JSExpressionStatement e (JSSemi ann')
+toStatement (JS (JSAstLiteral e ann')) = JSExpressionStatement e (JSSemi ann')
+
+renderToStatement :: Text -> JSStatement
+renderToStatement t = toStatement $ JS $ readJs $ Text.unpack t
+
+-- | standard window loader
+onLoadStatements :: [JSStatement] -> JS
+onLoadStatements js = JS $ JSAstProgram [JSAssignStatement (JSMemberDot (JSIdentifier JSNoAnnot "window") JSNoAnnot (JSIdentifier JSNoAnnot "onload")) (JSAssign JSNoAnnot) (JSFunctionExpression JSNoAnnot JSIdentNone JSNoAnnot JSLNil JSNoAnnot (JSBlock JSNoAnnot js JSNoAnnot)) JSSemiAuto] JSNoAnnot
+
+onLoadText :: Text -> Text
+onLoadText t = [qc| window.onload=function()\{{t}};|]
+
+onLoad :: PageJs -> PageJs
+onLoad (PageJs js) = PageJs $ onLoadStatements [toStatement js]
+onLoad (PageJsText js) = PageJsText $ onLoadText js
+
+fromText :: Text -> JS
+fromText = JS . readJs . Text.unpack
diff --git a/src/Web/Page/Render.hs b/src/Web/Page/Render.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Page/Render.hs
@@ -0,0 +1,225 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLabels #-}
+
+module Web.Page.Render
+  ( renderPage
+  , renderPageWith
+  , renderPageHtmlWith
+  , renderPageAsText
+  , renderPageToFile
+  , renderPageHtmlToFile
+  , renderPageTextWith
+  ) where
+
+import Control.Applicative
+import Control.Lens
+import Control.Monad
+import Data.Monoid
+import Data.Text (Text)
+import Data.Text.IO (writeFile)
+import Data.Text.Lazy (fromStrict, toStrict)
+import Data.Traversable
+import Lucid
+import Prelude hiding (writeFile)
+import Web.Page.Html
+import Web.Page.Types
+import qualified Data.Text as Text
+import qualified Lucid.Svg as Svg
+import qualified Web.Page.Css as Css
+import qualified Web.Page.Js as Js
+
+renderPage :: Page -> Html ()
+renderPage p =
+  (\(_, _, x) -> x) $ renderPageWith defaultPageConfig p
+
+renderPageHtmlWith :: PageConfig -> Page -> Html ()
+renderPageHtmlWith pc p =
+  (\(_, _, x) -> x) $ renderPageWith pc p
+
+renderPageWith :: PageConfig -> Page -> (Text, Text, Html ())
+renderPageWith pc p =
+  case pc ^. #concerns of
+    Inline -> (mempty, mempty, h)
+    Separated -> (css, js, h)
+  where
+    h =
+      case pc ^. #structure of
+        HeaderBody ->
+          doctype_ <>
+          with html_ [lang_ "en"]
+          (head_
+          (mconcat
+            [ meta_ [charset_ "utf-8"]
+            , cssInline
+            , mconcat libsCss'
+            , p ^. #htmlHeader
+            ]) <>
+          body_
+          (mconcat
+            [ p ^. #htmlBody
+            , mconcat libsJs'
+            , jsInline
+            ]))
+        Headless ->
+          mconcat
+            [ doctype_
+            , meta_ [charset_ "utf-8"]
+            , mconcat libsCss'
+            , cssInline
+            , p ^. #htmlHeader
+            , p ^. #htmlBody
+            , mconcat libsJs'
+            , jsInline
+            ]
+        Snippet ->
+          mconcat
+            [ mconcat libsCss'
+            , cssInline
+            , p ^. #htmlHeader
+            , p ^. #htmlBody
+            , mconcat libsJs'
+            , jsInline
+            ]
+        Svg ->
+          Svg.doctype_ <>
+          svg_
+            (Svg.defs_ $
+             mconcat
+               [ mconcat libsCss'
+               , cssInline
+               , p ^. #htmlBody
+               , mconcat libsJs'
+               , jsInline
+               ])
+    css = rendercss (p ^. #cssBody)
+    js = renderjs (p ^. #jsGlobal <> Js.onLoad (p ^. #jsOnLoad))
+    (renderjs, rendercss) = renderers $ pc ^. #pageRender
+    cssInline
+      | pc ^. #concerns == Separated || css == mempty = mempty
+      | otherwise = style_ [type_ "text/css"] css
+    jsInline
+      | pc ^. #concerns == Separated || js == mempty = mempty
+      | otherwise = script_ mempty js
+    libsCss' =
+      case pc ^. #concerns of
+        Inline -> p ^. #libsCss
+        Separated ->
+          p ^. #libsCss <>
+          [libCss (Text.pack $ pc ^. #filenames . #css)]
+    libsJs' =
+      case pc ^. #concerns of
+        Inline -> p ^. #libsJs
+        Separated ->
+          p ^. #libsJs <>
+          [libJs (Text.pack $ pc ^. #filenames . #js)]
+
+renderPageToFile :: FilePath -> PageConfig -> Page -> IO ()
+renderPageToFile dir pc page =
+  void $ sequenceA $ liftA2 writeFile' (pc ^. #filenames) (renderPageAsText pc page)
+  where
+    writeFile' fp s = unless (s == mempty) (writeFile (dir <> "/" <> fp) s)
+
+renderPageHtmlToFile :: FilePath -> PageConfig -> Page -> IO ()
+renderPageHtmlToFile file pc page =
+  writeFile file (toText $ renderPageHtmlWith pc page)
+
+renderPageAsText :: PageConfig -> Page -> Concerns Text
+renderPageAsText pc p =
+  case pc ^. #concerns of
+    Inline -> Concerns mempty mempty htmlt
+    Separated -> Concerns css js htmlt
+  where
+    htmlt = toText h
+    (css, js, h) = renderPageWith pc p
+
+rendererJs :: PageRender -> Js.PageJs -> Text
+rendererJs _ (Js.PageJsText js) = js
+rendererJs Minified (Js.PageJs js) = toStrict . Js.renderToText . Js.minifyJS . Js.unJS $ js
+rendererJs Pretty (Js.PageJs js) = toStrict . Js.renderToText . Js.unJS $ js
+
+rendererCss :: PageRender -> Css.PageCss -> Text
+rendererCss Minified (Css.PageCss css) = toStrict $ Css.renderWith Css.compact [] css
+rendererCss Pretty (Css.PageCss css) = toStrict $ Css.render css
+rendererCss _ (Css.PageCssText css) = css
+
+renderers :: PageRender -> (Js.PageJs -> Text, Css.PageCss -> Text)
+renderers p = (rendererJs p, rendererCss p)
+
+renderPageTextWith :: PageConfig -> PageText -> (Text, Text, Html ())
+renderPageTextWith pc p =
+  case pc ^. #concerns of
+    Inline -> (mempty, mempty, h)
+    Separated -> (css, js, h)
+  where
+    h =
+      case pc ^. #structure of
+        HeaderBody ->
+          doctype_ <>
+          with html_ [lang_ "en"]
+          (head_
+          (mconcat
+            [ meta_ [charset_ "utf-8"]
+            , toHtmlRaw cssInline
+            , mconcat (toHtmlRaw <$> libsCss')
+            , toHtmlRaw $ p ^. #htmlHeaderText
+            ]) <>
+          body_
+          (mconcat
+            [ toHtmlRaw $ p ^. #htmlBodyText
+            , mconcat (toHtmlRaw <$> libsJs')
+            , toHtmlRaw jsInline
+            ]))
+        Headless ->
+          mconcat
+            [ doctype_
+            , meta_ [charset_ "utf-8"]
+            , mconcat (toHtmlRaw <$> libsCss')
+            , toHtmlRaw cssInline
+            , toHtmlRaw $ p ^. #htmlHeaderText
+            , toHtmlRaw $ p ^. #htmlBodyText
+            , mconcat (toHtmlRaw <$> libsJs')
+            , toHtmlRaw jsInline
+            ]
+        Snippet ->
+          mconcat
+            [ mconcat (toHtmlRaw <$> libsCss')
+            , toHtmlRaw cssInline
+            , toHtmlRaw $ p ^. #htmlHeaderText
+            , toHtmlRaw $ p ^. #htmlBodyText
+            , mconcat (toHtmlRaw <$> libsJs')
+            , toHtmlRaw jsInline
+            ]
+        Svg ->
+          Svg.doctype_ <>
+          svg_
+            (Svg.defs_ $
+             mconcat
+               [ mconcat (toHtmlRaw <$> libsCss')
+               , toHtmlRaw cssInline
+               , toHtmlRaw $ p ^. #htmlBodyText
+               , mconcat (toHtmlRaw <$> libsJs')
+               , toHtmlRaw jsInline
+               ])
+    css = p ^. #cssBodyText
+    js = rendererJs Pretty (Js.PageJsText $ p ^. #jsGlobalText) <>
+         rendererJs Pretty (Js.onLoad (Js.PageJsText $ p ^. #jsOnLoadText))
+    cssInline
+      | pc ^. #concerns == Separated || css == mempty = mempty
+      | otherwise = renderText $ style_ [type_ "text/css"] css
+    jsInline
+      | pc ^. #concerns == Separated || js == mempty = mempty
+      | otherwise = renderText $ script_ mempty js
+    libsCss' =
+      case pc ^. #concerns of
+        Inline -> fromStrict <$> p ^. #libsCssText
+        Separated ->
+          (fromStrict <$> p ^. #libsCssText) <>
+          [renderText $ libCss (Text.pack $ pc ^. #filenames . #css)]
+    libsJs' =
+      case pc ^. #concerns of
+        Inline -> fromStrict <$> p ^. #libsJsText
+        Separated ->
+          (fromStrict <$> p ^. #libsJsText) <>
+          [renderText $ libJs (Text.pack $ pc ^. #filenames . #js)]
+
diff --git a/src/Web/Page/Rep.hs b/src/Web/Page/Rep.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Page/Rep.hs
@@ -0,0 +1,249 @@
+{-# LANGUAGE ApplicativeDo #-}
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TupleSections #-}
+{-# OPTIONS_GHC -Wall #-}
+
+module Web.Page.Rep
+  ( Element(..)
+  , RepF(..)
+  , Rep
+  , oneRep
+  , SharedRepF(..)
+  , SharedRep
+  , runOnce
+  , zeroState
+  , listify
+  , accordionListify
+  , accordionListifyMaybe
+  , listifyDefault
+  , listifyMaybe
+  , listifyMaybe'
+  , defaultListifyLabels
+  , valueModel
+  , valueConsume
+  , sharedModel
+  , sharedConsume
+  , runList
+  , runOnEvent
+  ) where
+
+import Box
+import Box.Cont ()
+import Control.Lens
+import Control.Monad.Morph
+import Data.Aeson
+import Data.Biapplicative
+import Data.Bifunctor (Bifunctor(..))
+import Data.HashMap.Strict hiding (foldr)
+import Data.Text (pack, Text)
+import Lucid
+import Protolude hiding ((<<*>>), Rep, empty)
+import Web.Page.Bootstrap
+import qualified Control.Foldl as L
+import qualified Streaming.Prelude as S
+
+-- | Abstracted message event elements
+data Element = Element
+  { element :: Text
+  , value :: Text
+  } deriving (Eq, Show, Generic)
+
+instance ToJSON Element
+
+instance FromJSON Element
+  where
+    parseJSON = withObject "Element" $ \v ->
+      Element <$>
+      v .: "element" <*>
+      v .: "value"
+
+fromJson' :: (FromJSON a) => Value -> Either Text a
+fromJson' v = case fromJSON v of
+  (Success a) -> Right a
+  (Error e) -> Left $ "Json conversion error: " <> pack e <> " of " <> show v
+
+data RepF r a = Rep
+  { rep :: r
+  , make :: HashMap Text Text -> (HashMap Text Text, Either Text a)
+  } deriving (Functor)
+
+type Rep a = RepF (Html ()) a
+
+instance (Semigroup r) => Semigroup (RepF r a) where
+  (Rep r0 a0) <> (Rep r1 a1) =
+    Rep
+    (r0 <> r1)
+    (\hm -> let (hm', x') = a0 hm in let (hm'', x'') = a1 hm' in (hm'', x' <> x''))
+
+instance (Monoid a, Monoid r) => Monoid (RepF r a) where
+  mempty = Rep mempty (,Right mempty)
+  mappend = (<>)
+
+instance Bifunctor RepF where
+  bimap f g (Rep r a) = Rep (f r) (second (fmap g) . a)
+
+instance Biapplicative RepF where
+  bipure r a = Rep r (, Right a)
+  (Rep fr fa) <<*>> (Rep r a) = Rep (fr r)
+    (\hm ->
+        let (hm', a') = a hm in let (hm'', fa') = fa hm' in (hm'', fa' <*> a'))
+
+instance (Monoid r) => Applicative (RepF r) where
+  pure = bipure mempty
+  Rep fh fm <*> Rep ah am =
+    Rep (fh <> ah) (\hm ->
+        let (hm', a') = am hm in let (hm'', fa') = fm hm' in (hm'', fa' <*> a'))
+
+oneRep :: (Monad m, MonadIO m) => Rep a -> (Rep a -> HashMap Text Text -> m ()) -> StateT (HashMap Text Text) m (HashMap Text Text, Either Text a)
+oneRep r@(Rep _ fa) action = do
+  m <- get
+  let (m',a) = fa m
+  put m'
+  lift $ action r m'
+  pure (m',a)
+
+newtype SharedRepF m r a = SharedRep
+  { unrep :: StateT (Int, HashMap Text Text) m (RepF r a)
+  } deriving Functor
+
+type SharedRep m a = SharedRepF m (Html ()) a 
+
+instance (Functor m) => Bifunctor (SharedRepF m) where
+  bimap f g (SharedRep s) = SharedRep $ fmap (bimap f g) s
+
+instance (Monad m) => Biapplicative (SharedRepF m) where
+  bipure r a = SharedRep $ pure $ bipure r a
+  (SharedRep f) <<*>> (SharedRep a) = SharedRep $ liftA2 (<<*>>) f a
+
+instance (Monad m, Monoid r) => Applicative (SharedRepF m r) where
+  pure = bipure mempty
+  SharedRep f <*> SharedRep a = SharedRep $ liftA2 (<*>) f a
+
+runOnce
+  :: (Monad m)
+  => SharedRep m a
+  -> (Html () -> HashMap Text Text -> m ())
+  -> m (HashMap Text Text, Either Text a)
+runOnce sr action = do
+  (Rep h fa, (_, m)) <- flip runStateT (0, empty) $ unrep sr
+  action h m
+  pure (fa m)
+
+zeroState
+  :: (Monad m)
+  => SharedRep m a
+  -> m (Html (), (HashMap Text Text, Either Text a))
+zeroState sr = do
+  (Rep h fa, (_, m)) <- flip runStateT (0, empty) $ unrep sr
+  pure (h, fa m)
+
+listify :: (Monad m) => (Text -> a -> SharedRep m a) -> [Text] -> [a] -> SharedRep m [a]
+listify sr labels as = foldr (\a x -> (:) <$> a <*> x) (pure []) (zipWith sr labels as)
+
+accordionListify :: (Monad m) => Maybe Text -> Text -> Maybe Text -> (Text -> a -> SharedRep m a) -> [Text] -> [a] -> SharedRep m [a]
+accordionListify title prefix open srf labels as = SharedRep $ do
+  (Rep h fa) <-
+    unrep $
+    first (accordion prefix open . zip labels ) $
+    foldr (\a x -> bimap (:) (:) a <<*>> x)
+    (pure []) (zipWith srf labels as)
+  h' <- zoom _1 h
+  pure (Rep (maybe mempty (h5_ . toHtml) title <> h') fa)
+
+accordionListifyMaybe :: (Monad m) => Maybe Text -> Text -> (a -> SharedRep m a) -> (Bool -> SharedRep m Bool) -> [Text] -> [(Bool, a)] -> SharedRep m [(Bool,a)]
+accordionListifyMaybe title prefix bodyf checkf labels xs = SharedRep $ do
+  (Rep h fa) <-
+    unrep $
+    first (accordionChecked prefix) $
+    first (zipWith (\l (ch,a) -> (l,a,ch)) labels) $
+    foldr (\a x -> bimap (:) (:) a <<*>> x)
+    (pure [])
+    ((\(ch, a) ->
+         (bimap (,) (,)
+          (checkf ch) <<*>> bodyf a)) <$> xs)
+  h' <- zoom _1 h
+  pure (Rep (maybe mempty (h5_ . toHtml) title <> h') fa)
+
+listifyDefault :: (Monad m) => Maybe Text -> Text -> (Text -> a -> SharedRep m a) -> [a] -> SharedRep m [a]
+listifyDefault t p srf as = accordionListify t p Nothing srf (defaultListifyLabels (length as)) as
+
+listifyMaybe :: (Monad m) => Maybe Text -> Text -> (Text -> Maybe a -> SharedRep m (Maybe a)) -> Int -> [a] -> SharedRep m [Maybe a]
+listifyMaybe t p srf n as = accordionListify t p Nothing srf (defaultListifyLabels n) (take n ((Just <$> as) <> repeat Nothing)) 
+
+listifyMaybe' :: (Monad m) => Maybe Text -> Text -> (Bool -> SharedRep m Bool) -> (a -> SharedRep m a) -> Int -> a -> [a] -> SharedRep m [a]
+listifyMaybe' t p brf srf n defa as = second (mconcat . fmap (\(b,a) -> bool [] [a] b)) $ accordionListifyMaybe t p srf brf (defaultListifyLabels n) (take n (((True,) <$> as) <> repeat (False, defa)))
+
+defaultListifyLabels :: Int -> [Text]
+defaultListifyLabels n = (\x -> "[" <> show x <> "]") <$> [0..n] :: [Text]
+
+valueModel :: (FromJSON a, MonadState s m) => (a -> s -> s) -> S.Stream (S.Of Value) m () -> S.Stream (S.Of (Either Text s)) m ()
+valueModel step s =
+  s &
+  S.map fromJson' &
+  S.partitionEithers &
+  hoist (S.chain (modify . step)) &
+  hoist (S.mapM (const get)) &
+  S.unseparate &
+  S.maps S.sumToEither
+
+-- | consume an Element using a Committer and a Value continuation
+valueConsume :: s -> (Element -> s -> s) -> Cont IO (Committer IO (Either Text s)) -> Cont_ IO Value -> IO s
+valueConsume init step comm vio = do
+  (c,e) <- atomically $ ends Unbounded
+  with_ vio (atomically . c)
+  etcM init (Transducer (valueModel step))
+    (Box <$> comm <*> (liftE <$> pure (Emitter (Just <$> e))))
+
+stepM :: MonadState s m => (s -> (s, b)) -> (a -> s -> s) -> a -> m (s, b)
+stepM sr step v = do
+  hm <- get
+  let (hm',b) = sr $ step v hm
+  put hm'
+  pure (hm', b)
+
+sharedModel :: (FromJSON a, MonadState s m) => (s -> (s, Either Text b)) -> (a -> s -> s) -> S.Stream (S.Of Value) m () -> S.Stream (S.Of (Either Text (s, Either Text b))) m ()
+sharedModel sr step s =
+  s &
+  S.map fromJson' &
+  S.partitionEithers &
+  hoist (S.mapM (stepM sr step)) &
+  S.unseparate &
+  S.maps S.sumToEither
+
+runList
+  :: (Monad m)
+  => SharedRep m a
+  -> [Value]
+  -> m [Either Text (HashMap Text Text, Either Text a)]
+runList sr vs = S.fst' <$> do
+  (faStep, (_,hm)) <- flip runStateT (0, empty) $ do
+    (Rep _ fa) <- unrep sr
+    pure fa
+  flip evalStateT hm $
+    L.purely S.fold L.list
+    (sharedModel faStep (\(Element k v) s -> insert k v s) (S.each vs))
+
+sharedConsume :: (s -> (s, Either Text b)) -> s -> (Element -> s -> s) -> Cont IO (Committer IO (Either Text (s, Either Text b))) -> Cont_ IO Value -> IO s
+sharedConsume sh init step comm vio = do
+  (c,e) <- atomically $ ends Unbounded
+  with_ vio (atomically . c)
+  etcM init (Transducer (sharedModel sh step))
+    (Box <$> comm <*> (liftE <$> pure (Emitter (Just <$> e))))
+
+runOnEvent
+  :: SharedRep IO a
+  -> (Rep a -> StateT (Int, HashMap Text Text) IO ())
+  -> (Either Text (HashMap Text Text, Either Text a) -> IO ())
+  -> Cont_ IO Value
+  -> IO (HashMap Text Text)
+runOnEvent sr hio eaction cv = flip evalStateT (0, empty) $ do
+  (Rep h fa) <- unrep sr
+  hio (Rep h fa)
+  m <- zoom _2 get
+  liftIO $ sharedConsume fa m (\(Element k v) s -> insert k v s)
+    (pure (Committer (\v -> eaction v >> pure True))) cv
diff --git a/src/Web/Page/Rep/Input.hs b/src/Web/Page/Rep/Input.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Page/Rep/Input.hs
@@ -0,0 +1,183 @@
+{-# LANGUAGE ApplicativeDo #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -Wall #-}
+
+module Web.Page.Rep.Input
+  ( repInput
+  , repMessage
+  , sliderI
+  , slider
+  , dropdown
+  , datalist
+  , dropdownSum
+  , colorPicker
+  , textbox
+  , textarea
+  , checkbox
+  , toggle
+  , button
+  , chooseFile
+  , maybeRep
+  , fiddle
+  , viaFiddle
+  ) where
+
+import Codec.Picture.Types (PixelRGB8(..))
+import Control.Category (id)
+import Control.Lens
+import Data.Attoparsec.Text
+import Data.Biapplicative
+import Data.Bifunctor (Bifunctor(..))
+import Data.HashMap.Strict
+import Data.Text (pack, Text)
+import Lucid
+import Protolude hiding ((<<*>>), Rep)
+import Web.Page.Bootstrap
+import Web.Page.Html
+import Web.Page.Html.Input
+import Web.Page.Types
+import Web.Page.Rep
+import Box.Cont ()
+
+-- | create a sharedRep from an Input
+repInput :: (Monad m, ToHtml a, Show a) => Parser a -> (a -> Text) -> Input a -> a -> SharedRep m a
+repInput p pr i a =
+  SharedRep $ do
+    name <- zoom _1 genName
+    zoom _2 (modify (insert name (pr a)))
+    pure $
+      Rep
+      (toHtml $ #inputVal .~ a $ #inputId .~ name $ i)
+      (\s ->
+        (s, join $
+        maybe (Left "lookup failed") Right $
+        either (Left . (\x -> name <> ": " <> x) . pack) Right . parseOnly p <$> lookup name s))
+
+-- | does not put a value into the HashMap on instantiation, consumes the value when found in the HashMap, and substitutes a default on lookup failure
+repMessage :: (Monad m, ToHtml a, Show a) => Parser a -> (a -> Text) -> Input a -> a -> a -> SharedRep m a
+repMessage p _ i def a =
+  SharedRep $ do
+    name <- zoom _1 genName
+    pure $
+      Rep
+      (toHtml $ #inputVal .~ a $ #inputId .~ name $ i)
+      (\s ->
+        (delete name s, join $
+        maybe (Right $ Right def) Right $
+        either (Left . pack) Right . parseOnly p <$> lookup name s))
+
+slider :: (Monad m) => Maybe Text -> Double -> Double -> Double -> Double ->
+  SharedRep m Double
+slider label l u s v = repInput double show
+  (Input v label mempty (Slider [min_ (pack $ show l), max_ (pack $ show u), step_ (pack $ show s)])) v
+
+sliderI :: (Monad m, ToHtml a, Integral a, Show a) => Maybe Text -> a -> a -> a -> a ->
+  SharedRep m a
+sliderI label l u s v = repInput decimal show 
+  (Input v label mempty (Slider [min_ (pack $ show l), max_ (pack $ show u), step_ (pack $ show s)])) v
+
+textbox :: (Monad m) => Maybe Text -> Text -> SharedRep m Text
+textbox label v = repInput takeText id
+  (Input v label mempty TextBox) v
+
+textarea :: (Monad m) => Int -> Maybe Text -> Text -> SharedRep m Text
+textarea rows label v = repInput takeText id
+  (Input v label mempty (TextArea rows)) v
+
+colorPicker :: (Monad m) => Maybe Text -> PixelRGB8 -> SharedRep m PixelRGB8
+colorPicker label v = repInput fromHex toHex
+  (Input v label mempty ColorPicker) v
+
+dropdown :: (Monad m, ToHtml a, Show a) =>
+  Parser a -> (a -> Text) -> Maybe Text -> [Text] -> a -> SharedRep m a
+dropdown p pr label opts v = repInput p pr 
+  (Input v label mempty (Dropdown opts)) v
+
+datalist :: (Monad m) => Maybe Text -> [Text] -> Text -> Text -> SharedRep m Text
+datalist label opts v id'' = repInput takeText show
+  (Input v label mempty (Datalist opts id'')) v
+
+dropdownSum :: (Monad m, ToHtml a, Show a) =>
+  Parser a -> (a -> Text) -> Maybe Text -> [Text] -> a -> SharedRep m a
+dropdownSum p pr label opts v = repInput p pr
+  (Input v label mempty (DropdownSum opts)) v
+
+checkbox :: (Monad m) => Maybe Text -> Bool -> SharedRep m Bool
+checkbox label v = repInput ((=="true") <$> takeText) (bool "false" "true")
+  (Input v label mempty (Checkbox v)) v
+
+toggle :: (Monad m) => Maybe Text -> Bool -> SharedRep m Bool
+toggle label v = repInput ((=="true") <$> takeText) (bool "false" "true")
+  (Input v label mempty (Toggle v label)) v
+
+button :: (Monad m) => Maybe Text -> SharedRep m Bool
+button label = repMessage (pure True) (bool "false" "true")
+  (Input False label mempty Button) False False
+
+chooseFile :: (Monad m) => Maybe Text -> Text -> SharedRep m Text
+chooseFile label v = repInput takeText show
+  (Input v label mempty ChooseFile) v
+
+checkboxShowJs :: (Monad m) => Maybe Text -> Text -> Bool -> SharedRep m Bool
+checkboxShowJs label cl v =
+  SharedRep $ do
+    name <- zoom _1 genName
+    zoom _2 (modify (insert name (bool "false" "true" v)))
+    pure $
+      Rep
+      (toHtml (Input v label name (Checkbox v)) <> scriptToggleShow name cl)
+      (\s ->
+        (s, join $
+        maybe (Left "lookup failed") Right $
+        either (Left . pack) Right . parseOnly ((=="true") <$> takeText) <$>
+        lookup name s))
+
+-- | represent a Maybe type using a checkbox hiding the underlying content on Nothing
+maybeRep :: (Monad m) => Maybe Text -> Bool -> SharedRep m a ->
+  SharedRep m (Maybe a)
+maybeRep label st sa = SharedRep $ do
+  className <- zoom _1 genName
+  unrep $ bimap (hmap className) mmap (checkboxShowJs label className st) <<*>> sa
+  where
+    hmap cl a b =
+      cardify (a, []) Nothing
+       ((Lucid.with div_
+        [class__ cl, style_
+                   ("display:" <> bool "none" "block" st)]
+        b),
+        [style_ "padding-top: 0.25rem; padding-bottom: 0.25rem;"])
+    mmap a b = bool Nothing (Just b) a
+
+-- | representation of web concerns (css, js & html)
+fiddle :: (Monad m) => Concerns Text -> SharedRep m (Concerns Text, Bool)
+fiddle (Concerns c j h) =
+  bimap
+  (\c' j' h' up -> (Lucid.with div_ [class__ "fiddle "] $ mconcat [up,h',j',c']))
+  (\c' j' h' up -> (Concerns c' j' h', up))
+  (textarea 10 (Just "css") c) <<*>>
+  textarea 10 (Just "js") j <<*>>
+  textarea 10 (Just "html") h <<*>>
+  button (Just "update")
+
+-- | turns a SharedRep into a fiddle
+viaFiddle
+  :: (Monad m)
+  => SharedRep m a
+  -> SharedRep m (Bool, Concerns Text, a)
+viaFiddle sr = SharedRep $ do
+  sr'@(Rep h _) <- unrep sr
+  hrep <- unrep $ textarea 10 (Just "html") (toText h)
+  crep <- unrep $ textarea 10 (Just "css") mempty
+  jrep <- unrep $ textarea 10 (Just "js") mempty
+  u <- unrep $ button (Just "update")
+  pure $
+    bimap
+    (\up a b c _ -> (Lucid.with div_ [class__ "fiddle "] $ mconcat [up, a, b, c]))
+    (\up a b c d -> (up, Concerns a b c, d))
+    u <<*>>
+    crep <<*>>
+    jrep <<*>>
+    hrep <<*>>
+    sr'
diff --git a/src/Web/Page/Server.hs b/src/Web/Page/Server.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Page/Server.hs
@@ -0,0 +1,37 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# OPTIONS_GHC -Wall #-}
+
+module Web.Page.Server
+  ( servePageWith
+  ) where
+
+import Control.Lens hiding (only)
+import Lucid (renderText)
+import Network.Wai.Middleware.Static (addBase, noDots, staticPolicy, only)
+import Protolude hiding (get)
+import Web.Page.Render
+import Web.Page.Types
+import Web.Scotty
+import qualified Control.Monad.State as State
+
+servePageWith :: RoutePattern -> PageConfig -> Page -> ScottyM ()
+servePageWith rp pc p =
+  sequence_ $ servedir <> [getpage]
+    where
+  getpage = case pc ^. #concerns of
+    Inline ->
+      get rp (html $ renderText $ renderPageHtmlWith pc p)
+    Separated -> let (css,js,h) = renderPageWith pc p in
+      do
+        middleware $ staticPolicy $ only [(cssfp,cssfp), (jsfp, jsfp)]
+        get rp (do
+                   State.lift $ writeFile' cssfp css
+                   State.lift $ writeFile' jsfp js
+                   html $ renderText h)
+  cssfp = pc ^. #filenames . #css
+  jsfp = pc ^. #filenames . #js
+  writeFile' fp s = unless (s == mempty) (writeFile fp s)
+  servedir = (\x -> middleware $ staticPolicy (noDots <> addBase x)) <$> pc ^. #localdirs
+
diff --git a/src/Web/Page/Types.hs b/src/Web/Page/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Page/Types.hs
@@ -0,0 +1,117 @@
+{-# LANGUAGE DeriveFoldable #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DeriveTraversable #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Web.Page.Types
+  ( Page(Page)
+  , PageText(PageText)
+  , PageConfig(PageConfig)
+  , defaultPageConfig
+  , Concern(..)
+  , Concerns(Concerns)
+  , suffixes
+  , concernNames
+  , PageConcerns(..)
+  , PageStructure(..)
+  , PageRender(..)
+  ) where
+
+import Control.Lens
+import Data.Generics.Labels()
+import Data.Semigroup ((<>))
+import Lucid
+import Protolude hiding ((<>))
+import qualified Web.Page.Css as Css
+import qualified Web.Page.Js as Js
+
+data Page =
+    Page
+    { libsCss :: [Html ()]
+    , libsJs :: [Html ()]
+    , cssBody :: Css.PageCss
+    , jsGlobal :: Js.PageJs
+    , jsOnLoad :: Js.PageJs
+    , htmlHeader :: Html ()
+    , htmlBody :: Html ()
+    } deriving (Show, Generic)
+
+instance Semigroup Page where
+  (<>) p0 p1 =
+    Page
+    (p0 ^. #libsCss <> p1 ^. #libsCss)
+    (p0 ^. #libsJs <> p1 ^. #libsJs)
+    (p0 ^. #cssBody <> p1 ^. #cssBody)
+    (p0 ^. #jsGlobal <> p1 ^. #jsGlobal)
+    (p0 ^. #jsOnLoad <> p1 ^. #jsOnLoad)
+    (p0 ^. #htmlHeader <> p1 ^. #htmlHeader)
+    (p0 ^. #htmlBody <> p1 ^. #htmlBody)
+
+instance Monoid Page where
+  mempty = Page [] [] mempty mempty mempty mempty mempty
+  mappend = (<>)
+
+data PageText =
+    PageText
+    { libsCssText :: [Text]
+    , libsJsText :: [Text]
+    , cssBodyText :: Text
+    , jsGlobalText :: Text
+    , jsOnLoadText :: Text
+    , htmlHeaderText :: Text
+    , htmlBodyText :: Text
+    } deriving (Show, Generic)
+
+data Concern = Css | Js | Html deriving (Show, Eq, Generic)
+
+data Concerns a =
+  Concerns
+  { css :: a
+  , js :: a
+  , html :: a
+  } deriving (Eq, Show, Foldable, Traversable, Generic)
+
+instance Functor Concerns where
+  fmap f (Concerns c j h) = Concerns (f c) (f j) (f h)
+
+instance Applicative Concerns where
+  pure a = Concerns a a a
+  Concerns f g h <*> Concerns a b c = Concerns (f a) (g b) (h c)
+
+suffixes :: Concerns FilePath
+suffixes = Concerns ".css" ".js" ".html"
+
+concernNames :: FilePath -> FilePath -> Concerns FilePath
+concernNames dir stem =
+  (\x->dir<>stem<>x) <$> suffixes
+
+data PageConcerns =
+  Inline |
+  Separated
+  deriving (Show, Eq, Generic)
+
+data PageStructure =
+  HeaderBody |
+  Headless |
+  Snippet |
+  Svg
+  deriving (Show, Eq, Generic)
+
+data PageRender =
+  Pretty |
+  Minified
+  deriving (Show, Eq, Generic)
+
+data PageConfig =
+  PageConfig
+  { concerns :: PageConcerns
+  , structure :: PageStructure
+  , pageRender :: PageRender
+  , filenames :: Concerns FilePath
+  , localdirs :: [FilePath]
+  } deriving (Show, Eq, Generic)
+
+defaultPageConfig :: PageConfig
+defaultPageConfig = PageConfig Inline HeaderBody Minified
+    (("default"<>) <$> suffixes) []
diff --git a/stack.yaml b/stack.yaml
new file mode 100644
--- /dev/null
+++ b/stack.yaml
@@ -0,0 +1,11 @@
+resolver: lts-14.11
+
+packages:
+- '.'
+
+extra-deps:
+  - lucid-svg-0.7.1
+  - box-0.0.1.4
+  - javascript-bridge-0.2.0
+
+allow-newer: true
diff --git a/test/test.hs b/test/test.hs
new file mode 100644
--- /dev/null
+++ b/test/test.hs
@@ -0,0 +1,155 @@
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import Control.Lens
+import Lucid
+import Protolude
+import Test.Tasty
+import Test.Tasty.Hspec
+import Web.Page
+import Web.Page.Examples
+import qualified Data.Text.IO as Text
+
+generatePage :: FilePath -> FilePath -> PageConfig -> Page -> IO ()
+generatePage dir stem pc =
+  renderPageToFile dir (#filenames .~ concernNames "" stem $ pc) 
+
+generatePages ::
+     Traversable t => FilePath -> t (FilePath, PageConfig, Page) -> IO ()
+generatePages dir xs =
+  void $ sequenceA $ (\(fp, pc, p) -> generatePage dir fp pc p) <$> xs
+
+genTest :: FilePath -> IO ()
+genTest dir =
+  void $ generatePages dir [("default", defaultPageConfig, page1), ("sep", cfg2, page2)]
+
+testVsFile :: FilePath -> FilePath -> PageConfig -> Page -> IO Bool
+testVsFile dir stem pc p = do
+  (t,t') <- textVsFile dir stem pc p
+  pure (t==t')
+
+textVsFile
+  :: FilePath
+  -> FilePath
+  -> PageConfig
+  -> Page
+  -> IO (Concerns Text, Concerns Text)
+textVsFile dir stem pc p = do
+  let names = concernNames "" stem
+  let pc' = #filenames .~ names $ pc
+  let t = renderPageAsText pc' p
+  case pc ^. #concerns of
+    Inline -> do
+      t' <- Text.readFile (dir <> names ^. #html)
+      return (t, Concerns mempty mempty t')
+    Separated -> do
+      t' <- sequenceA $ Text.readFile <$> (dir <>) <$> names
+      return (t, t')
+
+testsRender :: IO (SpecWith ())
+testsRender =
+  return $
+    describe "Web.Page.Render" $ do
+      it "run genTest 'test/canned/' to refresh canned files." True
+      it "renderPage mempty" $
+        renderText (renderPage mempty) `shouldBe`
+        "<!DOCTYPE HTML><html lang=\"en\"><head><meta charset=\"utf-8\"></head><body><script>window.onload=function(){}</script></body></html>"
+      let dir = "test/canned/"
+      it "renderPageToFile, renderPage (compared with default canned file)" $
+        testVsFile dir "default" defaultPageConfig page1 `shouldReturn` True
+      it "the various PageConfig's" $
+        testVsFile dir "sep" cfg2 page2 `shouldReturn` True
+
+testsBootstrap :: IO (SpecWith ())
+testsBootstrap =
+  return $
+    describe "Web.Page.Bootstrap" $ do
+      it "bootstrapPage versus canned" $
+        toText (renderPage bootstrapPage) `shouldBe`
+        "<!DOCTYPE HTML><html lang=\"en\"><head><meta charset=\"utf-8\"><link crossorigin=\"anonymous\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css\" integrity=\"sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T\" rel=\"stylesheet\"><meta charset=\"utf-8\"><meta content=\"width=device-width, initial-scale=1, shrink-to-fit=no\" name=\"viewport\"></head><body><script crossorigin=\"anonymous\" integrity=\"sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo\" src=\"https://code.jquery.com/jquery-3.3.1.slim.min.js\"></script><script crossorigin=\"anonymous\" integrity=\"sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1\" src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js\"></script><script crossorigin=\"anonymous\" integrity=\"sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM\" src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js\"></script><script>window.onload=function(){}</script></body></html>"
+      it "accordion versus canned" $
+        ( toText .
+          runIdentity .
+          flip evalStateT 0 .
+          accordion "acctest" Nothing $
+          (\x -> (Protolude.show x, "filler")) <$> [1..2::Int]) `shouldBe`
+        "<div id=\"acctest1\" class=\" accordion \"><div class=\" card \"><div id=\"acctest2\" class=\" card-header \"><h2 class=\" mb-0 \"><button data-toggle=\"collapse\" data-target=\"#acctest3\" aria-controls=\"acctest3\" type=\"button\" class=\" btn btn-link collapsed \" aria-expanded=\"false\">1</button></h2></div><div data-parent=\"#acctest1\" id=\"acctest3\" aria-labelledby=\"acctest2\" class=\" collapse \"><div class=\" card-body \">filler</div></div></div><div class=\" card \"><div id=\"acctest4\" class=\" card-header \"><h2 class=\" mb-0 \"><button data-toggle=\"collapse\" data-target=\"#acctest5\" aria-controls=\"acctest5\" type=\"button\" class=\" btn btn-link collapsed \" aria-expanded=\"false\">2</button></h2></div><div data-parent=\"#acctest1\" id=\"acctest5\" aria-labelledby=\"acctest4\" class=\" collapse \"><div class=\" card-body \">filler</div></div></div></div>"
+
+testsBridge :: IO (SpecWith ())
+testsBridge =
+  return $
+    describe "Web.Page.Bridge" $
+      it "bridgePage versus canned" $
+        toText (renderPage bridgePage) `shouldBe`
+                "<!DOCTYPE HTML><html lang=\"en\"><head><meta charset=\"utf-8\"></head><body><script>\nwindow.addEventListener('keydown',function(e) {\n  if(e.keyIdentifier=='U+000A' || e.keyIdentifier=='Enter' || e.keyCode==13) {\n    if(e.target.nodeName=='INPUT' && e.target.type !== 'textarea') {\n      e.preventDefault();\n      return false;\n    }\n  }\n}, true);\n\n\nfunction insertScript ($script) {\n  var s = document.createElement('script')\n  s.type = 'text/javascript'\n  if ($script.src) {\n    s.onload = callback\n    s.onerror = callback\n    s.src = $script.src\n  } else {\n    s.textContent = $script.innerText\n  }\n\n  // re-insert the script tag so it executes.\n  document.head.appendChild(s)\n\n  // clean-up\n  $script.parentNode.removeChild($script)\n}\n\nfunction runScripts ($container) {\n  // get scripts tags from a node\n  var $scripts = $container.querySelectorAll('script')\n  $scripts.forEach(function ($script) {\n    insertScript($script)\n  })\n}\n window.onload=function(){\nwindow.jsb = {ws: new WebSocket('ws://' + location.host + '/')};\njsb.ws.onmessage = (evt) => eval(evt.data);\n};</script></body></html>"
+
+testbs :: [Value]
+testbs =
+  [ Object (fromList [("element","1"),("value","b1")])
+  , Object (fromList [("element","2"),("value","b2")])
+  , Object (fromList [("element","3"),("value","b3")])
+  ]
+
+testvs :: [Value]
+testvs =
+  [ Object (fromList [("element","2"),("value","false")])
+  , Object (fromList [("element","2"),("value","true")])
+  , Object (fromList [("element","3"),("value","x")])
+  , Object (fromList [("element","4"),("value","")])
+  , Object (fromList [("element","5"),("value","2")])
+  , Object (fromList [("element","6"),("value","0.6")])
+  , Object (fromList [("element","7"),("value","false")])
+  , Object (fromList [("element","8"),("value","true")])
+  , Object (fromList [("element","9"),("value","5")])
+  , Object (fromList [("element","10"),("value","#00b4cc")])
+  ]
+
+testsRep :: IO (SpecWith ())
+testsRep =
+  return $
+    describe "Web.Page.Rep" $ do
+      it "Rep mempty" $
+        runIdentity (runList (pure (mempty :: Text)) []) `shouldBe` []
+      it "mempty passes values through to hashmap" $
+        runIdentity (runList (pure (mempty :: Text)) testbs) `shouldBe`
+        [ Right (fromList [("1","b1")], Right "")
+        , Right (fromList [("1","b1"),("2","b2")], Right "")
+        , Right (fromList [("1","b1"),("2","b2"),("3","b3")], Right "")
+        ]
+      it "button consumes an event and the value is transitory" $
+        runIdentity
+        (runList
+        ((,) <$> button Nothing <*> button Nothing)
+        testbs) `shouldBe`
+        [ Right (fromList [],Right (True,False))
+        , Right (fromList [],Right (False,True))
+        , Right (fromList [("3","b3")],Right (False,False))
+        ]
+      it "repExamples versus canned" $
+        runIdentity (runOnce repExamples mempty) `shouldBe`
+        (fromList [("7","3"),("1","sometext"),("4","0.5"),("2","no initial value & multi-line text\\nrenders is not ok?/"),("5","true"),("8","Square"),("3","3"),("6","false"),("9","#3880c8")],Right (RepExamples {repTextbox = "sometext", repTextarea = "no initial value & multi-line text\\nrenders is not ok?/", repSliderI = 3, repSlider = 0.5, repCheckbox = True, repToggle = False, repDropdown = 3, repShape = SquareShape, repColor = PixelRGB8 56 128 200}))
+      it "listifyExamples versus canned" $
+        runIdentity (runOnce (listifyExample 5) mempty) `shouldBe`
+        (fromList [("1","0"),("4","3"),("2","1"),("5","4"),("3","2"),("6","5")],Right [0,1,2,3,4,5])
+      it "fiddleExample versus canned" $
+        runIdentity (runOnce (fiddle fiddleExample) mempty) `shouldBe`
+        (fromList [("1",""),("2",""),("3","\n<div class=\" form-group-sm \"><label for=\"1\">fiddle example</label><input max=\"10.0\" value=\"3.0\" oninput=\"jsb.event({ &#39;element&#39;: this.id, &#39;value&#39;: this.value});\" step=\"1.0\" min=\"0.0\" id=\"1\" type=\"range\" class=\" custom-range  form-control-range \"></div>\n")],Right (Concerns "" "" "\n<div class=\" form-group-sm \"><label for=\"1\">fiddle example</label><input max=\"10.0\" value=\"3.0\" oninput=\"jsb.event({ &#39;element&#39;: this.id, &#39;value&#39;: this.value});\" step=\"1.0\" min=\"0.0\" id=\"1\" type=\"range\" class=\" custom-range  form-control-range \"></div>\n",False))
+
+      it "repExamples run through some canned events" $
+        runIdentity (runList (maybeRep Nothing True repExamples) testvs) `shouldBe`
+        [Right (fromList [("7","true"),("4","no initial value & multi-line text\\nrenders is not ok?/"),("2","false"),("5","3"),("8","false"),("11","#3880c8"),("3","sometext"),("6","0.5"),("9","3"),("10","Square")],Right Nothing),Right (fromList [("7","true"),("4","no initial value & multi-line text\\nrenders is not ok?/"),("2","true"),("5","3"),("8","false"),("11","#3880c8"),("3","sometext"),("6","0.5"),("9","3"),("10","Square")],Right (Just (RepExamples {repTextbox = "sometext", repTextarea = "no initial value & multi-line text\\nrenders is not ok?/", repSliderI = 3, repSlider = 0.5, repCheckbox = True, repToggle = False, repDropdown = 3, repShape = SquareShape, repColor = PixelRGB8 56 128 200}))),Right (fromList [("7","true"),("4","no initial value & multi-line text\\nrenders is not ok?/"),("2","true"),("5","3"),("8","false"),("11","#3880c8"),("3","x"),("6","0.5"),("9","3"),("10","Square")],Right (Just (RepExamples {repTextbox = "x", repTextarea = "no initial value & multi-line text\\nrenders is not ok?/", repSliderI = 3, repSlider = 0.5, repCheckbox = True, repToggle = False, repDropdown = 3, repShape = SquareShape, repColor = PixelRGB8 56 128 200}))),Right (fromList [("7","true"),("4",""),("2","true"),("5","3"),("8","false"),("11","#3880c8"),("3","x"),("6","0.5"),("9","3"),("10","Square")],Right (Just (RepExamples {repTextbox = "x", repTextarea = "", repSliderI = 3, repSlider = 0.5, repCheckbox = True, repToggle = False, repDropdown = 3, repShape = SquareShape, repColor = PixelRGB8 56 128 200}))),Right (fromList [("7","true"),("4",""),("2","true"),("5","2"),("8","false"),("11","#3880c8"),("3","x"),("6","0.5"),("9","3"),("10","Square")],Right (Just (RepExamples {repTextbox = "x", repTextarea = "", repSliderI = 2, repSlider = 0.5, repCheckbox = True, repToggle = False, repDropdown = 3, repShape = SquareShape, repColor = PixelRGB8 56 128 200}))),Right (fromList [("7","true"),("4",""),("2","true"),("5","2"),("8","false"),("11","#3880c8"),("3","x"),("6","0.6"),("9","3"),("10","Square")],Right (Just (RepExamples {repTextbox = "x", repTextarea = "", repSliderI = 2, repSlider = 0.6, repCheckbox = True, repToggle = False, repDropdown = 3, repShape = SquareShape, repColor = PixelRGB8 56 128 200}))),Right (fromList [("7","false"),("4",""),("2","true"),("5","2"),("8","false"),("11","#3880c8"),("3","x"),("6","0.6"),("9","3"),("10","Square")],Right (Just (RepExamples {repTextbox = "x", repTextarea = "", repSliderI = 2, repSlider = 0.6, repCheckbox = False, repToggle = False, repDropdown = 3, repShape = SquareShape, repColor = PixelRGB8 56 128 200}))),Right (fromList [("7","false"),("4",""),("2","true"),("5","2"),("8","true"),("11","#3880c8"),("3","x"),("6","0.6"),("9","3"),("10","Square")],Right (Just (RepExamples {repTextbox = "x", repTextarea = "", repSliderI = 2, repSlider = 0.6, repCheckbox = False, repToggle = True, repDropdown = 3, repShape = SquareShape, repColor = PixelRGB8 56 128 200}))),Right (fromList [("7","false"),("4",""),("2","true"),("5","2"),("8","true"),("11","#3880c8"),("3","x"),("6","0.6"),("9","5"),("10","Square")],Right (Just (RepExamples {repTextbox = "x", repTextarea = "", repSliderI = 2, repSlider = 0.6, repCheckbox = False, repToggle = True, repDropdown = 5, repShape = SquareShape, repColor = PixelRGB8 56 128 200}))),Right (fromList [("7","false"),("4",""),("2","true"),("5","2"),("8","true"),("11","#3880c8"),("3","x"),("6","0.6"),("9","5"),("10","#00b4cc")],Right (Just (RepExamples {repTextbox = "x", repTextarea = "", repSliderI = 2, repSlider = 0.6, repCheckbox = False, repToggle = True, repDropdown = 5, repShape = CircleShape, repColor = PixelRGB8 56 128 200})))]
+
+
+-- The tests
+tests :: IO TestTree
+tests = testGroup "the tests" <$> sequence
+  [ testSpec "Web.Page.Render" =<< testsRender
+  , testSpec "Web.Page.Bootstrap" =<< testsBootstrap
+  , testSpec "Web.Page.Bridge" =<< testsBridge
+  , testSpec "Web.Page.Rep" =<< testsRep
+  ]
+
+main :: IO ()
+main = defaultMain =<< tests
diff --git a/web-rep.cabal b/web-rep.cabal
new file mode 100644
--- /dev/null
+++ b/web-rep.cabal
@@ -0,0 +1,96 @@
+cabal-version: 1.12
+name:           web-rep
+version:        0.1.1
+synopsis:       representations of a web pag
+category: web
+description:    See readme.md for verbage (if any)
+bug-reports:    https://github.com/tonyday567/web-page/issues
+maintainer:     Tony Day <tonyday567@gmail.com>
+license:        MIT
+license-file:   LICENSE.md
+build-type:     Simple
+extra-source-files:
+    stack.yaml
+
+library
+  exposed-modules:
+      Web.Page
+      Web.Page.Bootstrap
+      Web.Page.Bridge
+      Web.Page.Css
+      Web.Page.Examples
+      Web.Page.Html
+      Web.Page.Html.Input
+      Web.Page.Js
+      Web.Page.Render
+      Web.Page.Rep
+      Web.Page.Rep.Input
+      Web.Page.Server
+      Web.Page.Types
+  hs-source-dirs:
+      src
+  build-depends:
+      JuicyPixels
+    , aeson
+    , attoparsec
+    , base >= 4.12 && <5
+    , bifunctors
+    , box
+    , clay
+    , foldl
+    , formatting
+    , generic-lens
+    , interpolatedstring-perl6
+    , javascript-bridge
+    , language-javascript
+    , lens
+    , lucid
+    , lucid-svg
+    , mmorph
+    , mtl
+    , protolude
+    , scotty
+    , streaming
+    , text
+    , transformers
+    , unordered-containers
+    , wai-middleware-static
+  default-language: Haskell2010
+
+executable page-example
+  main-is: example.hs
+  hs-source-dirs:
+      app
+  ghc-options: -funbox-strict-fields -fforce-recomp -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      attoparsec
+    , base >= 4.12 && <5
+    , box
+    , lens
+    , lucid
+    , optparse-generic
+    , protolude
+    , scotty
+    , text
+    , wai
+    , wai-extra
+    , wai-middleware-static
+    , web-rep
+  default-language: Haskell2010
+
+test-suite test
+  type: exitcode-stdio-1.0
+  main-is: test.hs
+  hs-source-dirs:
+      test
+  ghc-options: -Wall -O2 -threaded
+  build-depends:
+    base >= 4.12 && <5
+    , lens
+    , lucid
+    , protolude
+    , tasty
+    , tasty-hspec
+    , text
+    , web-rep
+  default-language: Haskell2010
