Full Code of seanhess/hyperbole for AI

main d1ea5d43346f cached
254 files
539.1 KB
157.6k tokens
174 symbols
1 requests
Download .txt
Showing preview only (597K chars total). Download the full file or copy to clipboard to get everything.
Repository: seanhess/hyperbole
Branch: main
Commit: d1ea5d43346f
Files: 254
Total size: 539.1 KB

Directory structure:
gitextract_46835c3y/

├── .dockerignore
├── .github/
│   └── workflows/
│       ├── haskell.yaml
│       └── packcheck.yaml
├── .gitignore
├── .hlint.yaml
├── .packcheck.ignore
├── CHANGELOG.md
├── DOCTODO.md
├── Dockerfile
├── LICENSE
├── README.md
├── bin/
│   ├── dev
│   ├── docgen
│   └── release
├── cabal.project
├── client/
│   ├── declarations.d.ts
│   ├── dist/
│   │   ├── action.d.ts
│   │   ├── browser.d.ts
│   │   ├── events.d.ts
│   │   ├── http.d.ts
│   │   ├── hyperbole.js
│   │   ├── hyperview.d.ts
│   │   ├── index.d.ts
│   │   ├── lib.d.ts
│   │   ├── message.d.ts
│   │   ├── response.d.ts
│   │   └── sockets.d.ts
│   ├── package.json
│   ├── src/
│   │   ├── action.ts
│   │   ├── browser.ts
│   │   ├── events.ts
│   │   ├── http.ts
│   │   ├── hyperview.ts
│   │   ├── index.ts
│   │   ├── lib.ts
│   │   ├── message.ts
│   │   ├── response.ts
│   │   └── sockets.ts
│   ├── tsconfig.json
│   ├── util/
│   │   └── live-reload.js
│   └── webpack.config.js
├── demo/
│   ├── .dockerignore
│   ├── App/
│   │   ├── Cache.hs
│   │   ├── Config.hs
│   │   ├── Docs/
│   │   │   ├── Markdown.hs
│   │   │   ├── Page.hs
│   │   │   └── Snippet.hs
│   │   ├── Docs.hs
│   │   ├── Page/
│   │   │   ├── Application.hs
│   │   │   ├── CSS.hs
│   │   │   ├── Concurrency.hs
│   │   │   ├── Examples.hs
│   │   │   ├── Forms.hs
│   │   │   ├── HyperboleEffect.hs
│   │   │   ├── Hyperviews.hs
│   │   │   ├── Interactivity.hs
│   │   │   ├── Intro/
│   │   │   │   ├── Basics.hs
│   │   │   │   └── Intro.hs
│   │   │   ├── OAuth2.hs
│   │   │   ├── SideEffects.hs
│   │   │   ├── State.hs
│   │   │   └── ViewFunctions.hs
│   │   ├── Route.hs
│   │   └── Style.hs
│   ├── App.hs
│   ├── Example/
│   │   ├── CSS/
│   │   │   ├── External.hs
│   │   │   ├── Loading.hs
│   │   │   ├── Tooltips.hs
│   │   │   └── Transitions.hs
│   │   ├── Chat.hs
│   │   ├── Colors.hs
│   │   ├── Concurrency/
│   │   │   ├── LazyLoading.hs
│   │   │   ├── Overlap.hs
│   │   │   ├── Polling.hs
│   │   │   ├── Progress.hs
│   │   │   └── Tasks.hs
│   │   ├── Contact.hs
│   │   ├── Contacts.hs
│   │   ├── Counter.hs
│   │   ├── Data/
│   │   │   └── ProgrammingLanguage.hs
│   │   ├── DataLists/
│   │   │   ├── Autocomplete.hs
│   │   │   ├── DataTable.hs
│   │   │   ├── Filter.hs
│   │   │   └── LoadMore.hs
│   │   ├── Docs/
│   │   │   ├── App.hs
│   │   │   ├── BasicPage.hs
│   │   │   ├── CSS.hs
│   │   │   ├── Client.hs
│   │   │   ├── Component.hs
│   │   │   ├── Encoding.hs
│   │   │   ├── Interactive.hs
│   │   │   ├── MultiPage.hs
│   │   │   ├── MultiView.hs
│   │   │   ├── Nested.hs
│   │   │   ├── Nesting.hs
│   │   │   ├── Page/
│   │   │   │   ├── Messages.hs
│   │   │   │   └── Users.hs
│   │   │   ├── Params.hs
│   │   │   ├── QueryMessage.hs
│   │   │   ├── Sessions.hs
│   │   │   ├── SideEffects.hs
│   │   │   ├── State.hs
│   │   │   ├── UniqueViewId.hs
│   │   │   └── ViewFunctions.hs
│   │   ├── Document.hs
│   │   ├── Effects/
│   │   │   ├── Debug.hs
│   │   │   ├── Todos.hs
│   │   │   └── Users.hs
│   │   ├── Errors.hs
│   │   ├── FormSimple.hs
│   │   ├── FormValidation.hs
│   │   ├── Interactivity/
│   │   │   ├── Events.hs
│   │   │   └── Inputs.hs
│   │   ├── Javascript.hs
│   │   ├── Push.hs
│   │   ├── Requests.hs
│   │   ├── Scrollbars.hs
│   │   ├── Simple.hs
│   │   ├── State/
│   │   │   ├── Effects.hs
│   │   │   ├── Query.hs
│   │   │   ├── Sessions.hs
│   │   │   ├── Stateless.hs
│   │   │   └── ViewState.hs
│   │   ├── Style/
│   │   │   └── Cyber.hs
│   │   ├── Style.hs
│   │   ├── Tags.hs
│   │   ├── Test.hs
│   │   ├── Todos/
│   │   │   ├── Todo.hs
│   │   │   └── TodoCSS.hs
│   │   ├── Trigger.hs
│   │   └── View/
│   │       ├── Icon.hs
│   │       ├── Inputs.hs
│   │       ├── Layout.hs
│   │       ├── Loader.hs
│   │       ├── Menu.hs
│   │       └── SortableTable.hs
│   ├── Main.hs
│   ├── README.md
│   ├── demo.cabal
│   ├── fourmolu.yaml
│   ├── hie.yaml
│   ├── package.yaml
│   └── static/
│       ├── custom.js
│       ├── cyber.css
│       ├── docs.js
│       ├── external.css
│       ├── prism.css
│       ├── prism.js
│       ├── test.js
│       └── todomvc.css
├── docs/
│   ├── Main.hs
│   ├── app-document.md
│   ├── app-effects.md
│   ├── app-live.md
│   ├── app-pages.md
│   ├── app-routes.md
│   ├── atomic.md
│   ├── comparison.md
│   ├── concurrency-overlap.md
│   ├── dev.md
│   ├── docgen.cabal
│   ├── effectful.md
│   ├── effects-custom.md
│   ├── effects-other.md
│   ├── forms-simple.md
│   ├── forms-validated.md
│   ├── hyperviews-intro.md
│   ├── hyperviews-multi.md
│   ├── hyperviews-nesting.md
│   ├── hyperviews-unique.md
│   ├── interactivity-events.md
│   ├── interactivity-events2.md
│   ├── interactivity-inputs.md
│   ├── interactivity-javascript.md
│   ├── interactivity-pushevent.md
│   ├── intro-downsides.md
│   ├── intro-links.md
│   ├── intro.md
│   ├── javascript_api.md
│   ├── multi-same.md
│   ├── nix.md
│   ├── outline.md
│   ├── package.yaml
│   ├── pages.md
│   ├── state-browser.md
│   ├── state-effects.md
│   ├── state-sessions.md
│   ├── state-stateless.md
│   ├── state-threading.md
│   ├── state-viewstate.md
│   ├── view-components.md
│   ├── view-functions-end.md
│   ├── view-functions-wrap.md
│   └── view-functions.md
├── flake.nix
├── fourmolu.yaml
├── hie.yaml
├── hyperbole.cabal
├── package.yaml
├── src/
│   └── Web/
│       ├── Hyperbole/
│       │   ├── Application.hs
│       │   ├── Data/
│       │   │   ├── Cookie.hs
│       │   │   ├── Encoded.hs
│       │   │   ├── JSON.hs
│       │   │   ├── Param.hs
│       │   │   ├── QueryData.hs
│       │   │   └── URI.hs
│       │   ├── Document.hs
│       │   ├── Effect/
│       │   │   ├── Client.hs
│       │   │   ├── GenRandom.hs
│       │   │   ├── Hyperbole.hs
│       │   │   ├── OAuth2.hs
│       │   │   ├── Query.hs
│       │   │   ├── Request.hs
│       │   │   ├── Response.hs
│       │   │   └── Session.hs
│       │   ├── HyperView/
│       │   │   ├── Event.hs
│       │   │   ├── Forms.hs
│       │   │   ├── Handled.hs
│       │   │   ├── Hyper.hs
│       │   │   ├── Input.hs
│       │   │   └── Types.hs
│       │   ├── HyperView.hs
│       │   ├── Page.hs
│       │   ├── Route.hs
│       │   ├── Server/
│       │   │   ├── Handler.hs
│       │   │   ├── Message.hs
│       │   │   ├── Options.hs
│       │   │   ├── Socket.hs
│       │   │   └── Wai.hs
│       │   ├── TypeList.hs
│       │   ├── Types/
│       │   │   ├── Client.hs
│       │   │   ├── Event.hs
│       │   │   ├── Request.hs
│       │   │   └── Response.hs
│       │   ├── View/
│       │   │   ├── CSS.hs
│       │   │   ├── Embed.hs
│       │   │   ├── Render.hs
│       │   │   ├── Tag.hs
│       │   │   ├── Types.hs
│       │   │   ├── ViewAction.hs
│       │   │   └── ViewId.hs
│       │   └── View.hs
│       └── Hyperbole.hs
└── test/
    ├── Spec.hs
    └── Test/
        ├── EncodedSpec.hs
        ├── FormSpec.hs
        ├── ParamSpec.hs
        ├── QuerySpec.hs
        ├── RouteSpec.hs
        ├── SessionSpec.hs
        ├── URISpec.hs
        ├── ViewActionSpec.hs
        ├── ViewIdSpec.hs
        └── ViewSpec.hs

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
.git
.stack-work
client/node_modules
Dockerfile
dist-newstyle


================================================
FILE: .github/workflows/haskell.yaml
================================================
name: Haskell CI

on:
  push:
    branches: [ "main", "ci" ]
  pull_request:
    branches: [ "main" ]

permissions:
  contents: read



jobs:
  build-cache:
    runs-on: ubuntu-latest
    container:
      image: haskell:9.8.2
    steps: 
      - uses: actions/checkout@v4

      - name: Cache Cabal
        id: cache-cabal
        uses: actions/cache@v4
        with:
          path: |
            /github/home/.cache
            /github/home/.config
            /github/home/.local
          key: ${{ runner.os }}-${{ hashFiles('**/cabal.project') }}-${{ hashFiles('**/*.cabal') }}
          # restore-keys: |
          #   ${{ runner.os }}-

      - name: Install dependencies
        run: |
          cabal update

      - name: Build dependencies
        run: |
          cabal build --only-dependencies --enable-tests --enable-benchmarks

      - name: Install skeletest-preprocessor
        run: |
          cabal install skeletest --installdir=$HOME/.local/bin --install-method=copy --overwrite-policy=always

      - name: Check Cache
        run: |
          ls -ahl /github/home/
          ls -ahl /github/home/.cache
          ls -ahl /github/home/.config
          ls -ahl /github/home/.local

  build-982:
    needs: build-cache
    runs-on: ubuntu-latest
    container:
      image: haskell:9.8.2
    steps: 
      - uses: actions/checkout@v4

      - name: Cache Cabal Restore
        id: cache-cabal-restore
        uses: actions/cache@v4
        with:
          path: |
            /github/home/.cache
            /github/home/.config
            /github/home/.local
          key: ${{ runner.os }}-${{ hashFiles('**/cabal.project') }}-${{ hashFiles('**/*.cabal') }}
          # restore-keys: |
          #   ${{ runner.os }}-build-${{ env.cache-name }}-
          #   ${{ runner.os }}-build-
          #   ${{ runner.os }}-

      - name: Check Cache
        run: |
          ls -ahl /github/home/
          ls -ahl /github/home/.cache
          ls -ahl /github/home/.config
          ls -ahl /github/home/.local

      - name: Source skeletest-preprocessor
        run: |
          echo "$HOME/.local/bin" >> $GITHUB_PATH

      - name: Build
        run: cabal build --enable-tests --enable-benchmarks all

      - name: Test
        run: cabal test

  # build-966:
  #   runs-on: ubuntu-latest
  #   container:
  #     image: haskell:9.6.6
  #   steps: *cabal-test


    # - uses: actions/setup-haskell@v1
    #   with:
    #     ghc-version: '9.6'
    #     cabal-version: '3.2'

    # - name: Cache
    #   uses: actions/cache@v3
    #   env:
    #     cache-name: cache-cabal
    #   with:
    #     path: ~/.cabal
    #     key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
    #     restore-keys: |
    #       ${{ runner.os }}-build-${{ env.cache-name }}-
    #       ${{ runner.os }}-build-
    #       ${{ runner.os }}-


================================================
FILE: .github/workflows/packcheck.yaml
================================================
# packcheck-0.7.1
# You can use any of the options supported by packcheck as environment
# variables here.  See https://github.com/composewell/packcheck for all
# options and their explanation.

name: packcheck

#-----------------------------------------------------------------------------
# Events on which the build should be triggered
#-----------------------------------------------------------------------------

on:
  push:
    branches: [ "main", "ci" ]
  pull_request:


#-----------------------------------------------------------------------------
# Build matrix
#-----------------------------------------------------------------------------

jobs:
  build:
    name: >-
      ${{ matrix.name }}
      ${{ matrix.command }}
      ${{ matrix.runner }}
      ${{ matrix.ghc_version }}
    env:
      # ------------------------------------------------------------------------
      # Common options
      # ------------------------------------------------------------------------
      # GHC_OPTIONS: "-Werror"
      CABAL_REINIT_CONFIG: y
      LC_ALL: C.UTF-8

      # ------------------------------------------------------------------------
      # What to build
      # ------------------------------------------------------------------------
      # DISABLE_TEST: "y"
      # DISABLE_BENCH: "y"
      # DISABLE_DOCS: "y"
      DISABLE_SDIST_BUILD: "y"
      # DISABLE_SDIST_GIT_CHECK: "y"
      # DISABLE_DIST_CHECKS: "y"

      # ------------------------------------------------------------------------
      # Selecting tool versions
      # ------------------------------------------------------------------------
      # For updating see: https://downloads.haskell.org/~ghcup/
      GHCUP_VERSION: 0.1.40.0
      GHCVER: ${{ matrix.ghc_version }}
      GHCUP_GHC_OPTIONS: ${{ matrix.ghcup_ghc_options }}
      # RESOLVER: ${{ matrix.stack_resolver }}

      # ------------------------------------------------------------------------
      # stack options
      # ------------------------------------------------------------------------
      # Note requiring a specific version of stack using STACKVER may fail due to
      # github API limit while checking and upgrading/downgrading to the specific
      # version.
      # STACKVER: "1.6.5"
      # STACK_UPGRADE: "y"
      # STACK_YAML: "stack.yaml"

      # ------------------------------------------------------------------------
      # cabal options
      # ------------------------------------------------------------------------
      CABAL_CHECK_RELAX: y
      CABAL_HACKAGE_MIRROR: "hackage.haskell.org:http://hackage.fpcomplete.com"
      CABAL_PROJECT: ${{ matrix.cabal_project }}

      # ------------------------------------------------------------------------
      # Where to find the required tools
      # ------------------------------------------------------------------------
      PATH: /opt/ghc/bin:/sbin:/usr/sbin:/bin:/usr/bin
      #TOOLS_DIR: /opt

      # ------------------------------------------------------------------------
      # Location of packcheck.sh (the shell script invoked to perform CI tests ).
      # ------------------------------------------------------------------------
      # You can either commit the packcheck.sh script at this path in your repo or
      # you can use it by specifying the PACKCHECK_REPO_URL option below in which
      # case it will be automatically copied from the packcheck repo to this path
      # during CI tests. In any case it is finally invoked from this path.
      PACKCHECK: "./packcheck.sh"
      # If you have not committed packcheck.sh in your repo at PACKCHECK
      # then it is automatically pulled from this URL.
      PACKCHECK_GITHUB_URL: "https://raw.githubusercontent.com/composewell/packcheck"
      PACKCHECK_GITHUB_COMMIT: "2856fb3010c7d0549537852cfa8500b4f1b58537"

      # ------------------------------------------------------------------------
      # Final build variables
      # ------------------------------------------------------------------------
      PACKCHECK_COMMAND: ${{ matrix.command }} ${{ matrix.pack_options }}

    # ubuntu seems to have better support than debian on CI systems
    runs-on: ${{ matrix.runner }}
    strategy:
      fail-fast: false
      matrix:
        include:

          - name: ci
            ghc_version: 9.12.1
            command: cabal
            runner: ubuntu-latest
            cabal_project: cabal.project

          - name: ci
            ghc_version: 9.10.1
            command: cabal
            runner: macos-latest
            cabal_project: cabal.project

          - name: ci-sdist
            ghc_version: 9.10.1
            command: cabal
            runner: ubuntu-latest
            pack_options: >-
              DISABLE_SDIST_BUILD=n

          - name: ci
            command: cabal
            runner: ubuntu-latest
            ghc_version: 9.8.4
            cabal_project: cabal.project

          - name: ci
            ghc_version: 9.6.6
            command: cabal
            runner: macos-latest
            cabal_project: cabal.project

          # - name: ci
          #   command: hlint
          #   runner: ubuntu-latest
          #   pack_options: >-
          #     HLINT_VERSION=3.6.1
          #     HLINT_OPTIONS="lint"
          #     HLINT_TARGETS="src test examples"

    steps:
    - uses: actions/checkout@v2
    - uses: actions/cache@v3
      name: Cache common directories
      with:
        path: |
          ~/.local
          ~/.cabal
          ~/.stack
          ~/.ghcup
        key: ${{ matrix.command }}-${{ matrix.ghc_version }}-${{ matrix.runner }}

    - name: Download packcheck
      run: |
        if test ! -e "$PACKCHECK"
        then
          if test -z "$PACKCHECK_GITHUB_COMMIT"
          then
              die "PACKCHECK_GITHUB_COMMIT is not specified."
          fi
          PACKCHECK_URL=${PACKCHECK_GITHUB_URL}/${PACKCHECK_GITHUB_COMMIT}/packcheck.sh
          curl --fail -sL -o "$PACKCHECK" $PACKCHECK_URL || exit 1
          chmod +x $PACKCHECK
        elif test ! -x "$PACKCHECK"
        then
            chmod +x $PACKCHECK
        fi

    - name: Run packcheck
      run: |
        bash -c "$PACKCHECK $PACKCHECK_COMMAND"


================================================
FILE: .gitignore
================================================
dist-newstyle
.DS_Store
tags
node_modules
# Auto-generated pre-commit config
.pre-commit-config.yaml
# Nix output dir
result
.direnv
client/dist/hyperbole.js.LICENSE.txt
Session.vim
.cabal.nix
/package.json
/package-lock.json



================================================
FILE: .hlint.yaml
================================================
- arguments:
    - -XOverloadedRecordDot

- ignore: {name: "Use <$>"}
- ignore: {name: "Use newtype instead of data"}

# Hlint is not aware of OverloadedRecordDot
# See https://github.com/ndmitchell/hlint/issues/1383
- ignore: { name: Redundant id }


================================================
FILE: .packcheck.ignore
================================================
client/*.d.ts
client/src/
client/dist/*.d.ts
client/package-lock.json
client/webpack.config.js
client/package.json
client/tsconfig.json
docs/
demo/
.dockerignore
.github/workflows/haskell.yaml
.github/workflows/packcheck.yaml
.gitignore
.hlint.yaml
.packcheck.ignore
DOCTODO.md
Dockerfile
bin/dev
bin/docgen
bin/release
cabal.project
flake.lock
flake.nix
fourmolu.yaml
hie.yaml
package.yaml


================================================
FILE: CHANGELOG.md
================================================
# Revision history for hyperbole

## 0.6.0 -- 2026-01-15

Improvements:
* `ViewState` - built in threaded state, defaults to `()`, for folks who really miss Elm
* `Concurrency` Controls - `Drop` vs `Replace` for overlapping updates
* `pushUpdate` - server push an update to an arbitrary view
* Long-running actions can be interrupted / cancelled
* https://hyperbole.live now has inline documentation, code snippets, and live examples

Breaking Changes:
* A few functions now require state, such as `trigger` and `target`

## 0.5.0 -- 2025-09-26

Improvements
* `trigger` actions in other views
* Javascript FFI
  * `window.Hyperbole` - API available from custom JS. `runAction` allows JS to trigger actions
  * `pushEvent` - send events to JS from the server
* Documents
  * Choose to configure with `View DocumentHead ()` instead of `ByteString` `->` `ByteString`
  * `quickStartDocument`
  * Live Reload
* Websocket - ping keepalive
* New form fields: `radio`, `select`
* `Web.Hyperbole.Effect.OAuth2` - Authentication 
* `Web.Hyperbole.Effect.GenRandom` - Simple random effect used by OAuth2
* Error handling, custom errors
* Examples
  * Many additions and improvements
  * External Stylesheet TodoMVC
  * OAuth2 example

Breaking Changes / Improvements
* `Web.Atomic.CSS` overhauled, and is now opt-in. Use new `@` and `~` operators to apply attributes and styles
* `Web.Hyperbole.Data.Param` - unified param encoding for Forms, ViewId, ViewAction, Sessions, Queries
* `Web.Hyperbole.Data.Encoding` - encoding for ViewId, ViewAction
* `Web.Hyperbole.Data.URI` - Standardize on `Network.URI`, extra utilities to manage paths
* `trigger`: required refactor of `Page` type alias to support type-checking: `Eff es (Page '[])` is now `Page es '[]`

## 0.4.3 -- 2025-01-31

* Bug fixes and improvements

## 0.4.2 -- 2025-01-21

* Cleaner HyperView class [(@cgeorgii)](https://github.com/cgeorgii)
  * data family Action
  * update
* Type-safe resolution of HyperViews
* Record-based Forms
  * textarea [(@tusharad)](https://github.com/tusharad)
* High-level sessions and query params
* Events: onLoad, onClick onInput, onSubmit, onDblClick, onKeyDown, onKeyUp
* Major refactoring
* Nix build and CI [(@Skyfold)](https://github.com/Skyfold)
* New Examples Live: https://docs.hyperbole.live
* New Examples Added:
  * TodoMVC
  * Forms - Simple
  * DataTable
  * Search - Filters
  * Search - Autocomplete

## 0.3.6 -- 2024-05-21

* First version. Released on an unsuspecting world.


================================================
FILE: DOCTODO.md
================================================
Documentation Outline
======================




================================================
FILE: Dockerfile
================================================
FROM haskell:9.8.2 AS base
WORKDIR /opt/build

RUN cabal update
RUN cabal install bytestring containers casing effectful text time string-interpolate file-embed http-api-data http-types wai warp wai-websockets network cookie string-conversions hpack websockets


FROM haskell:9.8.2 AS dependencies
WORKDIR /opt/build
COPY --from=base /root/.cache /root/.cache
COPY --from=base /root/.local /root/.local
COPY --from=base /root/.config /root/.config

# RUN apt-get update && apt-get install -y libpcre3 libpcre3-dev libcurl4-openssl-dev cron vim rsyslog
ADD ./package.yaml .
# ADD ./cabal.project .
# ADD ./docs/docgen.cabal .
# ADD ./demo/demo.cabal .
RUN hpack
RUN cabal update
RUN cabal build --only-dependencies

FROM haskell:9.8.2 AS build
WORKDIR /opt/build
COPY --from=dependencies /root/.cache /root/.cache
COPY --from=dependencies /root/.local /root/.local
COPY --from=dependencies /root/.config /root/.config
ADD ./package.yaml .
ADD ./cabal.project .
ADD ./client ./client
ADD ./test ./test
ADD ./src ./src
ADD ./demo ./demo
ADD ./docs ./docs
ADD *.md .
ADD LICENSE .
RUN hpack
RUN hpack demo
RUN hpack docs
RUN cabal build demo
RUN mkdir bin
RUN cd demo && export EXEC=$(cabal list-bin demo | tail -n1); cp "$EXEC" /opt/build/bin/demo


FROM ubuntu:24.04 AS app
WORKDIR /opt/app

RUN apt-get update
RUN apt-get install -y --no-install-recommends ca-certificates
RUN update-ca-certificates && rm -rf /var/lib/apt/lists/*

COPY --from=build /opt/build/bin/demo ./bin/demo
ADD ./client ./client
ADD ./demo/static ./demo/static

# ENV DYNAMO_LOCAL=False
ENTRYPOINT ["/opt/app/bin/demo"]


================================================
FILE: LICENSE
================================================
Copyright (c) 2023, Sean Hess

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.

    * Neither the name of Sean Hess nor the names of other
      contributors may be used to endorse or promote products derived
      from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: README.md
================================================
![Hyperbole](https://github.com/seanhess/hyperbole/raw/main/demo/static/logo-robot.png)

[![Hackage Version](https://img.shields.io/hackage/v/hyperbole?color=success)](https://hackage.haskell.org/package/hyperbole)

Create interactive HTML applications with type-safe serverside Haskell. Inspired by HTMX, Elm, and Phoenix LiveView.

[▶️ Simple Example](https://hyperbole.live/simple)

```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}

module Main where

import Data.Text (Text)
import Web.Atomic.CSS
import Web.Hyperbole

main :: IO ()
main = do
  run 3000 $ do
    liveApp quickStartDocument (runPage page)

page :: (Hyperbole :> es) => Page es '[Message]
page = do
  pure $ do
    hyper Message1 $ messageView "Hello"
    hyper Message2 $ messageView "World!"

data Message = Message1 | Message2
  deriving (Generic, ViewId)

instance HyperView Message es where
  data Action Message
    = Louder Text
    deriving (Generic, ViewAction)

  update (Louder msg) = do
    let new = msg <> "!"
    pure $ messageView new

messageView :: Text -> View Message ()
messageView msg = do
  button (Louder msg) ~ border 1 $ text msg
```



Documentation
-------------

Visit [hyperbole.live](https://hyperbole.live) for documentation and live examples. Also see the [Hackage Documentation](https://hackage.haskell.org/package/hyperbole/docs/Web-Hyperbole.html)

<a href="https://hyperbole.live">
  <img alt="Hyperbole Documentation" src="https://github.com/seanhess/hyperbole/raw/main/demo/static/demo-screenshot.jpg"/>
</a>



<!-- out of date!
* [HaskRead](https://github.com/tusharad/Reddit-Clone-Haskell) - A Reddit Clone
-->

Getting Started with Cabal
--------------------------

Create a new application:

    $ mkdir myapp
    $ cd myapp
    $ cabal init

Add hyperbole and text as dependencies to the `.cabal` file:

```
    build-depends:
        base
      , hyperbole
      , text

    default-language: GHC2021
```

Paste the above example into Main.hs, then run it:

    $ cabal run

Visit http://localhost:3000 to view the application


Learn More
----------

<!-- <a href="https://hackage.haskell.org/package/hyperbole/docs/Web-Hyperbole.html" target="_blank" style="border-radius: 20px; Background-color:#f8f8f8; gap: 20px; display: flex; flex-direction: row; align-items: center"> -->
<!--     <img src="https://github.com/seanhess/hyperbole/raw/main/docs/hackage.svg"> -->
<!-- </a> -->

* [Local Development](./docs/dev.md)
* [Comparison with Similar Frameworks](./docs/comparison.md)
* [Using NIX](./docs/nix.md)

In the Wild
---------------------

<a href="https://nso.edu">
  <img alt="National Solar Observatory" src="https://nso1.b-cdn.net/wp-content/uploads/2020/03/NSO-logo-orange-text.png" width="400"/>
</a>

The NSO uses Hyperbole to manage Level 2 Data pipelines for the [DKIST telescope](https://nso.edu/telescopes/dki-solar-telescope/). It uses complex user interfaces, workers, databases, and more. [The entire codebase is open source](https://github.com/DKISTDC/level2/). 






Contributors
------------

* [Sean Hess](seanhess)
* [Kamil Figiela](https://github.com/kfigiela)
* [Christian Georgii](https://github.com/cgeorgii)
* [Pfalzgraf Martin](https://github.com/Skyfold)
* [Tushar Adhatrao](https://github.com/tusharad)
* [Benjamin Thomas](https://github.com/benjamin-thomas)
* [Adithya Obilisetty](https://github.com/adithyaov)


================================================
FILE: bin/dev
================================================
#!/bin/bash


package() {
  hpack
  hpack docs
  hpack demo
}

watch() {
  ghcid -c "cabal repl demo lib:hyperbole" -T App.run -W --reload=./client/dist/hyperbole.js
}

client() {
  cd client
  # npx webpack -w --mode=development
  npx webpack -w
}

# run tests once (so we don't forget!)
cabal test

(trap 'kill 0' SIGINT; client & package && watch)


================================================
FILE: bin/docgen
================================================
#!/bin/bash

set -e

hpack demo
hpack docs
cabal run docs

cd /tmp/hyperbole
cabal haddock


================================================
FILE: bin/release
================================================
#!/bin/bash


# Make sure everything is working
hpack
cabal test


# Compile the JS client
cd client
npm install
npx webpack --mode=production
cd ..


# Compile the package
cabal sdist

# NEXT: Update branch release tags and push


================================================
FILE: cabal.project
================================================
tests: True
multi-repl: True
packages:
  .
  ./demo/
  ./docs/


================================================
FILE: client/declarations.d.ts
================================================
declare module 'omdomdom/lib/omdomdom.es.js' {
  export function create(node: any, ...args: any[]): any;
  export function patch(template: any, vNode: any, rootNode?: any): void;
  export function render(vNode: any, root: any): void;
}



================================================
FILE: client/dist/action.d.ts
================================================
import { Meta, ViewId, RequestId, EncodedAction, ViewState } from "./message";
export type ActionMessage = {
    viewId: ViewId;
    action: EncodedAction;
    requestId: RequestId;
    state?: ViewState;
    meta: Meta[];
    form: URLSearchParams | undefined;
};
export declare function actionMessage(id: ViewId, action: EncodedAction, state: ViewState | undefined, reqId: RequestId, form?: FormData): ActionMessage;
export declare function toSearch(form?: FormData): URLSearchParams | undefined;
export declare function renderActionMessage(msg: ActionMessage): string;
export declare function renderForm(form: URLSearchParams | undefined): string;
export type Request = {
    requestId: RequestId;
    isCancelled: boolean;
};
export declare function newRequest(): Request;
export declare function encodedParam(action: string, param: string): string;


================================================
FILE: client/dist/browser.d.ts
================================================
export declare function setQuery(query: string): void;


================================================
FILE: client/dist/events.d.ts
================================================
import { HyperView } from './hyperview';
export type UrlFragment = string;
export declare function listenKeydown(cb: (target: HyperView, action: string) => void): void;
export declare function listenKeyup(cb: (target: HyperView, action: string) => void): void;
export declare function listenKeyEvent(event: "keyup" | "keydown", cb: (target: HyperView, action: string) => void): void;
export declare function listenBubblingEvent(event: string, cb: (_target: HyperView, action: string) => void): void;
export declare function listenClick(cb: (target: HyperView, action: string) => void): void;
export declare function listenDblClick(cb: (target: HyperView, action: string) => void): void;
export declare function listenTopLevel(cb: (target: HyperView, action: string) => void): void;
export declare function listenLoad(node: HTMLElement): void;
export declare function listenMouseEnter(node: HTMLElement): void;
export declare function listenMouseLeave(node: HTMLElement): void;
export declare function listenChange(cb: (target: HyperView, action: string) => void): void;
export declare function listenInput(startedTyping: (target: HyperView) => void, cb: (target: HyperView, action: string) => void): void;
export declare function listenFormSubmit(cb: (target: HyperView, action: string, form: FormData) => void): void;


================================================
FILE: client/dist/http.d.ts
================================================


================================================
FILE: client/dist/hyperbole.js
================================================
/*! For license information please see hyperbole.js.LICENSE.txt */
(()=>{var e={296:e=>{function t(e,t=100,n={}){if("function"!=typeof e)throw new TypeError(`Expected the first parameter to be a function, got \`${typeof e}\`.`);if(t<0)throw new RangeError("`wait` must not be negative.");const{immediate:o}="boolean"==typeof n?{immediate:n}:n;let r,i,a,s,c;function u(){const t=r,n=i;return r=void 0,i=void 0,c=e.apply(t,n),c}function l(){const e=Date.now()-s;e<t&&e>=0?a=setTimeout(l,t-e):(a=void 0,o||(c=u()))}const d=function(...e){if(r&&this!==r&&Object.getPrototypeOf(this)===Object.getPrototypeOf(r))throw new Error("Debounced method called with different contexts of the same prototype.");r=this,i=e,s=Date.now();const n=o&&!a;return a||(a=setTimeout(l,t)),n&&(c=u()),c};return Object.defineProperty(d,"isPending",{get:()=>void 0!==a}),d.clear=()=>{a&&(clearTimeout(a),a=void 0)},d.flush=()=>{a&&d.trigger()},d.trigger=()=>{c=u(),d.clear()},d}e.exports.debounce=t,e.exports=t},147:e=>{"use strict";e.exports=JSON.parse('{"name":"web-ui","version":"0.6.0","description":"Development -----------","main":"index.js","directories":{"client":"client"},"scripts":{"build":"npx webpack"},"author":"","license":"ISC","devDependencies":{"ts-loader":"^9.4.1","typescript":"^4.8.3","uglify":"^0.1.5","webpack":"^5.88.2","webpack-cli":"^4.10.0"},"dependencies":{"omdomdom":"^0.3.2","debounce":"^2.2.0"}}')}},t={};function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}(()=>{"use strict";var e=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t=function(e,t){var n=e.length,o=-1;if(n)for(;++o<n&&!1!==t(e[o],o););},o=function(e,t,n){return e.node.insertBefore(t.node,n)};function r(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var o,r,i,a,s=[],c=!0,u=!1;try{if(i=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;c=!1}else for(;!(c=(o=i.call(n)).done)&&(s.push(o.value),s.length!==t);c=!0);}catch(e){u=!0,r=e}finally{try{if(!c&&null!=n.return&&(a=n.return(),Object(a)!==a))return}finally{if(u)throw r}}return s}}(e,t)||function(e,t){if(e){if("string"==typeof e)return i(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?i(e,t):void 0}}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function i(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=new Array(t);n<t;n++)o[n]=e[n];return o}var a="string",s="number",c="boolean",u={},l=function(e,t,n){return{attrName:e,propName:t,type:n}};t([["style","cssText"],["class","className"]],(function(e){var t=r(e,2),n=t[0],o=t[1];u[n]=l(n,o||n,a)})),t(["autofocus","draggable","hidden","checked","multiple","muted","selected"],(function(e){u[e]=l(e,e,c)})),t([["tabindex","tabIndex"]],(function(e){var t=r(e,2),n=t[0],o=t[1];u[n]=l(n,o,s)}));var d="xlink:",f="http://www.w3.org/1999/xlink",v="xml:",m="http://www.w3.org/XML/1998/namespace",p=function(e,t,n,o){switch(t){case a:n===u.style.propName?e.style[n]=null===o?"":o:e[n]=null===o?"":o;break;case s:if(null===o){var r=n.toLowerCase();e.removeAttribute(r)}else if("0"===o)e[n]=0;else if("-1"===o)e[n]=-1;else{var i=parseInt(o,10);isNaN(i)||(e[n]=i)}break;case c:["","true"].indexOf(o)<0?e[n]=!1:e[n]=!0}},h=function n(r,i,s){if(r&&i){s=s||i.node.parentNode;var c=r.content&&r.content!==i.content;if(r.type!==i.type||c)return s.replaceChild(r.node,i.node),function(e,t){for(var n in e)t[n]=e[n]}(r,i);(function(n,o){var r=[],i={};for(var s in o.attributes){var c=o.attributes[s],l=n.attributes[s];c!==l&&void 0===l&&r.push(s)}for(var h in n.attributes){var y=o.attributes[h],g=n.attributes[h];y!==g&&void 0!==g&&(i[h]=g)}!function(n,o){t(o,(function(t){if(e(u,t)){var o=u[t];p(n.node,o.type,o.propName,null)}else t in n.node&&p(n.node,a,t,null),n.node.removeAttribute(t);delete n.attributes[t]}))}(o,r),function(t,n){for(var o in n){var r=n[o];if(t.attributes[o]=r,e(u,o)){var i=u[o];p(t.node,i.type,i.propName,r)}else o.startsWith(d)?t.node.setAttributeNS(f,o,r):o.startsWith(v)?t.node.setAttributeNS(m,o,r):(o in t.node&&p(t.node,a,o,r),t.node.setAttribute(o,r||""))}}(o,i)})(r,i),function(n,r,i){var a=n.children.length,s=r.children.length;if(a||s){var c=function(n){var o={};for(var r in t(n,(function(t){var n=t.attributes["data-key"];(function(t,n){return!(!n||e(t,n)&&(console.warn("[OmDomDom]: Children with duplicate keys detected. Children with duplicate keys will be skipped, resulting in dropped node references. Keys must be unique and non-indexed."),1))})(o,n)&&(o[n]=t)})),o)return o}(r.children),u=Array(a);t(n.children,void 0!==c?function(e,t){var n=r.node.childNodes,a=e.attributes["data-key"];if(Object.prototype.hasOwnProperty.call(c,a)){var s=c[a];Array.prototype.indexOf.call(n,s.node)!==t&&o(r,s,n[t]),u[t]=s,delete c[a],i(e,u[t])}else o(r,e,n[t]),u[t]=e}:function(e,t){var n=r.children[t];void 0!==n?(i(e,n),u[t]=n):(r.node.appendChild(e.node),u[t]=e)}),r.children=u;var l=r.node.childNodes.length,d=l-a;if(d>0)for(;d>0;)r.node.removeChild(r.node.childNodes[l-1]),l--,d--}}(r,i,n)}},y=function n(o){var r,i=arguments.length>1&&void 0!==arguments[1]&&arguments[1];"string"==typeof o&&(r=o.trim().replace(/\s+</g,"<").replace(/>\s+/g,">"),o=(new DOMParser).parseFromString(r,"text/html").body);var a="BODY"===o.tagName,s=o.childNodes,c=s?s.length:0;if(a){if(c>1)throw new Error("[OmDomDom]: Your element should not have more than one root node.");if(0===c)throw new Error("[OmDomDom]: Your element should have at least one root node.");return n(s[0])}var l=3===o.nodeType?"text":8===o.nodeType?"comment":o.tagName.toLowerCase(),d=i||"svg"===l,f=1===o.nodeType?function(t){var n=function(t){return Array.prototype.reduce.call(t.attributes,(function(t,n){return e(u,n.name)||(t[n.name]=n.value),t}),{})}(t);return function(e,t){for(var n in u){var o=u[n].propName,r=e.getAttribute(n);n===u.style.attrName?t[n]=e.style[o]:"string"==typeof r&&(t[n]=r)}}(t,n),n}(o):{},v=c>0?null:o.textContent,m=Array(c);return t(s,(function(e,t){m[t]=n(e,d)})),{type:l,attributes:f,children:m,content:v,node:o,isSVGContext:d}};function g(e,t){var n=[];for(var o of t){let t=e(o);if(!t)break;n.push(t)}return n}function w(e){return{cookies:e.filter((e=>"Cookie"==e.key)).map((e=>e.value)),error:b("Error",e),query:b("Query",e),pageTitle:b("PageTitle",e),events:I("Event",e).map(k),actions:I("Trigger",e).map(q)}}function E(e){return w(g(C,e.trim().split("\n")))}function b(e,t){return t.find((t=>t.key==e))?.value}function I(e,t){return t.filter((t=>t.key==e)).map((e=>e.value))}function C(e){let t=e.match(/^(\w+)\: (.*)$/);if(t)return{key:t[1],value:t[2]}}function k(e){let[t,n]=T(e);return{name:t,detail:JSON.parse(n)}}function q(e){let[t,n]=T(e);return[t,n]}function T(e){let t=e.indexOf("|");if(-1===t){let t=new Error("Bad Encoding, Expected Segment");throw t.message=e,t}return[e.slice(0,t),e.slice(t+1)]}function L(e){if(!e)return;const t=new URLSearchParams;return e.forEach(((e,n)=>{t.append(n,e)})),t}let S=0;function A(e,t){return e+" "+function(e){return""==e?"|":e.replace(/_/g,"\\_").replace(/\s+/g,"_")}(t)}const M=`${"https:"===window.location.protocol?"wss:":"ws:"}//${window.location.host}${window.location.pathname}`;class x extends Error{constructor(e,t){super(e+"\n"+t),this.name="ProtocolError"}}var R=n(296);const D=e=>void 0!==e?.runAction;function O(e){let t=new Event("hyp-content",{bubbles:!0});e.dispatchEvent(t)}function N(e,t){e.querySelectorAll("[id]").forEach((n=>{n.runAction=function(e){return t(n,e)},n.concurrency=n.dataset.concurrency||"Drop",n.cancelActiveRequest=function(){n.activeRequest&&!n.activeRequest?.isCancelled&&(n.activeRequest.isCancelled=!0)},O(e)}))}function H(e,t){document.addEventListener(e,(function(n){if(!(n.target instanceof HTMLElement))return void console.warn("listenKeyEvent received event with non HTMLElment as EventTarget: %o",n);let o=n.target,r="on"+e+n.key,i=o.dataset[r];if(!i)return;n.preventDefault();const a=Q(o);a?t(a,i):console.error("Missing target: ",o)}))}function V(e,t){document.addEventListener(e,(function(n){if(!(n.target instanceof HTMLElement))return void console.warn("listenBubblingEvent received an event with non HTMLElment as EventTarget: %o",n);let o=n.target.closest("[data-on"+e+"]");if(!o)return;n.preventDefault();let r=Q(o);if(!r)return void console.error("Missing target: ",o);const i=o.dataset["on"+e];void 0!==i?t(r,i):console.error("Missing action: ",o,e)}))}function j(e){e.querySelectorAll("[data-onload]").forEach((e=>{let t=parseInt(e.dataset.delay||"")||0,n=e.dataset.onload;setTimeout((()=>{let t=Q(e);if(e.dataset.onload!=n)return;const o=new CustomEvent("hyp-load",{bubbles:!0,detail:{target:t,onLoad:n}});e.dispatchEvent(o)}),t)}))}function P(e){e.querySelectorAll("[data-onmouseenter]").forEach((e=>{let t=e.dataset.onmouseenter,n=U(e);e.onmouseenter=()=>{const o=new CustomEvent("hyp-mouseenter",{bubbles:!0,detail:{target:n,onMouseEnter:t}});e.dispatchEvent(o)}}))}function B(e){e.querySelectorAll("[data-onmouseleave]").forEach((e=>{let t=e.dataset.onmouseleave,n=U(e);e.onmouseleave=()=>{const o=new CustomEvent("hyp-mouseleave",{bubbles:!0,detail:{target:n,onMouseLeave:t}});e.dispatchEvent(o)}}))}function Q(e){const t=U(e);if(D(t))return t;console.error("Non HyperView target: ",t)}function U(e){let t=function(e){let t=e.closest("[data-target]");return t?.dataset.target||e.closest("[id]")?.id}(e),n=t&&document.getElementById(t);if(n)return n;console.error("Cannot find target: ",t,e)}let W,$=n(147);console.log("Hyperbole "+$.version+"b");let F=new Set;async function _(e,t,n){if(e.activeRequest&&!e.activeRequest?.isCancelled&&"Drop"==e.concurrency)return void console.warn("Drop action overlapping with active request ("+e.activeRequest+")",t);e._timeout=window.setTimeout((()=>{e.classList.add("hyp-loading")}),100);let o=e.dataset.state,r={requestId:++S,isCancelled:!1},i=function(e,t,n,o,r){return{viewId:e,action:t,state:n,requestId:o,meta:[{key:"Cookie",value:decodeURI(document.cookie)},{key:"Query",value:window.location.search}],form:L(r)}}(e.id,t,o,r.requestId,n);e.activeRequest=r,z.sendAction(i)}function G(e){let t=e.targetViewId||e.viewId,n=document.getElementById(t);if(!D(n))return void console.error("Missing Update HyperView Target: ",t,e);if(n.activeRequest?.requestId&&e.requestId<n.activeRequest.requestId)return console.warn("Ignore Stale Action ("+e.requestId+") vs ("+n.activeRequest.requestId+"): "+e.action),n;if(n.activeRequest?.isCancelled)return console.warn("Cancelled request",n.activeRequest?.requestId),delete n.activeRequest,n;let o=function(e){const t=(new DOMParser).parseFromString(e,"text/html"),n=t.querySelector("style");return{content:t.querySelector("div"),css:n}}(e.body);if(!o.content)return console.error("Empty Response!",e.body),n;!function(e){if(!e)return;const t=e.sheet?.cssRules;if(t)for(let e=0;e<t.length;e++){const n=t.item(e);n&&0==F.has(n.cssText)&&W.sheet&&(W.sheet.insertRule(n.cssText),F.add(n.cssText))}}(o.css);const r=y(n);let i=y(o.content),a=i.attributes;if(!e.meta.error&&a.id!=n.id)return void console.error("Mismatched ViewId in update - ",a.id," target:",n.id);let s=a["data-state"];i.attributes=r.attributes,h(i,r);let c=document.getElementById(n.id);return c?(O(c),void 0===s||"()"==s?delete c.dataset.state:c.dataset.state=s,K(e.meta,c),J(e.meta.cookies??[]),j(c),P(c),B(c),function(e){let t=e.querySelector("[autofocus]");t?.focus&&t.focus(),e.querySelectorAll("input[value]").forEach((e=>{let t=e.getAttribute("value");null!==t&&(e.value=t)})),e.querySelectorAll("input[type=checkbox]").forEach((e=>{let t="True"==e.dataset.checked;e.checked=t}))}(c),N(c,_),n):(console.warn("Target Missing: ",n.id),n)}function J(e){e.forEach((e=>{console.log("SetCookie: ",e),document.cookie=e}))}function K(e,t){null!=e.query&&function(e){if(e!=function(){const e=window.location.search;return e.startsWith("?")?e.substring(1):e}()){""!=e&&(e="?"+e);let t=location.pathname+e;window.history.replaceState({},"",t)}}(e.query),null!=e.pageTitle&&(document.title=e.pageTitle),e.events?.forEach((e=>{Y(e,t)})),e.actions?.forEach((([e,t])=>{X(e,t)}))}function Y(e,t){setTimeout((()=>{let n=new CustomEvent(e.name,{bubbles:!0,detail:e.detail});(t||document).dispatchEvent(n)}),10)}function X(e,t){setTimeout((()=>{let n=window.Hyperbole?.hyperView(e);n&&_(n,t)}),10)}document.addEventListener("DOMContentLoaded",(function(){K(E(document.getElementById("hyp.metadata")?.innerText??""),null);const e=document.body.querySelector("style");var t;null!==e?W=e:(console.warn("rootStyles missing from page, creating..."),W=document.createElement("style"),W.type="text/css",document.body.appendChild(W)),t=async function(e,t){_(e,t)},document.addEventListener("hyp-load",(function(e){let n=e.detail.onLoad,o=e.detail.target;t(o,n)})),document.addEventListener("hyp-mouseenter",(function(e){let n=e.detail.onMouseEnter,o=e.detail.target;t(o,n)})),document.addEventListener("hyp-mouseleave",(function(e){let n=e.detail.onMouseLeave,o=e.detail.target;t(o,n)})),j(document.body),P(document.body),B(document.body),N(document.body,_),V("click",(async function(e,t){_(e,t)})),V("dblclick",(async function(e,t){_(e,t)})),H("keydown",(async function(e,t){_(e,t)})),H("keyup",(async function(e,t){_(e,t)})),document.addEventListener("submit",(function(e){if(!(e.target instanceof HTMLFormElement))return void console.warn("listenFormSubmit received an event with non HTMLElment as EventTarget: %o",e);let t=e.target;if(!t.dataset.onsubmit)return void console.error("Missing onSubmit: ",t);e.preventDefault();let n=Q(t);const o=new FormData(t);n?async function(e,t,n){_(e,t,n)}(n,t.dataset.onsubmit,o):console.error("Missing target: ",t)})),document.addEventListener("change",(function(e){if(!(e.target instanceof HTMLElement))return void console.warn("listenChange received an event with non HTMLElment as EventTarget: %o",e);let t=e.target.closest("[data-onchange]");if(!t)return;if(e.preventDefault(),null===t.value)return void console.error("Missing input value:",t);let n=Q(t);n?t.dataset.onchange?async function(e,t){_(e,t)}(n,A(t.dataset.onchange,t.value)):console.error("Missing onchange: ",t):console.error("Missing target: listenChange")})),document.addEventListener("input",(function(e){if(!(e.target instanceof HTMLElement))return void console.warn("listenInput received an event with non HTMLElment as EventTarget: %o",e);const t=e.target.closest("[data-oninput]");if(!t)return;let n=parseInt(t.dataset.delay||"")||250;n<250&&console.warn("Input delay < 250 can result in poor performance."),e.preventDefault();const o=Q(t);o?(function(e){"Replace"==e.concurrency&&e.cancelActiveRequest()}(o),t.debouncedCallback||(t.debouncedCallback=R((()=>{if(!t.dataset.oninput)return void console.error("Missing onInput: ",t);const e=A(t.dataset.oninput,t.value);!async function(e,t){_(e,t)}(o,e)}),n)),t.debouncedCallback()):console.error("Missing target: ",t)}))}));const z=new class{constructor(e=M){this.hasEverConnected=!1,this.isConnected=!1,this.reconnectDelay=0,this.queue=[],this.events=new EventTarget;const t=new WebSocket(e);this.socket=t}connect(e=M,t=!1){const n=t?new WebSocket(e):this.socket;function o(e){console.error("Connect Error",e)}function r(e){console.error("Socket Error",e)}this.socket=n,n.addEventListener("error",o),n.addEventListener("open",(e=>{console.log("Websocket Connected"),this.hasEverConnected&&document.dispatchEvent(new Event("hyp-socket-reconnect")),this.isConnected=!0,this.hasEverConnected=!0,this.reconnectDelay=1e3,n.removeEventListener("error",o),n.addEventListener("error",r),document.dispatchEvent(new Event("hyp-socket-connect")),this.runQueue()})),n.addEventListener("close",(t=>{console.log("CLOSE SOCKET"),this.isConnected&&document.dispatchEvent(new Event("hyp-socket-disconnect")),this.isConnected=!1,n.removeEventListener("error",r),this.hasEverConnected&&(console.log("Reconnecting in "+this.reconnectDelay/1e3+"s"),setTimeout((()=>this.connect(e,!0)),this.reconnectDelay)),n.removeEventListener("error",r)})),n.addEventListener("message",(e=>this.onMessage(e)))}async sendAction(e){if(this.isConnected){let t=function(e){let t=["|ACTION|","ViewId: "+e.viewId,"Action: "+e.action];return e.state&&t.push("State: "+e.state),t.push("RequestId: "+e.requestId),[t.join("\n"),(o=e.meta,o.map((e=>e.key+": "+e.value)).join("\n"))].join("\n")+((n=e.form)?"\n\n"+n:"");var n,o}(e);this.socket.send(t)}else this.queue.push(e)}runQueue(){let e=this.queue.pop();e&&(console.log("runQueue: ",e),this.sendAction(e),this.runQueue())}onMessage(e){let{command:t,metas:n,rest:o}=function(e){let t=e.split("\n"),n=t[0],o=g(C,t.slice(1));return{command:n,metas:o,rest:function(e,t){let n=0;for(;n<t.length&&""==t[n];)n++;return t.slice(n)}(0,t.slice(o.length+1))}}(e.data),r=parseInt(i("RequestId"),0);function i(t){let o=b(t,n);if(!o)throw new x("Missing Required Metadata: "+t,e.data);return o}function a(e){let t=i("ViewId"),o=i("Action");return{requestId:r,targetViewId:void 0,viewId:t,action:o,meta:w(n),body:e.join("\n")}}switch(t){case"|UPDATE|":return this.dispatchEvent(new CustomEvent("update",{detail:function(e){let t=a(e);return t.targetViewId=b("TargetViewId",n),t}(o)}));case"|RESPONSE|":return this.dispatchEvent(new CustomEvent("response",{detail:a(o)}));case"|REDIRECT|":return this.dispatchEvent(new CustomEvent("redirect",{detail:function(e){let t=e[0];return{requestId:r,meta:w(n),url:t}}(o)}));case"|TRIGGER|":return this.dispatchEvent(new CustomEvent("trigger",{detail:function(e){let{requestId:t,meta:n,viewId:o,action:r}=a(e),[s,c]=q(i("Trigger"));return{requestId:t,meta:n,viewId:o,action:r,targetViewId:s,targetAction:c}}(o)}));case"|EVENT|":return this.dispatchEvent(new CustomEvent("event",{detail:function(e){let{requestId:t,meta:n,viewId:o,action:r}=a(e);return{requestId:t,meta:n,viewId:o,action:r,event:k(i("Event"))}}(o)}));default:throw new x("Unknown Server Command: "+t,e.data)}}addEventListener(e,t){this.events.addEventListener(e,t)}dispatchEvent(e){this.events.dispatchEvent(e)}disconnect(){this.isConnected=!1,this.hasEverConnected=!1,this.socket.close()}};z.connect(),z.addEventListener("update",(e=>{G(e.detail)})),z.addEventListener("response",(e=>function(e){let t=G(e);t&&(delete t.activeRequest,clearTimeout(t._timeout),t.classList.remove("hyp-loading"))}(e.detail))),z.addEventListener("redirect",(e=>{return t=e.detail,console.log("REDIRECT",t),J(t.meta.cookies??[]),void(window.location.href=t.url);var t})),z.addEventListener("trigger",(e=>{var t;X((t=e.detail).targetViewId,t.targetAction)})),z.addEventListener("event",(e=>function(e){let t=document.getElementById(e.viewId);Y(e.event,t)}(e.detail))),window.Hyperbole={runAction:_,parseMetadata:E,action:function(e,...t){return t.reduce(((e,t)=>e+" "+JSON.stringify(t)),e)},hyperView:function(e){let t=document.getElementById(e);if(D(t))return t;console.error("Element id="+e+" was not a HyperView")},socket:z}})()})();
//# sourceMappingURL=hyperbole.js.map

================================================
FILE: client/dist/hyperview.d.ts
================================================
import { type Request } from "./action";
export interface HyperView extends HTMLElement {
    runAction(action: string): Promise<void>;
    activeRequest?: Request;
    cancelActiveRequest(): void;
    concurrency: ConcurrencyMode;
    _timeout?: number;
}
export declare const isHyperView: (ele: any) => ele is HyperView;
export type ConcurrencyMode = string;
export declare function dispatchContent(node: HTMLElement): void;
export declare function enrichHyperViews(node: HTMLElement, runAction: (target: HyperView, action: string, form?: FormData) => Promise<void>): void;


================================================
FILE: client/dist/index.d.ts
================================================
import { SocketConnection } from './sockets';
import { ViewId, Metadata } from './message';
import { HyperView } from "./hyperview";
declare global {
    interface Window {
        Hyperbole?: HyperboleAPI;
    }
    interface DocumentEventMap {
        "hyp-load": CustomEvent;
        "hyp-mouseenter": CustomEvent;
        "hyp-mouseleave": CustomEvent;
    }
}
export interface HyperboleAPI {
    runAction(target: HTMLElement, action: string, form?: FormData): Promise<void>;
    action(con: string, ...params: any[]): string;
    hyperView(viewId: ViewId): HyperView | undefined;
    parseMetadata(input: string): Metadata;
    socket: SocketConnection;
}


================================================
FILE: client/dist/lib.d.ts
================================================
export declare function takeWhileMap<T, A>(pred: (val: T) => A | undefined, lines: T[]): A[];
export declare function dropWhile<T, A>(pred: (val: T) => A | undefined, lines: T[]): T[];


================================================
FILE: client/dist/message.d.ts
================================================
export type Meta = {
    key: string;
    value: string;
};
export type ViewId = string;
export type RequestId = number;
export type EncodedAction = string;
export type ViewState = string;
export type RemoteEvent = {
    name: string;
    detail: unknown;
};
export declare function renderMetas(meta: Meta[]): string;
export type Metadata = {
    cookies?: string[];
    error?: string;
    query?: string;
    events?: RemoteEvent[];
    actions?: [ViewId, EncodedAction][];
    pageTitle?: string;
};
export declare function toMetadata(meta: Meta[]): Metadata;
export declare function parseMetadata(input: string): Metadata;
export declare function metaValue(key: string, metas: Meta[]): string | undefined;
export declare function metaValuesAll(key: string, metas: Meta[]): string[];
export type SplitMessage = {
    command: string;
    metas: Meta[];
    rest: string[];
};
export declare function splitMessage(message: string): SplitMessage;
export declare function parseMeta(line: string): Meta | undefined;
export declare function parseRemoteEvent(input: string): RemoteEvent;
export declare function parseAction(input: string): [ViewId, string];


================================================
FILE: client/dist/response.d.ts
================================================
import { ViewId, Metadata } from './message';
export type Response = {
    meta: Metadata;
    body: ResponseBody;
};
export type ResponseBody = string;
export declare function parseResponse(res: ResponseBody): LiveUpdate;
export type LiveUpdate = {
    content: HTMLElement | null;
    css: HTMLStyleElement | null;
};
export declare class FetchError extends Error {
    viewId: ViewId;
    body: string;
    constructor(viewId: ViewId, msg: string, body: string);
}


================================================
FILE: client/dist/sockets.d.ts
================================================
import { ActionMessage } from './action';
import { ResponseBody } from "./response";
import { ViewId, RequestId, EncodedAction, Metadata, RemoteEvent } from "./message";
interface SocketConnectionEventMap {
    "update": CustomEvent<Update>;
    "response": CustomEvent<Update>;
    "redirect": CustomEvent<Redirect>;
    "trigger": CustomEvent<Trigger>;
    "event": CustomEvent<JSEvent>;
}
export declare class SocketConnection {
    socket: WebSocket;
    hasEverConnected: Boolean;
    isConnected: Boolean;
    reconnectDelay: number;
    queue: ActionMessage[];
    events: EventTarget;
    constructor(addr?: string);
    connect(addr?: string, createSocket?: boolean): void;
    sendAction(action: ActionMessage): Promise<void>;
    private runQueue;
    private onMessage;
    addEventListener<K extends keyof SocketConnectionEventMap>(e: K, cb: (ev: SocketConnectionEventMap[K]) => void): void;
    dispatchEvent<K extends keyof SocketConnectionEventMap>(e: SocketConnectionEventMap[K]): void;
    disconnect(): void;
}
export type Update = {
    requestId: RequestId;
    meta: Metadata;
    viewId: ViewId;
    targetViewId?: ViewId;
    action: EncodedAction;
    body: ResponseBody;
};
export type Redirect = {
    requestId: RequestId;
    meta: Metadata;
    url: string;
};
export type Trigger = {
    requestId: RequestId;
    meta: Metadata;
    viewId: ViewId;
    action: EncodedAction;
    targetViewId: ViewId;
    targetAction: string;
};
export type JSEvent = {
    requestId: RequestId;
    meta: Metadata;
    viewId: ViewId;
    action: EncodedAction;
    event: RemoteEvent;
};
export type MessageType = string;
export declare class ProtocolError extends Error {
    constructor(description: string, body: string);
}
export {};


================================================
FILE: client/package.json
================================================
{
  "name": "web-ui",
  "version": "0.6.0",
  "description": "Development -----------",
  "main": "index.js",
  "directories": {
    "client": "client"
  },
  "scripts": {
    "build": "npx webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "ts-loader": "^9.4.1",
    "typescript": "^4.8.3",
    "uglify": "^0.1.5",
    "webpack": "^5.88.2",
    "webpack-cli": "^4.10.0"
  },
  "dependencies": {
    "omdomdom": "^0.3.2",
    "debounce": "^2.2.0"
  }
}


================================================
FILE: client/src/action.ts
================================================

import { takeWhileMap } from "./lib"
import { Meta, ViewId, RequestId, EncodedAction, ViewState } from "./message"
import * as message from "./message"



export type ActionMessage = {
  viewId: ViewId
  action: EncodedAction
  requestId: RequestId
  state?: ViewState
  meta: Meta[]
  form: URLSearchParams | undefined
}




export function actionMessage(id: ViewId, action: EncodedAction, state: ViewState | undefined, reqId: RequestId, form?: FormData): ActionMessage {
  let meta: Meta[] = [
    { key: "Cookie", value: decodeURI(document.cookie) },
    { key: "Query", value: window.location.search }
  ]

  return { viewId: id, action, state, requestId: reqId, meta, form: toSearch(form) }
}

export function toSearch(form?: FormData): URLSearchParams | undefined {
  if (!form) return undefined

  const params = new URLSearchParams()

  form.forEach((value, key) => {
    params.append(key, value as string)
  })

  return params
}

export function renderActionMessage(msg: ActionMessage): string {
  let header = [
    "|ACTION|",
    "ViewId: " + msg.viewId,
    "Action: " + msg.action,
  ]


  if (msg.state) {
    header.push("State: " + msg.state)
  }

  header.push("RequestId: " + msg.requestId)

  return [
    header.join('\n'),
    message.renderMetas(msg.meta),
  ].join('\n') + renderForm(msg.form)
}


export function renderForm(form: URLSearchParams | undefined): string {
  if (!form) return ""
  return "\n\n" + form
}

let globalRequestId: RequestId = 0

export type Request = {
  requestId: RequestId
  isCancelled: boolean
}

export function newRequest(): Request {
  let requestId = ++globalRequestId
  return { requestId, isCancelled: false }
}



// Sanitized Encoding ------------------------------------

export function encodedParam(action: string, param: string): string {
  return action + ' ' + sanitizeParam(param)
}

function sanitizeParam(param: string): string {
  if (param == "") {
    return "|"
  }

  return param.replace(/_/g, "\\_").replace(/\s+/g, "_")
}


================================================
FILE: client/src/browser.ts
================================================

export function setQuery(query: string) {
  if (query != currentQuery()) {
    if (query != "") query = "?" + query
    let url = location.pathname + query
    // console.log("history.replaceState(", url, ")")
    window.history.replaceState({}, "", url)
  }
}

function currentQuery(): string {
  const query = window.location.search;
  return query.startsWith('?') ? query.substring(1) : query;
}


================================================
FILE: client/src/events.ts
================================================

import * as debounce from 'debounce'
import { encodedParam } from './action'
import { HyperView, isHyperView } from './hyperview'

export type UrlFragment = string

export function listenKeydown(cb: (target: HyperView, action: string) => void): void {
  listenKeyEvent("keydown", cb)
}

export function listenKeyup(cb: (target: HyperView, action: string) => void): void {
  listenKeyEvent("keyup", cb)
}

export function listenKeyEvent(event: "keyup" | "keydown", cb: (target: HyperView, action: string) => void): void {

  document.addEventListener(event, function(e: KeyboardEvent) {
    if (!(e.target instanceof HTMLElement)) {
      console.warn("listenKeyEvent received event with non HTMLElment as EventTarget: %o", e)
      return
    }
    let source = e.target

    let datasetKey = "on" + event + e.key
    let action = source.dataset[datasetKey]
    if (!action) return

    e.preventDefault()
    const target =  nearestHyperViewTarget(source)
    if (!target) {
      console.error("Missing target: ", source)
      return
    }
    cb(target, action)
  })
}

export function listenBubblingEvent(event: string, cb: (_target: HyperView, action: string) => void): void {
  document.addEventListener(event, function(e) {
    if (!(e.target instanceof HTMLElement)) {
      console.warn("listenBubblingEvent received an event with non HTMLElment as EventTarget: %o", e)
      return
    }
    let el = e.target

    // clicks can fire on internal elements. Find the parent with a click handler
    let source = el.closest<HTMLElement>("[data-on" + event + "]")
    if (!source) return

    e.preventDefault()
    let target = nearestHyperViewTarget(source)
    if (!target) {
      console.error("Missing target: ", source)
      return
    }
    const action = source.dataset["on" + event]
    if (action === undefined) {
      console.error("Missing action: ", source, event)
      return
    }
    cb(target, action)
  })
}

export function listenClick(cb: (target: HyperView, action: string) => void): void {
  listenBubblingEvent("click", cb)
}

export function listenDblClick(cb: (target: HyperView, action: string) => void): void {
  listenBubblingEvent("dblclick", cb)
}


export function listenTopLevel(cb: (target: HyperView, action: string) => void): void {
  document.addEventListener("hyp-load", function(e: CustomEvent) {
    let action = e.detail.onLoad
    let target = e.detail.target
    cb(target, action)
  })

  document.addEventListener("hyp-mouseenter", function(e: CustomEvent) {
    let action = e.detail.onMouseEnter
    let target = e.detail.target
    cb(target, action)
  })

  document.addEventListener("hyp-mouseleave", function(e: CustomEvent) {
    let action = e.detail.onMouseLeave
    let target = e.detail.target
    cb(target, action)
  })
}


export function listenLoad(node: HTMLElement): void {

  // it doesn't really matter WHO runs this except that it should have target
  node.querySelectorAll<HTMLElement>("[data-onload]").forEach((load) => {
    let delay = parseInt(load.dataset.delay || "") || 0
    let onLoad = load.dataset.onload
    // console.log("load start", load.dataset.onLoad)

    // load no longer exists!
    // we should clear the timeout or back out if the dom is replaced in the interem
    setTimeout(() => {
      let target = nearestHyperViewTarget(load)
      // console.log("load go", load.dataset.onLoad)

      if (load.dataset.onload != onLoad) {
        // the onLoad no longer exists
        return
      }

      const event = new CustomEvent("hyp-load", { bubbles: true, detail: { target, onLoad } })
      load.dispatchEvent(event)
    }, delay)
  })
}

export function listenMouseEnter(node: HTMLElement): void {
  node.querySelectorAll<HTMLElement>("[data-onmouseenter]").forEach((node) => {
    let onMouseEnter = node.dataset.onmouseenter

    let target = nearestAnyTarget(node)

    node.onmouseenter = () => {
      const event = new CustomEvent("hyp-mouseenter", { bubbles: true, detail: { target, onMouseEnter } })
      node.dispatchEvent(event)
    }
  })
}

export function listenMouseLeave(node: HTMLElement): void {
  node.querySelectorAll<HTMLElement>("[data-onmouseleave]").forEach((node) => {
    let onMouseLeave = node.dataset.onmouseleave

    let target = nearestAnyTarget(node)

    node.onmouseleave = () => {
      const event = new CustomEvent("hyp-mouseleave", { bubbles: true, detail: { target, onMouseLeave } })
      node.dispatchEvent(event)
    }
  })
}


export function listenChange(cb: (target: HyperView, action: string) => void): void {
  document.addEventListener("change", function(e) {
    if (!(e.target instanceof HTMLElement)) {
      console.warn("listenChange received an event with non HTMLElment as EventTarget: %o", e)
      return
    }
    let el = e.target

    let source = el.closest<HTMLInputElement>("[data-onchange]")

    if (!source) return
    e.preventDefault()

    if (source.value === null) {
      console.error("Missing input value:", source)
      return
    }

    let target = nearestHyperViewTarget(source)
    if (!target) {
      console.error("Missing target: listenChange")
      return
    }
    if (!source.dataset.onchange) {
      console.error("Missing onchange: ", source)
      return
    }
    let action = encodedParam(source.dataset.onchange, source.value)
    cb(target, action)
  })
}

interface LiveInputElement extends HTMLInputElement {
  debouncedCallback?: Function;
}

export function listenInput(startedTyping: (target: HyperView) => void, cb: (target: HyperView, action: string) => void): void {
  document.addEventListener("input", function(e) {
    if (!(e.target instanceof HTMLElement)) {
      console.warn("listenInput received an event with non HTMLElment as EventTarget: %o", e)
      return
    }
    let el = e.target
    const source = el.closest<LiveInputElement>("[data-oninput]")

    if (!source) return

    let delay = parseInt(source.dataset.delay || "") || 250
    if (delay < 250) {
      console.warn("Input delay < 250 can result in poor performance.")
    }

    e.preventDefault()

    const target = nearestHyperViewTarget(source)
    if (!target) {
      console.error("Missing target: ", source)
      return
    }

    // I want to CANCEL the active request as soon as we start typing
    startedTyping(target)

    if (!source.debouncedCallback) {
      source.debouncedCallback = debounce(() => {
        if (!source.dataset.oninput) {
          console.error("Missing onInput: ", source)
          return
        }
        const action = encodedParam(source.dataset.oninput, source.value)
        cb(target, action)
      }, delay)
    }

    source.debouncedCallback()
  })
}



export function listenFormSubmit(cb: (target: HyperView, action: string, form: FormData) => void): void {
  document.addEventListener("submit", function(e) {
    if (!(e.target instanceof HTMLFormElement)) {
      console.warn("listenFormSubmit received an event with non HTMLElment as EventTarget: %o", e)
      return
    }
    let form = e.target


    if (!form.dataset.onsubmit) {
      console.error("Missing onSubmit: ", form)
      return
    }

    e.preventDefault()

    let target = nearestHyperViewTarget(form)
    const formData = new FormData(form)
    if (!target) {
      console.error("Missing target: ", form)
      return
    }
    cb(target, form.dataset.onsubmit, formData)
  })
}

function nearestTargetId(node: HTMLElement): string | undefined {
  let targetData = node.closest<HTMLElement>("[data-target]")
  return targetData?.dataset.target || node.closest("[id]")?.id
}

function nearestHyperViewTarget(node: HTMLElement): HyperView | undefined {
  const target = nearestAnyTarget(node)

  if (!isHyperView(target)) {
    console.error("Non HyperView target: ", target)
    return
  }

  return target
}

function nearestAnyTarget(node: HTMLElement): HTMLElement | undefined {
  let targetId = nearestTargetId(node)
  let target = targetId && document.getElementById(targetId)

  if (!target) {
    console.error("Cannot find target: ", targetId, node)
    return
  }

  return target
}


================================================
FILE: client/src/http.ts
================================================
// import { ActionMessage, ParsedResponse } from './action'
// import { Response, FetchError } from "./response"

// export async function sendActionHttp(msg: ActionMessage): Promise<Response> {
//   // console.log("HTTP sendAction", msg.url.toString())
//   let url = window.location.href
//   let res = await fetch(url, {
//     method: "POST",
//     headers:
//     {
//       'Accept': 'text/html',
//       'Content-Type': 'application/x-www-form-urlencoded',
//       'Hyp-RequestId': msg.requestId,
//       'Hyp-ViewId': msg.viewId,
//       'Hyp-Action': msg.action
//     },
//     body: msg.form,
//     // we never want this to be redirected
//     redirect: "manual"
//   })
//
//   let body = await res.text()
//   let { metadata, rest } = parseMetadataHttp(body)
//
//   if (!res.ok) {
//     throw new FetchError(msg.viewId, body, body)
//   }
//
//   let response: Response = {
//     meta: metadata,
//     body: rest.join('\n')
//   }
//
//   return response
// }


// export function parseMetadataHttp(inp: string): ParsedResponse {
//   let lines = inp.split("\n")
//   // drop the <script> start line
//   let { metadata, rest } = splitMetadata(lines.slice(1))
//   // drop the </script> end line and 2x whitespace
//   return { metadata, rest: rest.slice(2) }
// }
//
//


================================================
FILE: client/src/hyperview.ts
================================================
import { type Request } from "./action";

export interface HyperView extends HTMLElement {
  runAction(action: string): Promise<void>;
  activeRequest?: Request;
  cancelActiveRequest(): void;
  concurrency: ConcurrencyMode;
  _timeout?: number;
}

export const isHyperView = (ele: any): ele is HyperView => {
  return ele?.runAction !== undefined;
};

export type ConcurrencyMode = string;

export function dispatchContent(node: HTMLElement): void {
  let event = new Event("hyp-content", { bubbles: true })
  node.dispatchEvent(event)
}

export function enrichHyperViews(node: HTMLElement, runAction: (target: HyperView, action: string, form?: FormData) => Promise<void>): void {
  // enrich all the hyperviews
  node.querySelectorAll<HyperView>("[id]").forEach((element) => {
    element.runAction = function(action: string) {
      return runAction(element, action)
    }

    element.concurrency = element.dataset.concurrency || "Drop"

    element.cancelActiveRequest = function() {
      if (element.activeRequest && !element.activeRequest?.isCancelled) {
        element.activeRequest.isCancelled = true
      }
    }

    dispatchContent(node)
  })
}


================================================
FILE: client/src/index.ts
================================================
import { patch, create } from "omdomdom/lib/omdomdom.es.js"
import { SocketConnection, Update, Redirect, Trigger, JSEvent } from './sockets'
import { listenChange, listenClick, listenDblClick, listenFormSubmit, listenLoad, listenTopLevel, listenInput, listenKeydown, listenKeyup, listenMouseEnter, listenMouseLeave } from './events'
import { actionMessage, newRequest } from './action'
import { ViewId, Metadata, parseMetadata, RemoteEvent, EncodedAction } from './message'
import { setQuery } from "./browser"
import { parseResponse, LiveUpdate } from './response'
import { dispatchContent, enrichHyperViews, HyperView, isHyperView } from "./hyperview"

let PACKAGE = require('../package.json');


// console.log("VERSION 2", INIT_PAGE, INIT_STATE)
console.log("Hyperbole " + PACKAGE.version + "b")


let rootStyles: HTMLStyleElement;
let addedRulesIndex = new Set();





// Run an action in a given HyperView
async function runAction(target: HyperView, action: string, form?: FormData) {

  if (target.activeRequest && !target.activeRequest?.isCancelled) {
    // Active Request!
    if (target.concurrency == "Drop") {
      console.warn("Drop action overlapping with active request (" + target.activeRequest + ")", action)
      return
    }
  }

  target._timeout = window.setTimeout(() => {
    // add loading after 100ms, not right away
    // if it runs shorter than that we probably don't want to show the user any loading feedback
    target.classList.add("hyp-loading")
  }, 100)

  let state = target.dataset.state

  let req = newRequest()
  let msg = actionMessage(target.id, action, state, req.requestId, form)

  // Set the requestId
  target.activeRequest = req

  sock.sendAction(msg)
}

function handleTrigger(trigger: Trigger) {
  runTrigger(trigger.targetViewId, trigger.targetAction)
}

function handleEvent(ev: JSEvent) {
  let target = document.getElementById(ev.viewId)
  runRemoteEvent(ev.event, target)
}

// TODO: redirect concurrency
function handleRedirect(red: Redirect) {
  console.log("REDIRECT", red)

  // the other metdata doesn't apply, they are all specific to the page
  applyCookies(red.meta.cookies ?? [])

  window.location.href = red.url
}

// in-process update
function handleResponse(res: Update) {
  // console.log("Handle Response", res)
  let target = handleUpdate(res)
  if (!target) return

  // clean up the request
  delete target.activeRequest
  clearTimeout(target._timeout)
  target.classList.remove("hyp-loading")
}

function handleUpdate(res: Update): HyperView | undefined {
  // console.log("|UPDATE|", res)

  let targetViewId = res.targetViewId || res.viewId
  let target = document.getElementById(targetViewId)

  if (!isHyperView(target)) {
    console.error("Missing Update HyperView Target: ", targetViewId, res)
    return
  }

  if (target.activeRequest?.requestId && res.requestId < target.activeRequest.requestId) {
    // this should only happen on Replace, since other requests should be dropped
    // but it's safe to assume we never want to apply an old requestId
    console.warn("Ignore Stale Action (" + res.requestId + ") vs (" + target.activeRequest.requestId + "): " + res.action)
    return target
  }
  else if (target.activeRequest?.isCancelled) {
    console.warn("Cancelled request", target.activeRequest?.requestId)
    delete target.activeRequest
    return target
  }

  let update: LiveUpdate = parseResponse(res.body)

  if (!update.content) {
    console.error("Empty Response!", res.body)
    return target
  }

  // First, update the stylesheet
  addCSS(update.css)


  // Patch the node
  const old: VNode = create(target)
  let next: VNode = create(update.content)
  let atts = next.attributes

  if (!res.meta.error && atts["id"] != target.id) {
    console.error("Mismatched ViewId in update - ", atts["id"], " target:", target.id)
    return
  }

  let state = atts["data-state"]
  next.attributes = old.attributes


  patch(next, old)


  // Emit relevant events
  let newTarget = document.getElementById(target.id)

  if (!newTarget) {
    console.warn("Target Missing: ", target.id)
    return target
  }

  dispatchContent(newTarget)

  // re-add state attribute 
  if (state === undefined || state == "()")
    delete newTarget.dataset.state
  else
    newTarget.dataset.state = state

  // execute the metadata, anything that doesn't interrupt the dom update
  runMetadata(res.meta, newTarget)
  applyCookies(res.meta.cookies ?? [])

  // now way for these to bubble)
  listenLoad(newTarget)
  listenMouseEnter(newTarget)
  listenMouseLeave(newTarget)
  fixInputs(newTarget)
  enrichHyperViews(newTarget, runAction)

  return target
}
// catch (err) {
//   console.error("Caught Error in HyperView (" + target.id + "):\n", err)
//
//   // Hyperbole catches handler errors, and the server controls what to display to the user on an error
//   //  but if you manage to crash your parent server process somehow, the response may be empty
//   target.innerHTML = err.body || "<div style='background:red;color:white;padding:10px'>Hyperbole Internal Error</div>"
// }

function applyCookies(cookies: string[]) {
  cookies.forEach((cookie: string) => {
    console.log("SetCookie: ", cookie)
    document.cookie = cookie
  })
}

function runMetadata(meta: Metadata, target: HTMLElement | null) {
  if (meta.query != null) {
    setQuery(meta.query)
  }

  if (meta.pageTitle != null) {
    document.title = meta.pageTitle
  }

  meta.events?.forEach((remoteEvent) => {
    runRemoteEvent(remoteEvent, target)
  })

  meta.actions?.forEach(([viewId, action]) => {
    runTrigger(viewId, action)
  })
}

function runRemoteEvent(remoteEvent: RemoteEvent, target: HTMLElement | null) {
  setTimeout(() => {
    let event = new CustomEvent(remoteEvent.name, { bubbles: true, detail: remoteEvent.detail })
    let eventTarget = target || document
    eventTarget.dispatchEvent(event)
  }, 10)
}

function runTrigger(viewId: ViewId, action: EncodedAction) {
  setTimeout(() => {
    let view = window.Hyperbole?.hyperView(viewId)
    if (view) {
      runAction(view, action)
    }
  }, 10)
}


function fixInputs(target: HTMLElement) {
  let focused = target.querySelector<HTMLInputElement>("[autofocus]")
  if (focused?.focus) {
    focused.focus()
  }

  target.querySelectorAll<HTMLInputElement>("input[value]").forEach((input) => {
    let val = input.getAttribute("value")
    if (val !== null) {
      input.value = val
    }
  })

  target.querySelectorAll<HTMLInputElement>("input[type=checkbox]").forEach((checkbox) => {
    let checked = checkbox.dataset.checked == "True"
    checkbox.checked = checked
  })
}

function addCSS(src: HTMLStyleElement | null) {
  if (!src) return;
  const rules = src.sheet?.cssRules
  if (!rules) return;
  for (let i = 0; i < rules.length; i++) {
    const rule = rules.item(i)
    if (rule && addedRulesIndex.has(rule.cssText) == false && rootStyles.sheet) {
      rootStyles.sheet.insertRule(rule.cssText);
      addedRulesIndex.add(rule.cssText);
    }
  }
}




function init() {
  // metadata attached to initial page loads need to be executed
  let meta = parseMetadata(document.getElementById("hyp.metadata")?.innerText ?? "")
  // runMetadataImmediate(meta)
  runMetadata(meta, null)

  const style = document.body.querySelector('style')

  if (style !== null) {
    rootStyles = style
  } else {
    console.warn("rootStyles missing from page, creating...")
    rootStyles = document.createElement("style")
    rootStyles.type = "text/css"
    document.body.appendChild(rootStyles)
  }

  listenTopLevel(async function(target: HyperView, action: string) {
    runAction(target, action)
  })

  listenLoad(document.body)
  listenMouseEnter(document.body)
  listenMouseLeave(document.body)
  enrichHyperViews(document.body, runAction)


  listenClick(async function(target: HyperView, action: string) {
    // console.log("CLICK", target.id, action)
    runAction(target, action)
  })

  listenDblClick(async function(target: HyperView, action: string) {
    // console.log("DBLCLICK", target.id, action)
    runAction(target, action)
  })

  listenKeydown(async function(target: HyperView, action: string) {
    // console.log("KEYDOWN", target.id, action)
    runAction(target, action)
  })

  listenKeyup(async function(target: HyperView, action: string) {
    // console.log("KEYUP", target.id, action)
    runAction(target, action)
  })

  listenFormSubmit(async function(target: HyperView, action: string, form: FormData) {
    // console.log("FORM", target.id, action, form)
    runAction(target, action, form)
  })

  listenChange(async function(target: HyperView, action: string) {
    runAction(target, action)
  })

  function onStartedTyping(target: HyperView) {
    if (target.concurrency == "Replace") {
      target.cancelActiveRequest()
    }
  }

  listenInput(onStartedTyping, async function(target: HyperView, action: string) {
    runAction(target, action)
  })
}






document.addEventListener("DOMContentLoaded", init)




const sock = new SocketConnection()
// Should we connect to the socket or not?
sock.connect()
sock.addEventListener("update", (ev: CustomEvent<Update>) => { handleUpdate(ev.detail) })
sock.addEventListener("response", (ev: CustomEvent<Update>) => handleResponse(ev.detail))
sock.addEventListener("redirect", (ev: CustomEvent<Redirect>) => handleRedirect(ev.detail))
sock.addEventListener("trigger", (ev: CustomEvent<Trigger>) => handleTrigger(ev.detail))
sock.addEventListener("event", (ev: CustomEvent<JSEvent>) => handleEvent(ev.detail))





type VNode = {
  // One of three value types are used:
  // - The tag name of the element
  // - "text" if text node
  // - "comment" if comment node
  type: string

  // An object whose key/value pairs are the attribute
  // name and value, respectively
  attributes: { [key: string]: string | undefined }

  // Is set to `true` if a node is an `svg`, which tells
  // Omdomdom to treat it, and its children, as such
  isSVGContext: Boolean

  // The content of a "text" or "comment" node
  content: string

  // An array of virtual node children
  children: Array<VNode>

  // The real DOM node
  node: Node
}





declare global {
  interface Window {
    Hyperbole?: HyperboleAPI;
  }
  interface DocumentEventMap {
    "hyp-load": CustomEvent;
    "hyp-mouseenter": CustomEvent;
    "hyp-mouseleave": CustomEvent;
  }
}

export interface HyperboleAPI {
  runAction(target: HTMLElement, action: string, form?: FormData): Promise<void>
  action(con: string, ...params: any[]): string
  hyperView(viewId: ViewId): HyperView | undefined
  parseMetadata(input: string): Metadata
  socket: SocketConnection
}

window.Hyperbole =
{
  runAction: runAction,
  parseMetadata: parseMetadata,
  action: function(con, ...params: any[]) {
    return params.reduce((str, param) => str + " " + JSON.stringify(param), con);
  },
  hyperView: function(viewId) {
    let element = document.getElementById(viewId)
    if (!isHyperView(element)) {
      console.error("Element id=" + viewId + " was not a HyperView")
      return
    }
    return element
  },
  socket: sock
}


================================================
FILE: client/src/lib.ts
================================================


export function takeWhileMap<T, A>(pred: (val: T) => A | undefined, lines: T[]): A[] {
  var output = []
  for (var line of lines) {
    let a = pred(line)
    if (a)
      output.push(a)
    else
      break;
  }

  return output
}

export function dropWhile<T, A>(pred: (val: T) => A | undefined, lines: T[]): T[] {
  let index = 0;
  while (index < lines.length && pred(lines[index])) {
    index++;
  }
  return lines.slice(index);
}



================================================
FILE: client/src/message.ts
================================================

import { takeWhileMap, dropWhile } from "./lib"



export type Meta = { key: string, value: string }
export type ViewId = string
export type RequestId = number
export type EncodedAction = string
export type ViewState = string

export type RemoteEvent = { name: string, detail: unknown }


export function renderMetas(meta: Meta[]): string {
  return meta.map(m => m.key + ": " + m.value).join('\n')
}

export type Metadata = {
  cookies?: string[]
  // redirect?: string
  error?: string
  query?: string
  events?: RemoteEvent[]
  actions?: [ViewId, EncodedAction][],
  pageTitle?: string
}


export function toMetadata(meta: Meta[]): Metadata {

  return {
    cookies: meta.filter(m => m.key == "Cookie").map(m => m.value),
    // redirect: metaValue("Redirect", meta),
    error: metaValue("Error", meta),
    query: metaValue("Query", meta),
    pageTitle: metaValue("PageTitle", meta),
    events: metaValuesAll("Event", meta).map(parseRemoteEvent),
    actions: metaValuesAll("Trigger", meta).map(parseAction),
  }
}

// viewId: meta.find(m => m.key == "VIEW-ID")?.value,

export function parseMetadata(input: string): Metadata {
  let metas = takeWhileMap(parseMeta, input.trim().split("\n"))
  return toMetadata(metas)
}


export function metaValue(key: string, metas: Meta[]): string | undefined {
  return metas.find(m => m.key == key)?.value
}

export function metaValuesAll(key: string, metas: Meta[]): string[] {
  return metas.filter(m => m.key == key).map(m => m.value)
}

export type SplitMessage = {
  command: string,
  metas: Meta[],
  rest: string[]
}


export function splitMessage(message: string): SplitMessage {
  let lines = message.split("\n")
  let command: string = lines[0]
  let metas: Meta[] = takeWhileMap(parseMeta, lines.slice(1))
  // console.log("Split Metadata", lines.length)
  // console.log(" [0]", lines[0])
  // console.log(" [1]", lines[1])
  let rest = dropWhile(l => l == "", lines.slice(metas.length + 1))

  return { command, metas, rest }
}

export function parseMeta(line: string): Meta | undefined {
  let match = line.match(/^(\w+)\: (.*)$/)
  if (match) {
    return {
      key: match[1],
      value: match[2]
    }
  }
}


export function parseRemoteEvent(input: string): RemoteEvent {
  let [name, data] = breakNextSegment(input)
  return {
    name,
    detail: JSON.parse(data)
  }
}

export function parseAction(input: string): [ViewId, string] {
  let [viewId, action] = breakNextSegment(input)
  return [viewId, action]
}

function breakNextSegment(input: string): [string, string] {
  let ix = input.indexOf('|')
  if (ix === -1) {
    let err = new Error("Bad Encoding, Expected Segment")
    err.message = input
    throw err
  }
  return [input.slice(0, ix), input.slice(ix + 1)]
}



================================================
FILE: client/src/response.ts
================================================

import { ViewId, Metadata } from './message'



export type Response = {
  meta: Metadata
  body: ResponseBody
}

export type ResponseBody = string

export function parseResponse(res: ResponseBody): LiveUpdate {
  const parser = new DOMParser()
  const doc = parser.parseFromString(res, 'text/html')
  const css = doc.querySelector<HTMLStyleElement>("style")
  const content = doc.querySelector<HTMLElement>("div")

  return {
    content: content,
    css: css
  }
}

export type LiveUpdate = {
  content: HTMLElement | null
  css: HTMLStyleElement | null
}


export class FetchError extends Error {
  viewId: ViewId
  body: string
  constructor(viewId: ViewId, msg: string, body: string) {
    super(msg)
    this.viewId = viewId
    this.name = "Fetch Error"
    this.body = body
  }
}


================================================
FILE: client/src/sockets.ts
================================================
import { ActionMessage, renderActionMessage } from './action'
import { ResponseBody } from "./response"
import * as message from "./message"
import { ViewId, RequestId, EncodedAction, metaValue, Metadata, RemoteEvent } from "./message"

const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const defaultAddress = `${protocol}//${window.location.host}${window.location.pathname}`

interface SocketConnectionEventMap {
  "update": CustomEvent<Update>;
  "response": CustomEvent<Update>;
  "redirect": CustomEvent<Redirect>;
  "trigger": CustomEvent<Trigger>;
  "event": CustomEvent<JSEvent>;
}

export class SocketConnection {
  socket: WebSocket

  hasEverConnected: Boolean = false
  isConnected: Boolean = false
  reconnectDelay: number = 0
  queue: ActionMessage[] = []
  events: EventTarget

  constructor(addr = defaultAddress) {
    this.events = new EventTarget()
    const sock = new WebSocket(addr)
    this.socket = sock
  }

  connect(addr = defaultAddress, createSocket = false) {
    const sock = createSocket ? new WebSocket(addr) : this.socket
    this.socket = sock

    function onConnectError(ev: Event) {
      console.error("Connect Error", ev)
    }

    function onSocketError(ev: Event) {
      console.error("Socket Error", ev)
    }


    // initial connection errors
    sock.addEventListener('error', onConnectError)

    sock.addEventListener('open', (_event) => {
      console.log("Websocket Connected")

      if (this.hasEverConnected) {
        document.dispatchEvent(new Event("hyp-socket-reconnect"))
      }

      this.isConnected = true
      this.hasEverConnected = true
      this.reconnectDelay = 1000
      sock.removeEventListener('error', onConnectError)
      sock.addEventListener('error', onSocketError)

      document.dispatchEvent(new Event("hyp-socket-connect"))

      this.runQueue()
    })

    sock.addEventListener('close', _ => {
      console.log("CLOSE SOCKET")
      if (this.isConnected) {
        document.dispatchEvent(new Event("hyp-socket-disconnect"))
      }

      this.isConnected = false
      sock.removeEventListener('error', onSocketError)

      // attempt to reconnect in 1s
      if (this.hasEverConnected) {
        console.log("Reconnecting in " + (this.reconnectDelay / 1000) + "s")
        setTimeout(() => this.connect(addr, true), this.reconnectDelay)
      }

      sock.removeEventListener('error', onSocketError)
    })

    sock.addEventListener('message', ev => this.onMessage(ev))
  }

  async sendAction(action: ActionMessage) {
    if (this.isConnected) {
      let msg = renderActionMessage(action)
      this.socket.send(msg)
    }
    else {
      this.queue.push(action)
    }
  }

  private runQueue() {
    // send all messages queued while disconnected 
    let next: ActionMessage | undefined = this.queue.pop()
    if (next) {
      console.log("runQueue: ", next)
      this.sendAction(next)
      this.runQueue()
    }
  }


  // full responses will never be sent over!
  private onMessage(event: MessageEvent) {
    let { command, metas, rest } = message.splitMessage(event.data)
    // console.log("MESSAGE", command, metas, rest)

    let requestId = parseInt(requireMeta("RequestId"), 0)

    function requireMeta(key: string): string {
      let val = metaValue(key, metas)
      if (!val) throw new ProtocolError("Missing Required Metadata: " + key, event.data)
      return val
    }

    function parseResponse(rest: string[]): Update {
      let viewId = requireMeta("ViewId")
      let action = requireMeta("Action")
      return {
        requestId,
        targetViewId: undefined,
        viewId,
        action,
        meta: message.toMetadata(metas),
        body: rest.join("\n"),
      }
    }

    function parseUpdate(rest: string[]): Update {
      let up = parseResponse(rest)
      // add the TargetViewId
      up.targetViewId = metaValue("TargetViewId", metas)
      return up
    }

    function parseRedirect(rest: string[]): Redirect {
      let url = rest[0]
      return {
        requestId,
        meta: message.toMetadata(metas),
        url
      }
    }

    function parseTrigger(rest: string[]): Trigger {
      let { requestId, meta, viewId, action } = parseResponse(rest)
      let [targetViewId, targetAction] = message.parseAction(requireMeta("Trigger"))
      return { requestId, meta, viewId, action, targetViewId, targetAction }
    }

    function parseEvent(rest: string[]): JSEvent {
      let { requestId, meta, viewId, action } = parseResponse(rest)
      let event = message.parseRemoteEvent(requireMeta("Event"))
      return { requestId, meta, viewId, action, event }
    }


    switch (command) {

      case "|UPDATE|":
        return this.dispatchEvent(new CustomEvent("update", { detail: parseUpdate(rest) }))

      case "|RESPONSE|":
        return this.dispatchEvent(new CustomEvent("response", { detail: parseResponse(rest) }))

      case "|REDIRECT|":
        return this.dispatchEvent(new CustomEvent("redirect", { detail: parseRedirect(rest) }))

      case "|TRIGGER|":
        return this.dispatchEvent(new CustomEvent("trigger", { detail: parseTrigger(rest) }))

      case "|EVENT|":
        return this.dispatchEvent(new CustomEvent("event", { detail: parseEvent(rest) }))

      default:
        throw new ProtocolError("Unknown Server Command: " + command, event.data)
    }
  }


  // so what if they send remote events in the page? trigger, redirect, page title, etc...
  // we aren't connected yet on a page thing

  // private async waitMessage(reqId: RequestId, id: ViewId): Promise<ParsedResponse> {
  //   return new Promise((resolve, reject) => {
  //     const onMessage = (event: MessageEvent) => {
  //       let data: string = event.data
  //       let lines = data.split("\n").slice(1)  // drop the command line
  //
  //       let parsed = splitMetadata(lines)
  //       let metadata: Metadata = parsed.metadata
  //
  //       if (!metadata.requestId) {
  //         console.error("Missing RequestId!", metadata, event.data)
  //         return
  //       }
  //
  //       if (metadata.requestId != reqId) {
  //         // skip, it's not us!
  //         return
  //       }
  //
  //
  //       // We have found our message. Remove the listener
  //       this.socket.removeEventListener('message', onMessage)
  //
  //       // set the cookies. These happen automatically in http
  //       metadata.cookies.forEach((cookie: string) => {
  //         document.cookie = cookie
  //       })
  //
  //       if (metadata.error) {
  //         reject(new FetchError(id, metadata.error, parsed.rest.join('\n')))
  //         return
  //       }
  //
  //       resolve(parsed)
  //     }
  //
  //     this.socket.addEventListener('message', onMessage)
  //     this.socket.addEventListener('error', reject)
  //   })
  // }

  addEventListener<K extends keyof SocketConnectionEventMap>(e: K, cb: (ev: SocketConnectionEventMap[K]) => void) {
    this.events.addEventListener(e,
      // @ts-ignore: HACK
      cb
    )
  }

  dispatchEvent<K extends keyof SocketConnectionEventMap>(e: SocketConnectionEventMap[K]) {
    this.events.dispatchEvent(e)
  }

  disconnect() {
    this.isConnected = false
    this.hasEverConnected = false
    this.socket.close()
  }
}


export type Update = {
  requestId: RequestId
  meta: Metadata
  viewId: ViewId
  targetViewId?: ViewId
  action: EncodedAction
  body: ResponseBody
}

export type Redirect = {
  requestId: RequestId
  meta: Metadata
  url: string
}

export type Trigger = {
  requestId: RequestId
  meta: Metadata
  viewId: ViewId
  action: EncodedAction
  targetViewId: ViewId
  targetAction: string
}

export type JSEvent = {
  requestId: RequestId
  meta: Metadata
  viewId: ViewId
  action: EncodedAction
  event: RemoteEvent
}

export type MessageType = string


// PARSING MESSAGE  ---------------------------------------

export class ProtocolError extends Error {
  constructor(description: string, body: string) {
    super(description + "\n" + body)
    this.name = "ProtocolError"
  }
}


================================================
FILE: client/tsconfig.json
================================================
{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "ES2020",
    "target": "ES2020",
    "lib": ["ES2020","DOM"],
    "allowJs": true,
    "moduleResolution": "node",
    "declaration": true,
    "strict": true
    // "skipLibCheck": true
    /*"declarationMap": true*/
  },
  "include": [
    "./src/**/*",
    "./declarations.d.ts"
  ]
}


================================================
FILE: client/util/live-reload.js
================================================
// This isn't magic. If you want custom behavior, copy and modify this however you like. 
//
// As with any custom js, add to a single page via the `script` combinator
//  page = do
//     pure $ do
//        el "This is my page"
//        script "custom.js"
//
// or to the entire app by adding a script tag to your document function. See Example.App.toDocument
//
// Consider conditionally adding it based on ENV
console.log("Live Reload enabled")

function showNotification(message) {
  const notification = document.createElement('div');
  notification.classList.add("live-reload")
  notification.innerHTML = message;
  jackIn(notification.style)
  notification.addEventListener('click', function() {
    notification.remove()
  })
  document.body.appendChild(notification);
}

document.addEventListener("hyp-socket-disconnect", () => {
  showNotification("DISCONNECTED - will reload on reconnect")
})

document.addEventListener("hyp-socket-reconnect", () => {
  setTimeout(() => {
    location.reload()
  }, 0)
})


// duplicate cyber style stuff here so the default live reload is fun
function jackIn(style) {
  style.position = 'fixed';
  style.bottom = '15px';
  style.left = '15px';
  style.right = '15px';
  style.backgroundColor = 'rgba(160, 63, 56, 1.0)';
  style.color = '#fff';
  style.borderTop = 'solid #EC6458 4px';
  style.padding = '15px';
  style.zIndex = '1000';
  style.clipPath = 'polygon(0 0, 100% 0, 100% calc(100% - 16px), calc(100% - 16px) 100%, 0 100%)';
}


================================================
FILE: client/webpack.config.js
================================================
const path = require('path');
// var PACKAGE = require('./package.json');
// var version = PACKAGE.version;

module.exports = {
  entry: "./src/index.ts",
  target: "web",
  devtool: "source-map",
  mode: "production",
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    mainFields: ['browser', 'module', 'main'],
    extensions: ['.tsx', '.ts', '.js'],
  },

  output: {
    // filename: `hyperbole-${version}.js`,
    filename: "hyperbole.js",
    path: path.resolve(__dirname, 'dist'),
  },

  // devServer: {
  //   contentBase: path.join(__dirname, 'dist'),
  //   compress: true,
  //   port: 9000,
  // },
}



================================================
FILE: demo/.dockerignore
================================================
dist-newstyle
.git


================================================
FILE: demo/App/Cache.hs
================================================
module App.Cache where

import Network.HTTP.Types (Header)
import Network.Wai.Middleware.Static

clientCache :: IO Options
clientCache = do
  container <- initCaching PublicStaticCaching
  -- container <- initCaching (CustomCaching customCache)
  pure $ defaultOptions{cacheContainer = container}

-- for testing if caching is working
customCache :: FileMeta -> [Header]
customCache (FileMeta lm etag _file) = do
  [("Cache-Control", "no-transform,public,max-age=30"), ("Last-Modified", lm), ("Etag", etag)]


================================================
FILE: demo/App/Config.hs
================================================
{-# LANGUAGE QuasiQuotes #-}

module App.Config where

import Data.Maybe (fromMaybe, isNothing)
import Effectful
import Effectful.Environment
import Effectful.Exception
import Network.HTTP.Client qualified as HTTP
import Network.HTTP.Client.TLS qualified as HTTPS
import Network.URI (parseURI)
import Web.Hyperbole.Data.URI
import Web.Hyperbole.Effect.OAuth2 (Config (..), Token (..))
import Web.Hyperbole.Effect.OAuth2 qualified as OAuth2

data App
data AppConfig = AppConfig
  { endpoint :: Endpoint App
  , manager :: HTTP.Manager
  , oauth :: OAuth2.Config
  , devMode :: Bool
  }

getAppConfigEnv :: (IOE :> es, Environment :> es) => Eff es AppConfig
getAppConfigEnv = do
  endpoint <- lookupEnvEndpoint "APP_ENDPOINT" -- default to localhost
  manager <- HTTPS.newTlsManager
  pure $
    AppConfig
      { endpoint = fromMaybe (Endpoint [uri|http://localhost:3000|]) endpoint
      , manager
      , oauth = dummyOAuthConfig
      , devMode = isNothing endpoint -- in dev mode if APP_ENDPOINT is not set (localhost)
      }

type Key = String

data ConfigError
  = BadEnv Key
  deriving (Show, Exception)

lookupEnvEndpoint :: (Environment :> es) => Key -> Eff es (Maybe (Endpoint a))
lookupEnvEndpoint k = do
  mstr <- lookupEnv k
  pure $ parseEndpoint mstr
 where
  parseEndpoint mstr = do
    input <- mstr
    url <- parseURI input
    pure $ Endpoint url

-- In a real app this would be read from ENV. See OAuth2.initConfigEnv
dummyOAuthConfig :: OAuth2.Config
dummyOAuthConfig =
  Config
    { clientId = Token "dummy client id"
    , clientSecret = Token "dummy client secret"
    , authorize = Endpoint [uri|https://oauth-mock.mock.beeceptor.com/oauth/authorize|]
    , token = Endpoint [uri|https://oauth-mock.mock.beeceptor.com/oauth/token/github|]
    }


================================================
FILE: demo/App/Docs/Markdown.hs
================================================
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -Wno-orphans #-}

module App.Docs.Markdown
  ( markdocs
  , markdump
  , nodeToView
  , embedFile
  ) where

import App.Docs.Snippet
import App.Route
import CMark
import Data.Char (isSpace)
import Data.Set
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Example.Colors (magenta)
import Example.Style qualified as Style
import Example.Style.Cyber qualified as Cyber
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import Web.Atomic.CSS
import Web.Hyperbole.Data.URI
import Web.Hyperbole.HyperView.Input (route)
import Web.Hyperbole.Route
import Web.Hyperbole.View

markdocs :: Text -> View c ()
markdocs md = do
  nodeToView $ commonmarkToNode [] $ cs md

markdump :: Text -> View c ()
markdump md = do
  code $ cs $ show $ commonmarkToNode [] $ cs md

nodeToView :: Node -> View c ()
nodeToView (Node _mpos typ childs) = do
  let inner = mapM_ nodeToView childs
  case typ of
    -- DOCUMENT -> mapM nodeToView childs
    -- THEMATIC_BREAK -> _
    -- PARAGRAPH -> _
    -- BLOCK_QUOTE -> _
    -- HTML_BLOCK Text -> _
    -- CUSTOM_BLOCK OnEnter OnExit -> _
    -- CODE_BLOCK Info Text -> _
    -- HEADING Level -> _
    -- LIST ListAttributes -> _
    -- ITEM -> _
    -- TEXT Text -> _
    -- SOFTBREAK -> _
    -- LINEBREAK -> _
    -- HTML_INLINE Text -> _
    -- CUSTOM_INLINE OnEnter OnExit -> _
    -- CODE Text -> _
    -- EMPH -> _
    -- STRONG -> _
    -- LINK url title -> _
    -- IMAGE url title -> _
    PARAGRAPH -> el inner
    TEXT t -> text t
    CODE t -> do
      inlineCode t
    HEADING lvl ->
      el ~ bold . headerLevel lvl $ inner
    LINK url _title ->
      case matchRoute @AppRoute (path url) of
        Nothing -> do
          case parseURIReference (cs url) of
            Nothing -> text $ "INVALID URI: " <> url
            Just u ->
              link u ~ Style.link @ att "target" "_blank" $ inner
        Just r ->
          route r ~ Style.link $ inner
    LIST (ListAttributes ORDERED_LIST _ _ _) ->
      tag "ol" ~ list Decimal . pad (L 32) $ inner
    LIST (ListAttributes BULLET_LIST _ _ _) ->
      tag "ul" ~ list Disc . pad (L 32) $ inner
    ITEM -> tag "li" inner
    DOCUMENT -> inner
    CODE_BLOCK _info t ->
      snippet $ raw t
    BLOCK_QUOTE -> el ~ Cyber.quote $ inner
    HTML_BLOCK t -> raw t
    SOFTBREAK -> inner
    EMPH -> tag' True "span" ~ italic $ inner
    STRONG -> tag' True "span" ~ bold $ inner
    x ->
      -- inner
      raw $ cs $ show x
 where
  headerLevel lvl =
    case lvl of
      1 -> fontSize 24
      2 -> fontSize 20
      _ -> fontSize 16

hackageDocsURI :: URI
hackageDocsURI = [uri|https://hackage-content.haskell.org/package/hyperbole/docs/Web-Hyperbole.html|]

inlineCode :: Text -> View c ()
inlineCode cd
  | cd `elem` typeKeywords = linkSymbolDocs cd typeFrag ~ color hackageSymbolColor
  | cd `elem` valueKeywords = linkSymbolDocs cd valFrag ~ color hackageSymbolColor
  | otherwise = tag' True "code" ~ color magenta $ text cd
 where
  typeFrag t = "#t:" <> cs t
  valFrag v = "#v:" <> cs v

  hackageSymbolColor :: HexColor
  hackageSymbolColor = "#9e358f"

linkSymbolDocs :: Text -> (Text -> String) -> View c ()
linkSymbolDocs sym frag = do
  link (hackageDocsURI{uriFragment = frag sym}) @ att "target" "_blank" $ do
    tag' True "code" $ text sym

typeKeywords :: Set Text
typeKeywords =
  [ "Page"
  , "View"
  , "HyperView"
  , "ViewId"
  , "ViewAction"
  , "ViewState"
  , "Action"
  , "Hyperbole"
  , "Effect"
  , "Query"
  , "Session"
  , "Require"
  , "Client"
  , "Request"
  , "Document"
  , "Path"
  , "Route"
  , "Eff"
  , "Page"
  , "Response"
  , "FromForm"
  , "Validated"
  , "Concurrency"
  , "Replace"
  , "Drop"
  ]

valueKeywords :: Set Text
valueKeywords =
  [ "context"
  , "update"
  , "form"
  , "validate"
  , "hyper"
  , "request"
  , "viewId"
  , "viewState"
  , "trigger"
  , "target"
  , "hyperState"
  , "runPage"
  , "document"
  , "routeRequest"
  , "matchRoute"
  , "liveApp"
  , "pushUpdate"
  , "onLoad"
  , "session"
  , "query"
  , "setQuery"
  , "setParam"
  , "param"
  , "modifyQuery"
  , "saveSession"
  , "deleteSession"
  , "quickStartDocument"
  , "search"
  , "loading"
  , "whenLoading"
  , "dropdown"
  , "option"
  , "button"
  , "onClick"
  , "onKeyDown"
  , "onKeyUp"
  , "onMouseEnter"
  , "onMouseLeave"
  , "onInput"
  ]

embedFile :: FilePath -> Q Exp
embedFile p = do
  addDependentFile p
  lns :: [Text] <- runIO $ T.lines <$> T.readFile p
  exps :: [Exp] <- traverse expandLine lns
  e :: Exp <- listE (fmap pure exps)
  [|T.unlines $(pure e)|]

expandLine :: Text -> Q Exp
expandLine l = do
  let whitespace = T.takeWhile isSpace l
  case parseLineEmbed l of
    Just (mn, tld) -> do
      e <- embedSource' mn (isTopLevel tld) (isCurrentDefinition tld)
      [|T.stripEnd $ T.unlines $ fmap (whitespace <>) $(pure e)|]
    Nothing -> do
      t <- expandText l
      lift t

expandText :: (MonadFail m) => Text -> m Text
expandText t = do
  let segs = T.splitOn "[[" t
  es :: [Text] <- mapM checkLink segs
  pure $ mconcat es
 where
  checkLink :: (MonadFail m) => Text -> m Text
  checkLink l = do
    case T.breakOn "]]" l of
      (txt, "") -> pure txt
      (lnk, rest) -> do
        mdlnk <- routeLink lnk
        pure $ mdlnk <> T.dropWhile (== ']') rest

  routeLink :: (MonadFail m) => Text -> m Text
  routeLink l =
    case matchRoute @AppRoute (path l) of
      Nothing -> error $ "Could not find page link: " <> cs l <> " " <> show (path l)
      Just r -> pure $ "[" <> routeTitle r <> "](" <> uriToText (routeUri r) <> ")"


================================================
FILE: demo/App/Docs/Page.hs
================================================
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE QuasiQuotes #-}

module App.Docs.Page
  ( PageAnchor (..)
  , sourceLink
  , example
  , example'
  , section
  , section'
  , camelTitle
  , Cyber.embed
  , Cyber.quote
  ) where

import App.Docs.Snippet (ModuleSource (..))
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Text qualified as T
import Example.Colors (AppColor (..))
import Example.Style qualified as Style
import Example.Style.Cyber qualified as Cyber
import Text.Casing (fromHumps, toWords)
import Web.Atomic.CSS
import Web.Hyperbole
import Web.Hyperbole.Data.URI

class PageAnchor n where
  pageAnchor :: n -> Text
  default pageAnchor :: n -> Text
  pageAnchor = T.toLower . T.replace " " "-" . sectionTitle

  sectionTitle :: n -> Text
  default sectionTitle :: (Show n) => n -> Text
  sectionTitle = camelTitle

  navEntry :: n -> Text
  default navEntry :: n -> Text
  navEntry = sectionTitle

  subnav :: [n]
  default subnav :: (Enum n, Bounded n) => [n]
  subnav = [minBound .. maxBound]

instance PageAnchor () where
  subnav = []

camelTitle :: (Show a) => a -> Text
camelTitle = cs . toWords . fromHumps . show

-- Sections ----------------------------------------------------------------------

sourceLink :: Path -> View c ()
sourceLink p =
  link sourceUrl ~ fontSize 14 @ att "target" "_blank" $ do
    text "</> Source"
 where
  sourceUrlBase = [uri|https://github.com/seanhess/hyperbole/blob/main/demo/|]
  sourceUrl = sourceUrlBase ./. p

example :: ModuleSource -> View c () -> View c ()
example (ModuleSource e) = example' (path $ cs e)

example' :: Path -> View c () -> View c ()
example' p cnt = do
  el ~ stack . Cyber.font $ do
    col ~ Cyber.embed $ cnt
    sourceLink p ~ popup (TR (-10) 0) . pad (XY 8 2) . bg PrimaryLight . color White . hover (bg Primary) -- . pad (TRBL 0 20 0 10) . border (L 3) . borderColor PrimaryLight . Cyber.clip 10

-- section :: AppRoute -> View c () -> View c ()
-- section r = section' (routeTitle r)

section' :: Text -> View c () -> View c ()
section' t cnt = do
  tag "section" ~ gap 10 . flexCol $ do
    row $ do
      el ~ bold . fontSize 28 . Cyber.font . Style.uppercase $ text t
    cnt

section :: (PageAnchor n) => n -> View c () -> View c ()
section n =
  section' (sectionTitle n)
    @ att "id" (pageAnchor n)

-- type Fragment = String
--
-- hackage :: Fragment -> Text -> View c ()
-- hackage uriFragment txt = do
--   let docs = [uri|https://hackage-content.haskell.org/package/hyperbole/docs/Web-Hyperbole.html|]
--   link docs{uriFragment} @ att "target" "_blank" ~ Style.link $ do
--     el ~ iconInline $ do
--       Icon.bookOpen
--       text txt


================================================
FILE: demo/App/Docs/Snippet.hs
================================================
{-# LANGUAGE TemplateHaskell #-}

module App.Docs.Snippet where

import Control.Monad (unless)
import Data.Char (isSpace)
import Data.List qualified as L
import Data.String (IsString)
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import System.Directory (doesFileExist, getCurrentDirectory)
import System.FilePath (normalise, (</>))
import Web.Atomic.CSS
import Web.Hyperbole.View

snippet :: View c () -> View c ()
snippet cnt = do
  tag' True "pre" ~ bg (HexColor "#F2F2F3") $ do
    tag' True "code" @ class_ "language-haskell" $ do
      cnt

codeblock :: Text -> View c ()
codeblock t =
  tag' True "pre" ~ monoline $ do
    tag' True "code" $ do
      raw t
 where
  monoline =
    utility
      "monoline"
      [ "line-height" :. "1"
      ]

rawMulti :: [Text] -> View c ()
rawMulti = raw . T.stripEnd . T.unlines

embedLines :: FilePath -> Int -> Int -> Q Exp
embedLines path start end = do
  addDependentFile path
  contents <- runIO (T.readFile path)
  let selected =
        T.unlines
          . take (end - start + 1)
          . drop (start - 1)
          . T.lines
          $ contents
  lift (T.unpack selected)

newtype TopLevelDefinition = TopLevelDefinition Text
  deriving newtype (Show, Eq, IsString)

newtype SourceCode = SourceCode {lines :: [Text]}

newtype ModuleName = ModuleName Text
  deriving newtype (Show, Eq, IsString)

modulePath :: ModuleName -> FilePath
modulePath (ModuleName mn) = cs $ "demo/" <> T.replace "." "/" mn <> ".hs"

{- | A top-level definition as text

> snippet $(topLevel "demo/Example/Page/Concurrency.hs" "instance (Debug :> es) => HyperView Polling")
-}
embedTopLevel :: ModuleName -> TopLevelDefinition -> Q Exp
embedTopLevel mn tld = do
  embedSource mn (isTopLevel tld) (isCurrentDefinition tld)

embedSource :: ModuleName -> (Text -> Bool) -> (Text -> Bool) -> Q Exp
embedSource mn isStart isCurrent = do
  e <- embedSource' mn isStart isCurrent
  [|T.unlines $(pure e)|]

embedSource' :: ModuleName -> (Text -> Bool) -> (Text -> Bool) -> Q Exp
embedSource' mn isStart isCurrent = do
  path <- runIO $ localFile $ modulePath mn
  addDependentFile path
  s <- runIO $ readSourceCode path
  let lns = selectLines isStart isCurrent s
  case lns of
    [] -> fail $ "Missing embed in: " ++ show mn
    _ -> lift lns

readSnippet :: FilePath -> TopLevelDefinition -> IO [Text]
readSnippet path tld = do
  s <- readSourceCode path
  pure $ findTopLevel tld s

readSourceCode :: FilePath -> IO SourceCode
readSourceCode path = SourceCode . T.lines <$> T.readFile path

-- returns lines of a top-level definition
findTopLevel :: TopLevelDefinition -> SourceCode -> [Text]
findTopLevel tld =
  selectLines (isTopLevel tld) (isCurrentDefinition tld)

-- isBlankLine line = T.null $ T.strip line

isCurrentDefinition :: TopLevelDefinition -> Text -> Bool
isCurrentDefinition tld line =
  isTopLevel tld line || not (isFullyOutdented line)

isTopLevel :: TopLevelDefinition -> Text -> Bool
isTopLevel (TopLevelDefinition def) line =
  if "^" `T.isPrefixOf` def
    then T.isPrefixOf (T.drop 1 def) line
    else T.isPrefixOf def $ T.dropWhile (== ' ') line

selectLines :: (Text -> Bool) -> (Text -> Bool) -> SourceCode -> [Text]
selectLines isStart isCurrent s =
  let rest = dropWhile (not . isStart) s.lines
   in dropWhileEnd isEmpty $ takeWhile isCurrent rest
 where
  isEmpty = T.null

dropWhileEnd :: (a -> Bool) -> [a] -> [a]
dropWhileEnd p as =
  reverse $ dropWhile p $ reverse as

isFullyOutdented :: Text -> Bool
isFullyOutdented line =
  case cs (T.take 1 line) of
    "" -> False
    [c] -> not $ isSpace c
    _ -> False

-- #EMBED Example.Docs.Interactive instance HyperView Titler
parseLineEmbed :: Text -> Maybe (ModuleName, TopLevelDefinition)
parseLineEmbed l = do
  rest <- T.stripPrefix "#EMBED " (T.stripStart l)
  (mn : tld) <- pure $ T.words rest
  pure (ModuleName mn, TopLevelDefinition $ T.unwords tld)

-- start with a relative OR absolute path, end up with a path to the file
-- works with any working directory
localFile :: FilePath -> IO FilePath
localFile p = do
  current <- getCurrentDirectory
  let lpath = addRelativeDemo current $ stripDir "demo" $ stripDir current p
  b <- doesFileExist lpath
  unless b $ do
    fail $ "Could not find file: " <> show lpath <> " in working dir: " <> current
  pure lpath
 where
  addRelativeDemo wd rp
    | "demo" `L.isSuffixOf` wd = rp
    | otherwise = "demo" </> rp

stripDir :: FilePath -> FilePath -> FilePath
stripDir dir p =
  maybe
    p
    (dropWhile (== '/'))
    (L.stripPrefix dir p)

newtype ModuleSource = ModuleSource FilePath
  deriving newtype (Show, Eq, IsString)

moduleSource :: Q Exp
moduleSource = do
  loc <- location
  let path = normalise $ loc_filename loc
  fp <- runIO $ localFile path
  lift fp

moduleSourceNamed :: ModuleName -> Q Exp
moduleSourceNamed mn = do
  fp <- runIO $ localFile $ modulePath mn
  lift fp


================================================
FILE: demo/App/Docs.hs
================================================
module App.Docs
  ( module App.Docs.Markdown
  , module App.Docs.Page
  , module App.Docs.Snippet
  ) where

import App.Docs.Markdown
import App.Docs.Page
import App.Docs.Snippet


================================================
FILE: demo/App/Page/Application.hs
================================================
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}

module App.Page.Application where

import App.Docs
import App.Route (AppRoute (Application))
import Effectful
import Example.CSS.External qualified as External
import Example.CSS.Transitions as Transitions
import Example.Interactivity.Events as Events
import Example.View.Layout
import Web.Hyperbole

data Sections
  = LiveApp
  | Document
  | Pages
  | TypeSafeRoutes
  | RunningEffects
  deriving (Eq, Generic, Show, Enum, Bounded, PageAnchor)

page :: (Hyperbole :> es) => Page es '[Animate, External.Items, Boxes]
page = do
  pure $ layoutSubnav @Sections Application $ do
    section LiveApp $ do
      markdocs $(embedFile "docs/app-live.md")

    section Document $ do
      markdocs $(embedFile "docs/app-document.md")

    section Pages $ do
      --
      markdocs $(embedFile "docs/app-pages.md")

    section TypeSafeRoutes $ do
      markdocs $(embedFile "docs/app-routes.md")

    section RunningEffects $ do
      markdocs $(embedFile "docs/app-effects.md")


================================================
FILE: demo/App/Page/CSS.hs
================================================
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}

module App.Page.CSS where

import App.Docs
import App.Route (AppRoute (CSS))
import Effectful
import Example.CSS.External qualified as External
import Example.CSS.Loading as Loading
import Example.CSS.Tooltips as Tooltips
import Example.CSS.Transitions as Transitions
import Example.Docs.CSS qualified as CSS
import Example.Interactivity.Events as Events
import Example.View.Layout
import Example.View.Loader as Loader
import Web.Atomic.CSS
import Web.Hyperbole
import Web.Hyperbole.HyperView.Types (Root (..))
import Web.Hyperbole.Page (subPage)

data CSSExample
  = Factoring
  | Transitions
  | Tooltips
  | Loading
  | External
  deriving (Eq, Generic, Show, Enum, Bounded)
instance PageAnchor CSSExample where
  sectionTitle = \case
    Factoring -> "Atomic CSS"
    Transitions -> "CSS Transitions"
    Tooltips -> "Tooltips"
    Loading -> "Loading"
    External -> "External Stylesheets"

page :: (Hyperbole :> es) => Page es '[Animate, External.Items, Boxes, Loader]
page = do
  ext <- subPage External.page
  pure $ layoutSubnav @CSSExample CSS $ do
    style Loader.css
    section Factoring $ do
      markdocs $(embedFile "docs/atomic.md")

      CSS.example ~ embed

    section Transitions $ do
      markdocs "If an update changes the `transition` property of a view, it will automatically animate with CSS Transitions, avoiding having the server compute animation frames."
      snippet $ do
        raw $(embedTopLevel "Example.CSS.Transitions" "viewSmall")
        raw "\n"
        raw $(embedTopLevel "Example.CSS.Transitions" "viewBig")
      example Transitions.source $ hyper Animate viewSmall

    section Tooltips $ do
      markdocs "For immediate feedback, create interactivity via Atomic CSS whenever possible."
      example Tooltips.source tooltips

    section Loading $ do
      markdocs "Use `whenLoading` to provide feedback while an `Action` is being processed"
      snippet $ do
        raw $(embedTopLevel "Example.CSS.Loading" "viewLoaders")

      example $(moduleSourceNamed "Example.CSS.Loading") $ do
        hyper Loader $ viewLoaders "..."

    section External $ do
      markdocs "You can opt-out of Atomic CSS and use external classes with `class_`"
      snippet $ do
        raw $(embedTopLevel "Example.CSS.External" "page")
      snippet $ do
        raw $(embedTopLevel "Example.CSS.External" "itemsView")
      example External.source $ do
        runViewContext Root () ext


================================================
FILE: demo/App/Page/Concurrency.hs
================================================
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}

module App.Page.Concurrency where

import App.Docs
import App.Route qualified as Route
import Control.Monad (forM_)
import Effectful
import Example.Concurrency.LazyLoading as Lazy
import Example.Concurrency.Overlap as Overlap
import Example.Concurrency.Polling as Polling
import Example.Concurrency.Progress as Progress
import Example.Concurrency.Tasks
import Example.Effects.Debug
import Example.Push qualified as Push
import Example.Style.Cyber (btn, font)
import Example.View.Layout (layoutSubnav)
import Example.View.Loader as Loader
import Web.Atomic.CSS
import Web.Hyperbole

data Section
  = Concurrency
  | OverlappingRequests
  | LazyLoading
  | Polling
  | PushUpdates
  deriving (Show, Eq, Enum, Bounded, PageAnchor)

page :: (Hyperbole :> es, Debug :> es) => Page es '[Poller, LazyData, Progress, Push.Tasks, OverlapDrop, OverlapReplace, LazyAll]
page = do
  pure $ layoutSubnav @Section Route.Concurrency $ do
    style Loader.css
    section Concurrency $ do
      markdocs "While individual `HyperView`s can only have one update in progress at a time, multiple `HyperView`s can overlap updates without issue"
      example Progress.source ~ font $ do
        hyper (Progress 1) $ viewProgressLoad 6
        hyper (Progress 2) $ viewProgressLoad 4
        hyper (Progress 3) $ viewProgressLoad 2
    -- hyper (Progress 4 200) viewProgressLoad
    -- hyper (Progress 5 250) viewProgressLoad

    section OverlappingRequests $ do
      markdocs $(embedFile "docs/concurrency-overlap.md")

      example $(moduleSourceNamed "Example.Concurrency.Overlap") $ do
        hyper OverlapDrop $ viewTimeDrop Nothing
        hyper OverlapReplace $ viewTimeReplace Nothing

    section LazyLoading $ do
      markdocs "Instead of preloading everything in our `Page`, a `HyperView` can load itself using `onLoad`"
      snippet $ raw $(embedTopLevel "Example.Concurrency.LazyLoading" "viewTaskLoad")
      example Lazy.source $ do
        hyper LazyAll viewLazyAll

    section Polling $ do
      markdocs "By including an `onLoad` in every view update, we can poll the server after a given delay"
      snippet $ raw $(embedTopLevel "Example.Concurrency.Polling" "viewPoll")
      example Polling.source $ do
        hyper Poller viewInit

    section PushUpdates $ do
      markdocs "Actions can call `pushUpdate` to send an intermediate update to the view. This can be simpler than polling."
      snippet $ raw $(embedTopLevel "Example.Push" "update")
      example Push.source $ do
        hyper Push.Tasks $ Push.taskView 0

data LazyAll = LazyAll
  deriving (Generic, ViewId)

instance HyperView LazyAll es where
  data Action LazyAll
    = ReloadAll
    deriving (Generic, ViewAction)

  type Require LazyAll = '[LazyData]

  update _ = do
    pure viewLazyAll

viewLazyAll :: View LazyAll ()
viewLazyAll = do
  col ~ gap 10 $ do
    row ~ flexWrap Wrap . font . gap 10 $ do
      forM_ pretendTasks $ \taskId -> do
        el ~ border 1 . width 120 . pad 5 $ do
          hyper (LazyData taskId) viewTaskLoad
    row $ button ReloadAll ~ btn $ "Reload"


================================================
FILE: demo/App/Page/Examples.hs
================================================
module App.Page.Examples where

import App.Docs
import App.Route as Route
import Example.Style as Style (link)
import Example.View.Layout
import Web.Atomic.CSS
import Web.Hyperbole

page :: (Hyperbole :> es) => Page es '[]
page = do
  pure $ layout (Examples OtherExamples) $ do
    section' "Data Lists" $ do
      col ~ gap 10 $ do
        card (Data SortableTable) "Sort by column, demonstrates view functions"
        card (Data Autocomplete) "Incremental search using only hyperbole"
        card (Data Filter) "Faceted search, live filtering of lists  "
        card (Data LoadMore) "Progressively load more items"

    section' "UI Demos" $ do
      col ~ gap 10 $ do
        card (Examples Tags) $ markdocs "Add and remove \"tags\" with an `<input>`"
        card (Examples Chat) "Demonstrates server pushes and concurrency. Open in multiple tabs"
        card (Examples Scrollbars) "Layouts with internal scrollbars"

    section' "Other Features" $ do
      card (Examples OAuth2) "Demonstration of OAuth2"

    section' "Reference Implementations" $ do
      card (Examples Todos) "using Atomic CSS"
      card (Examples TodosCSS) "using external classes"
 where
  card r cnt = do
    row ~ gap 5 $ do
      route r ~ Style.link $ do
        text $ routeTitle r
      el $ text "-"
      el cnt

-- cardBtn :: (Styleable h) => CSS h -> CSS h
-- cardBtn =
--   bgAnimated
--     . bgGradient White
--     . hover bgzero
--     . clip 10
--     . shadow ()
--
-- grid :: (Styleable h) => CSS h -> CSS h
-- grid =
--   utility
--     "grid-ex"
--     [ "display" :. "grid"
--     , "grid-template-columns" :. "repeat(auto-fit, minmax(200px, 1fr))"
--     ]
--
-- tile :: (Styleable h) => CSS h -> CSS h
-- tile =
--   utility
--     "tile"
--     [ "aspect-ratio" :. "16 / 9"
--     ]

-- section Effectful $ do
--   markdocs $(embedFile "docs/effectful.md")
--   example SideEffects.source $ do
--     hyper Titler titleView
--
-- section Other $ do
--   markdocs $(embedFile "docs/effects-other.md")
--   example SideEffects.source $ do
--     hyper SlowReader $ messageView "..."
--
-- section Custom $ do
--   markdocs $(embedFile "docs/effects-custom.md")


================================================
FILE: demo/App/Page/Forms.hs
================================================
{-# LANGUAGE TemplateHaskell #-}

module App.Page.Forms where

import App.Docs
import App.Route
import Example.FormSimple (AddContact (..))
import Example.FormSimple qualified as FormSimple
import Example.FormValidation (Signup (..))
import Example.FormValidation qualified as FormValidation
import Example.View.Layout
import Web.Hyperbole

data Sections
  = BasicForms
  | Validation
  deriving (Generic, Show, Bounded, Enum, PageAnchor)

page :: (Hyperbole :> es) => Page es '[Signup, AddContact]
page = do
  pure $ layoutSubnav @Sections (Forms FormSimple) $ do
    section BasicForms $ do
      markdocs $(embedFile "docs/forms-simple.md")

      example FormSimple.source $ do
        hyper AddContact FormSimple.formView'

    section Validation $ do
      markdocs $(embedFile "docs/forms-validated.md")

      example FormValidation.source $ do
        --
        hyper Signup $ FormValidation.formView genFields


================================================
FILE: demo/App/Page/HyperboleEffect.hs
================================================
module App.Page.HyperboleEffect where

import App.Route as Route hiding (Response, UserId)
import App.Docs
import Effectful
import Example.Errors (Errors (..), Users (..), viewCustom, viewExceptions, viewKnownUsers, viewSearchUsers)
import Example.Errors qualified as Errors
import Example.Requests (CheckRequest (..), ControlClient (..), ControlResponse (..))
import Example.Requests qualified as Requests
import Example.View.Layout (layoutSubnav)
import Web.Hyperbole hiding (Response)

data Sections
  = Requests
  | Response
  | ExceptionHandling
  | EdgeCases
  | HandleInViews
  | CustomErrorViews
  deriving (Show, Enum, Bounded, PageAnchor)

page :: (Hyperbole :> es) => Page es '[CheckRequest, ControlResponse, ControlClient, Errors, Users]
page = do
  r <- request
  pure $ layoutSubnav @Sections Route.HyperboleEffect $ do
    section Requests $ do
      markdocs "The `Hyperbole` `Effect` allows us to skip the normal update cycle to directly access the `Request` or manipulate the `Client`"
      example Requests.source $ do
        hyper CheckRequest $ Requests.viewRequest r

      example Requests.source $ do
        hyper ControlClient Requests.viewClient

    section Response $ do
      el "It also allows us to directly affect the response and the javascript client"
      example Requests.source $ hyper ControlResponse Requests.responseView

    section ExceptionHandling $ do
      el "Any uncaught exceptions thrown from a handler will be displayed in a bright red box inline in the corresponding HyperView"
      example Errors.source $ do
        hyper Exceptions viewExceptions

    section EdgeCases $ do
      el "You can use the same mechanism to exit execution early and display an application error to handle edge cases"
      example Errors.source $ do
        hyper KnownUsers viewKnownUsers

    section HandleInViews $ do
      el "Handle any expected errors in your view function, by making it accept a Maybe or Either"
      example Errors.source $ do
        hyper SearchUsers viewSearchUsers

    section CustomErrorViews $ do
      el "You can also exit execution early and display a custom view from application code or from caught execptions"
      example Errors.source $ do
        hyper Customs viewCustom


================================================
FILE: demo/App/Page/Hyperviews.hs
================================================
{-# LANGUAGE TemplateHaskell #-}

module App.Page.Hyperviews where

import App.Docs
import App.Route qualified as Route (AppRoute (..))
import Example.Counter (Counter (..))
import Example.Docs.MultiView qualified as Multi
import Example.Docs.Nesting qualified as Nesting
import Example.Docs.UniqueViewId qualified as UniqueViewId
import Example.Push as Push
import Example.Simple (Message (..))
import Example.Trigger as Trigger
import Example.View.Layout (layoutSubnav)
import Web.Hyperbole
import Web.Hyperbole.HyperView.Types (Root (..))
import Web.Hyperbole.Page (subPage)

data HyperSectuions
  = IndependentUpdates
  | UniqueViewid
  | Nesting
  | TargetingOtherHyperviews
  deriving (Show, Enum, Bounded, PageAnchor)

page :: (Hyperbole :> es) => Page es '[Counter, Message, UniqueViewId.Item, Nesting.ItemList, Targeted, Controls, Tasks]
page = do
  mlt <- subPage Multi.page
  uvd <- subPage UniqueViewId.page
  nst <- subPage Nesting.page
  pure $ layoutSubnav @HyperSectuions Route.Hyperviews $ do
    section IndependentUpdates $ do
      markdocs $(embedFile "docs/hyperviews-multi.md")
      example $(moduleSourceNamed "Example.Docs.MultiView") $ do
        runViewContext Root () mlt

    section UniqueViewid $ do
      markdocs $(embedFile "docs/hyperviews-unique.md")
      example $(moduleSourceNamed "Example.Docs.UniqueViewId") $ do
        runViewContext Root () uvd

    section Nesting $ do
      markdocs $(embedFile "docs/hyperviews-nesting.md")

      example $(moduleSourceNamed "Example.Docs.Nesting") $ do
        runViewContext Root () nst

    section TargetingOtherHyperviews $ do
      markdocs "Sometimes nesting isn't enough, and we need to directly communicate to other `HyperView`s. Below, we have an independent `HyperView` which displays a message, and two ways to control it:"
      example Trigger.source $ do
        hyper Targeted $ targetedView "..."

      markdocs "Use `trigger` to tell another `HyperView` to run an action"
      snippet $ do
        raw $(embedTopLevel "Example.Trigger" "instance HyperView Controls")

      example Trigger.source $ do
        hyper Controls controlView

      markdocs "You can use `target` in a `View` to use `Action`s from another `HyperView`"
      snippet $ do
        raw $(embedTopLevel "Example.Trigger" "targetView")

      example Trigger.source $ do
        hyper Controls targetView

      markdocs "Alternatively, you can use `pushUpdate` to directly update another view:"

      example Push.source $ do
        hyper Tasks $ taskView 0


================================================
FILE: demo/App/Page/Interactivity.hs
================================================
{-# LANGUAGE TemplateHaskell #-}

module App.Page.Interactivity where

import App.Docs
import App.Route hiding (Javascript)
import Example.Interactivity.Events
import Example.Interactivity.Inputs
import Example.Javascript as Javascript
import Example.View.Layout
import Web.Hyperbole

data Sections
  = Inputs
  | Events
  | Javascript
  deriving (Show, Bounded, Enum, PageAnchor)

page :: (Hyperbole :> es) => Page es '[Boxes, JBoxes, Message, TryEvents, Dropper]
page = do
  pure $ layoutSubnav @Sections Interactivity $ do
    -- NOTE: only include javascript on the pages you need it
    script "custom.js"

    section Inputs $ do
      markdocs $(embedFile "docs/interactivity-inputs.md")
      example $(moduleSourceNamed "Example.Interactivity.Inputs") $ hyper Dropper (selectPlanet Nothing)

    section Events $ do
      markdocs $(embedFile "docs/interactivity-events.md")
      example $(moduleSourceNamed "Example.Interactivity.Events") $ hyper TryEvents (viewEvents "")

      markdocs $(embedFile "docs/interactivity-events2.md")
      example $(moduleSourceNamed "Example.Interactivity.Events") $ hyper Boxes (viewBoxes Nothing)

    section Javascript $ do
      markdocs $(embedFile "docs/interactivity-javascript.md")
      example Javascript.source $ do
        hyper JBoxes $ viewJBoxes Nothing

      markdocs $(embedFile "docs/interactivity-pushevent.md")
      example Javascript.source $ do
        hyper Message viewMessage


================================================
FILE: demo/App/Page/Intro/Basics.hs
================================================
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}

module App.Page.Intro.Basics where

import App.Docs
import App.Route
import Data.String.Interpolate (i)
import Example.Counter (Counter)
import Example.Docs.Interactive qualified as Interactive
import Example.Docs.ViewFunctions qualified as ViewFunctions
import Example.Simple as Simple
import Example.View.Layout (layoutSubnav)
import Web.Atomic.CSS
import Web.Hyperbole
import Web.Hyperbole.HyperView.Types (Root (..))
import Web.Hyperbole.Page (subPage)

data Basics
  = GetRunning
  | HtmlViews
  | Interactive
  deriving (Show, Enum, Bounded)

instance PageAnchor Basics where
  sectionTitle Interactive = "Interactive HyperViews"
  sectionTitle a = camelTitle a

  navEntry Interactive = "HyperViews"
  navEntry a = sectionTitle a

page :: (Hyperbole :> es) => Page es '[Message, Counter, ViewFunctions.Message]
page = do
  int <- subPage Interactive.page
  -- mlt <- subPage Multi.page
  pure $ layoutSubnav @Basics Basics $ do
    section GetRunning getRunning
    section HtmlViews htmlViews

    -- section Styles $ do
    --   markdocs $(embedFile "docs/atomic.md")
    --   CSS.example ~ embed
    --   markdocs "See [Styles](/css) for more info"

    section Interactive $ do
      markdocs $(embedFile "docs/hyperviews-intro.md")

      example $(moduleSourceNamed "Example.Simple") $ do
        runViewContext Root () int
 where
  getRunning = do
    markdocs "Hyperbole applications are divided into top-level `Page`s, which run side effects, then return an HTML `View`"
    snippet $ raw $ $(embedTopLevel "Example.Docs.BasicPage" "hello")

    markdocs "Run an Application via [Warp](https://hackage.haskell.org/package/warp) and [WAI](https://hackage.haskell.org/package/wai). This runs on port 3000 and responds to everything with \"Hello World\""

    snippet $ do
      raw $ $(embedTopLevel "Example.Docs.BasicPage" "main")

    col ~ embed $ do
      "Hello World"

  htmlViews = do
    markdocs "`View`s are HTML fragments with a `context`"
    snippet $ raw $ $(embedTopLevel "Example.Docs.BasicPage" "helloWorld")

    --  WARNING: this doesn't render properly when embedded in markdown
    snippet $
      text
        [i|>>> renderText helloWorld
"<div>Hello World</div>"|]

    markdocs "We can factor `View`s into reusable functions:"
    snippet $ do
      rawMulti
        [ $(embedTopLevel "Example.Docs.BasicPage" "messageView")
        , $(embedTopLevel "Example.Docs.BasicPage" "page")
        ]

    col ~ embed $ do
      "Hello World"

    markdocs "Using [atomic-css](/css) we can use functions to factor styles as well"


================================================
FILE: demo/App/Page/Intro/Intro.hs
================================================
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}

module App.Page.Intro.Intro where

import App.Docs
import App.Route
import Data.String.Interpolate (i)
import Example.Colors
import Example.Counter (Counter)
import Example.Simple (Message)
import Example.Simple qualified as Simple
import Example.Style.Cyber qualified as Cyber
import Example.View.Layout (layout)
import Web.Atomic.CSS
import Web.Hyperbole
import Web.Hyperbole.HyperView.Types
import Web.Hyperbole.Page (subPage)

page :: (Hyperbole :> es) => Page es '[Message, Counter]
page = do
  simple <- subPage Simple.page

  pure $ layout Intro $ do
    col ~ gap 20 $ do
      row ~ color cyan . bg Dark . pad 20 $ do
        space
        col ~ gap 10 . overflow Hidden $ do
          row $ do
            space
            codeblock ~ scaleText $ do
              [i|╔═════════════════════════════════════════════════════════════════════════════╗
║                                                                             ║
║  ██╗  ██╗██╗   ██╗██████╗ ███████╗██████╗ ██████╗  ██████╗ ██╗     ███████╗ ║
║  ██║  ██║╚██╗ ██╔╝██╔══██╗██╔════╝██╔══██╗██╔══██╗██╔═══██╗██║     ██╔════╝ ║
║  ███████║ ╚████╔╝ ██████╔╝█████╗  ██████╔╝██████╔╝██║   ██║██║     █████╗   ║
║  ██╔══██║  ╚██╔╝  ██╔═══╝ ██╔══╝  ██╔══██╗██╔══██╗██║   ██║██║     ██╔══╝   ║
║  ██║  ██║   ██║   ██║     ███████╗██║  ██║██████╔╝╚██████╔╝███████╗███████╗ ║
║  ╚═╝  ╚═╝   ╚═╝   ╚═╝     ╚══════╝╚═╝  ╚═╝╚═════╝  ╚═════╝ ╚══════╝╚══════╝ ║
╚═════════════════════════════════════════════════════════════════════════════╝
|]
            space
          el ~ fontSize 18 . Cyber.font . bold . textAlign AlignCenter $ do
            el "Create interactive HTML applications with type-safe serverside Haskell."
            el "Inspired by HTMX, Elm, and Phoenix LiveView"
        space

      col ~ gap 10 $ do
        example $(moduleSourceNamed "Example.Simple") $ do
          runViewContext Root () simple

        snippet $ do
          raw $(embedTopLevel "Example.Simple" "{-# LANGUAGE")
          raw "\nmodule Main where\n\n"
          raw $(embedSource "Example.Simple" (isTopLevel "import") (const True))

        section' "But Why?" $ do
          markdocs $(embedFile "docs/intro.md")

        section' "When not to use Hyperbole?" $ do
          markdocs $(embedFile "docs/intro-downsides.md")

        section' "Documentation" $ do
          markdocs $(embedFile "docs/intro-links.md")
 where
  scaleText :: (Styleable h) => CSS h -> CSS h
  scaleText =
    utility
      "scale-text"
      [ "font-size" :. "clamp(0.4rem, 1.5vw, 1rem)"
      , "max-width" :. "100%"
      ]


================================================
FILE: demo/App/Page/OAuth2.hs
================================================
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}

module App.Page.OAuth2 where

import App.Config (AppConfig (..))
import App.Docs
import App.Route qualified as Route
import Data.Aeson (eitherDecode)
import Data.String.Conversions (cs)
import Data.Text (Text, pack)
import Effectful
import Effectful.Reader.Dynamic
import Example.Style.Cyber as Cyber (btn, font)
import Example.View.Layout
import Network.HTTP.Client qualified as HTTP
import Web.Atomic.CSS
import Web.Hyperbole
import Web.Hyperbole.Data.URI (Endpoint (..), (./.))
import Web.Hyperbole.Effect.OAuth2 (Access, OAuth2, Token (..))
import Web.Hyperbole.Effect.OAuth2 qualified as OAuth2
import Web.Hyperbole.Types.Response (ResponseError (ErrAuth))

--------------------------------------------------------------------------------
-- App Specific Login
--------------------------------------------------------------------------------

-- This code belongs in an application-wide module
-- This example uses a mock OAuth2 server: https://app.beeceptor.com/mock-server/oauth-mock

data UserSession = UserSession
  { auth :: OAuth2.Authenticated
  , email :: Text
  }
  deriving (Generic, ToEncoded, FromEncoded)
instance Session UserSession where
  -- we want it to work on any page, not just this one
  cookiePath = Just []

openLogin :: (Hyperbole :> es, OAuth2 :> es, Reader AppConfig :> es) => Eff es a
openLogin = do
  Endpoint appRoot <- (.endpoint) <$> ask @AppConfig
  let redirectUrl = appRoot ./. routePath (Route.Examples Route.OAuth2Authenticate)
  u <- OAuth2.authUrl redirectUrl "email"
  redirect u

logout :: (Hyperbole :> es) => Eff es ()
logout = deleteSession @UserSession

-- | Target of the redirect after the user logs in via OAuth2
handleRedirect :: (Hyperbole :> es, OAuth2 :> es, Reader AppConfig :> es, IOE :> es) => Eff es Response
handleRedirect = do
  authCode <- OAuth2.validateCode
  auth <- OAuth2.exchangeAuth authCode
  info <- fetchUserInfo auth.accessToken
  saveSession @UserSession $ UserSession auth info.email
  redirect $ routeUri (Route.Examples Route.OAuth2)

data GithubUserInfo = GithubUserInfo
  { email :: Text
  }
  deriving (Generic, FromJSON, Show)

-- | Example authenticated request using an oauth access token. in a real app, this should be in an external effect, not IOE
fetchUserInfo :: (IOE :> es, Reader AppConfig :> es, Hyperbole :> es) => Token Access -> Eff es GithubUserInfo
fetchUserInfo (Token accessTok) = do
  app <- ask @AppConfig
  req <- HTTP.parseRequest "https://oauth-mock.mock.beeceptor.com/userinfo/github"
  res <- liftIO (HTTP.httpLbs (HTTP.applyBearerAuth (cs accessTok) req) app.manager)
  case eitherDecode @GithubUserInfo (HTTP.responseBody res) of
    Left e -> respondError $ ErrAuth $ "Could not parse user info: " <> pack (show e)
    Right info -> do
      liftIO $ putStrLn "GOT"
      liftIO $ print info
      pure info

--------------------------------------------------------------------------------
-- Page / Views
--------------------------------------------------------------------------------

page
  :: (Hyperbole :> es, OAuth2 :> es, Reader AppConfig :> es)
  => Page es '[Contents]
page = do
  muser <- lookupSession @UserSession
  pure $ layout (Route.Examples Route.OAuth2) $ do
    col ~ gap 10 $ do
      el "Hyperbole provides some helpers to make OAuth2 easier. This is done in 2 steps:"
      el "1. Initiate the login via the OAuth provider given a redirect url"
      el "2. After the redirect, the library validates the response and fetches an access token from the oauth provider."
      el "The developer can then make authenticated requests, and store a user session"
      example $(moduleSource) $ do
        hyper Contents $ viewContents muser

data Contents = Contents
  deriving (Generic, ViewId)

instance (OAuth2 :> es, Reader AppConfig :> es) => HyperView Contents es where
  data Action Contents
    = Logout
    | Login
    deriving (Generic, ViewAction)

  update Login = do
    openLogin
  update Logout = do
    logout
    pure $ viewContents Nothing

viewContents :: Maybe UserSession -> View Contents ()
viewContents mt = do
  col ~ gap 10 $ do
    maybe viewUnauthorized viewAuthorized mt

viewUnauthorized :: View Contents ()
viewUnauthorized = do
  message "Logged Out!"
  col ~ gap 5 $ do
    button Login "Login" ~ btn

viewAuthorized :: UserSession -> View Contents ()
viewAuthorized user = do
  let auth = user.auth
  message "Successfully Logged In!"
  el ~ pad 5 . grid' . gap 10 $ do
    dataItem "Email" user.email
    dataItem "Token Type" $ pack $ show auth.tokenType
    dataItem "Access Token" auth.accessToken.value
    dataItem "Expires In" $ pack $ show auth.expiresIn
    dataItem "Refresh Token" $ pack $ show auth.refreshToken
    dataItem "Scope" $ pack $ show auth.scope
  button Logout "Logout" ~ btn
 where
  dataItem :: Text -> Text -> View c ()
  dataItem lbl cnt = do
    el ~ bold $ do
      text lbl
    el ~ overflow Hidden $ text cnt

  grid' :: (Styleable h) => CSS h -> CSS h
  grid' =
    utility
      "grid"
      [ "display" :. "grid"
      , "grid-template-columns" :. "max-content auto"
      , "align-items" :. "center"
      ]

message :: View c () -> View c ()
message x = el x ~ pad 10 . Cyber.font . border 1


================================================
FILE: demo/App/Page/SideEffects.hs
================================================
{-# LANGUAGE TemplateHaskell #-}

module App.Page.SideEffects where

import App.Docs
import App.Route as Route (AppRoute (SideEffects))
import Example.Counter (Counter (..))
import Example.Docs.SideEffects as SideEffects
import Example.View.Layout (layoutSubnav)
import Web.Hyperbole

data EffectsSection
  = Effectful
  | Other
  | Custom
  deriving (Show, Enum, Bounded)

instance PageAnchor EffectsSection where
  sectionTitle Other = "Reader and More"
  sectionTitle Custom = "Databases and Custom Effects"
  sectionTitle a = camelTitle a

page :: (Hyperbole :> es) => Page es '[Counter, SlowReader, Titler]
page = do
  pure $ layoutSubnav @EffectsSection Route.SideEffects $ do
    section Effectful $ do
      markdocs $(embedFile "docs/effectful.md")
      example SideEffects.source $ do
        hyper Titler titleView

    section Other $ do
      markdocs $(embedFile "docs/effects-other.md")
      example SideEffects.source $ do
        hyper SlowReader $ messageView "..."

    section Custom $ do
      markdocs $(embedFile "docs/effects-custom.md")


================================================
FILE: demo/App/Page/State.hs
================================================
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}

module App.Page.State where

import App.Docs
import App.Route (AppRoute (State))
import Effectful.Concurrent
import Effectful.Concurrent.STM (TVar)
import Effectful.Reader.Dynamic
import Example.Counter as Threaded
import Example.State.Effects as Effects
import Example.State.Query (QueryPrefs (..))
import Example.State.Query qualified as Query
import Example.State.Sessions qualified as Session
import Example.State.Stateless
import Example.State.ViewState qualified as ViewState
import Example.View.Layout (layoutSubnav)
import Web.Hyperbole

data StateSection
  = Stateless
  | ActionThreading
  | ViewState
  | BrowserQuery
  | BrowserSessions
  | WithEffects
  deriving (Show, Enum, Bounded)

instance PageAnchor StateSection

page :: (Hyperbole :> es, Reader (TVar Int) :> es, Concurrent :> es) => Page es '[Threaded.Counter, Swapper, QueryPrefs, Session.Contents, Effects.Counter, ViewState.Counter]
page = do
  ssn <- session @Session.Preferences
  qry <- query @Query.Preferences
  cnt <- getCount
  pure $ layoutSubnav @StateSection State $ do
    section Stateless $ do
      markdocs $(embedFile "docs/state-stateless.md")

      example $(moduleSourceNamed "Example.State.Stateless") $ do
        hyper Swapper viewSwap

    section ActionThreading $ do
      markdocs $(embedFile "docs/state-threading.md")

      example $(moduleSourceNamed "Example.Counter") $ do
        hyper Threaded.Counter $ Threaded.viewCount 0

    section ViewState $ do
      markdocs $(embedFile "docs/state-viewstate.md")

      example $(moduleSourceNamed "Example.State.ViewState") $ do
        hyperState ViewState.CounterState 0 ViewState.viewCount

    section BrowserQuery $ do
      markdocs $(embedFile "docs/state-browser.md")

      example $(moduleSourceNamed "Example.State.Query") $ do
        hyper QueryPrefs $ Query.viewPreferences qry

    section BrowserSessions $ do
      markdocs $(embedFile "docs/state-sessions.md")

      example $(moduleSourceNamed "Example.State.Sessions") $ do
        hyper Session.Contents $ Session.viewContent ssn

    section WithEffects $ do
      markdocs $(embedFile "docs/state-effects.md")

      example $(moduleSourceNamed "Example.State.Effects") $ do
        hyper Effects.Counter $ Effects.viewCount cnt


================================================
FILE: demo/App/Page/ViewFunctions.hs
================================================
{-# LANGUAGE TemplateHaskell #-}

module App.Page.ViewFunctions where

import App.Docs
import App.Route qualified as Route
import Example.Docs.ViewFunctions as VF
import Example.Push qualified as Push
import Example.View.Layout (layoutSubnav)
import Web.Atomic.CSS
import Web.Hyperbole

data Basics
  = ViewFunctions
  | NotComponents
  deriving (Show, Enum, Bounded)
instance PageAnchor Basics

page :: (Hyperbole :> es) => Page es '[Message, Toggler, Progress, Push.Tasks]
page = do
  pure $ layoutSubnav @Basics Route.ViewFunctions $ do
    section ViewFunctions $ do
      markdocs $(embedFile "docs/view-functions.md")

      example VF.source $ do
        hyper VFMessage $ messageView "Hello"

    section NotComponents $ do
      markdocs $(embedFile "docs/view-components.md")

      example VF.source $ do
        hyper Toggler $ toggler False

      col ~ pad (T 20) . gap 10 $ do
        markdocs $(embedFile "docs/view-functions-wrap.md")

      example VF.source $ do
        -- hyper Push.Tasks $ Push.taskView 0
        hyper Progress $ workingHard 0.1

      col ~ pad (T 20) . gap 10 $ do
        markdocs $(embedFile "docs/view-functions-end.md")


================================================
FILE: demo/App/Route.hs
================================================
{-# LANGUAGE OverloadedLists #-}

module App.Route where

import Data.String.Conversions (cs)
import Data.Text (Text, unpack)
import Text.Casing (fromHumps, toWords)
import Text.Read (readMaybe)
import Web.Hyperbole
import Web.Hyperbole.Data.URI
import Web.Hyperbole.Route

type UserId = Int

data AppRoute
  = Main
  | Intro
  | Basics
  | CSS
  | Simple
  | Hello Hello
  | Contacts ContactRoute
  | Interactivity
  | SideEffects
  | Hyperviews
  | State
  | Counter
  | Forms FormRoute
  | HyperboleEffect
  | Response
  | Concurrency
  | Data DataRoute
  | Examples ExamplesRoute
  | Errors
  | Javascript
  | Test TestRoute
  | ViewFunctions
  | Application
  deriving (Eq, Generic, Show)
instance Route AppRoute where
  baseRoute = Just Main

-- -- View Route
-- data IntroRoute
--   = IntroMain
--   | Pages
--   | Views
--   | HyperViews
--   | ViewFunctions
--   | CSS CSSRoute
--   deriving (Eq, Generic, Show)
-- instance Route IntroRoute where
--   baseRoute = Just IntroMain

data FormRoute
  = FormSimple
  | FormValidation
  deriving (Eq, Generic, Show)
instance Route FormRoute where
  baseRoute = Just FormSimple

data DataRoute
  = DataLists
  | SortableTable
  | Autocomplete
  | Filter
  | LoadMore
  deriving (Eq, Generic, Show)
instance Route DataRoute where
  baseRoute = Just DataLists

-- data StateRoute
--   = StateRoot
--   | Actions
--   | StateView
--   | Effects
--   | Query
--   | Sessions
--   deriving (Eq, Generic, Show)
-- instance Route StateRoute where
--   baseRoute = Just StateRoot

data ContactRoute
  = ContactsAll
  | Contact UserId
  deriving (Eq, Generic, Show)
instance Route ContactRoute where
  baseRoute = Just ContactsAll

  matchRoute [contactId] = do
    cid <- readMaybe $ unpack contactId
    pure $ Contact cid
  matchRoute [] = pure ContactsAll
  matchRoute other = genMatchRoute other.segments

  routePath (Contact uid) = routePath uid
  routePath ContactsAll = []

data ExamplesRoute
  = OtherExamples
  | Todos
  | TodosCSS -- A version using the CSS from TodoMVC project
  | Tags
  | OAuth2Authenticate
  | OAuth2
  | Chat
  | Scrollbars
  deriving (Eq, Generic, Show)
instance Route ExamplesRoute where
  baseRoute = Just OtherExamples

data TestRoute
  = TestMain
  | TestState
  deriving (Eq, Generic, Show)
instance Route TestRoute where
  baseRoute = Just TestMain

data Hello
  = Greet Text
  | Redirected
  | RedirectNow
  deriving (Eq, Generic, Route, Show)

routeTitle :: AppRoute -> Text
routeTitle (Hello _) = "Hello World"
routeTitle CSS = "Styles"
-- routeTitle (Intro IntroMain) = "Intro"
-- routeTitle (Intro (CSS _)) = "Atomic CSS"
-- routeTitle (Intro r) = defaultTitle r
routeTitle (Contacts ContactsAll) = "Contacts"
routeTitle State = "Managing State"
routeTitle Hyperviews = "More HyperViews"
-- routeTitle (State StateRoot) = "State"
-- routeTitle (State StateView) = "Built-in State"
-- routeTitle (State Actions) = "Managing State"
-- routeTitle (State Query) = "Query"
-- routeTitle (State Sessions) = "Sessions"
routeTitle (Forms FormSimple) = "Forms"
routeTitle (Forms FormValidation) = "Form Validation"
routeTitle (Data d) = defaultTitle d
routeTitle Errors = "Error Handling"
routeTitle (Examples Todos) = "TodoMVC"
routeTitle (Examples TodosCSS) = "TodoMVC (CSS version)"
routeTitle (Examples OAuth2) = "OAuth2"
routeTitle (Examples OtherExamples) = "Examples"
routeTitle (Examples e) = defaultTitle e
routeTitle r = defaultTitle r

defaultTitle :: (Show r) => r -> Text
defaultTitle = cs . toWords . fromHumps . show


================================================
FILE: demo/App/Style.hs
================================================
module App.Style where

import Example.Colors
import Web.Atomic.CSS

-- btn :: (Styleable h) => CSS h -> CSS h
-- btn = btn' Primary
--
-- btn' :: (Styleable h) => AppColor -> CSS h -> CSS h
-- btn' clr =
--   bg clr
--     . hover (bg (hovClr clr))
--     . color (txtClr clr)
--     . pad 10
--     . shadow ()
--     . rounded 3
--  where
--   hovClr Primary = PrimaryLight
--   hovClr c = c
--   txtClr _ = White

btnLight :: (Styleable h) => CSS h -> CSS h
btnLight =
  base
    . border 2
    . borderColor Secondary
    . color Secondary
    . hover (borderColor SecondaryLight . color SecondaryLight)
 where
  base = pad (XY 15 8)

h1 :: (Styleable h) => CSS h -> CSS h
h1 = bold . fontSize 32

invalid :: (Styleable h) => CSS h -> CSS h
invalid = color Danger

success :: (Styleable h) => CSS h -> CSS h
success = color Success

link :: (Styleable h) => CSS h -> CSS h
link = color Primary . underline

input :: (Styleable h) => CSS h -> CSS h
input = border 1 . pad 8

strikethrough :: (Styleable h) => CSS h -> CSS h
strikethrough =
  utility "strike" ["text-decoration" :. "line-through"]

uppercase :: (Styleable h) => CSS h -> CSS h
uppercase = utility "upper" ["text-transform" :. "uppercase"]


================================================
FILE: demo/App.hs
================================================
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-unused-imports #-}

module App where

import App.Cache (clientCache)
import App.Config
import App.Docs.Page
import App.Page.Application qualified as Application
import App.Page.CSS qualified as CSS
import App.Page.Concurrency qualified as Concurrency
import App.Page.Examples qualified as Examples
import App.Page.Forms qualified as Forms
import App.Page.HyperboleEffect qualified as Hyp
import App.Page.Hyperviews qualified as Hyperviews
import App.Page.Interactivity qualified as Interactivity
import App.Page.Intro.Basics qualified as Basics
import App.Page.Intro.Intro qualified as Intro
import App.Page.OAuth2 qualified as OAuth2
import App.Page.SideEffects qualified as SideEffects
import App.Page.State qualified as State
import App.Page.ViewFunctions qualified as ViewFunctions
import App.Route as Route
import Control.Concurrent
  ( MVar
  , ThreadId
  , forkFinally
  , killThread
  , newEmptyMVar
  , putMVar
  , takeMVar
  )
import Control.Monad (forever, when, (>=>))
import Data.ByteString.Lazy qualified as BL
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Maybe (fromMaybe)
import Data.String.Conversions (cs)
import Data.String.Interpolate (i)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Lazy qualified as L
import Data.Text.Lazy.Encoding qualified as L
import Data.Version (showVersion)
import Effectful
import Effectful.Concurrent.STM
import Effectful.Dispatch.Dynamic
import Effectful.Environment (runEnvironment)
import Effectful.Reader.Dynamic
import Effectful.State.Static.Local
import Example.Chat qualified as Chat
import Example.Colors
import Example.Contact qualified as Contact
import Example.Contacts qualified as Contacts
import Example.Counter qualified as Counter
import Example.DataLists.Autocomplete qualified as Autocomplete
import Example.DataLists.DataTable qualified as DataTable
import Example.DataLists.Filter qualified as Filter
import Example.DataLists.LoadMore qualified as LoadMore
import Example.Effects.Debug as Debug
import Example.Effects.Todos (Todos, runTodosSession)
import Example.Effects.Users as Users
import Example.Scrollbars qualified as Scrollbars
import Example.State.Effects qualified as Effects
import Example.State.Query qualified as Query
import Example.State.Sessions qualified as Sessions
import Example.State.ViewState qualified as ViewState
import Example.Style qualified as Style
import Example.Style.Cyber qualified as Cyber
import Example.Tags qualified as Tags
import Example.Test qualified as Test
import Example.Todos.Todo qualified as Todo
import Example.Todos.TodoCSS qualified as TodoCSS
import Example.View.Layout as Layout (layout)
import Foreign.Store (Store (..), lookupStore, readStore, storeAction, withStore)
import GHC.Generics (Generic)
import GHC.Word (Word32)
import Network.HTTP.Client qualified as HTTP
import Network.HTTP.Client.TLS qualified as HTTPS
import Network.HTTP.Types (Header, Method, QueryItem, hCacheControl, methodPost, status200, status404)
import Network.Wai qualified as Wai
import Network.Wai.Handler.Warp qualified as Warp
import Network.Wai.Middleware.Static as Static (CacheContainer, CachingStrategy (..), Options (..), addBase)
import Network.Wai.Middleware.Static qualified as Static
import Network.WebSockets (Connection, PendingConnection, acceptRequest, defaultConnectionOptions)
import Paths_demo (version)
import Paths_demo qualified as Pt
import Safe (readMay)
import System.Environment qualified as SE
import System.IO (BufferMode (LineBuffering), hSetBuffering, stdout)
import Web.Atomic.CSS
import Web.Hyperbole
import Web.Hyperbole.Application
import Web.Hyperbole.Effect.GenRandom
import Web.Hyperbole.Effect.OAuth2 (OAuth2, runOAuth2)
import Web.Hyperbole.Effect.OAuth2 qualified as OAuth2
import Web.Hyperbole.Server.Options (defaultError)
import Web.Hyperbole.Types.Response

run :: IO ()
run = do
  hSetBuffering stdout LineBuffering

  port <- do
    mStr <- SE.lookupEnv "PORT"
    pure $ fromMaybe 3000 (readMay =<< mStr)
  putStrLn $ "Starting Examples on http://localhost:" <> show port

  users <- Users.initUsers
  (count, room, config) <- runEff $ runEnvironment $ do
    c <- runConcurrent Effects.initCounter
    room <- runConcurrent Chat.initChatRoom
    a <- getAppConfigEnv
    pure (c, room, a)

  cache <- clientCache

  Warp.run port $
    Static.staticPolicyWithOptions cache (addBase "client/dist") $
      Static.staticPolicy (addBase "demo/static") $ do
        devReload config $ exampleApp config users count room
 where
  devReload :: AppConfig -> Application -> Application
  devReload config
    | config.devMode = Wai.modifyResponse $ Wai.mapResponseHeaders $ \hs -> ("Connection", "Close") : hs
    | otherwise = id

exampleApp :: AppConfig -> UserStore -> TVar Int -> Chat.Room -> Application
exampleApp config users count chats = do
  liveAppWith
    (ServerOptions (document documentHead) serverError)
    (runApp . routeRequest $ router)
 where
  runApp :: (Hyperbole :> es, IOE :> es) => Eff (OAuth2 : GenRandom : Concurrent : Debug : Users : Todos : Reader AppConfig : es) a -> Eff es a
  runApp = runReader config . runTodosSession . runUsersIO users . runDebugIO . runConcurrent . runRandom . runOAuth2 config.oauth config.manager

  router :: forall es. (Hyperbole :> es, OAuth2 :> es, Todos :> es, Users :> es, Debug :> es, Concurrent :> es, IOE :> es, GenRandom :> es, Reader AppConfig :> es) => AppRoute -> Eff es Response
  router Counter = runPage Counter.page
  router (Hello h) = runPage $ hello h
  router (Contacts (Contact uid)) = Contact.response uid
  router (Contacts ContactsAll) = runPage Contacts.page
  router Concurrency = runPage Concurrency.page
  router (Data r) =
    case r of
      DataLists -> redirect $ routeUri (Data SortableTable)
      SortableTable -> runPage DataTable.page
      Autocomplete -> runPage Autocomplete.page
      Filter -> runPage Filter.page
      LoadMore -> runPage LoadMore.page
  router Errors = redirect (routeUri HyperboleEffect)
  router (Forms _) = runPage Forms.page
  router HyperboleEffect = runPage Hyp.page
  router Hyperviews = runPage Hyperviews.page
  router Route.Response = redirect (routeUri HyperboleEffect)
  router State = runReader count $ runPage State.page
  router SideEffects = runReader @Text "Secret Message!" $ runPage SideEffects.page
  router Intro = runPage Intro.page
  router Basics = runPage Basics.page
  router Application = runPage Application.page
  router ViewFunctions = runPage ViewFunctions.page
  -- router (Intro HyperViews) = runPage IntroHyperViews.page
  -- router (Intro Pages) = runPage IntroPages.page
  -- router (Intro ViewFunctions) = runPage IntroViewFunctions.page
  router CSS = runPage CSS.page
  router Interactivity = runPage Interactivity.page
  router (Examples Chat) = runReader chats $ runPage Chat.page
  router (Examples OtherExamples) = runPage Examples.page
  router (Examples Todos) = runPage Todo.page
  router (Examples Tags) = runPage Tags.page
  router (Examples TodosCSS) = runPage TodoCSS.page
  router Javascript = redirect (routeUri Interactivity)
  router (Examples OAuth2) = runPage OAuth2.page
  router (Examples OAuth2Authenticate) = OAuth2.handleRedirect
  router (Examples Scrollbars) = runPage Scrollbars.page
  router Simple = redirect (routeUri Intro)
  -- router Counter = redirect (routeUri $ State StateRoot)
  router (Test TestMain) = runPage Test.page
  router (Test TestState) = runPage ViewState.page
  router Main = do
    redirect (routeUri Intro)

  -- Nested Router
  hello :: (Hyperbole :> es, Debug :> es) => Hello -> Page es '[]
  hello RedirectNow = do
    redirect (routeUri $ Hello Redirected)
  hello (Greet who) = do
    pure $ layout (Hello $ Greet who) $ do
      row ~ gap 6 . pad 10 $ do
        el "Hello:"
        el $ text who
  hello Redirected = do
    pure $ layout HyperboleEffect $ do
      col ~ pad 10 . gap 10 $ do
        el "You were redirected"
        route HyperboleEffect ~ Style.link $ "Go Back"

  -- Use the embedded version for real applications (see quickStartDocument).
  -- The link to /hyperbole.js here is just to make local development easier
  documentHead :: View DocumentHead ()
  documentHead = do
    title "Hyperbole Examples"
    mobileFriendly
    stylesheet "/cyber.css"
    script "/hyperbole.js"
    stylesheet "/prism.css"
    script "/prism.js" @ att "defer" ""
    script "/docs.js" @ att "defer" ""
    style "html { scroll-behavior: smooth; }\n body { background-color: #e0e7f1; font-family: font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Noto Sans\", Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\") }, button { font-family: 'Share Tech Mono'}"
    style cssEmbed

    when config.devMode $ do
      script' scriptLiveReload

  serverError :: ResponseError -> ServerError
  -- serverError NotFound = ServerError "NotFound" $ Cyber.cyberError "Custom Not Found!"
  serverError (ErrCustom s) = s
  serverError err =
    let msg = defaultErrorMessage err
     in ServerError
          { message = msg
          , body = Cyber.cyberError $ Cyber.glitch msg
          }

{- | Made for local development
 -
 - ghcid --setup=Main.update --command="cabal repl exe:examples lib:hyperbole test" --run=Main.update --warnings
 -
 - Start or restart the server.
newStore is from foreign-store.
A Store holds onto some data across ghci reloads
-}
update :: IO ()
update = do
  mtidStore <- lookupStore tidStoreNum
  case mtidStore of
    -- no server running
    Nothing -> do
      done <- storeAction doneStore newEmptyMVar
      tid <- start done
      _ <- storeAction (Store tidStoreNum) (newIORef tid)
      return ()
    -- server is already running
    Just tidStore -> do
      restartAppInNewThread tidStore
 where
  -- callCommand "xmonadctl refreshFirefox"

  doneStore :: Store (MVar ())
  doneStore = Store 0

  -- shut the server down with killThread and wait for the done signal
  restartAppInNewThread :: Store (IORef ThreadId) -> IO ()
  restartAppInNewThread tidStore = modifyStoredIORef tidStore $ \tid -> do
    killThread tid
    withStore doneStore takeMVar
    readStore doneStore >>= start

  -- \| Start the server in a separate thread.
  start
    :: MVar ()
    -- \^ Written to when the thread is killed.
    -> IO ThreadId
  start done = do
    forkFinally
      App.run
      -- Note that this implies concurrency
      -- between shutdownApp and the next app that is starting.
      -- Normally this should be fine
      (\_ -> putMVar done ())

tidStoreNum :: Word32
tidStoreNum = 1

modifyStoredIORef :: Store (IORef a) -> (a -> IO a) -> IO ()
modifyStoredIORef store f = withStore store $ \ref -> do
  v <- readIORef ref
  f v >>= writeIORef ref

cacheMiddleware :: Application -> Application
cacheMiddleware = Wai.modifyResponse addCache
 where
  addCache = Wai.mapResponseHeaders ((hCacheControl, "private, max-age=60") :)


================================================
FILE: demo/Example/CSS/External.hs
================================================
{-# LANGUAGE TemplateHaskell #-}

module Example.CSS.External where

import Data.Text (Text)
import App.Docs
import Web.Hyperbole

source :: ModuleSource
source = $(moduleSource)

main :: IO ()
main = do
  run 3000 $ do
    liveApp quickStartDocument (runPage page)

page :: (Hyperbole :> es) => Page es '[Items]
page = do
  pure $ do
    -- you can choose to include a stylesheet only on pages
    -- that use it or load it globally in your document function
    stylesheet "external.css"
    hyper Items $ itemsView "one"

data Items = Items
  deriving (Generic, ViewId)

instance HyperView Items es where
  data Action Items = Select Text
    deriving (Generic, ViewAction)

  update (Select t) = do
    pure $ itemsView t

itemsView :: Text -> View Items ()
itemsView sel = do
  el @ class_ "parent" $ do
    item "one"
    item "two"
    item "three"
    item "four"
    item "five"
 where
  selected i =
    if sel == i
      then class_ "selected"
      else id

  item i =
    -- the class_ attribute MERGES classes if you set it more than once
    button (Select i) @ class_ "item" . selected i $ text i


================================================
FILE: demo/Example/CSS/Loading.hs
================================================
{-# LANGUAGE UndecidableInstances #-}

module Example.CSS.Loading where

import Data.Text (Text)
import Example.Effects.Debug
import Example.Style.Cyber (btn)
import Web.Atomic.CSS
import Web.Hyperbole

data Loader = Loader
  deriving (Generic, ViewId)

instance (Debug :> es) => HyperView Loader es where
  data Action Loader
    = LoadSlow
    deriving (Generic, ViewAction)

  update LoadSlow = do
    delay 1000
    pure $ viewLoaders "OK!"

viewLoaders :: Text -> View Loader ()
viewLoaders status = do
  col ~ gap 10 $ do
    row ~ gap 10 . whenLoading flexRow . display None $ do
      loadingBars
      el "Loading..."
    el ~ whenLoading (display None) $ text status
    button LoadSlow ~ btn . whenLoading (opacity 0.5) $ "Load Slow"

loadingBars :: View c ()
loadingBars = el ~ cls "loader" $ none


================================================
FILE: demo/Example/CSS/Tooltips.hs
================================================
{-# LANGUAGE TemplateHaskell #-}

module Example.CSS.Tooltips where

import App.Docs
import Example.Colors
import Web.Atomic.CSS
import Web.Hyperbole

source :: ModuleSource
source = $(moduleSource)

tooltips :: View c ()
tooltips = do
  col ~ pad 10 . gap 10 . width 300 $ do
    mapM_ viewItemRow ["One", "Two", "Three", "Four", "Five", "Six"]
 where
  viewItemRow item = do
    col ~ stack . showTooltips . hover (color Primary) . pointer $ do
      el ~ border 1 . bg White . pad 5 $ text item
      el ~ cls "tooltip" . popup (TR 10 10) . zIndex 1 . visibility Hidden $ do
        col ~ border 2 . gap 5 . bg White . pad 5 $ do
          el ~ bold $ "DETAILS"
          el $ text item
          el "details about this item"

  showTooltips =
    css
      "tooltips"
      ".tooltips:hover > .tooltip"
      (declarations (visibility Visible))


================================================
FILE: demo/Example/CSS/Transitions.hs
================================================
{-# LANGUAGE TemplateHaskell #-}

module Example.CSS.Transitions where

import App.Docs
import Example.Style.Cyber (btn)
import Web.Atomic.CSS
import Web.Hyperbole

source :: ModuleSource
source = $(moduleSource)

data Animate = Animate
  deriving (Generic, ViewId)

instance HyperView Animate es where
  data Action Animate
    = Expand
    | Collapse
    deriving (Generic, ViewAction)
  update Expand = do
    pure viewBig
  update Collapse = do
    pure viewSmall

viewSmall :: View Animate ()
viewSmall = do
  col ~ gap 10 . transition 300 (Width 200) $ do
    el "Small"
    button Expand "Expand" ~ btn

viewBig :: View Animate ()
viewBig =
  col ~ gap 10 . transition 300 (Width 400) $ do
    el "Expanded"
    button Collapse "Collapse" ~ btn


================================================
FILE: demo/Example/Chat.hs
================================================
{-# LANGUAGE UndecidableInstances #-}

module Example.Chat where

import App.Route
import Control.Monad (forM_, forever)
import Data.Text (Text)
import Effectful
import Effectful.Concurrent
import Effectful.Concurrent.STM
import Effectful.Reader.Dynamic
import Effectful.State.Dynamic (modify)
import Example.Colors
import Example.Style qualified as Style
import Example.Style.Cyber (embed)
import Example.Style.Cyber as Cyber (btn, font)
import Example.View.Layout (layout)
import Web.Atomic.CSS
import Web.Hyperbole
import Web.Hyperbole.Data.Encoded (Encoded (..), FromEncoded (..), ToEncoded (..))

page :: (Hyperbole :> es, Concurrent :> es, Reader Room :> es) => Page es '[Content, Chats, NewMessage]
page = do
  pure $ layout (Examples Chat) $ do
    el "Demonstrates server pushes and concurrency. Open in two tabs with different usernames to test."
    col ~ embed . Cyber.font $ do
      hyper Content $ contentView Nothing

type Username = Text

data Content = Content
  deriving (Generic, ViewId)

instance HyperView Content es where
  data Action Content = Login | Logout
    deriving (Generic, ViewAction)

  type Require Content = '[Chats, NewMessage]

  update Login = do
    LoginForm u <- formData
    pure $ contentView (Just u)
  update Logout =
    pure $ contentView Nothing

data LoginForm = LoginForm
  { username :: Text
  }
  deriving (Generic, FromForm)

contentView :: Maybe Username -> View Content ()
contentView mu = do
  case mu of
    Nothing -> do
      form Login ~ flexRow . gap 10 $ do
        field "username" $ do
          input Username @ placeholder "Username" . autofocus ~ Style.input
        submit "Login" ~ btn
    Just u -> do
      col ~ gap 10 $ do
        row ~ gap 10 $ do
          el "Welcome "
          el ~ bold $ text u
          space
          button Logout ~ btn $ "logout"
        hyperState Chats mempty $ chatsLoad u
        hyper (NewMessage u) messageView

-- Chat Room -------------------------------------

data Message = Message
  { sender :: Username
  , body :: Text
  }
  deriving (Generic, ToParam, FromParam)

newtype Room = Room (TChan Message)
newtype Subscription = Subscription (TChan Message)

initChatRoom :: (Concurrent :> es) => Eff es Room
initChatRoom = Room <$> newBroadcastTChanIO

subscribeChatRoom :: (Concurrent :> es) => Room -> Eff es Subscription
subscribeChatRoom (Room chan) = fmap Subscription <$> atomically $ dupTChan chan

waitMessage :: (Concurrent :> es) => Subscription -> Eff es Message
waitMessage (Subscription chan) = atomically $ readTChan chan

sendMessage :: (Concurrent :> es) => Room -> Message -> Eff es ()
sendMessage (Room chan) msg = atomically $ writeTChan chan msg

-- Encoding for message history since starting
newtype AllMessages = AllMessages [Message]
  deriving newtype (Semigroup, Monoid)

instance ToEncoded AllMessages where
  toEncoded (AllMessages ms) = Encoded "" (fmap toParam ms)
instance FromEncoded AllMessages where
  parseEncoded (Encoded _ ps) =
    AllMessages <$> mapM parseParam ps

--- Chat Updates ---------------------------------------------

data Chats = Chats
  deriving (Generic)
instance ViewId Chats where
  type ViewState Chats = AllMessages

instance (Concurrent :> es, Reader Room :> es, IOE :> es) => HyperView Chats es where
  data Action Chats = Stream Username
    deriving (Generic, ViewAction)

  update (Stream u) = do
    room <- ask
    sub <- subscribeChatRoom room

    sendMessage room $ Message u "I have arrived!"

    forever (streamChats sub)
   where
    streamChats room = do
      -- Block until we receive a message from the duplicated channel
      msg <- waitMessage room

      -- store all the messages we've seen in our view state
      modify $ addMessage msg

      -- update the view
      pushUpdate $ chatsView u

addMessage :: Message -> AllMessages -> AllMessages
addMessage msg (AllMessages ms) = AllMessages $ msg : ms

allMessages :: View Chats AllMessages
allMessages = do
  AllMessages ms <- viewState
  pure $ AllMessages $ reverse ms

chatsLoad :: Username -> View Chats ()
chatsLoad user = el @ onLoad (Stream user) 100 $ "..."

chatsView :: Username -> View Chats ()
chatsView _user = do
  AllMessages chats <- allMessages
  col ~ gap 5 . pad 5 . minHeight 400 . border 1 . bg GrayLight $ do
    forM_ chats $ \chat -> do
      el $ do
        text chat.sender
        text ": "
        text chat.body

--- New Message Form ------------------------------

data NewMessage = NewMessage Username
  deriving (Generic, ViewId)

instance (Concurrent :> es, Reader Room :> es, IOE :> es) => HyperView NewMessage es where
  data Action NewMessage = SendMessage
    deriving (Generic, ViewAction)

  update SendMessage = do
    room <- ask
    NewMessage user <- viewId
    MessageForm msg <- formData
    sendMessage room $ Message user msg
    -- NOTE: this doesn't show an update at all, but we are subscribed to the channel and will get a push like everyone else
    pure messageView

data MessageForm = MessageForm
  { message :: Text
  }
  deriving (Generic, FromForm)

messageView :: View NewMessage ()
messageView = do
  form SendMessage ~ flexRow . gap 10 $ do
    field "message" $ do
      input TextInput @ placeholder "type your message here" . value "" . autofocus ~ Style.input . grow
    submit "Send" ~ btn


================================================
FILE: demo/Example/Colors.hs
================================================
{-# LANGUAGE LambdaCase #-}

module Example.Colors where

import Web.Atomic.CSS
import Web.Hyperbole

data AppColor
  = White
  | Light
  | GrayLight
  | GrayDark
  | Dark
  | DarkHighlight
  | Success
  | Danger
  | Warning
  | Primary
  | PrimaryLight
  | Secondary
  | SecondaryLight
  deriving (Show, Read, Eq, Generic, ToJSON, FromJSON, ToParam, FromParam)

instance Default AppColor where
  def = White

instance ToColor AppColor where
  colorValue White = "#FFF"
  colorValue Light = "#F2F2F3"
  colorValue GrayLight = "#E3E5E9"
  colorValue GrayDark = "#2С3С44"
  -- colorValue Dark = "#2E3842" -- "#232C41"
  colorValue Dark = "#121726" -- "#232C41"
  colorValue DarkHighlight = "#343945" -- "#232C41"
  colorValue Primary = "#4171b7"
  colorValue PrimaryLight = "#6D9BD3"
  -- colorValue PrimaryLight = "#e2ebf6"
  colorValue Secondary = "#5D5A5C"
  colorValue SecondaryLight = "#9D999C"
  -- colorValue Success = "67C837"
  colorValue Success = "#149e5a"
  colorValue Danger = midRed
  colorValue Warning = "#e1c915"

lightRed :: HexColor
lightRed = HexColor "#EC6458"

midRed :: HexColor
midRed = HexColor "#A03F38"

darkRed :: HexColor
darkRed = HexColor "#722C2A"

cyan :: HexColor
cyan = "#0FF"

magenta :: HexColor
magenta = "#E44072"

light :: AppColor -> HexColor
light PrimaryLight = "#a8c3e5"
light Primary = colorValue PrimaryLight
-- light Danger = "#ef8379"
light Danger = lightRed
light c = colorValue c

hoverColor :: AppColor -> HexColor
hoverColor = \case
  White -> colorValue Light
  c -> light c

contrastColor :: AppColor -> HexColor
contrastColor = \case
  Primary -> colorValue White
  PrimaryLight -> colorValue White
  Danger -> colorValue White
  _ -> colorValue Dark


================================================
FILE: demo/Example/Concurrency/LazyLoading.hs
================================================
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}

module Example.Concurrency.LazyLoading where

import App.Docs
import Effectful
import Example.Colors
import Example.Concurrency.Tasks
import Example.Effects.Debug
import Web.Atomic.CSS
import Web.Hyperbole
import Web.Hyperbole.Effect.GenRandom

-----------------------------------------------------------
-- Lazy Loading Expensive Data
-----------------------------------------------------------

data LazyData = LazyData TaskId
  deriving (Generic, ViewId)

instance (Debug :> es, GenRandom :> es) => HyperView LazyData es where
  data Action LazyData
    = Details
    deriving (Generic, ViewAction)

  update Details = do
    LazyData taskId <- viewId
    task <- pretendLoadTask taskId
    pure $ viewTaskDetails task

viewTaskLoad :: View LazyData ()
viewTaskLoad = do
  -- 100ms after rendering, get the details
  el @ onLoad Details 100 ~ bg GrayLight . textAlign AlignCenter $ do
    text "..."

viewTaskDetails :: Task -> View LazyData ()
viewTaskDetails task = do
  el ~ color Success . textAlign AlignCenter $ do
    text task.details

source :: ModuleSource
source = $(moduleSource)


================================================
FILE: demo/Example/Concurrency/Overlap.hs
================================================
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}

module Example.Concurrency.Overlap where

import App.Docs
import Data.Text (Text, pack)
import Effectful
import Example.Effects.Debug
import Example.Style.Cyber (btn)
import Example.View.Loader as Loader
import Web.Atomic.CSS
import Web.Hyperbole

-- Concurrency = Drop  ---------------------------

data OverlapDrop = OverlapDrop
  deriving (Generic, ViewId)

instance (Debug :> es) => HyperView OverlapDrop es where
  data Action OverlapDrop
    = GetTimeDrop
    deriving (Generic, ViewAction)

  -- this is the default, not necessary to specify
  -- type Concurrency OverlapDrop = Drop

  update GetTimeDrop = do
    t <- getTimeSlowly
    pure $ viewTimeDrop (Just t)

viewTimeDrop :: Maybe UTCTime -> View OverlapDrop ()
viewTimeDrop = viewTime GetTimeDrop "Drop"

-- Concurrency = Replace  --------------------------

data OverlapReplace = OverlapReplace
  deriving (Generic, ViewId)

instance (Debug :> es) => HyperView OverlapReplace es where
  data Action OverlapReplace
    = GetTimeReplace
    deriving (Generic, ViewAction)

  type Concurrency OverlapReplace = Replace

  update GetTimeReplace = do
    t <- getTimeSlowly
    pure $ viewTimeReplace (Just t)

viewTimeReplace :: Maybe UTCTime -> View OverlapReplace ()
viewTimeReplace = viewTime GetTimeReplace "Replace"

-- Utilities -----------------------------------------------

getTimeSlowly :: (Debug :> es) => Eff es UTCTime
getTimeSlowly = do
  delay 2000
  systemTime

viewTime :: (ViewAction (Action id)) => Action id -> Text -> Maybe UTCTime -> View id ()
viewTime runTime loadLbl mtime = do
  row ~ gap 10 $ do
    button runTime ~ btn $ text loadLbl
    Loader.loading
    case mtime of
      Nothing -> none
      Just t -> el ~ whenLoading (display None) $ text $ pack $ show t

source :: ModuleSource
source = $(moduleSource)


================================================
FILE: demo/Example/Concurrency/Polling.hs
================================================
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}

module Example.Concurrency.Polling where

import App.Docs
import Data.Text (pack)
import Effectful
import Example.Effects.Debug
import Example.Style.Cyber (btn)
import Web.Atomic.CSS
import Web.Hyperbole

-----------------------------------------------------------
-- Simple Polling
-----------------------------------------------------------

data Poller = Poller
  deriving (Generic, ViewId)

instance (Debug :> es) => HyperView Poller es where
  data Action Poller
    = Reload Int
    | Stop
    | Pause Int
    deriving (Generic, ViewAction)

  -- to stop, return a view without an onLoad
  update (Pause n) = do
    pure $ viewPaused n
  update Stop = do
    pure viewStopped
  update (Reload n) = do
    pure $ viewPoll n

viewInit :: View Poller ()
viewInit = do
  row $ do
    button (Reload 1) "Start Polling" ~ btn

viewStopped :: View Poller ()
viewStopped = do
  row $ do
    button (Reload 1) "Restart Polling" ~ btn

viewPaused :: Int -> View Poller ()
viewPaused n = do
  col ~ gap 10 $ do
    row $ do
      button (Reload n) "Resume" ~ btn
    viewStatus n

viewPoll :: Int -> View Poller ()
viewPoll n = do
  -- reload every 200ms + round trip delay
  col @ onLoad (Reload (n + 1)) 250 ~ gap 10 $ do
    row ~ gap 5 $ do
      button (Pause n) "Pause" ~ btn
      button Stop "Stop" ~ btn
    viewStatus n

viewStatus :: Int -> View Poller ()
viewStatus n = do
  el $ do
    text "Polling... "
    text $ pack $ show n

source :: ModuleSource
source = $(moduleSource)


================================================
FILE: demo/Example/Concurrency/Progress.hs
================================================
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}

module Example.Concurrency.Progress where

import App.Docs
import Control.Monad (when)
import Data.Text (pack)
import Effectful
import Example.Colors
import Example.Concurrency.Tasks
import Example.Effects.Debug
import Example.View.Inputs (progressBar)
import Web.Atomic.CSS
import Web.Hyperbole
import Web.Hyperbole.Effect.GenRandom

-----------------------------------------------------------
-- Overlapping Progress Bars
-----------------------------------------------------------

type PercentPerTick = Int

data Progress = Progress TaskId
  deriving (Generic, ViewId)

instance (Debug :> es, GenRandom :> es) => HyperView Progress es where
  data Action Progress
    = GoProgress PercentPerTick
    deriving (Generic, ViewAction)

  update (GoProgress progPerTick) = do
    _ <- tick 0
    pure $ viewProgress 100
   where
    tick current = do
      -- pretend we did some work
      -- this will not block other hyperviews from updating
      delay 50
      let total = current + progPerTick

      when (total < 100) $ do
        pushUpdate $ viewProgress total
        tick total

viewProgressLoad :: PercentPerTick -> View Progress ()
viewProgressLoad p = el @ onLoad (GoProgress p) 50 $ none

viewProgress :: Int -> View Progress ()
viewProgress prg
  | prg >= 100 = viewComplete
  | otherwise = viewUpdating
 where
  viewComplete = do
    row ~ bg Success . color White . pad 5 $ "Complete"

  viewUpdating = do
    let pct = fromIntegral prg / 100
    Progress taskId <- viewId
    progressBar pct $ do
      el ~ grow $ text $ "Task" <> pack (show taskId)

source :: ModuleSource
source = $(moduleSource)


================================================
FILE: demo/Example/Concurrency/Tasks.hs
================================================
module Example.Concurrency.Tasks where

import Data.Text (Text, pack)
import Effectful
import Example.Effects.Debug
import Web.Hyperbole.Effect.GenRandom

-- Fake Tasks Effect ----------------------------------------

type TaskId = Int

data Task = Task
  { taskId :: TaskId
  , details :: Text
  }

pretendLoadTask :: (Debug :> es, GenRandom :> es) => TaskId -> Eff es Task
pretendLoadTask taskId = do
  randomDelay <- genRandom (100, 1000)
  delay randomDelay

  pure $ Task taskId $ pack (show taskId)

pretendTasks :: [TaskId]
pretendTasks = [1 .. 30]


================================================
FILE: demo/Example/Contact.hs
================================================
{-# LANGUAGE UndecidableInstances #-}

module Example.Contact where

import App.Route (UserId)
import App.Route qualified as Route
import Data.Maybe (fromMaybe)
import Data.String.Conversions
import Data.Text (Text, pack)
import App.Docs
import Effectful
import Effectful.Reader.Dynamic
import Example.Colors
import Example.Effects.Debug
import Example.Effects.Users (User (..), Users)
import Example.Effects.Users qualified as Users
import Example.Style qualified as Style
import Example.Style.Cyber (btn)
import Example.View.Layout
import Web.Atomic.CSS
import Web.Hyperbole

-- Example adding a reader context to the page, based on an argument from the AppRoute
response :: (Hyperbole :> es, Users :> es, Debug :> es) => UserId -> Eff es Response
response uid = runReader uid $ runPage page

-- The page assumes all effects have been added
page
  :: forall es
   . (Hyperbole :> es, Users :> es, Debug :> es, Reader UserId :> es)
  => Page es '[Contact]
page = do
  uid <- ask
  u <- Users.find uid
  pure $ layout (Route.Contacts Route.ContactsAll) $ do
    section' "Contact" $ do
      hyper (Contact uid) $ contactView u

-- Contact ----------------------------------------------------

data Contact = Contact UserId
  deriving (Generic, ViewId)

instance (Users :> es, Debug :> es) => HyperView Contact es where
  data Action Contact
    = Edit
    | Save
    | ViewContact
    deriving (Generic, ViewAction)

  update action = do
    -- No matter which action we are performing, let's look up the user to make sure it exists
    Contact uid <- viewId
    u <- Users.find uid
    case action of
      ViewContact -> do
        pure $ contactView u
      Edit -> do
        pure $ contactEditView u
      Save -> do
        delay 1000
        unew <- parseUser uid
        Users.save unew
        pure $ contactView unew

data ContactForm f = ContactForm
  { firstName :: Field f Text
  , lastName :: Field f Text
  , age :: Field f Int
  , info :: Field f Text
  }
  deriving (Generic, FromFormF, GenFields FieldName, GenFields Maybe)

parseUser :: (Hyperbole :> es) => Int -> Eff es User
parseUser uid = do
  ContactForm{firstName, lastName, age, info} <- formData @(ContactForm Identity)
  pure User{id = uid, isActive = True, firstName, lastName, age, info}

contactView :: User -> View Contact ()
contactView = contactView' Edit

contactView' :: (ViewId c, ViewAction (Action c)) => Action c -> User -> View c ()
contactView' edit u = do
  col ~ gap 10 $ do
    row ~ fld $ do
      el (text "First Name:")
      text u.firstName

    row ~ fld $ do
      el (text "Last Name:")
      text u.lastName

    row ~ fld $ do
      el (text "Age:")
      text (cs $ show u.age)

    row ~ fld $ do
      el (text "Info:")
      text u.info

    row ~ fld $ do
      el (text "Active:")
      text (cs $ show u.isActive)

    button edit "Edit" ~ btn
 where
  fld = gap 10

contactEditView :: User -> View Contact ()
contactEditView u = do
  el contactLoading ~ display None . whenLoading flexCol
  el (contactEdit ViewContact Save u) ~ whenLoading (display None)

contactEdit :: (ViewId c, ViewAction (Action c)) => Action c -> Action c -> User -> View c ()
contactEdit onView onSave u = do
  col ~ gap 10 $ do
    contactForm onSave contactFromUser
    button onView (text "Cancel") ~ Style.btnLight
 where
  contactFromUser :: ContactForm Maybe
  contactFromUser =
    ContactForm
      { firstName = Just u.firstName
      , lastName = Just u.lastName
      , age = Just u.age
      , info = Just u.info
      }

contactForm :: (ViewId id, ViewAction (Action id)) => Action id -> ContactForm Maybe -> View id ()
contactForm onSubmit c = do
  let f = fieldNames @ContactForm
  form onSubmit ~ gap 10 $ do
    field f.firstName ~ fld $ do
      label $ do
        text "First Name:"
        input Name @ value (fromMaybe "" c.firstName) ~ Style.input

    field f.lastName ~ fld $ do
      label $ do
        text "Last Name:"
        input Name @ value (fromMaybe "" c.lastName) ~ Style.input

    field f.info ~ fld $ do
      label $ do
        text "Info:"
        textarea c.info @ value (fromMaybe "" c.info) ~ Style.input

    field f.age ~ fld $ do
      label $ do
        text "Age:"
        input Number @ value (maybe "" (pack . show) c.age) ~ inp

    submit "Submit" ~ btn
 where
  fld :: (Styleable a) => CSS a -> CSS a
  fld = flexRow . gap 10
  inp = Style.input

contactLoading :: View id ()
contactLoading = el ~ (bg Warning . pad 10) $ "Loading..."


================================================
FILE: demo/Example/Contacts.hs
================================================
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}

module Example.Contacts where

import App.Docs
import App.Route (UserId)
import App.Route qualified as Route
import Control.Monad (forM_)
import Effectful
import Example.Colors
import Example.Contact (ContactForm, contactForm, contactLoading, contactView', parseUser)
import Example.Contact qualified as Contact
import Example.Effects.Debug
import Example.Effects.Users (User (..), Users)
import Example.Effects.Users qualified as Users
import Example.Style qualified as Style
import Example.Style.Cyber (btn, btn', btnLight)
import Example.View.Layout
import Web.Atomic.CSS
import Web.Hyperbole

page
  :: forall es
   . (Hyperbole :> es, Users :> es, Debug :> es)
  => Page es '[Contacts, InlineContact, NewContact]
page = do
  us <- Users.all
  pure $ layout (Route.Contacts Route.ContactsAll) $ do
    example $(moduleSource) $ do
      hyper Contacts $ allContactsView Nothing us

-- Contacts ----------------------------------------------

data Contacts = Contacts
  deriving (Generic, ViewId)

data Filter
  = Active
  | Inactive
  deriving (Eq, Show, Read, Generic, ToJSON, FromJSON, ToParam, FromParam)

instance (Users :> es, Debug :> es) => HyperView Contacts es where
  data Action Contacts
    = Reload (Maybe Filter)
    | AddUser
    | DeleteUser UserId
    deriving (Generic, ViewAction)

  type Require Contacts = '[InlineContact, NewContact]

  update = \case
    Reload mf -> do
      us <- Users.all
      pure $ allContactsView mf us
    AddUser -> do
      uid <- Users.nextId
      u <- parseUser uid
      Users.save u
      us <- Users.all
      pure $ allContactsView Nothing us
    DeleteUser uid -> do
      Users.delete uid
      us <- Users.all
      pure $ allContactsView Nothing us

-- TODO: get the form to close when submitted

allContactsView :: Maybe Filter -> [User] -> View Contacts ()
allContactsView fil us = col ~ gap 20 $ do
  row ~ gap 10 $ do
    el ~ pad 10 $ "Filter: "
    dropdown Reload fil $ do
      option Nothing ""
      option (Just Active) "Active!"
      option (Just Inactive) "Inactive"

  row ~ gap 10 $ do
    let filtered = filter (filterUsers fil) us
    forM_ filtered $ \u -> do
      el ~ border 1 . pad 10 $ do
        hyper (InlineContact u.id) $ contactView u
        row $ do
          space
          route (Route.Contacts $ Route.Contact u.id) "details" ~ Style.link

  row ~ gap 10 $ do
    button (Reload Nothing) ~ Style.btnLight $ "Reload"
    target (InlineContact 2) () $ button Edit ~ Style.btnLight $ "Edit Sara"

  hyper NewContact newContactButton
 where
  filterUsers Nothing _ = True
  filterUsers (Just Active) u = u.isActive
  filterUsers (Just Inactive) u = not u.isActive

-- New Contact Form / Button ----------------------------------
-- Note that it is easier to nest hyperviews here because NewContact has sufficiently different state
--   * It doesn't need to know the users
--   * It DOES need to track it's open / close state
--   * We use target to submit the form to the Contacts parent view

data NewContact = NewContact
  deriving (Generic, ViewId)

instance (Users :> es) => HyperView NewContact es where
  data Action NewContact
    = ShowForm
    | CloseForm
    deriving (Generic, ViewAction)

  type Require NewContact = '[Contacts]

  update action =
    case action of
      ShowForm -> pure newContactForm
      CloseForm -> pure newContactButton

newContactButton :: View NewContact ()
newContactButton = do
  button ShowForm ~ btn $ "Add Contact"

newContactForm :: View NewContact ()
newContactForm = do
  row ~ pad 10 . gap 10 . border 1 $ do
    target Contacts () $ do
      contactForm AddUser (genFields :: ContactForm Maybe)
    col $ do
      space
      button CloseForm ~ btnLight $ "Cancel"

-- Reuse Contact View ----------------------------------
-- We want to use the same view as Example.Contact, but customize the edit view to have a delete button
-- Note that we re-implement the actions and the handler
-- Just create functions to deduplicate code and use them here

data InlineContact = InlineContact UserId
  deriving (Generic, ViewId)

instance (Users :> es, Debug :> es) => HyperView InlineContact es where
  data Action InlineContact
    = Edit
    | ViewContact
    | Save
    deriving (Generic, ViewAction)

  type Require InlineContact = '[Contacts]

  update a = do
    InlineContact uid <- viewId
    u <- Users.find uid
    case a of
      ViewContact ->
        pure $ contactView u
      Edit ->
        pure $ contactEdit u
      Save -> do
        delay 1000
        unew <- parseUser uid
        Users.save unew
        pure $ contactView unew

-- See how we reuse the contactView' from Example.Contact
contactView :: User -> View InlineContact ()
contactView = contactView' Edit

-- See how we reuse the contactEdit' and contactLoading from Example.Contact
contactEdit :: User -> View InlineContact ()
contactEdit u = do
  el ~ (display None . whenLoading flexCol) $ contactLoading
  col ~ (whenLoading (display None) . gap 10) $ do
    Contact.contactEdit ViewContact Save u
    target Contacts () $ button (DeleteUser u.id) ~ btn' Danger . pad (XY 10 0) $ text "Delete"


================================================
FILE: demo/Example/Counter.hs
================================================
{-# LANGUAGE UndecidableInstances #-}

module Example.Counter where

import Data.Text (pack)
import Effectful
import Example.Style.Cyber as Style
import Web.Atomic.CSS
import Web.Hyperbole as Hyperbole

page :: (Hyperbole :> es) => Page es '[Counter]
page = do
  pure $ hyper Counter (viewCount 0)

data Counter = Counter
  deriving (Generic, ViewId)

instance HyperView Counter es where
  data Action Counter
    = Increment Int
    | Decrement Int
    deriving (Generic, ViewAction)

  update (Increment n) = do
    pure $ viewCount (n + 1)
  update (Decrement n) = do
    pure $ viewCount (n - 1)

viewCount :: Int -> View Counter ()
viewCount n = row $ do
  col ~ gap 10 $ do
    el ~ dataFeature $ text $ pack $ show n
    row ~ gap 10 $ do
      button (Decrement n) "Decrement" ~ Style.btn
      button (Increment n) "Increment" ~ Style.btn


================================================
FILE: demo/Example/Data/ProgrammingLanguage.hs
================================================
{-# LANGUAGE DerivingVia #-}

module Example.Data.ProgrammingLanguage where

import Data.Text (Text, isInfixOf, toLower)
import Web.Hyperbole

data ProgrammingLanguage = ProgrammingLanguage
  { family :: LanguageFamily
  , name :: Text
  , features :: [TypeFeature]
  , description :: Text
  }
  deriving (Generic, ToParam, FromParam)
instance Eq ProgrammingLanguage where
  p1 == p2 = p1.name == p2.name

data LanguageFamily
  = Functional
  | ObjectOriented
  deriving (Eq, Show, ToJSON, FromJSON, Ord, Generic, ToParam, FromParam)

data TypeFeature
  = Dynamic
  | Typed
  | Generics
  | TypeClasses
  | TypeFamilies
  deriving (Eq, Show, ToJSON, FromJSON, Generic, ToParam, FromParam)

isMatchLanguage :: Text -> ProgrammingLanguage -> Bool
isMatchLanguage term p =
  isInfixOf (toLower term) . toLower $ p.name

allLanguages :: [ProgrammingLanguage]
allLanguages =
  [ ProgrammingLanguage ObjectOriented "JavaScript" [Dynamic] "A versatile scripting language mainly used for web development."
  , ProgrammingLanguage ObjectOriented "Java" [Typed] "A robust, platform-independent language commonly used for enterprise applications."
  , ProgrammingLanguage ObjectOriented "TypeScript" [Typed, Generics] "A superset of JavaScript that adds static typing."
  , ProgrammingLanguage ObjectOriented "Python" [Dynamic] "A beginner-friendly language with a wide range of applications, from web to data science."
  , ProgrammingLanguage ObjectOriented "PHP" [Dynamic] "A server-side scripting language primarily used for web development."
  , ProgrammingLanguage ObjectOriented "Go" [Typed, Generics] "A statically typed, compiled language designed for simplicity and efficiency."
  , ProgrammingLanguage ObjectOriented "C++" [Typed] "A powerful language for system programming, game development, and high-performance applications."
  , ProgrammingLanguage ObjectOriented "C#" [Typed, Generics] "A language developed by Microsoft, widely used for developing Windows and web applications."
  , ProgrammingLanguage ObjectOriented "Objective-C" [Typed] "A language used primarily for macOS and iOS application development before Swift."
  , ProgrammingLanguage ObjectOriented "Rust" [Typed, Generics, TypeClasses, TypeFamilies] "A memory-safe language focused on performance and reliability."
  , ProgrammingLanguage ObjectOriented "Ruby" [Dynamic] "A dynamic language known for its simplicity and used in web frameworks like Ruby on Rails."
  , ProgrammingLanguage ObjectOriented "Swift" [Typed, Generics] "A modern language for iOS and macOS application development."
  , ProgrammingLanguage Functional "Haskell" [Typed, Generics, TypeClasses, TypeFamilies] "An elegant functional language for those with excellent taste."
  , ProgrammingLanguage Functional "Elm" [Typed, Generics] "A functional language for building reliable web front-end applications."
  , ProgrammingLanguage Functional "Scheme" [Dynamic] "A minimalist, functional dialect of Lisp."
  ]


================================================
FILE: demo/Example/DataLists/Autocomplete.hs
================================================
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}

module Example.DataLists.Autocomplete where

import App.Docs
import App.Route as Route
import Control.Monad (forM_)
import Data.Text (Text)
import Data.Text qualified as T
import Effectful
import Example.Colors
import Example.Data.ProgrammingLanguage (ProgrammingLanguage (..), allLanguages, isMatchLanguage)
import Example.DataLists.Filter as Filter (chosenView, clearButton, resultsTable)
import Example.View.Layout
import Safe (atMay)
import Web.Atomic.CSS
import Web.Hyperbole
import Prelude hiding (even, odd)

page :: (Hyperbole :> es) => Page es '[LiveSearch]
page = do
  pure $ layout (Data Autocomplete) $ do
    el "Create a serverside autocomplete with a combination of onInput and onKeyDown"
    example $(moduleSource) $ do
      hyper LiveSearch $ liveSearchView allLanguages 0 ""

data LiveSearch = LiveSearch
  deriving (Generic, ViewId)

instance (IOE :> es) => HyperView LiveSearch es where
  data Action LiveSearch
    = SearchTerm Int Text
    | Select (Maybe ProgrammingLanguage)
    deriving (Generic, ViewAction)

  -- favor the latest thing typed
  type Concurrency LiveSearch = Replace

  update (SearchTerm current term) = do
    pure $ liveSearchView allLanguages current term
  update (Select Nothing) = do
    pure $ liveSearchView allLanguages 0 ""
  update (Select (Just lang)) = do
    pure $ selectedView lang

selectedView :: ProgrammingLanguage -> View LiveSearch ()
selectedView selected = do
  col ~ gap 10 $ do
    Filter.chosenView selected

liveSearchView :: [ProgrammingLanguage] -> Int -> Text -> View LiveSearch ()
liveSearchView langs current term = do
  col ~ gap 10 $ do
    el ~ stack $ do
      search (SearchTerm current) 250 @ searchKeys . placeholder "search programming languages" . value term . autofocus ~ border 1 . pad 10 . grow
      Filter.clearButton (SearchTerm current) term
      col ~ popup (TRBL 50 0 0 0) . shownIfMatches $ do
        searchPopup matchedLanguages currentSearchLang
    Filter.resultsTable (Select . Just) langs
 where
  matchedLanguages = filter (isMatchLanguage term) langs

  currentSearchLang = matchedLanguages `atMay` current

  -- Only show the search popup if there is a search term and matchedLanguages
  shownIfMatches =
    if T.null term || null matchedLanguages then display None else flexCol

  searchKeys =
    onKeyDown Enter (Select currentSearchLang)
      . onKeyDown ArrowDown (SearchTerm (current + 1) term)
      . onKeyDown ArrowUp (SearchTerm (current - 1) term)

searchPopup :: [ProgrammingLanguage] -> Maybe ProgrammingLanguage -> View LiveSearch ()
searchPopup shownLangs highlighted = do
  col ~ border 1 . bg White $ do
    forM_ shownLangs $ \lang -> do
      button (Select (Just lang)) ~ hover (bg Light) . selected lang . pad 5 $ do
        text lang.name
 where
  selected l = if Just l == highlighted then bg Light else id


================================================
FILE: demo/Example/DataLists/DataTable.hs
================================================
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}

module Example.DataLists.DataTable where

import App.Docs
import App.Route as Route
import Data.List (sortOn)
import Data.Text (pack)
import Effectful
import Example.Data.ProgrammingLanguage (ProgrammingLanguage (..), allLanguages)
import Example.View.Layout
import Example.View.SortableTable (dataTable, sortBtn, sortColumn)
import Web.Atomic.CSS
import Web.Hyperbole
import Prelude hiding (even, odd)

-- DataTable -> do
--   el "Complex reusable View Functions allow us to "

page :: (Hyperbole :> es) => Page es '[Languages]
page = do
  pure $ layout (Data SortableTable) $ do
    el "We can write view Functions that work in any view, like a dataTable"
    example $(moduleSource) $ do
      hyper Languages $ languagesView Nothing allLanguages

data Languages = Languages
  deriving (Generic, ViewId)

data SortField
  = SortName
  | SortDescription
  | SortFamily
  deriving (Show, Read, Eq, Generic, ToJSON, FromJSON, ToParam, FromParam)

instance HyperView Languages es where
  data Action Languages
    = SortOn SortField
    deriving (Generic, ViewAction)

  update (SortOn fld) = do
    let sorted = sortOnField fld allLanguages
    pure $ languagesView (Just fld) sorted

sortOnField :: SortField -> [ProgrammingLanguage] -> [ProgrammingLanguage]
sortOnField = \case
  SortName -> sortOn (.name)
  SortDescription -> sortOn (.description)
  SortFamily -> sortOn (.family)

languagesView :: Maybe SortField -> [ProgrammingLanguage] -> View Languages ()
languagesView fld langs =
  table langs ~ dataTable $ do
    sortColumn (sortBtn "Language" (SortOn SortName) (fld == Just SortName)) (.name)
    sortColumn (sortBtn "Family" (SortOn SortFamily) (fld == Just SortFamily)) $ \d -> pack $ show d.family
    sortColumn (sortBtn "Description" (SortOn SortDescription) (fld == Just SortDescription)) (.description)


================================================
FILE: demo/Example/DataLists/Filter.hs
================================================
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}

module Example.DataLists.Filter where

import App.Docs
import App.Route as Route
import Data.Text (Text, pack)
import Effectful hiding (Dynamic)
import Example.Colors
import Example.Data.ProgrammingLanguage (LanguageFamily (..), ProgrammingLanguage (..), TypeFeature (..), allLanguages, isMatchLanguage)
import Example.View.Icon as Icon
import Example.View.Inputs (toggleCheckbox)
import Example.View.Layout
import Web.Atomic.CSS
import Web.Hyperbole
import Prelude hiding (even, odd)

page :: (Hyperbole :> es, IOE :> es) => Page es '[Languages]
page = do
  filters <- query
  pure $ layout (Data Filter) $ do
    el "Incrementally search a list of data, storing parameters in the query string"
    example $(moduleSource) $ do
      hyper Languages $ languagesView filters

data Languages = Languages
  deriving (Generic, ViewId)

-- Filters available from the query
-- See Example.Data.ProgrammingLanguage
data Filters = Filters
  { features :: [TypeFeature]
  , family :: Maybe LanguageFamily
  , term :: Text
  }
  deriving (Generic, Show, FromQuery, ToQuery)

instance (IOE :> es) => HyperView Languages es where
  data Action Languages
    = SearchTerm Text
    | Select ProgrammingLanguage
    | Feature TypeFeature Bool
    | SetFamily (Maybe LanguageFamily)
    deriving (Generic, ViewAction)

  -- favor the latest thing entered / typed
  type Concurrency Languages = Replace

  update = \case
    Select lang -> do
      pure $ chosenView lang
    SearchTerm term -> do
      filters <- modFilters $ \f -> f{term}
      pure $ languagesView filters
    Feature feature selected -> do
      filters <- modFilters $ \f -> setFeatures feature selected f
      pure $ languagesView filters
    SetFamily f -> do
      filters <- modFilters $ \Filters{features, term} -> Filters{family = f, features, term}
      pure $ languagesView filters
   where
    setFeatures feature selected Filters{term, family, features} =
      let features' = if selected then addFeature feature features else delFeature feature features
       in Filters{term, family, features = features'}
    addFeature f fs
      | f `elem` fs = fs
      | otherwise = f : fs
    delFeature feature =
      filter (/= feature)
    modFilters f = do
      filts <- query
      let filts' = f filts
      setQuery filts'
      pure filts'

-- apply our filters, return any languages that match
filterLanguages :: Filters -> [ProgrammingLanguage]
filterLanguages filts =
  filter match allLanguages
 where
  match lang =
    isMatchLanguage filts.term lang
      && matchFamily filts.family lang
      && matchFeatures filts.features lang
  matchFamily Nothing _ = True
  matchFamily (Just fam) lang = lang.family == fam
  matchFeatures feats lang =
    all (\f -> f `elem` lang.features) feats

languagesView :: Filters -> View Languages ()
languagesView filters = do
  let matched = filterLanguages filters
  col ~ gap 10 . grow $ do
    filtersView filters
    resultsTable Select matched

filtersView :: Filters -> View Languages ()
filtersView filters = do
  el ~ stack . grow $ do
    search SearchTerm 250 @ placeholder "filter programming languages" . value filters.term . autofocus ~ border 1 . pad 10
    clearButton SearchTerm filters.term

  row $ do
    col ~ gap 5 $ do
      el ~ bold $ "Language Family"
      familyDropdown filters
    space
    col ~ gap 5 $ do
      el ~ bold $ "Type System Features"
      feature Dynamic
      feature Typed
      feature Generics
      feature TypeClasses
      feature TypeFamilies
 where
  feature f =
    row ~ gap 10 $ do
      toggleCheckbox (Feature f) (f `elem` filters.features)
      el $ text (featureName f)

  featureName f = pack $ show f

familyDropdown :: Filters -> View Languages ()
familyDropdown filters =
  dropdown SetFamily filters.family ~ border 1 . pad 10 $ do
    option Nothing "Any"
    option (Just ObjectOriented) "Object Oriented"
    option (Just Functional) "Functional"

clearButton :: (ViewAction (Action id)) => (Text -> Action id) -> Text -> View id ()
clearButton clear term =
  el ~ popup (R 0) . pad 10 . showClearBtn $ do
    button (clear "") ~ width 24 . hover (color PrimaryLight) $ Icon.xCircle
 where
  showClearBtn =
    case term of
      "" -> display None
      _ -> id

chosenView :: ProgrammingLanguage -> View c ()
chosenView lang = do
  row ~ gap 10 $ do
    el "You chose:"
    el $ text lang.name
  el ~ (if lang.name == "Haskell" then id else display None) $ "You are as wise as you are attractive"

resultsTable :: (ViewAction (Action id)) => (ProgrammingLanguage -> Action id) -> [ProgrammingLanguage] -> View id ()
resultsTable onSelect langs = do
  col ~ gap 15 $ do
    mapM_ languageRow langs
 where
  languageRow lang = do
    col ~ gap 5 $ do
      row ~ gap 5 $ do
        el ~ bold $ text lang.name
        space
        button (onSelect lang) ~ pad (XY 10 2) . border 1 . hover (bg GrayLight) . rows $ "Select"

      row $ viewFamily lang.family

      row ~ gap 5 $ do
        el $ text lang.description

  rows = textAlign AlignCenter . border 1 . borderColor GrayLight

viewFamily :: LanguageFamily -> View c ()
viewFamily fam = do
  el ~ bg Light . pad (XY 10 2) . fontSize 16 . textA
Download .txt
gitextract_46835c3y/

├── .dockerignore
├── .github/
│   └── workflows/
│       ├── haskell.yaml
│       └── packcheck.yaml
├── .gitignore
├── .hlint.yaml
├── .packcheck.ignore
├── CHANGELOG.md
├── DOCTODO.md
├── Dockerfile
├── LICENSE
├── README.md
├── bin/
│   ├── dev
│   ├── docgen
│   └── release
├── cabal.project
├── client/
│   ├── declarations.d.ts
│   ├── dist/
│   │   ├── action.d.ts
│   │   ├── browser.d.ts
│   │   ├── events.d.ts
│   │   ├── http.d.ts
│   │   ├── hyperbole.js
│   │   ├── hyperview.d.ts
│   │   ├── index.d.ts
│   │   ├── lib.d.ts
│   │   ├── message.d.ts
│   │   ├── response.d.ts
│   │   └── sockets.d.ts
│   ├── package.json
│   ├── src/
│   │   ├── action.ts
│   │   ├── browser.ts
│   │   ├── events.ts
│   │   ├── http.ts
│   │   ├── hyperview.ts
│   │   ├── index.ts
│   │   ├── lib.ts
│   │   ├── message.ts
│   │   ├── response.ts
│   │   └── sockets.ts
│   ├── tsconfig.json
│   ├── util/
│   │   └── live-reload.js
│   └── webpack.config.js
├── demo/
│   ├── .dockerignore
│   ├── App/
│   │   ├── Cache.hs
│   │   ├── Config.hs
│   │   ├── Docs/
│   │   │   ├── Markdown.hs
│   │   │   ├── Page.hs
│   │   │   └── Snippet.hs
│   │   ├── Docs.hs
│   │   ├── Page/
│   │   │   ├── Application.hs
│   │   │   ├── CSS.hs
│   │   │   ├── Concurrency.hs
│   │   │   ├── Examples.hs
│   │   │   ├── Forms.hs
│   │   │   ├── HyperboleEffect.hs
│   │   │   ├── Hyperviews.hs
│   │   │   ├── Interactivity.hs
│   │   │   ├── Intro/
│   │   │   │   ├── Basics.hs
│   │   │   │   └── Intro.hs
│   │   │   ├── OAuth2.hs
│   │   │   ├── SideEffects.hs
│   │   │   ├── State.hs
│   │   │   └── ViewFunctions.hs
│   │   ├── Route.hs
│   │   └── Style.hs
│   ├── App.hs
│   ├── Example/
│   │   ├── CSS/
│   │   │   ├── External.hs
│   │   │   ├── Loading.hs
│   │   │   ├── Tooltips.hs
│   │   │   └── Transitions.hs
│   │   ├── Chat.hs
│   │   ├── Colors.hs
│   │   ├── Concurrency/
│   │   │   ├── LazyLoading.hs
│   │   │   ├── Overlap.hs
│   │   │   ├── Polling.hs
│   │   │   ├── Progress.hs
│   │   │   └── Tasks.hs
│   │   ├── Contact.hs
│   │   ├── Contacts.hs
│   │   ├── Counter.hs
│   │   ├── Data/
│   │   │   └── ProgrammingLanguage.hs
│   │   ├── DataLists/
│   │   │   ├── Autocomplete.hs
│   │   │   ├── DataTable.hs
│   │   │   ├── Filter.hs
│   │   │   └── LoadMore.hs
│   │   ├── Docs/
│   │   │   ├── App.hs
│   │   │   ├── BasicPage.hs
│   │   │   ├── CSS.hs
│   │   │   ├── Client.hs
│   │   │   ├── Component.hs
│   │   │   ├── Encoding.hs
│   │   │   ├── Interactive.hs
│   │   │   ├── MultiPage.hs
│   │   │   ├── MultiView.hs
│   │   │   ├── Nested.hs
│   │   │   ├── Nesting.hs
│   │   │   ├── Page/
│   │   │   │   ├── Messages.hs
│   │   │   │   └── Users.hs
│   │   │   ├── Params.hs
│   │   │   ├── QueryMessage.hs
│   │   │   ├── Sessions.hs
│   │   │   ├── SideEffects.hs
│   │   │   ├── State.hs
│   │   │   ├── UniqueViewId.hs
│   │   │   └── ViewFunctions.hs
│   │   ├── Document.hs
│   │   ├── Effects/
│   │   │   ├── Debug.hs
│   │   │   ├── Todos.hs
│   │   │   └── Users.hs
│   │   ├── Errors.hs
│   │   ├── FormSimple.hs
│   │   ├── FormValidation.hs
│   │   ├── Interactivity/
│   │   │   ├── Events.hs
│   │   │   └── Inputs.hs
│   │   ├── Javascript.hs
│   │   ├── Push.hs
│   │   ├── Requests.hs
│   │   ├── Scrollbars.hs
│   │   ├── Simple.hs
│   │   ├── State/
│   │   │   ├── Effects.hs
│   │   │   ├── Query.hs
│   │   │   ├── Sessions.hs
│   │   │   ├── Stateless.hs
│   │   │   └── ViewState.hs
│   │   ├── Style/
│   │   │   └── Cyber.hs
│   │   ├── Style.hs
│   │   ├── Tags.hs
│   │   ├── Test.hs
│   │   ├── Todos/
│   │   │   ├── Todo.hs
│   │   │   └── TodoCSS.hs
│   │   ├── Trigger.hs
│   │   └── View/
│   │       ├── Icon.hs
│   │       ├── Inputs.hs
│   │       ├── Layout.hs
│   │       ├── Loader.hs
│   │       ├── Menu.hs
│   │       └── SortableTable.hs
│   ├── Main.hs
│   ├── README.md
│   ├── demo.cabal
│   ├── fourmolu.yaml
│   ├── hie.yaml
│   ├── package.yaml
│   └── static/
│       ├── custom.js
│       ├── cyber.css
│       ├── docs.js
│       ├── external.css
│       ├── prism.css
│       ├── prism.js
│       ├── test.js
│       └── todomvc.css
├── docs/
│   ├── Main.hs
│   ├── app-document.md
│   ├── app-effects.md
│   ├── app-live.md
│   ├── app-pages.md
│   ├── app-routes.md
│   ├── atomic.md
│   ├── comparison.md
│   ├── concurrency-overlap.md
│   ├── dev.md
│   ├── docgen.cabal
│   ├── effectful.md
│   ├── effects-custom.md
│   ├── effects-other.md
│   ├── forms-simple.md
│   ├── forms-validated.md
│   ├── hyperviews-intro.md
│   ├── hyperviews-multi.md
│   ├── hyperviews-nesting.md
│   ├── hyperviews-unique.md
│   ├── interactivity-events.md
│   ├── interactivity-events2.md
│   ├── interactivity-inputs.md
│   ├── interactivity-javascript.md
│   ├── interactivity-pushevent.md
│   ├── intro-downsides.md
│   ├── intro-links.md
│   ├── intro.md
│   ├── javascript_api.md
│   ├── multi-same.md
│   ├── nix.md
│   ├── outline.md
│   ├── package.yaml
│   ├── pages.md
│   ├── state-browser.md
│   ├── state-effects.md
│   ├── state-sessions.md
│   ├── state-stateless.md
│   ├── state-threading.md
│   ├── state-viewstate.md
│   ├── view-components.md
│   ├── view-functions-end.md
│   ├── view-functions-wrap.md
│   └── view-functions.md
├── flake.nix
├── fourmolu.yaml
├── hie.yaml
├── hyperbole.cabal
├── package.yaml
├── src/
│   └── Web/
│       ├── Hyperbole/
│       │   ├── Application.hs
│       │   ├── Data/
│       │   │   ├── Cookie.hs
│       │   │   ├── Encoded.hs
│       │   │   ├── JSON.hs
│       │   │   ├── Param.hs
│       │   │   ├── QueryData.hs
│       │   │   └── URI.hs
│       │   ├── Document.hs
│       │   ├── Effect/
│       │   │   ├── Client.hs
│       │   │   ├── GenRandom.hs
│       │   │   ├── Hyperbole.hs
│       │   │   ├── OAuth2.hs
│       │   │   ├── Query.hs
│       │   │   ├── Request.hs
│       │   │   ├── Response.hs
│       │   │   └── Session.hs
│       │   ├── HyperView/
│       │   │   ├── Event.hs
│       │   │   ├── Forms.hs
│       │   │   ├── Handled.hs
│       │   │   ├── Hyper.hs
│       │   │   ├── Input.hs
│       │   │   └── Types.hs
│       │   ├── HyperView.hs
│       │   ├── Page.hs
│       │   ├── Route.hs
│       │   ├── Server/
│       │   │   ├── Handler.hs
│       │   │   ├── Message.hs
│       │   │   ├── Options.hs
│       │   │   ├── Socket.hs
│       │   │   └── Wai.hs
│       │   ├── TypeList.hs
│       │   ├── Types/
│       │   │   ├── Client.hs
│       │   │   ├── Event.hs
│       │   │   ├── Request.hs
│       │   │   └── Response.hs
│       │   ├── View/
│       │   │   ├── CSS.hs
│       │   │   ├── Embed.hs
│       │   │   ├── Render.hs
│       │   │   ├── Tag.hs
│       │   │   ├── Types.hs
│       │   │   ├── ViewAction.hs
│       │   │   └── ViewId.hs
│       │   └── View.hs
│       └── Hyperbole.hs
└── test/
    ├── Spec.hs
    └── Test/
        ├── EncodedSpec.hs
        ├── FormSpec.hs
        ├── ParamSpec.hs
        ├── QuerySpec.hs
        ├── RouteSpec.hs
        ├── SessionSpec.hs
        ├── URISpec.hs
        ├── ViewActionSpec.hs
        ├── ViewIdSpec.hs
        └── ViewSpec.hs
Download .txt
SYMBOL INDEX (174 symbols across 21 files)

FILE: client/dist/action.d.ts
  type ActionMessage (line 2) | type ActionMessage = {
  type Request (line 14) | type Request = {

FILE: client/dist/events.d.ts
  type UrlFragment (line 2) | type UrlFragment = string;

FILE: client/dist/hyperbole.js
  function t (line 2) | function t(e,t=100,n={}){if("function"!=typeof e)throw new TypeError(`Ex...
  function n (line 2) | function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={expo...
  function r (line 2) | function r(e,t){return function(e){if(Array.isArray(e))return e}(e)||fun...
  function i (line 2) | function i(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=new Ar...
  function g (line 2) | function g(e,t){var n=[];for(var o of t){let t=e(o);if(!t)break;n.push(t...
  function w (line 2) | function w(e){return{cookies:e.filter((e=>"Cookie"==e.key)).map((e=>e.va...
  function E (line 2) | function E(e){return w(g(C,e.trim().split("\n")))}
  function b (line 2) | function b(e,t){return t.find((t=>t.key==e))?.value}
  function I (line 2) | function I(e,t){return t.filter((t=>t.key==e)).map((e=>e.value))}
  function C (line 2) | function C(e){let t=e.match(/^(\w+)\: (.*)$/);if(t)return{key:t[1],value...
  function k (line 2) | function k(e){let[t,n]=T(e);return{name:t,detail:JSON.parse(n)}}
  function q (line 2) | function q(e){let[t,n]=T(e);return[t,n]}
  function T (line 2) | function T(e){let t=e.indexOf("|");if(-1===t){let t=new Error("Bad Encod...
  function L (line 2) | function L(e){if(!e)return;const t=new URLSearchParams;return e.forEach(...
  function A (line 2) | function A(e,t){return e+" "+function(e){return""==e?"|":e.replace(/_/g,...
  class x (line 2) | class x extends Error{constructor(e,t){super(e+"\n"+t),this.name="Protoc...
    method constructor (line 2) | constructor(e,t){super(e+"\n"+t),this.name="ProtocolError"}
  function O (line 2) | function O(e){let t=new Event("hyp-content",{bubbles:!0});e.dispatchEven...
  function N (line 2) | function N(e,t){e.querySelectorAll("[id]").forEach((n=>{n.runAction=func...
  function H (line 2) | function H(e,t){document.addEventListener(e,(function(n){if(!(n.target i...
  function V (line 2) | function V(e,t){document.addEventListener(e,(function(n){if(!(n.target i...
  function j (line 2) | function j(e){e.querySelectorAll("[data-onload]").forEach((e=>{let t=par...
  function P (line 2) | function P(e){e.querySelectorAll("[data-onmouseenter]").forEach((e=>{let...
  function B (line 2) | function B(e){e.querySelectorAll("[data-onmouseleave]").forEach((e=>{let...
  function Q (line 2) | function Q(e){const t=U(e);if(D(t))return t;console.error("Non HyperView...
  function U (line 2) | function U(e){let t=function(e){let t=e.closest("[data-target]");return ...
  function _ (line 2) | async function _(e,t,n){if(e.activeRequest&&!e.activeRequest?.isCancelle...
  function G (line 2) | function G(e){let t=e.targetViewId||e.viewId,n=document.getElementById(t...
  function J (line 2) | function J(e){e.forEach((e=>{console.log("SetCookie: ",e),document.cooki...
  function K (line 2) | function K(e,t){null!=e.query&&function(e){if(e!=function(){const e=wind...
  function Y (line 2) | function Y(e,t){setTimeout((()=>{let n=new CustomEvent(e.name,{bubbles:!...
  function X (line 2) | function X(e,t){setTimeout((()=>{let n=window.Hyperbole?.hyperView(e);n&...
  method constructor (line 2) | constructor(e=M){this.hasEverConnected=!1,this.isConnected=!1,this.recon...
  method connect (line 2) | connect(e=M,t=!1){const n=t?new WebSocket(e):this.socket;function o(e){c...
  method sendAction (line 2) | async sendAction(e){if(this.isConnected){let t=function(e){let t=["|ACTI...
  method runQueue (line 2) | runQueue(){let e=this.queue.pop();e&&(console.log("runQueue: ",e),this.s...
  method onMessage (line 2) | onMessage(e){let{command:t,metas:n,rest:o}=function(e){let t=e.split("\n...
  method addEventListener (line 2) | addEventListener(e,t){this.events.addEventListener(e,t)}
  method dispatchEvent (line 2) | dispatchEvent(e){this.events.dispatchEvent(e)}
  method disconnect (line 2) | disconnect(){this.isConnected=!1,this.hasEverConnected=!1,this.socket.cl...

FILE: client/dist/hyperview.d.ts
  type HyperView (line 2) | interface HyperView extends HTMLElement {
  type ConcurrencyMode (line 10) | type ConcurrencyMode = string;

FILE: client/dist/index.d.ts
  type Window (line 5) | interface Window {
  type DocumentEventMap (line 8) | interface DocumentEventMap {
  type HyperboleAPI (line 14) | interface HyperboleAPI {

FILE: client/dist/message.d.ts
  type Meta (line 1) | type Meta = {
  type ViewId (line 5) | type ViewId = string;
  type RequestId (line 6) | type RequestId = number;
  type EncodedAction (line 7) | type EncodedAction = string;
  type ViewState (line 8) | type ViewState = string;
  type RemoteEvent (line 9) | type RemoteEvent = {
  type Metadata (line 14) | type Metadata = {
  type SplitMessage (line 26) | type SplitMessage = {

FILE: client/dist/response.d.ts
  type Response (line 2) | type Response = {
  type ResponseBody (line 6) | type ResponseBody = string;
  type LiveUpdate (line 8) | type LiveUpdate = {
  class FetchError (line 12) | class FetchError extends Error {

FILE: client/dist/sockets.d.ts
  type SocketConnectionEventMap (line 4) | interface SocketConnectionEventMap {
  class SocketConnection (line 11) | class SocketConnection {
  type Update (line 27) | type Update = {
  type Redirect (line 35) | type Redirect = {
  type Trigger (line 40) | type Trigger = {
  type JSEvent (line 48) | type JSEvent = {
  type MessageType (line 55) | type MessageType = string;
  class ProtocolError (line 56) | class ProtocolError extends Error {

FILE: client/src/action.ts
  type ActionMessage (line 8) | type ActionMessage = {
  function actionMessage (line 20) | function actionMessage(id: ViewId, action: EncodedAction, state: ViewSta...
  function toSearch (line 29) | function toSearch(form?: FormData): URLSearchParams | undefined {
  function renderActionMessage (line 41) | function renderActionMessage(msg: ActionMessage): string {
  function renderForm (line 62) | function renderForm(form: URLSearchParams | undefined): string {
  type Request (line 69) | type Request = {
  function newRequest (line 74) | function newRequest(): Request {
  function encodedParam (line 83) | function encodedParam(action: string, param: string): string {
  function sanitizeParam (line 87) | function sanitizeParam(param: string): string {

FILE: client/src/browser.ts
  function setQuery (line 2) | function setQuery(query: string) {
  function currentQuery (line 11) | function currentQuery(): string {

FILE: client/src/events.ts
  type UrlFragment (line 6) | type UrlFragment = string
  function listenKeydown (line 8) | function listenKeydown(cb: (target: HyperView, action: string) => void):...
  function listenKeyup (line 12) | function listenKeyup(cb: (target: HyperView, action: string) => void): v...
  function listenKeyEvent (line 16) | function listenKeyEvent(event: "keyup" | "keydown", cb: (target: HyperVi...
  function listenBubblingEvent (line 39) | function listenBubblingEvent(event: string, cb: (_target: HyperView, act...
  function listenClick (line 66) | function listenClick(cb: (target: HyperView, action: string) => void): v...
  function listenDblClick (line 70) | function listenDblClick(cb: (target: HyperView, action: string) => void)...
  function listenTopLevel (line 75) | function listenTopLevel(cb: (target: HyperView, action: string) => void)...
  function listenLoad (line 96) | function listenLoad(node: HTMLElement): void {
  function listenMouseEnter (line 121) | function listenMouseEnter(node: HTMLElement): void {
  function listenMouseLeave (line 134) | function listenMouseLeave(node: HTMLElement): void {
  function listenChange (line 148) | function listenChange(cb: (target: HyperView, action: string) => void): ...
  type LiveInputElement (line 180) | interface LiveInputElement extends HTMLInputElement {
  function listenInput (line 184) | function listenInput(startedTyping: (target: HyperView) => void, cb: (ta...
  function listenFormSubmit (line 228) | function listenFormSubmit(cb: (target: HyperView, action: string, form: ...
  function nearestTargetId (line 254) | function nearestTargetId(node: HTMLElement): string | undefined {
  function nearestHyperViewTarget (line 259) | function nearestHyperViewTarget(node: HTMLElement): HyperView | undefined {
  function nearestAnyTarget (line 270) | function nearestAnyTarget(node: HTMLElement): HTMLElement | undefined {

FILE: client/src/hyperview.ts
  type HyperView (line 3) | interface HyperView extends HTMLElement {
  type ConcurrencyMode (line 15) | type ConcurrencyMode = string;
  function dispatchContent (line 17) | function dispatchContent(node: HTMLElement): void {
  function enrichHyperViews (line 22) | function enrichHyperViews(node: HTMLElement, runAction: (target: HyperVi...

FILE: client/src/index.ts
  constant PACKAGE (line 10) | let PACKAGE = require('../package.json');
  function runAction (line 25) | async function runAction(target: HyperView, action: string, form?: FormD...
  function handleTrigger (line 52) | function handleTrigger(trigger: Trigger) {
  function handleEvent (line 56) | function handleEvent(ev: JSEvent) {
  function handleRedirect (line 62) | function handleRedirect(red: Redirect) {
  function handleResponse (line 72) | function handleResponse(res: Update) {
  function handleUpdate (line 83) | function handleUpdate(res: Update): HyperView | undefined {
  function applyCookies (line 171) | function applyCookies(cookies: string[]) {
  function runMetadata (line 178) | function runMetadata(meta: Metadata, target: HTMLElement | null) {
  function runRemoteEvent (line 196) | function runRemoteEvent(remoteEvent: RemoteEvent, target: HTMLElement | ...
  function runTrigger (line 204) | function runTrigger(viewId: ViewId, action: EncodedAction) {
  function fixInputs (line 214) | function fixInputs(target: HTMLElement) {
  function addCSS (line 233) | function addCSS(src: HTMLStyleElement | null) {
  function init (line 249) | function init() {
  type VNode (line 339) | type VNode = {
  type Window (line 369) | interface Window {
  type DocumentEventMap (line 372) | interface DocumentEventMap {
  type HyperboleAPI (line 379) | interface HyperboleAPI {

FILE: client/src/lib.ts
  function takeWhileMap (line 3) | function takeWhileMap<T, A>(pred: (val: T) => A | undefined, lines: T[])...
  function dropWhile (line 16) | function dropWhile<T, A>(pred: (val: T) => A | undefined, lines: T[]): T...

FILE: client/src/message.ts
  type Meta (line 6) | type Meta = { key: string, value: string }
  type ViewId (line 7) | type ViewId = string
  type RequestId (line 8) | type RequestId = number
  type EncodedAction (line 9) | type EncodedAction = string
  type ViewState (line 10) | type ViewState = string
  type RemoteEvent (line 12) | type RemoteEvent = { name: string, detail: unknown }
  function renderMetas (line 15) | function renderMetas(meta: Meta[]): string {
  type Metadata (line 19) | type Metadata = {
  function toMetadata (line 30) | function toMetadata(meta: Meta[]): Metadata {
  function parseMetadata (line 45) | function parseMetadata(input: string): Metadata {
  function metaValue (line 51) | function metaValue(key: string, metas: Meta[]): string | undefined {
  function metaValuesAll (line 55) | function metaValuesAll(key: string, metas: Meta[]): string[] {
  type SplitMessage (line 59) | type SplitMessage = {
  function splitMessage (line 66) | function splitMessage(message: string): SplitMessage {
  function parseMeta (line 78) | function parseMeta(line: string): Meta | undefined {
  function parseRemoteEvent (line 89) | function parseRemoteEvent(input: string): RemoteEvent {
  function parseAction (line 97) | function parseAction(input: string): [ViewId, string] {
  function breakNextSegment (line 102) | function breakNextSegment(input: string): [string, string] {

FILE: client/src/response.ts
  type Response (line 6) | type Response = {
  type ResponseBody (line 11) | type ResponseBody = string
  function parseResponse (line 13) | function parseResponse(res: ResponseBody): LiveUpdate {
  type LiveUpdate (line 25) | type LiveUpdate = {
  class FetchError (line 31) | class FetchError extends Error {
    method constructor (line 34) | constructor(viewId: ViewId, msg: string, body: string) {

FILE: client/src/sockets.ts
  type SocketConnectionEventMap (line 9) | interface SocketConnectionEventMap {
  class SocketConnection (line 17) | class SocketConnection {
    method constructor (line 26) | constructor(addr = defaultAddress) {
    method connect (line 32) | connect(addr = defaultAddress, createSocket = false) {
    method sendAction (line 87) | async sendAction(action: ActionMessage) {
    method runQueue (line 97) | private runQueue() {
    method onMessage (line 109) | private onMessage(event: MessageEvent) {
    method addEventListener (line 230) | addEventListener<K extends keyof SocketConnectionEventMap>(e: K, cb: (...
    method dispatchEvent (line 237) | dispatchEvent<K extends keyof SocketConnectionEventMap>(e: SocketConne...
    method disconnect (line 241) | disconnect() {
  type Update (line 249) | type Update = {
  type Redirect (line 258) | type Redirect = {
  type Trigger (line 264) | type Trigger = {
  type JSEvent (line 273) | type JSEvent = {
  type MessageType (line 281) | type MessageType = string
  class ProtocolError (line 286) | class ProtocolError extends Error {
    method constructor (line 287) | constructor(description: string, body: string) {

FILE: client/util/live-reload.js
  function showNotification (line 14) | function showNotification(message) {
  function jackIn (line 37) | function jackIn(style) {

FILE: demo/static/custom.js
  function listenServerEvents (line 22) | function listenServerEvents() {

FILE: demo/static/docs.js
  function highlightNav (line 34) | function highlightNav(activeId) {

FILE: demo/static/prism.js
  function u (line 3) | function u(e){s.highlightedCode=e,a.hooks.run("before-insert",s),s.eleme...
  function i (line 3) | function i(e,n,t,r){this.type=e,this.content=n,this.alias=t,this.length=...
  function l (line 3) | function l(e,n,t,r){e.lastIndex=n;var a=e.exec(t);if(a&&r&&a[1]){var i=a...
  function o (line 3) | function o(e,n,t,r,s,g){for(var f in t)if(t.hasOwnProperty(f)&&t[f]){var...
  function s (line 3) | function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e...
  function u (line 3) | function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a...
  function c (line 3) | function c(e,n,t){for(var r=n.next,a=0;a<t&&r!==e.tail;a++)r=r.next;n.ne...
  function f (line 3) | function f(){a.manual||a.highlightAll()}
Condensed preview — 254 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (592K chars).
[
  {
    "path": ".dockerignore",
    "chars": 62,
    "preview": ".git\n.stack-work\nclient/node_modules\nDockerfile\ndist-newstyle\n"
  },
  {
    "path": ".github/workflows/haskell.yaml",
    "chars": 2917,
    "preview": "name: Haskell CI\n\non:\n  push:\n    branches: [ \"main\", \"ci\" ]\n  pull_request:\n    branches: [ \"main\" ]\n\npermissions:\n  co"
  },
  {
    "path": ".github/workflows/packcheck.yaml",
    "chars": 6210,
    "preview": "# packcheck-0.7.1\n# You can use any of the options supported by packcheck as environment\n# variables here.  See https://"
  },
  {
    "path": ".gitignore",
    "chars": 227,
    "preview": "dist-newstyle\n.DS_Store\ntags\nnode_modules\n# Auto-generated pre-commit config\n.pre-commit-config.yaml\n# Nix output dir\nre"
  },
  {
    "path": ".hlint.yaml",
    "chars": 250,
    "preview": "- arguments:\n    - -XOverloadedRecordDot\n\n- ignore: {name: \"Use <$>\"}\n- ignore: {name: \"Use newtype instead of data\"}\n\n#"
  },
  {
    "path": ".packcheck.ignore",
    "chars": 391,
    "preview": "client/*.d.ts\nclient/src/\nclient/dist/*.d.ts\nclient/package-lock.json\nclient/webpack.config.js\nclient/package.json\nclien"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 2479,
    "preview": "# Revision history for hyperbole\n\n## 0.6.0 -- 2026-01-15\n\nImprovements:\n* `ViewState` - built in threaded state, default"
  },
  {
    "path": "DOCTODO.md",
    "chars": 47,
    "preview": "Documentation Outline\n======================\n\n\n"
  },
  {
    "path": "Dockerfile",
    "chars": 1593,
    "preview": "FROM haskell:9.8.2 AS base\nWORKDIR /opt/build\n\nRUN cabal update\nRUN cabal install bytestring containers casing effectful"
  },
  {
    "path": "LICENSE",
    "chars": 1516,
    "preview": "Copyright (c) 2023, Sean Hess\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\n"
  },
  {
    "path": "README.md",
    "chars": 3448,
    "preview": "![Hyperbole](https://github.com/seanhess/hyperbole/raw/main/demo/static/logo-robot.png)\n\n[![Hackage Version](https://img"
  },
  {
    "path": "bin/dev",
    "chars": 351,
    "preview": "#!/bin/bash\n\n\npackage() {\n  hpack\n  hpack docs\n  hpack demo\n}\n\nwatch() {\n  ghcid -c \"cabal repl demo lib:hyperbole\" -T A"
  },
  {
    "path": "bin/docgen",
    "chars": 91,
    "preview": "#!/bin/bash\n\nset -e\n\nhpack demo\nhpack docs\ncabal run docs\n\ncd /tmp/hyperbole\ncabal haddock\n"
  },
  {
    "path": "bin/release",
    "chars": 230,
    "preview": "#!/bin/bash\n\n\n# Make sure everything is working\nhpack\ncabal test\n\n\n# Compile the JS client\ncd client\nnpm install\nnpx web"
  },
  {
    "path": "cabal.project",
    "chars": 63,
    "preview": "tests: True\nmulti-repl: True\npackages:\n  .\n  ./demo/\n  ./docs/\n"
  },
  {
    "path": "client/declarations.d.ts",
    "chars": 237,
    "preview": "declare module 'omdomdom/lib/omdomdom.es.js' {\n  export function create(node: any, ...args: any[]): any;\n  export functi"
  },
  {
    "path": "client/dist/action.d.ts",
    "chars": 854,
    "preview": "import { Meta, ViewId, RequestId, EncodedAction, ViewState } from \"./message\";\nexport type ActionMessage = {\n    viewId:"
  },
  {
    "path": "client/dist/browser.d.ts",
    "chars": 55,
    "preview": "export declare function setQuery(query: string): void;\n"
  },
  {
    "path": "client/dist/events.d.ts",
    "chars": 1319,
    "preview": "import { HyperView } from './hyperview';\nexport type UrlFragment = string;\nexport declare function listenKeydown(cb: (ta"
  },
  {
    "path": "client/dist/http.d.ts",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "client/dist/hyperbole.js",
    "chars": 19248,
    "preview": "/*! For license information please see hyperbole.js.LICENSE.txt */\n(()=>{var e={296:e=>{function t(e,t=100,n={}){if(\"fun"
  },
  {
    "path": "client/dist/hyperview.d.ts",
    "chars": 576,
    "preview": "import { type Request } from \"./action\";\nexport interface HyperView extends HTMLElement {\n    runAction(action: string):"
  },
  {
    "path": "client/dist/index.d.ts",
    "chars": 662,
    "preview": "import { SocketConnection } from './sockets';\nimport { ViewId, Metadata } from './message';\nimport { HyperView } from \"."
  },
  {
    "path": "client/dist/lib.d.ts",
    "chars": 185,
    "preview": "export declare function takeWhileMap<T, A>(pred: (val: T) => A | undefined, lines: T[]): A[];\nexport declare function dr"
  },
  {
    "path": "client/dist/message.d.ts",
    "chars": 1155,
    "preview": "export type Meta = {\n    key: string;\n    value: string;\n};\nexport type ViewId = string;\nexport type RequestId = number;"
  },
  {
    "path": "client/dist/response.d.ts",
    "chars": 468,
    "preview": "import { ViewId, Metadata } from './message';\nexport type Response = {\n    meta: Metadata;\n    body: ResponseBody;\n};\nex"
  },
  {
    "path": "client/dist/sockets.d.ts",
    "chars": 1757,
    "preview": "import { ActionMessage } from './action';\nimport { ResponseBody } from \"./response\";\nimport { ViewId, RequestId, Encoded"
  },
  {
    "path": "client/package.json",
    "chars": 479,
    "preview": "{\n  \"name\": \"web-ui\",\n  \"version\": \"0.6.0\",\n  \"description\": \"Development -----------\",\n  \"main\": \"index.js\",\n  \"directo"
  },
  {
    "path": "client/src/action.ts",
    "chars": 2005,
    "preview": "\nimport { takeWhileMap } from \"./lib\"\nimport { Meta, ViewId, RequestId, EncodedAction, ViewState } from \"./message\"\nimpo"
  },
  {
    "path": "client/src/browser.ts",
    "chars": 400,
    "preview": "\nexport function setQuery(query: string) {\n  if (query != currentQuery()) {\n    if (query != \"\") query = \"?\" + query\n   "
  },
  {
    "path": "client/src/events.ts",
    "chars": 8107,
    "preview": "\nimport * as debounce from 'debounce'\nimport { encodedParam } from './action'\nimport { HyperView, isHyperView } from './"
  },
  {
    "path": "client/src/http.ts",
    "chars": 1295,
    "preview": "// import { ActionMessage, ParsedResponse } from './action'\n// import { Response, FetchError } from \"./response\"\n\n// exp"
  },
  {
    "path": "client/src/hyperview.ts",
    "chars": 1160,
    "preview": "import { type Request } from \"./action\";\n\nexport interface HyperView extends HTMLElement {\n  runAction(action: string): "
  },
  {
    "path": "client/src/index.ts",
    "chars": 11095,
    "preview": "import { patch, create } from \"omdomdom/lib/omdomdom.es.js\"\nimport { SocketConnection, Update, Redirect, Trigger, JSEven"
  },
  {
    "path": "client/src/lib.ts",
    "chars": 441,
    "preview": "\n\nexport function takeWhileMap<T, A>(pred: (val: T) => A | undefined, lines: T[]): A[] {\n  var output = []\n  for (var li"
  },
  {
    "path": "client/src/message.ts",
    "chars": 2750,
    "preview": "\nimport { takeWhileMap, dropWhile } from \"./lib\"\n\n\n\nexport type Meta = { key: string, value: string }\nexport type ViewId"
  },
  {
    "path": "client/src/response.ts",
    "chars": 790,
    "preview": "\nimport { ViewId, Metadata } from './message'\n\n\n\nexport type Response = {\n  meta: Metadata\n  body: ResponseBody\n}\n\nexpor"
  },
  {
    "path": "client/src/sockets.ts",
    "chars": 8056,
    "preview": "import { ActionMessage, renderActionMessage } from './action'\nimport { ResponseBody } from \"./response\"\nimport * as mess"
  },
  {
    "path": "client/tsconfig.json",
    "chars": 403,
    "preview": "{\n  \"compilerOptions\": {\n    \"outDir\": \"./dist/\",\n    \"sourceMap\": true,\n    \"noImplicitAny\": true,\n    \"module\": \"ES202"
  },
  {
    "path": "client/util/live-reload.js",
    "chars": 1485,
    "preview": "// This isn't magic. If you want custom behavior, copy and modify this however you like. \n//\n// As with any custom js, a"
  },
  {
    "path": "client/webpack.config.js",
    "chars": 719,
    "preview": "const path = require('path');\n// var PACKAGE = require('./package.json');\n// var version = PACKAGE.version;\n\nmodule.expo"
  },
  {
    "path": "demo/.dockerignore",
    "chars": 19,
    "preview": "dist-newstyle\n.git\n"
  },
  {
    "path": "demo/App/Cache.hs",
    "chars": 508,
    "preview": "module App.Cache where\n\nimport Network.HTTP.Types (Header)\nimport Network.Wai.Middleware.Static\n\nclientCache :: IO Optio"
  },
  {
    "path": "demo/App/Config.hs",
    "chars": 1772,
    "preview": "{-# LANGUAGE QuasiQuotes #-}\n\nmodule App.Config where\n\nimport Data.Maybe (fromMaybe, isNothing)\nimport Effectful\nimport "
  },
  {
    "path": "demo/App/Docs/Markdown.hs",
    "chars": 5711,
    "preview": "{-# LANGUAGE OverloadedLists #-}\n{-# LANGUAGE QuasiQuotes #-}\n{-# LANGUAGE TemplateHaskell #-}\n{-# OPTIONS_GHC -Wno-orph"
  },
  {
    "path": "demo/App/Docs/Page.hs",
    "chars": 2699,
    "preview": "{-# LANGUAGE DefaultSignatures #-}\n{-# LANGUAGE OverloadedLists #-}\n{-# LANGUAGE QuasiQuotes #-}\n\nmodule App.Docs.Page\n "
  },
  {
    "path": "demo/App/Docs/Snippet.hs",
    "chars": 5010,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule App.Docs.Snippet where\n\nimport Control.Monad (unless)\nimport Data.Char (isSpace"
  },
  {
    "path": "demo/App/Docs.hs",
    "chars": 179,
    "preview": "module App.Docs\n  ( module App.Docs.Markdown\n  , module App.Docs.Page\n  , module App.Docs.Snippet\n  ) where\n\nimport App."
  },
  {
    "path": "demo/App/Page/Application.hs",
    "chars": 1035,
    "preview": "{-# LANGUAGE LambdaCase #-}\n{-# LANGUAGE TemplateHaskell #-}\n\nmodule App.Page.Application where\n\nimport App.Docs\nimport "
  },
  {
    "path": "demo/App/Page/CSS.hs",
    "chars": 2491,
    "preview": "{-# LANGUAGE LambdaCase #-}\n{-# LANGUAGE TemplateHaskell #-}\n\nmodule App.Page.CSS where\n\nimport App.Docs\nimport App.Rout"
  },
  {
    "path": "demo/App/Page/Concurrency.hs",
    "chars": 3135,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule App.Page.Concurrency where\n\nimport App.Do"
  },
  {
    "path": "demo/App/Page/Examples.hs",
    "chars": 2169,
    "preview": "module App.Page.Examples where\n\nimport App.Docs\nimport App.Route as Route\nimport Example.Style as Style (link)\nimport Ex"
  },
  {
    "path": "demo/App/Page/Forms.hs",
    "chars": 921,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule App.Page.Forms where\n\nimport App.Docs\nimport App.Route\nimport Example.FormSimpl"
  },
  {
    "path": "demo/App/Page/HyperboleEffect.hs",
    "chars": 2254,
    "preview": "module App.Page.HyperboleEffect where\n\nimport App.Route as Route hiding (Response, UserId)\nimport App.Docs\nimport Effect"
  },
  {
    "path": "demo/App/Page/Hyperviews.hs",
    "chars": 2538,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule App.Page.Hyperviews where\n\nimport App.Docs\nimport App.Route qualified as Route "
  },
  {
    "path": "demo/App/Page/Interactivity.hs",
    "chars": 1450,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule App.Page.Interactivity where\n\nimport App.Docs\nimport App.Route hiding (Javascri"
  },
  {
    "path": "demo/App/Page/Intro/Basics.hs",
    "chars": 2625,
    "preview": "{-# LANGUAGE QuasiQuotes #-}\n{-# LANGUAGE TemplateHaskell #-}\n\nmodule App.Page.Intro.Basics where\n\nimport App.Docs\nimpor"
  },
  {
    "path": "demo/App/Page/Intro/Intro.hs",
    "chars": 2626,
    "preview": "{-# LANGUAGE QuasiQuotes #-}\n{-# LANGUAGE TemplateHaskell #-}\n\nmodule App.Page.Intro.Intro where\n\nimport App.Docs\nimport"
  },
  {
    "path": "demo/App/Page/OAuth2.hs",
    "chars": 5329,
    "preview": "{-# LANGUAGE OverloadedLists #-}\n{-# LANGUAGE OverloadedStrings #-}\n{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE Undeci"
  },
  {
    "path": "demo/App/Page/SideEffects.hs",
    "chars": 1064,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule App.Page.SideEffects where\n\nimport App.Docs\nimport App.Route as Route (AppRoute"
  },
  {
    "path": "demo/App/Page/State.hs",
    "chars": 2321,
    "preview": "{-# LANGUAGE QuasiQuotes #-}\n{-# LANGUAGE TemplateHaskell #-}\n\nmodule App.Page.State where\n\nimport App.Docs\nimport App.R"
  },
  {
    "path": "demo/App/Page/ViewFunctions.hs",
    "chars": 1166,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule App.Page.ViewFunctions where\n\nimport App.Docs\nimport App.Route qualified as Rou"
  },
  {
    "path": "demo/App/Route.hs",
    "chars": 3512,
    "preview": "{-# LANGUAGE OverloadedLists #-}\n\nmodule App.Route where\n\nimport Data.String.Conversions (cs)\nimport Data.Text (Text, un"
  },
  {
    "path": "demo/App/Style.hs",
    "chars": 1209,
    "preview": "module App.Style where\n\nimport Example.Colors\nimport Web.Atomic.CSS\n\n-- btn :: (Styleable h) => CSS h -> CSS h\n-- btn = "
  },
  {
    "path": "demo/App.hs",
    "chars": 10963,
    "preview": "{-# LANGUAGE OverloadedStrings #-}\n{-# OPTIONS_GHC -Wno-unused-imports #-}\n\nmodule App where\n\nimport App.Cache (clientCa"
  },
  {
    "path": "demo/Example/CSS/External.hs",
    "chars": 1113,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule Example.CSS.External where\n\nimport Data.Text (Text)\nimport App.Docs\nimport Web."
  },
  {
    "path": "demo/Example/CSS/Loading.hs",
    "chars": 810,
    "preview": "{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.CSS.Loading where\n\nimport Data.Text (Text)\nimport Example.Effects."
  },
  {
    "path": "demo/Example/CSS/Tooltips.hs",
    "chars": 849,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule Example.CSS.Tooltips where\n\nimport App.Docs\nimport Example.Colors\nimport Web.At"
  },
  {
    "path": "demo/Example/CSS/Transitions.hs",
    "chars": 752,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule Example.CSS.Transitions where\n\nimport App.Docs\nimport Example.Style.Cyber (btn)"
  },
  {
    "path": "demo/Example/Chat.hs",
    "chars": 5306,
    "preview": "{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Chat where\n\nimport App.Route\nimport Control.Monad (forM_, forever)"
  },
  {
    "path": "demo/Example/Colors.hs",
    "chars": 1704,
    "preview": "{-# LANGUAGE LambdaCase #-}\n\nmodule Example.Colors where\n\nimport Web.Atomic.CSS\nimport Web.Hyperbole\n\ndata AppColor\n  = "
  },
  {
    "path": "demo/Example/Concurrency/LazyLoading.hs",
    "chars": 1167,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Concurrency.LazyLoading where\n\nim"
  },
  {
    "path": "demo/Example/Concurrency/Overlap.hs",
    "chars": 1877,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Concurrency.Overlap where\n\nimport"
  },
  {
    "path": "demo/Example/Concurrency/Polling.hs",
    "chars": 1559,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Concurrency.Polling where\n\nimport"
  },
  {
    "path": "demo/Example/Concurrency/Progress.hs",
    "chars": 1691,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Concurrency.Progress where\n\nimpor"
  },
  {
    "path": "demo/Example/Concurrency/Tasks.hs",
    "chars": 556,
    "preview": "module Example.Concurrency.Tasks where\n\nimport Data.Text (Text, pack)\nimport Effectful\nimport Example.Effects.Debug\nimpo"
  },
  {
    "path": "demo/Example/Contact.hs",
    "chars": 4477,
    "preview": "{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Contact where\n\nimport App.Route (UserId)\nimport App.Route qualifie"
  },
  {
    "path": "demo/Example/Contacts.hs",
    "chars": 5217,
    "preview": "{-# LANGUAGE LambdaCase #-}\n{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Conta"
  },
  {
    "path": "demo/Example/Counter.hs",
    "chars": 848,
    "preview": "{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Counter where\n\nimport Data.Text (pack)\nimport Effectful\nimport Exa"
  },
  {
    "path": "demo/Example/Data/ProgrammingLanguage.hs",
    "chars": 2953,
    "preview": "{-# LANGUAGE DerivingVia #-}\n\nmodule Example.Data.ProgrammingLanguage where\n\nimport Data.Text (Text, isInfixOf, toLower)"
  },
  {
    "path": "demo/Example/DataLists/Autocomplete.hs",
    "chars": 2911,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.DataLists.Autocomplete where\n\nimp"
  },
  {
    "path": "demo/Example/DataLists/DataTable.hs",
    "chars": 1886,
    "preview": "{-# LANGUAGE LambdaCase #-}\n{-# LANGUAGE TemplateHaskell #-}\n\nmodule Example.DataLists.DataTable where\n\nimport App.Docs\n"
  },
  {
    "path": "demo/Example/DataLists/Filter.hs",
    "chars": 5401,
    "preview": "{-# LANGUAGE LambdaCase #-}\n{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.DataL"
  },
  {
    "path": "demo/Example/DataLists/LoadMore.hs",
    "chars": 1950,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule Example.DataLists.LoadMore where\n\nimport App.Docs\nimport App.Route as Route\nimp"
  },
  {
    "path": "demo/Example/Docs/App.hs",
    "chars": 2002,
    "preview": "module Example.Docs.App where\n\nimport Data.Text (Text)\nimport Effectful\nimport Effectful.Concurrent\nimport Effectful.Dis"
  },
  {
    "path": "demo/Example/Docs/BasicPage.hs",
    "chars": 470,
    "preview": "{-# OPTIONS_GHC -Wno-missing-signatures #-}\n\nmodule Example.Docs.BasicPage where\n\nimport Data.Text (Text)\nimport Web.Hyp"
  },
  {
    "path": "demo/Example/Docs/CSS.hs",
    "chars": 448,
    "preview": "{-# OPTIONS_GHC -Wno-missing-signatures #-}\n{-# OPTIONS_GHC -Wno-unused-binds #-}\n{-# OPTIONS_GHC -Wno-unused-top-binds "
  },
  {
    "path": "demo/Example/Docs/Client.hs",
    "chars": 161,
    "preview": "module Example.Docs.Client where\n\nimport Web.Hyperbole\n\npage :: (Hyperbole :> es) => Page es '[]\npage = do\n  pageTitle \""
  },
  {
    "path": "demo/Example/Docs/Component.hs",
    "chars": 348,
    "preview": "module Example.Docs.Component where\n\nimport Data.Text (Text)\nimport Example.Colors\nimport Web.Atomic.CSS\nimport Web.Hype"
  },
  {
    "path": "demo/Example/Docs/Encoding.hs",
    "chars": 229,
    "preview": "{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Docs.Encoding where\n\nimport Data.Text (Text)\nimport Web.Hyperbole\n"
  },
  {
    "path": "demo/Example/Docs/Interactive.hs",
    "chars": 241,
    "preview": "module Example.Docs.Interactive where\n\nimport Example.Simple\nimport Web.Hyperbole\n\npage :: Page es '[Message]\npage = do\n"
  },
  {
    "path": "demo/Example/Docs/MultiPage.hs",
    "chars": 852,
    "preview": "{-# OPTIONS_GHC -Wno-missing-signatures #-}\n\nmodule Example.Docs.MultiPage where\n\nimport Example.Docs.Interactive qualif"
  },
  {
    "path": "demo/Example/Docs/MultiView.hs",
    "chars": 329,
    "preview": "module Example.Docs.MultiView where\n\nimport Example.Counter (Counter (..), viewCount)\nimport Example.Simple (Message (.."
  },
  {
    "path": "demo/Example/Docs/Nested.hs",
    "chars": 1295,
    "preview": "module Example.Docs.Nested where\n\nimport Control.Monad (forM_)\nimport Data.Text (Text)\nimport Web.Hyperbole\n\npage :: (Hy"
  },
  {
    "path": "demo/Example/Docs/Nesting.hs",
    "chars": 985,
    "preview": "module Example.Docs.Nesting where\n\nimport Control.Monad (forM_)\nimport Example.Colors\nimport Example.Docs.UniqueViewId h"
  },
  {
    "path": "demo/Example/Docs/Page/Messages.hs",
    "chars": 116,
    "preview": "module Example.Docs.Page.Messages where\n\nimport Web.Hyperbole\n\npage :: Page es '[]\npage = pure $ el \"Messages page\"\n"
  },
  {
    "path": "demo/Example/Docs/Page/Users.hs",
    "chars": 118,
    "preview": "module Example.Docs.Page.Users where\n\nimport Web.Hyperbole\n\npage :: Int -> Page es '[]\npage _ = pure $ el \"User page\"\n"
  },
  {
    "path": "demo/Example/Docs/Params.hs",
    "chars": 1363,
    "preview": "module Example.Docs.Params where\n\nimport Data.Text (Text)\nimport Web.Atomic.CSS\nimport Web.Hyperbole\n\ndata Filters = Fil"
  },
  {
    "path": "demo/Example/Docs/QueryMessage.hs",
    "chars": 712,
    "preview": "module Example.Docs.QueryMessage where\n\nimport Data.Maybe (fromMaybe)\nimport Data.Text (Text)\nimport Web.Atomic.CSS\nimpo"
  },
  {
    "path": "demo/Example/Docs/Sessions.hs",
    "chars": 954,
    "preview": "module Example.Docs.Sessions where\n\nimport Web.Atomic.CSS\nimport Web.Hyperbole\n\ndata AppColor\n  = White\n  | Red\n  | Gree"
  },
  {
    "path": "demo/Example/Docs/SideEffects.hs",
    "chars": 2329,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Docs.SideEffects where\n\nimport Da"
  },
  {
    "path": "demo/Example/Docs/State.hs",
    "chars": 592,
    "preview": "{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Docs.State where\n\nimport Data.Text (Text)\nimport Web.Atomic.CSS\nim"
  },
  {
    "path": "demo/Example/Docs/UniqueViewId.hs",
    "chars": 1273,
    "preview": "module Example.Docs.UniqueViewId where\n\nimport Control.Monad (forM_)\nimport Data.Text (Text, pack)\nimport Example.Colors"
  },
  {
    "path": "demo/Example/Docs/ViewFunctions.hs",
    "chars": 1983,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Docs.ViewFunctions where\n\nimport "
  },
  {
    "path": "demo/Example/Document.hs",
    "chars": 364,
    "preview": "module Example.Document where\n\nimport Web.Hyperbole\n\nmain :: IO ()\nmain = do\n  run 3000 $ liveApp (document documentHead"
  },
  {
    "path": "demo/Example/Effects/Debug.hs",
    "chars": 1060,
    "preview": "{-# LANGUAGE LambdaCase #-}\n{-# LANGUAGE QuasiQuotes #-}\n\nmodule Example.Effects.Debug\n  ( Milliseconds\n  , Debug (..)\n "
  },
  {
    "path": "demo/Example/Effects/Todos.hs",
    "chars": 3069,
    "preview": "{-# LANGUAGE DerivingVia #-}\n{-# LANGUAGE LambdaCase #-}\n\nmodule Example.Effects.Todos where\n\nimport Data.Map (Map)\nimpo"
  },
  {
    "path": "demo/Example/Effects/Users.hs",
    "chars": 2281,
    "preview": "{-# LANGUAGE LambdaCase #-}\n\nmodule Example.Effects.Users where\n\nimport App.Route (UserId)\nimport Control.Concurrent.MVa"
  },
  {
    "path": "demo/Example/Errors.hs",
    "chars": 3856,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule Example.Errors where\n\nimport App.Docs\nimport Effectful.Exception\nimport Example"
  },
  {
    "path": "demo/Example/FormSimple.hs",
    "chars": 5128,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule Example.FormSimple where\n\nimport App.Docs\nimport Data.Text (Text, pack)\nimport "
  },
  {
    "path": "demo/Example/FormValidation.hs",
    "chars": 3304,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule Example.FormValidation where\n\nimport Data.Text (Text, pack)\nimport Data.Text qu"
  },
  {
    "path": "demo/Example/Interactivity/Events.hs",
    "chars": 2089,
    "preview": "module Example.Interactivity.Events where\n\nimport Data.Text (Text, pack)\nimport Example.Colors\nimport Example.Style.Cybe"
  },
  {
    "path": "demo/Example/Interactivity/Inputs.hs",
    "chars": 855,
    "preview": "module Example.Interactivity.Inputs where\n\nimport Data.Text (pack)\nimport Web.Atomic.CSS\nimport Web.Hyperbole hiding (bu"
  },
  {
    "path": "demo/Example/Javascript.hs",
    "chars": 1262,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule Example.Javascript where\n\nimport Data.Text (Text, pack)\nimport App.Docs\nimport "
  },
  {
    "path": "demo/Example/Push.hs",
    "chars": 1271,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Push where\n\nimport App.Docs\nimpor"
  },
  {
    "path": "demo/Example/Requests.hs",
    "chars": 3009,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule Example.Requests where\n\nimport Data.String.Conversions (cs)\nimport Data.Text (T"
  },
  {
    "path": "demo/Example/Scrollbars.hs",
    "chars": 1838,
    "preview": "module Example.Scrollbars where\n\nimport Control.Monad (forM_)\nimport Data.String.Conversions (cs)\nimport Data.Text (Text"
  },
  {
    "path": "demo/Example/Simple.hs",
    "chars": 824,
    "preview": "{-# LANGUAGE DataKinds #-}\n{-# LANGUAGE DeriveAnyClass #-}\n{-# LANGUAGE OverloadedStrings #-}\n{-# LANGUAGE TypeFamilies "
  },
  {
    "path": "demo/Example/State/Effects.hs",
    "chars": 1924,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.State.Effects where\n\nimport App.D"
  },
  {
    "path": "demo/Example/State/Query.hs",
    "chars": 2222,
    "preview": "{-# LANGUAGE AllowAmbiguousTypes #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.State.Query where\n\nimport Dat"
  },
  {
    "path": "demo/Example/State/Sessions.hs",
    "chars": 2478,
    "preview": "{-# LANGUAGE AllowAmbiguousTypes #-}\n{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Exam"
  },
  {
    "path": "demo/Example/State/Stateless.hs",
    "chars": 605,
    "preview": "module Example.State.Stateless where\n\nimport Example.Style.Cyber (btn)\nimport Web.Atomic.CSS\nimport Web.Hyperbole\n\ndata "
  },
  {
    "path": "demo/Example/State/ViewState.hs",
    "chars": 904,
    "preview": "module Example.State.ViewState where\n\nimport Data.Text (pack)\nimport Example.Style.Cyber (btn, dataFeature)\nimport Web.A"
  },
  {
    "path": "demo/Example/Style/Cyber.hs",
    "chars": 2731,
    "preview": "module Example.Style.Cyber where\n\nimport Data.Text (Text, pack)\nimport Example.Colors\nimport Web.Atomic.CSS\nimport Web.A"
  },
  {
    "path": "demo/Example/Style.hs",
    "chars": 1213,
    "preview": "module Example.Style where\n\nimport Example.Colors\nimport Web.Atomic.CSS\n\n-- btn :: (Styleable h) => CSS h -> CSS h\n-- bt"
  },
  {
    "path": "demo/Example/Tags.hs",
    "chars": 1364,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule Example.Tags where\n\nimport App.Docs\nimport App.Route qualified as Route\nimport "
  },
  {
    "path": "demo/Example/Test.hs",
    "chars": 1345,
    "preview": "module Example.Test where\n\nimport Control.Monad (forM_)\nimport Data.String.Conversions (cs)\nimport Data.Text (Text)\nimpo"
  },
  {
    "path": "demo/Example/Todos/Todo.hs",
    "chars": 5837,
    "preview": "{-# LANGUAGE DerivingVia #-}\n{-# LANGUAGE TemplateHaskell #-}\n{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Todo"
  },
  {
    "path": "demo/Example/Todos/TodoCSS.hs",
    "chars": 7934,
    "preview": "{-# LANGUAGE UndecidableInstances #-}\n\nmodule Example.Todos.TodoCSS (page) where\n\nimport App.Route hiding (Filter)\nimpor"
  },
  {
    "path": "demo/Example/Trigger.hs",
    "chars": 1136,
    "preview": "{-# LANGUAGE TemplateHaskell #-}\n\nmodule Example.Trigger where\n\nimport Data.Text (Text)\nimport App.Docs\nimport Example.S"
  },
  {
    "path": "demo/Example/View/Icon.hs",
    "chars": 3626,
    "preview": "{-# LANGUAGE QuasiQuotes #-}\n\nmodule Example.View.Icon where\n\nimport Data.String.Interpolate (i)\nimport Data.Text (Text)"
  },
  {
    "path": "demo/Example/View/Inputs.hs",
    "chars": 603,
    "preview": "module Example.View.Inputs where\n\nimport Example.Colors\nimport Web.Atomic.CSS\nimport Web.Hyperbole\n\ntoggleCheckbox :: (V"
  },
  {
    "path": "demo/Example/View/Layout.hs",
    "chars": 3042,
    "preview": "{-# LANGUAGE AllowAmbiguousTypes #-}\n{-# LANGUAGE DefaultSignatures #-}\n{-# LANGUAGE LambdaCase #-}\n{-# LANGUAGE QuasiQu"
  },
  {
    "path": "demo/Example/View/Loader.hs",
    "chars": 926,
    "preview": "{-# LANGUAGE QuasiQuotes #-}\n\nmodule Example.View.Loader where\n\nimport Data.ByteString (ByteString)\nimport Data.String.I"
  },
  {
    "path": "demo/Example/View/Menu.hs",
    "chars": 2050,
    "preview": "{-# LANGUAGE AllowAmbiguousTypes #-}\n\nmodule Example.View.Menu where\n\nimport App.Docs\nimport App.Route\nimport Control.Mo"
  },
  {
    "path": "demo/Example/View/SortableTable.hs",
    "chars": 1254,
    "preview": "module Example.View.SortableTable where\n\nimport Data.Text (Text)\nimport Example.Colors\nimport Example.Style qualified as"
  },
  {
    "path": "demo/Main.hs",
    "chars": 60,
    "preview": "module Main where\n\nimport App\n\nmain :: IO ()\nmain = App.run\n"
  },
  {
    "path": "demo/README.md",
    "chars": 114,
    "preview": "Hyperbole Examples\n===================\n\nVisit https://docs.hyperbole.live to view these examples with source code\n"
  },
  {
    "path": "demo/demo.cabal",
    "chars": 4201,
    "preview": "cabal-version: 2.2\n\n-- This file has been generated from package.yaml by hpack version 0.37.0.\n--\n-- see: https://github"
  },
  {
    "path": "demo/fourmolu.yaml",
    "chars": 1570,
    "preview": "# # Number of spaces per indentation step\nindentation: 2\n#\n# # Max line length for automatic line breaking\n# column-limi"
  },
  {
    "path": "demo/hie.yaml",
    "chars": 17,
    "preview": "cradle:\n  cabal:\n"
  },
  {
    "path": "demo/package.yaml",
    "chars": 1423,
    "preview": "name:               demo\nversion:            0.6.0\nsynopsis:           Interactive HTML apps using type-safe serverside "
  },
  {
    "path": "demo/static/custom.js",
    "chars": 750,
    "preview": "console.log(\"Custom JS!\")\n\nwindow.onload = function() {\n  let boxes = Hyperbole.hyperView(\"JBoxes\")\n  console.log(\"Found"
  },
  {
    "path": "demo/static/cyber.css",
    "chars": 3235,
    "preview": "@font-face {\n  font-family: 'Share Tech Mono';\n  src: url('/ShareTechMono-Regular.ttf') format('truetype');\n  font-weigh"
  },
  {
    "path": "demo/static/docs.js",
    "chars": 1484,
    "preview": "\nconsole.log(\"CUSTOM DOCS JS 2\")\n\nconst sections = document.querySelectorAll(\"section[id]\")\nconst navLinks = document.qu"
  },
  {
    "path": "demo/static/external.css",
    "chars": 373,
    "preview": ".item {\n  border: 1px dashed;\n  padding: 5px;\n  padding-left: 10px;\n  padding-right: 10px;\n}\n\n.item:hover {\n  border-col"
  },
  {
    "path": "demo/static/prism.css",
    "chars": 1503,
    "preview": "/* PrismJS 1.30.0\nhttps://prismjs.com/download#themes=prism-okaidia&languages=markup+css+clike+javascript+haskell */\ncod"
  },
  {
    "path": "demo/static/prism.js",
    "chars": 19952,
    "preview": "/* PrismJS 1.30.0\nhttps://prismjs.com/download#themes=prism&languages=markup+css+clike+javascript+haskell */\nvar _self=\""
  },
  {
    "path": "demo/static/test.js",
    "chars": 253,
    "preview": "console.log('test.js')\n\n\nwindow.addEventListener('load', function() {\n\n  let other = Hyperbole.hyperView(\"Other\")\n  docu"
  },
  {
    "path": "demo/static/todomvc.css",
    "chars": 720,
    "preview": "/* Undo the CSS reset for the TODOMVC example. This is only needed for the examples, because\n * we need to apply the res"
  },
  {
    "path": "docs/Main.hs",
    "chars": 7765,
    "preview": "{-# LANGUAGE QuasiQuotes #-}\n\nmodule Main where\n\nimport Control.Exception (SomeException, try)\nimport Data.Char (isAlpha"
  },
  {
    "path": "docs/app-document.md",
    "chars": 431,
    "preview": "The first argument is a `document` function. This turns an initial page fragment into a full document, complete with `<s"
  },
  {
    "path": "docs/app-effects.md",
    "chars": 422,
    "preview": "Your application will want to support various side effects. It's helpful to create a single function that runs all share"
  },
  {
    "path": "docs/app-live.md",
    "chars": 328,
    "preview": "In [[/basics]] we showed how to run a simple application:\n\n    #EMBED Example.Docs.BasicPage main\n\n Let's go over the ar"
  },
  {
    "path": "docs/app-pages.md",
    "chars": 628,
    "preview": "The second argument of `liveApp` is an `Effect` monad which returns a `Response`. We will rarely return a `Response` dir"
  },
  {
    "path": "docs/app-routes.md",
    "chars": 706,
    "preview": "Since we have more than one `Page`, we need a way to choose between them. You could create a manual function `Hyperbole "
  },
  {
    "path": "docs/atomic.md",
    "chars": 364,
    "preview": "Hyperbole encourages using the [atomic-css](https://github.com/seanhess/atomic-css)  package to factor styles with haske"
  },
  {
    "path": "docs/comparison.md",
    "chars": 1062,
    "preview": "Comparison with Similar Frameworks\n==================================\n\n[HTMX](https://htmx.org/)\n----------------\n\nSimil"
  },
  {
    "path": "docs/concurrency-overlap.md",
    "chars": 665,
    "preview": "By default, if an `Action` triggers for a particular `HyperView` when one is already being processed, the system will `D"
  },
  {
    "path": "docs/dev.md",
    "chars": 682,
    "preview": "Local Development\n=================\n\nDownload and install [NPM](https://nodejs.org/en/download). On a mac, can be instal"
  },
  {
    "path": "docs/docgen.cabal",
    "chars": 1271,
    "preview": "cabal-version: 2.2\n\n-- This file has been generated from package.yaml by hpack version 0.37.0.\n--\n-- see: https://github"
  },
  {
    "path": "docs/effectful.md",
    "chars": 619,
    "preview": "Hyperbole relies heavily on [Effectful](https://hackage.haskell.org/package/effectful) to run and compose side effects. "
  },
  {
    "path": "docs/effects-custom.md",
    "chars": 1274,
    "preview": "We could run a database using the `IOE` effect, but it is better to describe the high-level operations available to the "
  },
  {
    "path": "docs/effects-other.md",
    "chars": 571,
    "preview": "If we want to use an `Effect` besides `Hyperbole`, add it as a constraint to any `Page` and `HyperView` that needs it\n\nT"
  },
  {
    "path": "docs/forms-simple.md",
    "chars": 418,
    "preview": "We can render and parse `form`s via a record. This can be a simple record:\n\n    #EMBED Example.FormSimple data ContactFo"
  },
  {
    "path": "docs/forms-validated.md",
    "chars": 339,
    "preview": "We can use a Higher-Kinded `form` not only for field names and values, but to `validate` form fields\n\n    #EMBED Example"
  },
  {
    "path": "docs/hyperviews-intro.md",
    "chars": 1320,
    "preview": "Let's get interactive! Using `Hyperbole`, we divide our `Page` into independent live subsections called `HyperView`s\n\nTo"
  },
  {
    "path": "docs/hyperviews-multi.md",
    "chars": 317,
    "preview": "We can add as many `HyperView`s to a `Page` as we want. Each will update independently. These can be copies of the same "
  },
  {
    "path": "docs/hyperviews-nesting.md",
    "chars": 741,
    "preview": "We can nest smaller, more specific `HyperView`s inside of a larger parent. You might need this technique to display a li"
  },
  {
    "path": "docs/hyperviews-unique.md",
    "chars": 890,
    "preview": "`ViewId` values must be unique. So if we want more than one of the same `HyperView` on the same `Page`, we need a way to"
  },
  {
    "path": "docs/interactivity-events.md",
    "chars": 318,
    "preview": "Hyperbole provides various events that can be tied to specific `Action`s. Up to this point, we've only used them via the"
  },
  {
    "path": "docs/interactivity-events2.md",
    "chars": 346,
    "preview": "The following example demonstrates using `onMouseEnter` and `onMouseLeave`\n\nRemember it is better to use Atomic CSS to p"
  },
  {
    "path": "docs/interactivity-inputs.md",
    "chars": 339,
    "preview": "We've seen `button` in quite a few examples. Hyperbole provides a few other high-level inputs to easily tie interactivit"
  },
  {
    "path": "docs/interactivity-javascript.md",
    "chars": 748,
    "preview": "Include custom js on a page with the script tag on only the page where it is needed, or globally via your `docuemnt` fun"
  },
  {
    "path": "docs/interactivity-pushevent.md",
    "chars": 357,
    "preview": "## PushEvent\n\nThe server can push an event to be dispatched on a `HyperView`\n\n    #EMBED Example.Javascript update Alert"
  },
  {
    "path": "docs/intro-downsides.md",
    "chars": 840,
    "preview": "__Beginners__ - It uses some advanced Haskell features, and requires using [Effectful](https://hackage.haskell.org/packa"
  },
  {
    "path": "docs/intro-links.md",
    "chars": 369,
    "preview": "This site has detailed documentation with inline examples. Click the \"source\" link to see the real source code for the e"
  },
  {
    "path": "docs/intro.md",
    "chars": 1957,
    "preview": "Single Page Applications (SPAs) require the programmer to write two programs: a Javascript client and a Server, which bo"
  },
  {
    "path": "docs/javascript_api.md",
    "chars": 2142,
    "preview": "Javascript API\n-----------------\n\nRequirements\n\n1. Call runAction() - you ought to be able to tell the server to run a p"
  },
  {
    "path": "docs/multi-same.md",
    "chars": 632,
    "preview": "We can embed more than one of the same `HyperView` as long as the _value_ of `ViewId` is unique. Let's update `Message` "
  },
  {
    "path": "docs/nix.md",
    "chars": 3555,
    "preview": "Usage with NIX\n==============\n\n\nHow to Import Flake\n-------------------\n\nYou can import this flake's overlay to add `hyp"
  },
  {
    "path": "docs/outline.md",
    "chars": 946,
    "preview": "# TODO: ConcurrencyMode = Replace, etc\n# TODO: feedback - loading\n\nIntro\n------\n\n\n\n\nBasics\n------\n\n* Get Running\n* Html "
  },
  {
    "path": "docs/package.yaml",
    "chars": 970,
    "preview": "name:               docgen\nversion:            0.5.0\nsynopsis:           Interactive HTML apps using type-safe serversid"
  },
  {
    "path": "docs/pages.md",
    "chars": 766,
    "preview": "\nAn app will usually have multiple 'Page's with different 'Route's that each map to a unique url path:\n\n@\n#EMBED Example"
  },
  {
    "path": "docs/state-browser.md",
    "chars": 725,
    "preview": "The state `Action` threading and `ViewState` both live in on the web page itself, and are reset when the user navigates "
  },
  {
    "path": "docs/state-effects.md",
    "chars": 579,
    "preview": "For any real application, most persistent state will need to use a separate `Effect`, like a database. In [[/sideeffects"
  },
  {
    "path": "docs/state-sessions.md",
    "chars": 612,
    "preview": "Likewise we can store state in a browser cookie using `Session`. This is useful for user preferences, login state, and a"
  },
  {
    "path": "docs/state-stateless.md",
    "chars": 219,
    "preview": "By default, `HyperView`s are stateless. Nothing is stored in the server connection. `HyperView` `update`s are the direct"
  },
  {
    "path": "docs/state-threading.md",
    "chars": 471,
    "preview": "The simplest way to add state to a `HyperView` is to pass it back and forth between the `Action` and the `View`. In this"
  },
  {
    "path": "docs/state-viewstate.md",
    "chars": 761,
    "preview": "`Hyperbole` can manage action-threaded state automatically by setting `ViewState` in your `ViewId`:\n\n    #EMBED Example."
  },
  {
    "path": "docs/view-components.md",
    "chars": 788,
    "preview": "You may be tempted to use `HyperView`s to create reusable \\\"_Components_\\\". This leads to object-oriented designs that d"
  },
  {
    "path": "docs/view-functions-end.md",
    "chars": 285,
    "preview": "Don't use `HyperView`s to keep your code DRY. Instead, think about which subsections of a page ought to update independe"
  },
  {
    "path": "docs/view-functions-wrap.md",
    "chars": 152,
    "preview": "View functions can be containers which wrap other Views:\n\n    #EMBED Example.View.Inputs progressBar\n\n    #EMBED Example"
  },
  {
    "path": "docs/view-functions.md",
    "chars": 696,
    "preview": "We showed in [[basics]] how we can factor `View`s into functions. It's best practice to have a main `View` function for "
  },
  {
    "path": "flake.nix",
    "chars": 8374,
    "preview": "{\n  description = \"hyperbole overlay, development and hyperbole-demo\";\n\n  nixConfig = {\n    extra-substituters = [\n     "
  },
  {
    "path": "fourmolu.yaml",
    "chars": 1569,
    "preview": "# # Number of spaces per indentation step\nindentation: 2\n#\n# # Max line length for automatic line breaking\n# column-limi"
  },
  {
    "path": "hie.yaml",
    "chars": 17,
    "preview": "cradle:\n  cabal:\n"
  },
  {
    "path": "hyperbole.cabal",
    "chars": 5258,
    "preview": "cabal-version: 2.2\n\n-- This file has been generated from package.yaml by hpack version 0.37.0.\n--\n-- see: https://github"
  },
  {
    "path": "package.yaml",
    "chars": 2143,
    "preview": "name:               hyperbole\nversion:            0.6.0\nsynopsis:           Interactive HTML apps using type-safe server"
  },
  {
    "path": "src/Web/Hyperbole/Application.hs",
    "chars": 3316,
    "preview": "module Web.Hyperbole.Application\n  ( waiApp\n  , websocketsOr\n  , defaultConnectionOptions\n  , liveApp\n  , liveAppWith\n  "
  }
]

// ... and 54 more files (download for full content)

About this extraction

This page contains the full source code of the seanhess/hyperbole GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 254 files (539.1 KB), approximately 157.6k tokens, and a symbol index with 174 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!