Koyomi

Koyomi — Dates for Phoenix

Koyomi (暦, "calendar") is dates for Phoenix. It gives your app:

The pickers are a prebuilt JavaScript bundle in the Hex package. Your app does not run Node.js for them: you add the hook and one style sheet.

Try it: Notebook

Koyomi in Notebook: a due date, a range of days, a repeat rule, and a day and time picker open on its time slots, with the weekends and the past days disabled by the rules

Notebook is a complete Phoenix app made with Koyomi and Kotoba. Each page has a due date, the days it covers, a review on a day and a time from the free slots, and a repeat rule; the changeset checks the same rules as the calendar, and the notebook's own calendar shows the days of every page. Clone it, run mix setup and mix phx.server, and sign in as Hattori Hanzo (its README has the details) to try every picker in your browser.

What the pickers do

Pickers date_picker, date_range_picker, dates_picker, month_picker, week_picker, year_picker, time_picker, datetime_picker, recurrence_picker, and calendar for a calendar on the page
Fields segments in the locale's order and digits, typed or changed with the arrow keys, the number keyboard on a phone, two-digit years, paste
Words dates, ranges and times typed in words, in English, Portuguese, Spanish, French, German, Italian, Dutch and Japanese, and month and weekday names in every locale, with a preview
Calendar one or more months side by side, a list of months that scrolls on a phone, months and years to jump to, week numbers, holidays of 26 countries, presets
Ranges drawn as the pointer moves, the length in days or nights, days out of reach disabled while the end is chosen
Rules bounds relative to today, disabled days, ranges and weekdays, the shortest and longest range, counts, times and steps; the same in Koyomi.Changeset
Locales names, digits, first day of the week and weekend from the browser's CLDR data, right-to-left languages, the 12 or 24-hour clock
Calendars Gregorian, ISO, Japanese, Buddhist, ROC, Persian, Islamic (four kinds), Hebrew, Chinese, Dangi, Indian, Ethiopic, Coptic
Time zones today in a zone, wall clock times posted with the zone's offset, the hour the clocks skip or repeat shown and resolved
Day content labels, tones, marks, descriptions, disabled days and time slots from the LiveView, in sync or async, refreshed on demand
Forms the forms of the browser's own inputs (2027-03-15, 2027-W11), the browser's own input before the hook mounts, phx-change and used_input?, pages without LiveView
Types Koyomi.DateRange, Koyomi.DateTimeRange, Koyomi.Dates, Koyomi.Month, Koyomi.Week, Koyomi.Recurrence
Repeat daily, weekly, monthly and yearly rules (RFC 5545), said in words in eight languages, with the next dates
Theming CSS custom properties, the Sumi theme, dark mode, forced colours, reduced motion
Accessibility a labelled group of spinbuttons, a dialog, the WAI-ARIA grid with one tab stop, announcements, 44px targets on touch screens

Install

Koyomi needs Elixir 1.18 or later (it uses the built-in JSON module), Phoenix 1.8 and Phoenix LiveView 1.2.

Add koyomi to the deps in mix.exs:

def deps do
  [
    {:koyomi, "~> 0.1"}
  ]
end

Then run:

mix deps.get
mix koyomi.install

The installer adds the hook to assets/js/app.js and the style sheet to assets/css/app.css, and prints what it changed. Give --dry-run to see the changes first. It does not add what a file already has, so you can run it again.

The manual steps

To make the same changes by hand, import the hook in assets/js/app.js and add it to the hooks of the LiveSocket:

import { Koyomi } from "koyomi"

const liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken},
  hooks: {...colocatedHooks, Koyomi},
})

Import the style sheet in assets/css/app.css (and the Sumi theme, if you want it: Sumi is the design system of Hattori AI, the makers of Koyomi and Kotoba). Put these lines below the other @import lines of app.css:

@import "../../deps/koyomi/priv/static/koyomi.css";
/* @import "../../deps/koyomi/priv/static/koyomi-sumi.css"; */

A date and time picker with a time_zone other than UTC, and rules with a :time_zone, need a time zone database on the server, such as tz or tzdata:

config :elixir, :time_zone_database, Tz.TimeZoneDatabase

NODE_PATH=deps

import { Koyomi } from "koyomi" resolves through the NODE_PATH of your esbuild profile, which must have deps/. The Phoenix generators already set it. If your profile has no NODE_PATH, add it in config/config.exs:

env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}

Use

schema "bookings" do
  field :check_in, :date
  field :stay, Koyomi.DateRange
end

@stay_rules Koyomi.Rules.new(min: :today, max_days: 14)

def changeset(booking, attrs) do
  booking
  |> cast(attrs, [:check_in, :stay])
  |> Koyomi.Changeset.validate_range(:stay, @stay_rules)
end
<.form for={@form} phx-change="validate" phx-submit="save">
  <label id="stay-label">Stay</label>
  <.date_range_picker field={@form[:stay]} label_id="stay-label" rules={@stay_rules} months={2} nights />
</.form>

Import the components in the html_helpers of your web module:

import Koyomi.Components

Each picker posts its value with the form, in the form of the browser's own inputs (2027-03-15, or stay[from] and stay[to] for a range), so the form's own phx-change and phx-submit events have it, and the Ecto type of the field casts it.

Guides

Development

The picker source is TypeScript in assets/. The built files in priv/static are not in the repository; the Hex package has them.

mix deps.get
mix koyomi.build    # npm ci in assets/ when necessary, then esbuild
mix dev             # the development server on http://localhost:4099

mix koyomi.build runs in the :dev environment and needs npm. It empties priv/static and writes the four files of the package: koyomi.esm.js, koyomi.cjs.js, koyomi.css and koyomi-sumi.css.

mix dev runs dev.exs: a Phoenix endpoint with every picker in a form (/), day content from a fake availability service, slow and async (/days), the calendar systems of the world (/calendars), a page without LiveView (/plain), and the Sumi theme at /?theme=sumi (and &dark=1). Every page takes locale, calendar and zone query params. Set PORT for another port. An esbuild watcher rebuilds the bundle, and the page reloads.

Tests

mix test                   # the ExUnit suite
mix test --include build   # also mix koyomi.build and mix hex.build
mix test.js                # the unit tests of the TypeScript modules
mix test.e2e               # the Playwright browser tests
mix precommit              # compile, format, credo --strict and test
mix dialyzer

mix test.js runs the tests of the pure modules in assets/test/ (the date arithmetic, the locales and calendars, the words parser and the recurrence rules) with Node's test runner. test/fixtures/recurrence.json holds the recurrence rules to the same results in Elixir and in TypeScript.

The browser tests in e2e/ use Playwright, in Chromium, Firefox and WebKit (the engine of Safari). Install the browsers once:

cd e2e && npm ci && npx playwright install chromium firefox webkit

mix test.e2e runs every spec in the three browsers, and E2E_BROWSERS picks some of them. It runs npm ci in e2e/ when e2e/node_modules is missing, builds the bundle, and starts its own development server, without watchers, on port 4098 (set E2E_PORT for another port). A mix dev on port 4099 can keep running.

Release

mix release (in this repository, an alias that replaces Mix's release task) builds the bundle, checks that priv/static holds only the four bundle files (mix koyomi.release_check), publishes to Hex, and tags and pushes v<version>.

License

Koyomi is released under the MIT License. See LICENSE.

The icons are from Lucide (ISC License; some from Feather, MIT License), and the table of week data by region follows the Unicode CLDR (Unicode License v3). See NOTICE.