# elm-cally

A pure-Elm port of [Cally](https://github.com/WickyNilliams/cally), the
small, dependency-light calendar web components. No JavaScript, no ports,
no `Intl`, no shadow DOM -- `elm-cally` renders the same markup and the
same `part` attributes as the JS version with `Html.node`, so it can be
styled with (a light-DOM rewrite of) Cally's own CSS.

Three pickers are provided: `Cally.Date` (single date), `Cally.Range`
(date range) and `Cally.Multi` (multiple dates), each built from a shared
`Cally.Month` grid. Cally's test suite is the specification: every
upstream test is either ported (see `tests/`) or explicitly skipped with a
reason, and `upstream/ported-tests.json` / `check-coverage.mjs` enforce
that mapping in CI.

## Install

Published on the Elm package registry:
[package.elm-lang.org/packages/alexbruf/elm-cally/1.1.0](https://package.elm-lang.org/packages/alexbruf/elm-cally/1.1.0)

```sh
elm install alexbruf/elm-cally
```

Module docs for every exposed module live on that page. For the default
look, add the stylesheet: `Cally.Css.stylesheet` ships it inside the
package (see [Styling](#styling)), and `cally.css` at the root of this
repo is the same bytes as a file.

## Usage

```elm
module Main exposing (main)

import Browser
import Cally.Date as Date
import Cally.Locale as Locale
import Cally.Month as Month
import Date
import Html exposing (Html)


type alias Model =
    { value : Date.Value
    , picker : Date.Model
    }


type Msg
    = PickerMsg Date.Msg
    | Changed Date.Value


config : Date.Config Msg
config =
    Date.defaultConfig
        { id = "my-date-picker"
        , today = today -- a `Date.Date` from `elm/time` + `justinmimbs/date`
        , locale = Locale.enGB
        , toMsg = PickerMsg
        , onChange = Changed
        }


init : Model
init =
    { value = Nothing
    , picker = Date.init config Nothing
    }


view : Model -> Html Msg
view model =
    Date.view config
        model.value
        model.picker
        [ Month.view { offset = 0 } ]


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        PickerMsg subMsg ->
            let
                ( picker, cmd ) =
                    Date.update config model.value subMsg model.picker
            in
            ( { model | picker = picker }, Cmd.map PickerMsg cmd )

        Changed value ->
            ( { model | value = value }, Cmd.none )
```

`Cally.Month.view` (and `Cally.SelectYear.view` / `Cally.SelectMonth.view`)
take a `Context msg`, which `Date.view` / `Range.view` / `Multi.view` build
and pass to each child function -- that's how Elm reproduces Cally's
light-DOM slotting: children are just functions of `Context`, so they can
be placed in any order, including inside the `heading` slot for a custom
year/month picker. See `CLAUDE.md` for the full API contract and
`example/` for range, multi, multi-month, week-number and RTL usage.

### Styling

The default look is one stylesheet. Since 1.1.0 it ships **inside the
package** as `Cally.Css.stylesheet`, a plain `String`, because the Elm
package registry only distributes `src/`, `elm.json`, `README.md` and
`LICENSE` -- a file at the repository root can never reach you through
`elm install`.

Put it on the page with a `<style>` node:

```elm
import Cally.Css
import Html exposing (Html, node, text)

view : Model -> Html Msg
view model =
    Html.div []
        [ node "style" [] [ text Cally.Css.stylesheet ]
        , Cally.Date.view config model.value model.picker [ Month.view { offset = 0 } ]
        ]
```

The rules are global (they target element names and `part` attributes), so
render that node once, anywhere in the tree. `Cally.Css.version` is the
package version the string was generated from -- a useful cache key if a
build step writes it out to a real `.css` file instead.

`cally.css` at the repository root is the same bytes, kept in sync by
`scripts/gen-css-module.mjs` and checked in CI, so a build pipeline that
prefers a file can still copy it (that is what `example/` does).

The stylesheet is a translation of Cally's own shadow-DOM stylesheet into
plain CSS: no
shadow DOM means no `::part()`, so `:host` becomes the element name and
`::part(x)` becomes the attribute selector `[part~="x"]`, which matches
space-separated `part` tokens exactly the way `::part()` does. If you
already have Cally-targeting CSS, the rewrite is mechanical:

```css
/* before (targets the Cally web component) */
calendar-date::part(day selected) {
  background: hotpink;
}

/* after (targets elm-cally's light-DOM markup) */
calendar-date [part~="day"][part~="selected"] {
  background: hotpink;
}
```

See `docs/migrate-css.md` for the full rule, a `sed`/`perl` regex that
performs it, and more examples.

## Development

```sh
bun install                      # root dev tooling (elm-test, elm-review, elm-format, playwright)
cd upstream && npm ci            # pins the upstream `cally` version (Dependabot-owned)
node upstream/scripts/fetch-upstream-src.mjs   # clones cally's source at the pinned tag

bunx elm-test                    # Layer 1: elm-test (Test.Html.Query/Event)
bunx elm-review                  # jfmengels standard config
bunx elm-format --validate src tests example/src
bunx elm make --docs=docs.json
bun run css:generate                             # cally.css -> src/Cally/Css.elm (committed)
bun run css:check                                # fail if src/Cally/Css.elm is stale

node upstream/scripts/list-upstream-tests.mjs    # print every upstream describe/it title
node upstream/scripts/generate-ported-tests.mjs  # regenerate upstream/ported-tests.json from tests/
node upstream/scripts/check-coverage.mjs         # fail if any upstream title is unmapped
node upstream/scripts/capture-parts.mjs          # regenerate upstream/parts-fixture.json via real Cally in Playwright

bunx playwright test             # Layer 2: tests-browser/, against example/
```

Use `bun`/`bunx`, not `npm`/`npx`, everywhere except inside `upstream/`,
which is a separate npm project so Dependabot can bump the pinned `cally`
version on its own.

See `CLAUDE.md` for the full working contract (upstream facts, API
contract, DOM ids, behaviour rules, test conventions) and `SPEC.md` for
the original brief.

## Attribution

`elm-cally` is a from-scratch Elm implementation, ported test-for-test and
markup-for-markup from [Cally](https://github.com/WickyNilliams/cally) by
Nick Williams and contributors, MIT licensed. No Cally source code is
vendored or redistributed; the upstream repository is cloned into
`upstream/.cache/` (gitignored) only as a reference for the port and its
tests.

`elm-cally` itself is MIT licensed -- see `LICENSE`.
