diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..fc26186 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "corex": { + "url": "http://localhost:4004/corex/mcp" + } + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1953f66..63f7684 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,6 @@ on: permissions: contents: read -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - jobs: test: name: Tests (OTP ${{ matrix.otp }} | Elixir ${{ matrix.elixir }}) @@ -56,17 +53,6 @@ jobs: - name: Install Mix dependencies run: mix deps.get - - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: "24" - cache: npm - cache-dependency-path: assets/package-lock.json - - - name: Install npm dependencies - working-directory: assets - run: npm ci - - name: Run tests env: WALLABY_CHROME_BINARY: ${{ steps.setup-chrome.outputs.chrome-path }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9735b47..872bdf3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,9 +15,6 @@ concurrency: group: pages cancel-in-progress: false -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - jobs: build-and-deploy: if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' }} @@ -52,17 +49,6 @@ jobs: - name: Install Mix dependencies run: mix deps.get - - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: "24" - cache: npm - cache-dependency-path: assets/package-lock.json - - - name: Install npm dependencies - working-directory: assets - run: npm ci - - name: Build static site run: mix build diff --git a/AGENTS.md b/AGENTS.md index 65d0e03..89c7e36 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,17 +7,17 @@ There is one product: the static marketing/blog site. Standard commands live in `mix.exs` aliases — prefer those; the notes below only cover non-obvious cloud gotchas. ### Toolchain activation -- The Elixir/OTP toolchain is provided by **asdf** (versions pinned in `.tool-versions`: - Erlang 28.3.1, Elixir 1.19.5-otp-28). Node/npm come from **nvm**. Both are activated by - `~/.bashrc`, so a normal interactive shell already has `mix` and `npm`. -- If a command reports `mix: command not found` or `npm: command not found` (e.g. in a bare - non-login shell), activate them first: - `. "$HOME/.asdf/asdf.sh"` and `export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"`. +- Elixir/OTP versions are pinned in `.tool-versions` (Erlang 28.3.1, Elixir 1.19.5-otp-28). Use + **mise** (`mise install`) or **asdf** if present. There is no `assets/package.json`; Node is not + required for builds. +- If a command reports `mix: command not found` (e.g. in a bare non-login shell), activate the + toolchain first: `eval "$(mise activate bash)"` or `. "$HOME/.asdf/asdf.sh"`. ### Running / building -- Dev server: `mix tableau.server` → site at `http://localhost:4999` (home `/`, `/blog`, `/tags`). - In dev it also starts Corex MCP at `http://localhost:4004/corex/mcp`. Tailwind/esbuild run in - `--watch` mode and live-reload picks up changes to `lib/`, `_posts/`, `_data/`, `assets/`. +- Dev server: `mix server` (alias: port check, then `tableau.server`) → site at + `http://localhost:4999` (home `/`, `/blog`, `/tags`). In dev it also starts Corex MCP at + `http://localhost:4004/corex/mcp`. Tailwind/esbuild run in `--watch` mode and live-reload picks + up changes to `lib/`, `_posts/`, `_data/`, `assets/`. - Adding a blog post = create a Markdown file under `_posts/` with `layout: Soonex.PostLayout` front matter; the running dev server regenerates it automatically. - Rebuild only assets: `mix assets.build`. Production build: `MIX_ENV=prod mix build` → `_site/`. @@ -38,6 +38,7 @@ There is one product: the static marketing/blog site. Standard commands live in ### Gotchas - Live reload needs `inotify-tools` (installed). If the dev server logs `inotify-tools is needed ... fs_inotify_bootstrap_error`, the file watcher is disabled and content - changes won't hot-reload — restart the server after installing it. -- Port 4999 is shared by the dev server and the test harness. Stop `mix tableau.server` before - running `mix test`, otherwise the test's Bandit server can't bind the port. + changes won't hot-reload — restart the server after installing it. `mix server` fails fast on + Linux when `inotifywait` is missing. +- Port 4999 is shared by the dev server and the test harness. Stop `mix server` before running + `mix test`, otherwise the test's Bandit server can't bind the port. diff --git a/CHANGELOG.md b/CHANGELOG.md index 99ea2db..65b2241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # Changelog +## 0.3.0 + +- Redesign the home page with an editorial layout: display-size hero with a photo stage and floating countdown, photo-backed live preview, capabilities bento grid, workflow tabs with steps and copy commands, a proof section with stats, image-led journal cards, FAQ, and a closing waitlist band over photography. +- Rewrite marketing copy as a product launch rather than template notes. +- Customize through Corex Design only: four themes (neo, uno, duo, leo) with light and dark modes in `config :corex_design`. Drop per-theme skin CSS and JS (`assets/css/skins/*`, `assets/js/skins/*`, `assets/css/layout.css`, `assets/js/landing*.js`). +- Keep the Template Options theme switcher (`assets/js/theme.js`, `Soonex.Theme`) and the Corex tech marquee with its default styling. +- Add `:photo` tone to `Soonex.Layouts.Section` plus display, tile, photo frame, and scrim helpers in `Soonex.Layouts.Shell`. +- Replace all photography with Unsplash abstract imagery (credited in the README); remove unused portraits and grain texture. +- Fix the JavaScript logo to the official yellow mark and restore the official Tableau project logo in place of the Tableau Software mark; remove unused tech icons. +- Replace the journal with six current posts, including a Markdown style guide post. +- Rename the scale section module to `Soonex.HomePage.Proof`; remove the unused `Soonex.HomePage.Trust`. +- Update `phoenix_live_view` to 1.2.12, `makeup` to 1.2.3, and `ex_slop` to 0.4.5. +- Rewrite the README. + +## 0.2.2 + +- Harden for Corex 0.2.2+: drop `:corex_design` from Mix `compilers`; keep `mix corex.design.build` in `assets.build`, `pre.test`, and `build`; pin Corex family deps to `~> 0.2` with `corex_design` as `runtime: false`. +- Drop `assets/package.json` and Lenis; CI/deploy no longer run `npm ci`. +- Upgrade `a11y_audit` to `~> 0.5.0`; Wallaby axe check stays green on the home page. +- Add `mix server` (`soonex.port_check` then `tableau.server`) and restore AGENTS.md for Cloud Agent workflows. + +## 0.2.1 + +- Restage the home as a production launch landing: full-bleed hero, waitlist object on the still, English section ids, and distinct band skeletons. +- Drop the three-up plan cards, Latin module names, and tech-icon footer. Log tease and four-room exhibit replace them. +- Upgrade Corex packages to 0.2.1 (`corex`, `corex_design`, `corex_mcp`). +- Keep stock Corex themes (neo/uno/duo/leo) and self-host their type stacks. +- Production marketing chrome: SVG lockup, sticky condensed header, waitlist-card hero, full-width timer. +- Overlay every allowed Corex design key (seeds, light/dark colors, radius, font, typography) plus top-level `scales:`. +- Home bands each use one signature component (carousel, data_table, tabs, collapsible, date_picker, tooltip). FAQ keeps the sticky split. +- Blog, tags, and 404 use `layout_heading`, article cards, and pagination instead of `data_list` rows. +- Header countdown uses `hidden` + `inert` until past the hero sentinel (no transform peek). +- Drop homepage/blog BEM CSS in favor of inline Tailwind + Corex tokens. +- Use the common Tailwind marketing section (`py-24` / heading cluster / `mt-16` body) with a full-bleed stage (`max-w-[90rem]`). +- Override Tableau's `mdex ~> 0.11.1` pin to `mdex ~> 0.13.2` (CVE-2026-53426 / 53427 / 53428 / 53429 / 54888 / 54889). +- Add `mix soonex.server` / `mix soonex.port_check` so a taken :4999 fails with the occupant instead of Tableau's premature “server started” log. +- Drop Lenis and `assets/package.json`; native `scroll-smooth` is enough. +- Enable Corex accessibility (`accessibility: true`) with the official `--a11y` dialog (localStorage, no Phoenix plug). `corex_design` is a runtime dependency for that. +- Tiny `hosts.css` after `corex.css`. No token opacity. + ## 0.2.0 - Upgrade to Corex 0.2 (`corex`, `corex_design`, `corex_mcp`); replace Designex with `mix corex.design.build`. diff --git a/README.md b/README.md index 4268933..8e8eb5d 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,159 @@ # Soonex -English-only **Tableau** static site: Corex components, theme/mode toggles, and the same contrast pipeline as the SaaS template. +Soonex is a launch page kit for Phoenix teams. It builds a coming-soon site as static HTML with [Tableau](https://github.com/elixir-tools/tableau), styles it with [Corex](https://hexdocs.pm/corex) design tokens, and hydrates accessible Corex components in the browser. -**Related:** multi-locale variant at [github.com/corex-ui/soonex_i18n](https://github.com/corex-ui/soonex_i18n). Corex docs on Hex: [installation](https://hexdocs.pm/corex/installation.html), [Tableau + Corex](https://hexdocs.pm/corex/tableau.html), [Design](https://hexdocs.pm/corex/design.html), [update guide](https://hexdocs.pm/corex/update.html). +**Live demo:** [corex-ui.github.io/soonex](https://corex-ui.github.io/soonex) -## Prerequisites +There is also a multi-locale variant: [corex-ui/soonex_i18n](https://github.com/corex-ui/soonex_i18n). -- Elixir ~> 1.17 -- Node.js (for `npm install` in `assets/`) -- Hex packages `corex`, `corex_design`, and `corex_mcp` (`~> 0.2.0`) +## Features + +- **Landing page:** hero with launch countdown, tech marquee, live component preview, capabilities grid, workflow tabs, proof section, journal highlights, FAQ, and a closing waitlist band. +- **Four themes:** `neo`, `uno`, `duo`, and `leo`, each with light and dark modes generated from color seeds in `config :corex_design`. Visitors can switch them in **Template Options**. +- **Waitlist form:** email, role select, and opt-in switch with toast feedback. It stores nothing until you connect a provider. +- **Journal:** Markdown posts with cover images, tags, RSS (`/feed.xml`), sitemap, and pagination. +- **Accessibility panel:** text size, contrast, motion, focus ring, and link underline, saved in the visitor's browser. +- **Cookie consent and privacy page:** optional categories stay off unless the visitor allows them. +- **No Node toolchain:** Tailwind v4 and esbuild run as Mix tasks. There is no `package.json`. + +## Requirements + +- Erlang/OTP 28 and Elixir 1.19 (pinned in [`.tool-versions`](.tool-versions); the project supports Elixir `~> 1.17` and CI also runs 1.17 and 1.18) +- Hex packages `corex`, `corex_design`, and `corex_mcp` at `~> 0.2` +- Linux only: `inotify-tools` for live reload in the dev server +- For `mix test`: Google Chrome and a matching `chromedriver` ## Quick start ```shell +git clone https://github.com/corex-ui/soonex.git cd soonex -mix deps.get +mix setup +mix server +``` + +Open [http://localhost:4999](http://localhost:4999). The dev server watches `lib/`, `_posts/`, `_data/`, and `assets/` and reloads on change. + +In development, Corex MCP also runs at `http://localhost:4004/corex/mcp`. See [`.cursor/mcp.json`](.cursor/mcp.json) for an editor configuration example. + +## Commands + +| Command | What it does | +| --- | --- | +| `mix setup` | Fetch dependencies and build Corex design CSS | +| `mix server` | Check that ports 4999 and 4004 are free, then start the Tableau dev server | +| `mix assets.build` | Rebuild Corex design CSS, Tailwind, and esbuild output | +| `mix corex.design.build` | Regenerate design tokens and component CSS after changing `config :corex_design` | +| `MIX_ENV=prod mix build` | Production build into `_site/` with minified CSS and JavaScript | +| `mix test` | Build the site, then run the Wallaby axe accessibility check on the home page | +| `mix credo` | Lint | +| `mix soonex.gen.post My title` | Create a new post in `_posts/` | + +## Project structure + +```text +config/config.exs Tableau, Tailwind, esbuild, and config :corex_design (themes) +lib/pages/home_page.ex Home page section order +lib/pages/home/ One module per home section (hero, logos, showcase, features, ...) +lib/layouts/ Root layout, nav, footer, post layout, Shell and Section helpers +lib/soonex/launch.ex Launch date used by the hero badge and countdown +assets/css/ Tailwind entry, fonts, and small host helpers +assets/js/site.js Corex hook registration, theme and mode persistence, waitlist toast +extra/ Static files copied to the site root (images, fonts, favicons) +_posts/ Journal posts (Markdown) +test/ Wallaby accessibility test +``` + +## Customizing + +### Brand and SEO + +- Logo lockup: [`lib/layouts/brand.ex`](lib/layouts/brand.ex) and [`extra/images/logo.svg`](extra/images/logo.svg) +- Page titles and meta descriptions: [`lib/layouts/root_layout.ex`](lib/layouts/root_layout.ex) +- Open Graph image: [`extra/images/og.svg`](extra/images/og.svg) + +### Launch date + +Change `@target` in [`lib/soonex/launch.ex`](lib/soonex/launch.ex). The hero badge and countdown both read it. + +### Themes + +Each theme in `config :corex_design` (in [`config/config.exs`](config/config.exs)) accepts `seeds`, `colors.light` and `colors.dark`, `dimensions.radius`, `dimensions.font`, and `typography`, plus top-level `scales`. After editing, run: + +```shell mix corex.design.build -cd assets && npm install && cd .. -mix tableau.server ``` -- Dev site: `http://localhost:4999` (home at `/`). -- Production output: `MIX_ENV=prod mix build` → `_site/`. -- Set **`SOONEX_PUBLIC_URL`** for your real origin; otherwise the demo default is `https://corex-ui.github.io/soonex`. +Keep styling in Corex `ui-*` modifiers and design tokens in HEEx. [`assets/css/hosts.css`](assets/css/hosts.css) and [`assets/css/chrome.css`](assets/css/chrome.css) only hold small layout helpers. -With `MIX_ENV=dev`, Corex MCP listens at `http://localhost:4004/corex/mcp` (Tableau stays on 4999). Configure your editor using [`.cursor/mcp.json`](.cursor/mcp.json) as an example. +### Home sections -Rebuild assets: `mix assets.build`. +Sections live in [`lib/pages/home/`](lib/pages/home/) and are composed in [`lib/pages/home_page.ex`](lib/pages/home_page.ex). Shared spacing and type classes are in [`lib/layouts/shell.ex`](lib/layouts/shell.ex), and [`lib/layouts/section.ex`](lib/layouts/section.ex) provides the `block` component with `:root`, `:surface`, `:inverse`, and `:photo` tones. -## Customize (where to edit) +Photos live in [`extra/images/photos/`](extra/images/photos/) and post covers in [`extra/images/covers/`](extra/images/covers/). -- **Brand / SEO:** [`lib/layouts/root_layout.ex`](lib/layouts/root_layout.ex), [`lib/pages/root_index_page.ex`](lib/pages/root_index_page.ex). -- **Themes:** [`lib/soonex/theme.ex`](lib/soonex/theme.ex) — `data-theme` on `` must match themes in `config :corex_design`. -- **Content:** home sections in [`lib/pages/home/`](lib/pages/home/), composed by [`lib/pages/home_page.ex`](lib/pages/home_page.ex). -- **Blog:** index at [`/blog`](lib/pages/blog_index_page.ex); posts under [`_posts/`](_posts/) with `Soonex.PostLayout`; tags at [`/tags`](lib/pages/tags_index_page.ex). -- **Posts / data:** [`_posts/`](_posts/), [`_data/`](_data/), optional `title` / `description` in YAML. +### Waitlist -MDX-style Tableau extras (tags, `include_dir`, sitemap) are summarized in **Tableau data, tags, and static extras** in the longer notes below. +The form is in [`lib/pages/home/waitlist.ex`](lib/pages/home/waitlist.ex) and the toast in [`assets/js/waitlist.js`](assets/js/waitlist.js). Point the form at your email provider or endpoint before launch; field names are `waitlist[email]`, `waitlist[role]`, and `waitlist[notes]`. -## Rename this template +### Journal -1. Commit or branch (no undo). -2. From the repo root: `mix project.rename your_otp_app` (snake_case). See [`lib/mix/tasks/project.rename.ex`](lib/mix/tasks/project.rename.ex). +Posts are Markdown files in [`_posts/`](_posts/) with `layout: Soonex.PostLayout`. Front matter supports `title`, `date`, `permalink`, `description`, `image`, `image_alt`, `tags`, and `sitemap`. The post [Writing in the journal](_posts/2026-09-08-writing-in-the-journal.md) shows every supported Markdown feature. In development only, draft posts in `_drafts/` and work-in-progress pages in `_wip/` are rendered too, along with future-dated posts. + +### Accessibility and cookies + +The accessibility panel is configured in [`lib/soonex/accessibility.ex`](lib/soonex/accessibility.ex), and cookie consent in [`lib/soonex/cookie_consent.ex`](lib/soonex/cookie_consent.ex). Update [`lib/pages/privacy_page.ex`](lib/pages/privacy_page.ex) for your jurisdiction. + +## Deploying + +1. Set `SOONEX_PUBLIC_URL` to your production origin. Subpaths such as `https://example.github.io/my-launch` are supported; asset paths are prefixed automatically. Without it, builds use `https://corex-ui.github.io/soonex`. +2. Run `MIX_ENV=prod mix build`. If you changed permalinks, delete `_site/` first. +3. Publish `_site/` to any static host. [`lib/pages/not_found_page.ex`](lib/pages/not_found_page.ex) generates `404.html`. + +For GitHub Pages, [`.github/workflows/deploy.yml`](.github/workflows/deploy.yml) deploys on pushes to `main` after [CI](.github/workflows/ci.yml) passes. Set **Settings > Pages > Source** to **GitHub Actions**. + +## Renaming the project + +1. Commit your work first; the rename cannot be undone automatically. +2. Run `mix project.rename your_app` (snake_case). See [`lib/mix/tasks/project.rename.ex`](lib/mix/tasks/project.rename.ex). 3. Run `mix format` and `mix compile`. -4. **`_posts/*.md`:** only the YAML `layout:` line is rewritten; post bodies stay as-is. -## Corex assets and JS +Only the `layout:` line in `_posts/*.md` is rewritten; post bodies are left as they are. + +## Troubleshooting + +**Port 4999 is already in use.** `mix server` and `mix test` both use port 4999. Find and stop the other process, then retry: + +```shell +ss -ltnp 'sport = :4999' # Linux +lsof -nP -iTCP:4999 -sTCP:LISTEN # macOS +``` + +**Live reload does not work on Linux.** Install `inotify-tools` and restart the dev server. + +**Wallaby cannot start a session.** Chrome and `chromedriver` must match versions. You can point at specific binaries with `WALLABY_CHROME_BINARY` and `WALLABY_CHROMEDRIVER_PATH`. -- `assets/js/site.js` imports `corex/*`; Esbuild resolves via **`NODE_PATH`** including `deps` ([`config/config.exs`](config/config.exs)). -- Run **`mix corex.design.build`** after upgrading Corex / changing `config :corex_design`. -- Generated CSS lives under `assets/corex/` (gitignored). -- Client UI: [`assets/js/theme.js`](assets/js/theme.js), [`assets/js/mode.js`](assets/js/mode.js); landing motion under [`assets/js/landing*.js`](assets/js). +**Styles look stale after a Corex upgrade.** Run `mix corex.design.build`; generated CSS lives in `assets/corex/` (gitignored). -## Production and hosting +## Image credits -- GitHub Actions: [`.github/workflows/deploy.yml`](.github/workflows/deploy.yml) publishes Pages **only after CI succeeds** on a **push to `main`**. In the repo, set Pages **Source** to **GitHub Actions**. -- Clear `_site/` when permalinks change (`rm -rf _site`) so stale paths are not published. -- **404:** [`lib/pages/not_found_page.ex`](lib/pages/not_found_page.ex) emits `_site/404.html` for static hosts. `mix tableau.server` 404 behavior is still Tableau’s default. +Photography is from [Unsplash](https://unsplash.com) under the [Unsplash License](https://unsplash.com/license). -## Tableau data, tags, and static extras +| File | Photographer | +| --- | --- | +| `photos/hero.jpg` | [Andrew Kliatskyi](https://unsplash.com/@kirp) | +| `photos/texture.jpg` | [Adrien Olichon](https://unsplash.com/@adrienolichon) | +| `photos/ribbons.jpg` | [Milad Fakurian](https://unsplash.com/@fakurian) | +| `photos/closing.jpg` | [Pawel Czerwinski](https://unsplash.com/@pawel_czerwinski) | +| `covers/waves.jpg` | [Milad Fakurian](https://unsplash.com/@fakurian) | +| `covers/spectrum.jpg` | [Milad Fakurian](https://unsplash.com/@fakurian) | +| `covers/haze.jpg` | [MagicPattern](https://unsplash.com/@magicpattern) | +| `covers/dusk.jpg` | [Martin Martz](https://unsplash.com/@martz90) | +| `covers/orbit.jpg` | [Martin Martz](https://unsplash.com/@martz90) | +| `covers/current.jpg` | [Martin Martz](https://unsplash.com/@martz90) | -- **`@data`:** YAML (etc.) under [`_data/`](_data/) merged into the page assigns (e.g. `@data["facts"]["headline"]`). -- **Tags:** enabled in [`config/config.exs`](config/config.exs); index at **`/tags`** via [`lib/pages/tags_index_page.ex`](lib/pages/tags_index_page.ex). -- **Static passthrough:** [`static/`](static/) via `include_dir` on `config :tableau, :config`. -- **Sitemap:** optional `sitemap:` on `use Tableau.Page` or in post front matter. +Tool logos in `extra/images/tech/` come from [Simple Icons](https://simpleicons.org) (CC0), except `tableau.jpg`, which is the [Tableau project](https://github.com/elixir-tools/tableau) logo. -## Drafts +## License -Drafts and WIP live in `_drafts` and `_wip` (see `config/dev.exs` / `config/prod.exs`). +This repository does not include a license file yet. Add one before you redistribute a fork. diff --git a/_posts/.keep b/_posts/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/_posts/2026-05-04-introducing-soonex.md b/_posts/2026-05-04-introducing-soonex.md new file mode 100644 index 0000000..1c56c4d --- /dev/null +++ b/_posts/2026-05-04-introducing-soonex.md @@ -0,0 +1,42 @@ +--- +layout: Soonex.PostLayout +title: Introducing Soonex +date: 2026-05-04 09:00:00 +0000 +permalink: /blog/introducing-soonex/ +description: A launch page kit for Phoenix teams. Static HTML, accessible Corex components, and a waitlist that works on day one. +image: /images/covers/waves.jpg +image_alt: Flowing purple and pink gradient waves +tags: + - Launch + - Product +sitemap: + priority: 0.9 + changefreq: monthly +--- + +Every product starts with a page that says "soon." It is usually the first thing people see, and it is often the least considered: a stock template, a form that goes nowhere, and contrast nobody checked. + +Soonex is our answer. It is a launch page kit built on [Tableau](https://github.com/elixir-tools/tableau) and [Corex](https://hexdocs.pm/corex), so Phoenix teams can ship a polished, accessible coming-soon site with the tools they already use. + +## What you get + +- **A complete landing page**: hero with countdown, live component preview, capabilities, workflow, FAQ, and a closing waitlist band. +- **Four themes**: neo, uno, duo, and leo, each with light and dark modes generated from a few color seeds. +- **A working waitlist**: validated email field, role select, opt-in switch, and toast feedback, ready to connect to your provider. +- **A journal**: Markdown posts with tags, RSS, and pagination, so early followers can watch you build. +- **Accessibility controls**: text size, contrast, motion, focus ring, and link underline, saved on the visitor's device. + +## Why static + +A launch page should load instantly and cost nothing to host. Tableau renders plain HTML into `_site/`, which you can publish to GitHub Pages, Netlify, Cloudflare Pages, or any CDN. Corex hooks hydrate the interactive parts in the browser, so tabs, accordions, and selects still behave like real widgets. + +## Getting started + +```bash +git clone https://github.com/corex-ui/soonex.git +cd soonex +mix setup +mix server +``` + +Then open `http://localhost:4999`. From there, [One config block, four themes](/blog/one-config-four-themes/) walks through branding, and [Shipping to GitHub Pages](/blog/shipping-to-github-pages/) covers deployment. diff --git a/_posts/2026-05-08-docs.md b/_posts/2026-05-08-docs.md deleted file mode 100644 index e4e09e8..0000000 --- a/_posts/2026-05-08-docs.md +++ /dev/null @@ -1,180 +0,0 @@ ---- -layout: Soonex.PostLayout -title: Lorem ipsum dolor sit amet -date: 2026-05-08 12:00:00 +0000 -permalink: /blog/using-the-soonex-template/ -description: Consectetur adipiscing elit, sed do eiusmod tempor—Markdown gallery with tables, images, alerts, footnotes, and code. -tags: - - Elixir - - Tableau - - Corex -sitemap: - priority: 0.9 - changefreq: monthly ---- - -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea **commodo consequat**. - -## Primis in faucibus - -### Vestibulum ante ipsum - -#### Aenean lacinia bibendum - -Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat *cupidatat* non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - -Use **bold**, *italic*, ***combined***, `inline code`, and ~~strikethrough~~ in the same paragraph when you need emphasis variety. - -### Links and autolink - -Markdown link: [Hexdocs Corex](https://hexdocs.pm/corex). Bare URL with autolink: https://hexdocs.pm/mdex/MDEx.html. - ---- - -## Lists and tasks - -### Unordered - -- Lorem ipsum dolor sit amet - - Consectetur adipiscing elit - - Sed do eiusmod tempor -- Ut labore et dolore magna aliqua -- Quis nostrud exercitation ullamco - -### Ordered - -1. Incididunt ut labore et dolore -2. Magna aliqua ut enim ad minim -3. Veniam quis nostrud exercitation - -### Task list - -- [x] Lorem ipsum checked item -- [ ] Dolor sit amet pending -- [ ] Consectetur adipiscing unfinished - ---- - -## Table - -| Lorem | Ipsum | Dolor | -| --- | --- | --- | -| Sit amet | Consectetur | Adipiscing | -| Elit sed | Do eiusmod | Tempor | -| Incididunt | Ut labore | Et dolore | -| Magna aliqua | Ut enim | Ad minim | - -| Feature | Status | Note | -| --- | :---: | --- | -| Tables | Ready | `extension: [table: true]` | -| Task lists | Ready | GitHub-style checkboxes | -| Alerts | Ready | NOTE / TIP / WARNING | -| Footnotes | Ready | Collected at document end | - ---- - -## Blockquote - -> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam. - -> Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat. - ---- - -## Images - -Default Open Graph asset: - -![Lorem Open Graph placeholder](/images/og.svg) - -Tech marks used elsewhere on the site: - -![Elixir](/images/tech/elixir.svg) - -![Phoenix](/images/tech/phoenix.svg) - -![Tailwind](/images/tech/tailwind.svg) - -![Hex](/images/tech/hex.svg) - ---- - -## GitHub-style alerts - -> [!NOTE] -> Lorem ipsum dolor sit amet—neutral context readers should notice without panic. - -> [!TIP] -> Sed do eiusmod tempor: shortcuts, conventions, and quality-of-life details belong here. - -> [!IMPORTANT] -> Ut enim ad minim veniam—call out security, data loss, or breaking changes. - -> [!WARNING] -> Quis nostrud exercitation may surprise operators if ordering differs by environment. - -> [!CAUTION] -> Duis aute irure dolor: outcome depends on timing, locale, or host configuration. - ---- - -## Footnotes - -Lorem ipsum dolor sit amet, consectetur adipiscing elit[^alpha]. Sed do eiusmod tempor incididunt ut labore[^beta]. - -[^alpha]: Lorem footnote: first reference collected at the end of the document. -[^beta]: Ipsum footnote: second definition for MDEx `footnotes: true`. - ---- - -## Code - -Fenced Elixir: - -```elixir -defmodule Lorem.Ipsum do - @moduledoc "Dolor sit amet consectetur." - - def dolor(sit) when is_binary(sit) do - sit - |> String.trim() - |> String.upcase() - end - - def dolor(_), do: :error -end -``` - -Fenced bash: - -```bash -mix deps.get -mix setup -mix assets.build -mix tableau.build -``` - -Fenced JavaScript: - -```javascript -export function lorem(ipsum = []) { - return ipsum.map((item) => String(item).trim()).filter(Boolean) -} -``` - -Inline: run `mix tableau.server` then open `http://localhost:4999`. - ---- - -## Horizontal rule and HTML - -Trusted raw HTML with `render: [unsafe: true]`: - -
-Lorem ipsum details / summary -

Consectetur adipiscing elit. Press Ctrl+K style shortcuts when documenting keyboards.

-
- ---- - -> **Tip:** After `mix tableau.build`, open the permalink under `_site/` to confirm tables, alerts, images, and footnotes shipped as expected. diff --git a/_posts/2026-06-01-shipping.md b/_posts/2026-06-01-shipping.md deleted file mode 100644 index 6a2a7e1..0000000 --- a/_posts/2026-06-01-shipping.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -layout: Soonex.PostLayout -title: Sed do eiusmod tempor incididunt -date: 2026-06-01 12:00:00 +0000 -permalink: /blog/shipping-tableau-corex/ -description: Ut labore et dolore magna aliqua—another Lorem post with lists, a table, image, and alert. -tags: - - Tableau - - Deploy -sitemap: - priority: 0.7 - changefreq: monthly ---- - -Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum **dolore**. - -## Magna aliqua - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - -### Checklist - -- [x] Lorem build pipeline -- [x] Ipsum asset alias -- [ ] Dolor deploy gate - -### Quick table - -| Step | Command | Outcome | -| --- | --- | --- | -| Design | `mix corex.design.build` | Tokens + CSS | -| Assets | `mix assets.build` | Tailwind + esbuild | -| Site | `mix tableau.build` | `_site/` tree | - -![Soonex Open Graph](/images/og.svg) - -> [!TIP] -> Lorem ipsum: keep CI green before Pages deploys so broken pushes never reach the public CDN. - -```elixir -defmodule Shipping.Demo do - def ready?, do: true -end -``` - -Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus[^ship]. - -[^ship]: Footnote lorem: ship notes stay short so the kitchen-sink post can carry the full gallery. diff --git a/_posts/2026-06-02-one-config-four-themes.md b/_posts/2026-06-02-one-config-four-themes.md new file mode 100644 index 0000000..20f0300 --- /dev/null +++ b/_posts/2026-06-02-one-config-four-themes.md @@ -0,0 +1,53 @@ +--- +layout: Soonex.PostLayout +title: One config block, four themes +date: 2026-06-02 09:00:00 +0000 +permalink: /blog/one-config-four-themes/ +description: How Soonex turns a handful of color seeds into four complete themes with readable light and dark modes. +image: /images/covers/spectrum.jpg +image_alt: Abstract blue, orange, and yellow fluid shapes +tags: + - Design + - Themes +sitemap: + priority: 0.8 + changefreq: monthly +--- + +Soonex has no theme stylesheets to maintain. The whole look comes from one block in `config/config.exs`, which Corex Design turns into tokens and component CSS. + +## Seeds, not palettes + +Each theme declares a few seed colors. Corex generates the full scale from them, including the ink, surface, border, and accent pairs, and checks that text stays readable against its background. + +```elixir +config :corex_design, + default_theme: :neo, + themes: %{ + neo: %{seeds: %{accent: "#7c5cff"}}, + uno: %{seeds: %{accent: "#0f766e"}} + } +``` + +After editing, rebuild the generated CSS: + +```bash +mix corex.design.build +``` + +## What you can change + +| Key | What it controls | +| --- | --- | +| `seeds` | Base colors the palette is generated from | +| `colors.light` / `colors.dark` | Per-mode overrides for specific tokens | +| `dimensions.radius` | Corner rounding across components | +| `dimensions.font` | Font family stacks | +| `typography` | Type scale and weights | + +## Try it live + +Open **Template Options** in the bottom corner of any page to switch between neo, uno, duo, and leo, or flip between light and dark mode. Your choice is saved locally, so it sticks as you move around the site. + +> [!TIP] +> Keep the contrast targets that Corex generates. If a color feels too strong, adjust the seed instead of overriding individual text tokens. diff --git a/_posts/2026-06-30-static-html-live-components.md b/_posts/2026-06-30-static-html-live-components.md new file mode 100644 index 0000000..4458507 --- /dev/null +++ b/_posts/2026-06-30-static-html-live-components.md @@ -0,0 +1,43 @@ +--- +layout: Soonex.PostLayout +title: Static HTML with live components +date: 2026-06-30 09:00:00 +0000 +permalink: /blog/static-html-live-components/ +description: Tableau renders the page, Corex hooks bring the widgets to life, and no Phoenix server runs in production. +image: /images/covers/current.jpg +image_alt: Dark teal abstract curves with a warm orange glow +tags: + - Engineering +sitemap: + priority: 0.8 + changefreq: monthly +--- + +Soonex pages are plain HTML files. Yet the tabs switch, the accordion expands, the select opens, and the countdown ticks. Here is how that works. + +## Rendering with Tableau + +Each section of the home page is a HEEx function component under `lib/pages/home/`. Tableau calls them at build time and writes the result to `_site/`. Blog posts are Markdown files in `_posts/` rendered through the same layout. + +## Hydrating with Corex + +Corex components render markup with `phx-hook` attributes. In the browser, `assets/js/site.js` registers the matching hooks, and each one attaches a [Zag](https://zagjs.com) state machine that handles keyboard navigation, focus, and ARIA state. + +```javascript +import { Marquee } from "corex/marquee" +``` + +Most hooks load lazily, so the first paint only pays for what is on screen. The marquee loads eagerly because it sits near the top of the page. + +## No Node toolchain + +Tailwind and esbuild run as Mix tasks with pinned binaries. There is no `package.json`, and CI never runs `npm install`. + +```bash +mix assets.build # Corex design CSS, Tailwind, esbuild +mix build # full production build into _site/ +``` + +## Checked on every run + +`mix test` builds the site, starts a local server, and runs an axe accessibility audit on the home page in headless Chrome. If a change breaks contrast or semantics, the suite fails before anything ships. diff --git a/_posts/2026-07-21-accessibility-on-every-device.md b/_posts/2026-07-21-accessibility-on-every-device.md new file mode 100644 index 0000000..2facc9e --- /dev/null +++ b/_posts/2026-07-21-accessibility-on-every-device.md @@ -0,0 +1,43 @@ +--- +layout: Soonex.PostLayout +title: Accessibility on every device +date: 2026-07-21 09:00:00 +0000 +permalink: /blog/accessibility-on-every-device/ +description: Text size, contrast, motion, focus, and link underline controls that visitors set once and keep. +image: /images/covers/haze.jpg +image_alt: Soft blurred purple and blue gradient +tags: + - Accessibility +sitemap: + priority: 0.8 + changefreq: monthly +--- + +People read the web in different ways. Soonex ships the Corex accessibility panel so visitors can adjust the page to suit them, without an account and without a backend. + +## The controls + +Open the person icon in the bottom corner of any page: + +- **Text size** scales the whole interface, not just body copy. +- **Contrast** switches to a higher-contrast palette generated from the same theme. +- **Motion** reduces animation, including smooth scrolling and card hover effects. +- **Focus** makes the keyboard focus ring more prominent. +- **Link underline** underlines every link for easier scanning. + +Preferences are stored in `localStorage` under `phx:a11y` and applied by a small head script before the page paints, so there is no flash of the default style. + +## Respecting system settings + +Soonex also follows `prefers-reduced-motion` and the visitor's light or dark preference until they pick a mode themselves. + +## Configuring it + +The panel lives in `lib/soonex/accessibility.ex`. After changing accessibility options in your Corex config, rebuild the design assets: + +```bash +mix corex.design.build +``` + +> [!NOTE] +> Accessibility settings count as necessary preferences, so they are saved even when a visitor declines optional cookies. diff --git a/_posts/2026-08-18-shipping-to-github-pages.md b/_posts/2026-08-18-shipping-to-github-pages.md new file mode 100644 index 0000000..3adf09d --- /dev/null +++ b/_posts/2026-08-18-shipping-to-github-pages.md @@ -0,0 +1,51 @@ +--- +layout: Soonex.PostLayout +title: Shipping to GitHub Pages +date: 2026-08-18 09:00:00 +0000 +permalink: /blog/shipping-to-github-pages/ +description: Set your public URL, build into _site/, and let CI gate every deploy. +image: /images/covers/orbit.jpg +image_alt: Overlapping teal circles with a soft orange edge light +tags: + - Launch + - Engineering +sitemap: + priority: 0.8 + changefreq: monthly +--- + +Soonex builds to a folder of static files, so deploying is mostly about telling it where it will live. + +## 1. Set your public URL + +Production builds read `SOONEX_PUBLIC_URL` to generate canonical links, Open Graph tags, and asset paths. If your site lives under a subpath, such as a GitHub project page, the prefix is applied automatically. + +```bash +export SOONEX_PUBLIC_URL="https://example.github.io/my-launch" +``` + +Without it, builds fall back to the demo URL `https://corex-ui.github.io/soonex`. + +## 2. Build + +```bash +MIX_ENV=prod mix build +``` + +This compiles the project, builds Corex design CSS, renders every page with Tableau, and minifies CSS and JavaScript into `_site/`. + +> [!IMPORTANT] +> If you change permalinks, delete `_site/` before building so stale pages are not published. + +## 3. Deploy with GitHub Actions + +The repo includes two workflows: + +1. **CI** (`.github/workflows/ci.yml`) runs the test suite, including the axe audit, on every push and pull request. +2. **Deploy** (`.github/workflows/deploy.yml`) publishes `_site/` to GitHub Pages, but only after CI succeeds on `main`. + +In your repository settings, set **Pages > Source** to **GitHub Actions**. That is the whole setup. + +## Other hosts + +Any static host works. Point it at `_site/` after running the build, and make sure `404.html` is served for missing pages. diff --git a/_posts/2026-09-08-writing-in-the-journal.md b/_posts/2026-09-08-writing-in-the-journal.md new file mode 100644 index 0000000..ccaa31a --- /dev/null +++ b/_posts/2026-09-08-writing-in-the-journal.md @@ -0,0 +1,103 @@ +--- +layout: Soonex.PostLayout +title: Writing in the journal +date: 2026-09-08 09:00:00 +0000 +permalink: /blog/writing-in-the-journal/ +description: A style guide for journal posts, showing every Markdown feature the Soonex pipeline renders. +image: /images/covers/dusk.jpg +image_alt: Purple and coral abstract hills under a glowing sphere +tags: + - Journal + - Guides +sitemap: + priority: 0.6 + changefreq: monthly +--- + +Journal posts are Markdown files in `_posts/` with `layout: Soonex.PostLayout` in the front matter. Generate a new one with: + +```bash +mix soonex.gen.post My first update +``` + +This post doubles as a reference for everything the Markdown pipeline supports. + +## Front matter + +```yaml +--- +layout: Soonex.PostLayout +title: My first update +date: 2026-09-08 09:00:00 +0000 +permalink: /blog/my-first-update/ +description: One sentence for cards and search results. +image: /images/covers/dusk.jpg +image_alt: Describe the image for screen readers +tags: + - Launch +--- +``` + +## Text + +Use **bold**, *italic*, ***both***, `inline code`, and ~~strikethrough~~. Links work as [Markdown links](https://hexdocs.pm/corex) and bare URLs autolink: https://hexdocs.pm/mdex. + +## Lists + +1. Write the post +2. Preview it with `mix server` +3. Commit and push + +- [x] Cover image chosen +- [x] Description under 160 characters +- [ ] Shared with the waitlist + +## Quotes and alerts + +> Ship the page you would want to land on. + +> [!NOTE] +> Notes call out useful context. + +> [!TIP] +> Tips suggest a better way to do something. + +> [!WARNING] +> Warnings flag something that could go wrong. + +## Tables + +| Command | Purpose | +| --- | --- | +| `mix server` | Dev server with live reload | +| `mix assets.build` | Rebuild CSS and JavaScript | +| `MIX_ENV=prod mix build` | Production build into `_site/` | + +## Code + +```elixir +defmodule Soonex.Launch do + @target ~U[2026-12-01 00:00:00Z] + + def countdown_ms do + max(DateTime.diff(@target, DateTime.utc_now(), :millisecond), 0) + end +end +``` + +## Images + +![Elixir logo](/images/tech/elixir.svg) + +## Footnotes + +The launch date lives in one module[^launch]. + +[^launch]: `Soonex.Launch` feeds both the hero badge and the countdown timer. + +## Raw HTML + +
+Keyboard shortcuts +

Use Tab to move between controls and Enter to activate them.

+
diff --git a/assets/css/blog.css b/assets/css/blog.css deleted file mode 100644 index c1c8ca6..0000000 --- a/assets/css/blog.css +++ /dev/null @@ -1,191 +0,0 @@ -@layer components { - .blog__nav { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--spacing-space-sm); - margin: 0; - margin-block-start: var(--spacing-space); - min-height: 1.5rem; - } - - .blog__hero { - padding-block: clamp(2.5rem, 5vw, 4rem) clamp(2rem, 4vw, 3rem); - } - - .blog__hero--post { - border-block-end: 1px solid var(--color-border); - } - - .blog__head { - position: relative; - display: flex; - flex-direction: column; - gap: var(--spacing-space); - max-width: 40rem; - } - - .blog__eyebrow { - display: inline-flex; - align-items: center; - margin: 0; - font-size: var(--text-xs); - line-height: var(--text-xs--line-height); - font-weight: var(--font-weight-semibold); - letter-spacing: 0.18em; - text-transform: uppercase; - color: var(--color-brand-text); - } - - .blog__display { - margin: 0; - font-size: clamp(var(--text-3xl), 4vw + 0.5rem, var(--text-5xl)); - line-height: 1.08; - letter-spacing: -0.025em; - font-weight: var(--font-weight-bold); - color: var(--color-ink); - text-wrap: balance; - } - - .blog__display__accent { - color: var(--color-brand-text); - } - - .blog__lede { - margin: 0; - font-size: var(--text-lg); - line-height: var(--text-lg--line-height); - color: var(--color-ink-muted); - text-wrap: pretty; - max-width: 36rem; - } - - .blog__meta { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--spacing-space-sm); - margin: 0; - font-size: var(--text-sm); - line-height: var(--text-sm--line-height); - color: var(--color-ink-muted); - } - - .blog__listing { - padding-block: clamp(2rem, 4vw, 3.5rem) clamp(3rem, 6vw, 5rem); - background-color: color-mix(in oklab, var(--color-surface) 55%, var(--color-root)); - border-block: 1px solid var(--color-border); - } - - .blog__grid { - display: grid; - grid-template-columns: 1fr; - gap: var(--spacing-space-xl); - width: 100%; - } - - @media (min-width: 48rem) { - .blog__grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } - - .blog__card { - position: relative; - display: flex; - flex-direction: column; - gap: var(--spacing-space); - min-height: 0; - height: 100%; - padding: var(--spacing-size-lg); - border-radius: var(--radius-xl); - border: 1px solid var(--color-border); - background: var(--color-surface); - box-shadow: var(--shadow-ui); - text-decoration: none; - color: inherit; - transition: - box-shadow 0.2s ease, - transform 0.2s ease; - } - - .blog__card:hover { - box-shadow: var(--shadow-md); - transform: translateY(-2px); - } - - .blog__card--static { - height: auto; - width: fit-content; - max-width: 100%; - margin-inline: auto; - margin-block-start: 0; - border-start-start-radius: 0; - border-start-end-radius: 0; - border-block-start-width: 0; - } - - .blog__card--static:hover { - box-shadow: var(--shadow-ui); - transform: none; - } - - .blog__card__top { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--spacing-space); - } - - .blog__card__date { - margin: 0; - font-size: var(--text-xs); - line-height: var(--text-xs--line-height); - font-weight: var(--font-weight-medium); - letter-spacing: 0.06em; - text-transform: uppercase; - color: var(--color-ink-muted); - } - - .blog__card__arrow { - flex: none; - width: 1.25rem; - height: 1.25rem; - color: var(--color-brand-text); - opacity: 0; - transform: translateX(-0.25rem); - transition: - opacity 0.2s ease, - transform 0.2s ease; - } - - .blog__card:hover .blog__card__arrow, - .blog__card:focus-within .blog__card__arrow { - opacity: 1; - transform: translateX(0); - } - - .blog__card__title { - margin: 0; - font-size: var(--text-xl); - line-height: var(--text-xl--line-height); - font-weight: var(--font-weight-semibold); - letter-spacing: -0.02em; - color: var(--color-ink); - text-wrap: pretty; - } - - .blog__card__excerpt { - margin: 0; - flex: 1; - font-size: var(--text-base); - line-height: var(--text-base--line-height); - color: var(--color-ink-muted); - text-wrap: pretty; - } - - .blog__card__tags { - margin-block-start: auto; - padding-block-start: var(--spacing-space-sm); - } -} diff --git a/assets/css/chrome.css b/assets/css/chrome.css new file mode 100644 index 0000000..899e850 --- /dev/null +++ b/assets/css/chrome.css @@ -0,0 +1,63 @@ +/* Minimal host helpers. Prefer Corex ui-* modifiers and design tokens in HEEx. */ + +html { + scroll-behavior: smooth; +} + +html[data-motion="reduce"] { + scroll-behavior: auto; +} + +.soonex-band-inverse .soonex-on-inverse { + color: var(--color-accent-contrast); +} + +.soonex-band-inverse .soonex-muted-on-inverse { + color: color-mix(in oklab, var(--color-accent-contrast) 72%, var(--color-accent)); +} + +.soonex-band-inverse .soonex-column-title { + color: var(--color-accent-contrast); +} + +.soonex-band-inverse .soonex-column-body { + color: color-mix(in oklab, var(--color-accent-contrast) 72%, var(--color-accent)); +} + +.soonex-columns > * + * { + border-block-start: 1px solid var(--color-border); + padding-block-start: 3rem; +} + +@media (min-width: 1024px) { + .soonex-columns > * + * { + border-block-start: none; + border-inline-start: 1px solid var(--color-border); + padding-block-start: 0; + padding-inline-start: 3rem; + } +} + +.soonex-card-motion img { + transition: transform 600ms var(--soonex-ease); +} + +.soonex-card-motion:hover img { + transform: scale(1.03); +} + +html[data-motion="reduce"] .soonex-card-motion img { + transition: none; + transform: none; +} + +@media (prefers-reduced-motion: reduce) { + .soonex-card-motion img { + transition: none; + transform: none; + } +} + +.tabs.tabs--wide { + max-width: none; +} diff --git a/assets/css/fonts.css b/assets/css/fonts.css new file mode 100644 index 0000000..286daa4 --- /dev/null +++ b/assets/css/fonts.css @@ -0,0 +1,101 @@ +/* Self-hosted latin woff2. Family names match Corex 0.2 theme stacks. SIL OFL. */ + +@font-face { + font-family: Outfit; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url("../fonts/outfit-latin-wght-normal.woff2") format("woff2-variations"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: Manrope; + font-style: normal; + font-display: swap; + font-weight: 200 800; + src: url("../fonts/manrope-latin-wght-normal.woff2") format("woff2-variations"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "DM Sans"; + font-style: normal; + font-display: swap; + font-weight: 100 1000; + src: url("../fonts/dm-sans-latin-wght-normal.woff2") format("woff2-variations"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: Sora; + font-style: normal; + font-display: swap; + font-weight: 100 800; + src: url("../fonts/sora-latin-wght-normal.woff2") format("woff2-variations"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "JetBrains Mono"; + font-style: normal; + font-display: swap; + font-weight: 100 800; + src: url("../fonts/jetbrains-mono-latin-wght-normal.woff2") format("woff2-variations"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "Work Sans"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url("../fonts/work-sans-latin-wght-normal.woff2") format("woff2-variations"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "Playfair Display"; + font-style: normal; + font-display: swap; + font-weight: 400 900; + src: url("../fonts/playfair-display-latin-wght-normal.woff2") format("woff2-variations"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "IBM Plex Sans"; + font-style: normal; + font-display: swap; + font-weight: 100 700; + src: url("../fonts/ibm-plex-sans-latin-wght-normal.woff2") format("woff2-variations"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "IBM Plex Mono"; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url("../fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "IBM Plex Mono"; + font-style: normal; + font-display: swap; + font-weight: 500; + src: url("../fonts/ibm-plex-mono-latin-500-normal.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/assets/css/home-numbers.css b/assets/css/home-numbers.css deleted file mode 100644 index f3c398f..0000000 --- a/assets/css/home-numbers.css +++ /dev/null @@ -1,107 +0,0 @@ -@layer components { - - .home__section { - width: 100%; - box-sizing: border-box; - padding-block: clamp(3rem, 7vw, 6rem); - padding-inline: max( - clamp(1rem, 4vw, 2rem), - env(safe-area-inset-left) - ); - } - - .home__section__inner { - width: 100%; - max-width: 80rem; - margin-inline: auto; - } - - .home__section--alt { - background-color: color-mix(in oklab, var(--color-surface) 55%, var(--color-root)); - border-block: 1px solid var(--color-border); - } - - .home__numbers { - display: grid; - grid-template-columns: 1fr; - gap: 0; - border-block: 1px solid var(--color-border); - } - - @media (min-width: 40rem) { - .home__numbers { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } - - @media (min-width: 64rem) { - .home__numbers { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } - } - - .home__numbers__cell { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - gap: var(--spacing-size-sm); - padding: clamp(2.5rem, 5vw, 4rem) clamp(1.5rem, 3.5vw, 2.5rem); - border-block-start: 1px solid var(--color-border); - } - - @media (min-width: 40rem) { - .home__numbers__cell { - border-block-start: 0; - border-inline-start: 1px solid var(--color-border); - } - - .home__numbers__cell:nth-child(2n + 1) { - border-inline-start: 0; - } - } - - @media (min-width: 64rem) { - .home__numbers__cell { - align-items: flex-start; - text-align: start; - } - - .home__numbers__cell:nth-child(2n + 1) { - border-inline-start: 1px solid var(--color-border); - } - - .home__numbers__cell:first-child { - border-inline-start: 0; - } - } - - .home__numbers__value { - font-size: clamp(var(--text-5xl), 6vw, var(--text-7xl)); - line-height: 1; - letter-spacing: -0.04em; - font-weight: var(--font-weight-bold); - color: var(--color-ink); - } - - .home__numbers__value__suffix { - color: var(--color-brand-text); - } - - .home__numbers__label { - font-size: var(--text-xs); - line-height: var(--text-xs--line-height); - text-transform: uppercase; - letter-spacing: 0.18em; - font-weight: var(--font-weight-semibold); - color: var(--color-ink-muted); - } - - .home__numbers__hint { - margin: 0; - margin-block-start: var(--spacing-space-sm); - font-size: var(--text-sm); - line-height: var(--text-sm--line-height); - color: var(--color-ink-muted); - } -} diff --git a/assets/css/home.css b/assets/css/home.css deleted file mode 100644 index aa609bc..0000000 --- a/assets/css/home.css +++ /dev/null @@ -1,140 +0,0 @@ -/* Homepage section stacks need more air than space-* tokens alone */ -#home > section, -#home > header { - padding-block: var(--spacing-size-xl); -} - -#home .home-stack { - gap: var(--spacing-size-xl); -} - -#home .home-highlights-marquee .marquee [data-scope="marquee"][data-part="edge"] { - --edge-background: var(--color-root); -} - -/* Timer digits hardcode ink; map ui-success to success text for countdown color */ -.timer.ui-success [data-scope="timer"][data-part="item"], -.timer.ui-success [data-scope="timer"][data-part="separator"] { - color: var(--ctl-ink-text, var(--color-success-text)); -} - -.timer.ui-success [data-scope="timer"][data-part="item-label"] { - color: var(--color-ink-muted); - opacity: 1; -} - -/* Hero countdown: freestanding composition (no nested card) */ -.hero-countdown { - display: flex; - flex-direction: column; - gap: var(--spacing-size-md); -} - -.hero-countdown__head { - display: flex; - flex-direction: column; - gap: var(--spacing-space-xs); - text-align: center; -} - -@media (min-width: 1024px) { - .hero-countdown__head { - text-align: start; - } -} - -.hero-countdown__eyebrow { - margin: 0; - font-size: var(--text-sm); - font-weight: var(--font-weight-semibold); - letter-spacing: 0.2em; - text-transform: uppercase; - color: var(--color-brand-text); -} - -.hero-countdown__title { - margin: 0; - font-size: var(--text-xl); - line-height: var(--text-xl--line-height); - font-weight: var(--font-weight-semibold); - letter-spacing: -0.02em; - color: var(--color-ink); - text-wrap: balance; -} - -.hero-countdown__note { - margin: 0; - text-align: center; - font-size: var(--text-sm); - line-height: var(--text-sm--line-height); - color: var(--color-ink-muted); -} - -@media (min-width: 1024px) { - .hero-countdown__note { - text-align: start; - } -} - -.hero-countdown__timer [data-scope="timer"][data-part="area"] { - display: flex; - flex-wrap: nowrap; - align-items: stretch; - justify-content: center; - gap: 0; - width: 100%; - padding-inline: 0.5rem; -} - -.hero-countdown__timer [data-scope="timer"][data-part="separator"] { - display: none; -} - -.hero-countdown__timer [data-timer-segment] { - display: flex; - flex: 1 1 0; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--spacing-space-xs); - position: relative; - z-index: 1; - min-width: 0; - aspect-ratio: 1; - margin-inline: -0.4rem; - padding: var(--spacing-space-md); - border: 1px solid var(--color-border); - border-radius: var(--radius-full); - background: var(--color-surface); - box-shadow: 0 1px 2px color-mix(in oklab, var(--color-ink) 8%, transparent); -} - -.hero-countdown__timer [data-timer-segment]:nth-child(1) { - z-index: 4; -} - -.hero-countdown__timer [data-timer-segment]:nth-child(2) { - z-index: 3; -} - -.hero-countdown__timer [data-timer-segment]:nth-child(3) { - z-index: 2; -} - -.hero-countdown__timer [data-timer-segment]:nth-child(4) { - z-index: 1; -} - -.hero-countdown__timer [data-scope="timer"][data-part="item"] { - width: 100%; - min-width: 2.2ch; - color: var(--color-success-text); - font-weight: var(--font-weight-bold); - letter-spacing: -0.04em; -} - -.hero-countdown__timer [data-scope="timer"][data-part="item-label"] { - color: var(--color-ink-muted); - opacity: 1; - letter-spacing: 0.14em; -} diff --git a/assets/css/hosts.css b/assets/css/hosts.css new file mode 100644 index 0000000..98f5180 --- /dev/null +++ b/assets/css/hosts.css @@ -0,0 +1,85 @@ +/* Host polish after Corex. Tokens only. Prefer ui-* modifiers in HEEx. */ + +[data-theme] { + --soonex-ease: cubic-bezier(0.22, 1, 0.36, 1); +} + +.button, +.link.ui-nav { + transition-timing-function: var(--soonex-ease); +} + +.native-input.ui-width-full, +.clipboard.ui-width-full, +.radio-group.ui-width-full, +.tags-input.ui-width-full, +.number-input.ui-width-full, +.date-picker.ui-width-full, +.data-table.ui-width-full, +.select.ui-width-full, +.marquee.ui-width-full, +.collapsible.ui-width-full, +.accordion.ui-width-full, +.switch.ui-width-full { + max-width: none; + width: 100%; +} + +.clipboard.ui-width-fit, +.clipboard.ui-width-fit [data-scope="clipboard"][data-part="root"] { + width: fit-content; + max-width: 100%; +} + +.collapsible.ui-width-full [data-scope="collapsible"][data-part="trigger"] { + max-width: none; + width: 100%; + place-self: stretch; +} + +.accordion.ui-width-full [data-scope="accordion"][data-part="item-trigger"] { + max-width: none; + width: 100%; +} + +#soonex-waitlist-role [data-scope="select"][data-part="positioner"] { + position: absolute; +} + +#theme-switcher [data-scope="select"][data-part="positioner"], +#theme-switcher [data-scope="select"][data-part="content"], +[id="select:theme-switcher:positioner"], +[id="select:theme-switcher:content"] { + z-index: 80; +} + +#site-controls [data-scope="floating-panel"][data-part="body"], +#site-controls [data-scope="floating-panel"][data-part="content"] { + overflow: visible; +} + +.data-table.ui-width-fit { + width: fit-content; + max-width: 100%; +} + +.native-input [data-scope="native-input"][data-part="label"]:has(.sr-only), +.radio-group [data-scope="radio-group"][data-part="label"]:has(.sr-only) { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.accordion [data-scope="accordion"][data-part="item-trigger"] { + padding-block: var(--spacing-space); +} + +pre.code.code--wide { + max-width: none; +} diff --git a/assets/css/prose.css b/assets/css/prose.css index d12e214..97e11c1 100644 --- a/assets/css/prose.css +++ b/assets/css/prose.css @@ -88,9 +88,9 @@ .markdown.corex-callout { margin-block: var(--spacing-space-lg); padding: var(--spacing-space); - border-radius: var(--radius-md); border: 1px solid var(--color-border); background: var(--color-surface); + box-shadow: 4px 4px 0 0 var(--color-ink); max-width: 100%; box-sizing: border-box; } @@ -143,6 +143,7 @@ .typo.prose.markdown img { max-width: min(100%, 40rem); height: auto; + border-radius: var(--radius-xl); } .typo.prose.markdown img[src*="/images/tech/"] { diff --git a/assets/css/site.css b/assets/css/site.css index 6811dea..e566d2e 100644 --- a/assets/css/site.css +++ b/assets/css/site.css @@ -1,23 +1,13 @@ @import "tailwindcss"; @plugin "../vendor/heroicons"; -@import "../node_modules/lenis/dist/lenis.css"; +@import "./fonts.css"; @import "../corex/corex.css"; +@import "./hosts.css"; @source "../corex"; @source "../../lib"; -@import "./blog.css"; +@import "./chrome.css"; @import "./prose.css"; -@import "./home.css"; -@import "./home-numbers.css"; @custom-variant dark (&:where([data-mode=dark], [data-mode=dark] *)); - -html.lenis, -html.lenis body { - height: auto; -} - -.lenis:not(.lenis-autoToggle).lenis-stopped { - overflow: clip; -} diff --git a/assets/js/cookie-consent.js b/assets/js/cookie-consent.js new file mode 100644 index 0000000..0c84b24 --- /dev/null +++ b/assets/js/cookie-consent.js @@ -0,0 +1,169 @@ +/** Site cookie consent banner + preferences dialog */ + +const STORAGE_KEY = "soonex:cookie-consent" +const BANNER_ID = "cookie-consent-banner" +const DIALOG_ID = "cookie-consent-dialog" +const SWITCHES = { + analytics: "cookie-consent-analytics", + marketing: "cookie-consent-marketing", +} + +function readConsent() { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return null + const parsed = JSON.parse(raw) + if (!parsed || typeof parsed !== "object") return null + return { + necessary: true, + analytics: Boolean(parsed.analytics), + marketing: Boolean(parsed.marketing), + version: parsed.version || 1, + } + } catch (_e) { + return null + } +} + +function writeConsent(consent) { + const next = { + necessary: true, + analytics: Boolean(consent.analytics), + marketing: Boolean(consent.marketing), + version: 1, + updatedAt: new Date().toISOString(), + } + localStorage.setItem(STORAGE_KEY, JSON.stringify(next)) + document.documentElement.dataset.cookieConsent = "set" + document.documentElement.dataset.cookieAnalytics = next.analytics ? "1" : "0" + document.documentElement.dataset.cookieMarketing = next.marketing ? "1" : "0" + return next +} + +function setSwitch(id, checked) { + const el = document.getElementById(id) + if (!el) return + el.dispatchEvent( + new CustomEvent("corex:switch:set-checked", { + bubbles: false, + detail: {checked}, + }) + ) +} + +function isSwitchChecked(id) { + const el = document.getElementById(id) + if (!el) return false + const input = el.querySelector('input[type="checkbox"]') + if (input) return input.checked + return Boolean(el.querySelector('[data-state="checked"]')) +} + +function syncSwitches(consent) { + const apply = () => { + setSwitch(SWITCHES.analytics, Boolean(consent?.analytics)) + setSwitch(SWITCHES.marketing, Boolean(consent?.marketing)) + } + apply() + window.setTimeout(apply, 50) + window.setTimeout(apply, 250) +} + +let revealTimer = 0 + +function showBanner() { + const banner = document.getElementById(BANNER_ID) + if (!banner) return + document.documentElement.dataset.cookieConsent = "pending" + window.clearTimeout(revealTimer) + const reveal = () => { + banner.hidden = false + } + // Defer until after first paint so the banner cannot become LCP. + window.requestAnimationFrame(() => { + revealTimer = window.setTimeout(reveal, 1200) + }) +} + +function hideBanner() { + const banner = document.getElementById(BANNER_ID) + if (!banner) return + window.clearTimeout(revealTimer) + document.documentElement.dataset.cookieConsent = "set" + banner.hidden = true +} + +function setDialogOpen(open) { + const dialog = document.getElementById(DIALOG_ID) + if (!dialog) return + dialog.dispatchEvent( + new CustomEvent("corex:dialog:set-open", { + bubbles: false, + detail: {id: DIALOG_ID, open: Boolean(open)}, + }) + ) +} + +function openPreferences() { + const consent = readConsent() || {analytics: false, marketing: false} + syncSwitches(consent) + setDialogOpen(true) +} + +function applyChoice(action) { + let consent + if (action === "accept") { + consent = writeConsent({analytics: true, marketing: true}) + } else if (action === "reject") { + consent = writeConsent({analytics: false, marketing: false}) + } else if (action === "save") { + consent = writeConsent({ + analytics: isSwitchChecked(SWITCHES.analytics), + marketing: isSwitchChecked(SWITCHES.marketing), + }) + } else { + return + } + + syncSwitches(consent) + hideBanner() + setDialogOpen(false) +} + +function initCookieConsent() { + const banner = document.getElementById(BANNER_ID) + if (!banner) return + + const existing = readConsent() + if (existing) { + writeConsent(existing) + syncSwitches(existing) + hideBanner() + } else { + syncSwitches({analytics: false, marketing: false}) + showBanner() + } + + document.addEventListener("click", (event) => { + const button = event.target.closest("[data-cookie-consent]") + if (!button) return + const action = button.getAttribute("data-cookie-consent") + if (action === "open") { + event.preventDefault() + openPreferences() + return + } + if (action === "reset") { + event.preventDefault() + syncSwitches({analytics: false, marketing: false}) + return + } + applyChoice(action) + }) +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initCookieConsent) +} else { + initCookieConsent() +} diff --git a/assets/js/landing-hero.js b/assets/js/landing-hero.js deleted file mode 100644 index 6fda1c2..0000000 --- a/assets/js/landing-hero.js +++ /dev/null @@ -1,30 +0,0 @@ -import { animate, stagger } from "motion" - -export function runLandingHero(root, reducedMotion, easeOut) { - if (reducedMotion) { - root.querySelectorAll("[data-hero-word]").forEach((el) => { - if (el instanceof HTMLElement) el.style.opacity = "1" - }) - return - } - - const heroWords = root.querySelectorAll("[data-hero] [data-hero-word]") - if (heroWords.length) { - animate( - heroWords, - { opacity: [0, 1], y: [24, 0], filter: ["blur(8px)", "blur(0px)"] }, - { duration: 0.85, delay: stagger(0.06, { start: 0.1 }), ease: easeOut }, - ) - } - - const heroSiblings = root.querySelectorAll( - "[data-hero] > :not(h1):not(:has([data-hero-word]))", - ) - if (heroSiblings.length) { - animate( - heroSiblings, - { opacity: [0, 1], y: [16, 0] }, - { duration: 0.7, delay: stagger(0.08, { start: 0.55 }), ease: easeOut }, - ) - } -} diff --git a/assets/js/landing-parallax.js b/assets/js/landing-parallax.js deleted file mode 100644 index 5bd702c..0000000 --- a/assets/js/landing-parallax.js +++ /dev/null @@ -1,19 +0,0 @@ -import { scroll } from "motion" - -export function runLandingParallax(root, reducedMotion) { - const stops = [] - if (reducedMotion) return stops - - const parallax = root.querySelectorAll("[data-parallax]") - parallax.forEach((el) => { - if (!(el instanceof HTMLElement)) return - const stop = scroll((_p, _info) => { - const r = el.getBoundingClientRect() - const mid = r.top + r.height * 0.5 - const offset = (window.innerHeight * 0.5 - mid) * 0.05 - el.style.transform = `translate3d(0, ${offset.toFixed(2)}px, 0)` - }) - stops.push(stop) - }) - return stops -} diff --git a/assets/js/landing-reveal.js b/assets/js/landing-reveal.js deleted file mode 100644 index cbc9965..0000000 --- a/assets/js/landing-reveal.js +++ /dev/null @@ -1,28 +0,0 @@ -import { animate, inView, stagger } from "motion" - -export function runLandingReveal(root, easeOut) { - const stops = [] - const reveals = root.querySelectorAll("[data-reveal]") - reveals.forEach((section) => { - if (!(section instanceof HTMLElement)) return - const stop = inView( - section, - () => { - const children = section.querySelectorAll("[data-stagger] > *") - const targets = children.length ? children : [section] - animate( - targets, - { opacity: [0, 1], y: [20, 0] }, - { - duration: 0.6, - delay: children.length ? stagger(0.07) : 0, - ease: easeOut, - }, - ) - }, - { margin: "-10% 0px -10% 0px", amount: 0.15 }, - ) - stops.push(stop) - }) - return stops -} diff --git a/assets/js/landing-scroll-chrome.js b/assets/js/landing-scroll-chrome.js deleted file mode 100644 index eccc4ab..0000000 --- a/assets/js/landing-scroll-chrome.js +++ /dev/null @@ -1,68 +0,0 @@ -const stickyRevealPx = 96 - -export function scrollProgress01() { - const lenis = globalThis.__landingLenis - if (lenis && typeof lenis.progress === "number") { - return Math.min(1, Math.max(0, lenis.progress)) - } - const doc = document.documentElement - const maxScroll = Math.max(1, doc.scrollHeight - window.innerHeight) - return Math.min(1, Math.max(0, window.scrollY / maxScroll)) -} - -export function bindLandingScrollChrome(stickyBar, heroBoundary, progressFill) { - const stickyEl = stickyBar instanceof HTMLElement ? stickyBar : null - const boundaryEl = heroBoundary instanceof HTMLElement ? heroBoundary : null - const progressOk = progressFill instanceof HTMLElement - - if (!stickyEl && !progressOk) { - return () => {} - } - - if (stickyEl) { - stickyEl.style.transition = - "opacity 0.25s ease-out, transform 0.25s ease-out" - } - - let alive = true - let rafId = null - - const pastReveal = () => { - if (boundaryEl) { - const r = boundaryEl.getBoundingClientRect() - return boundaryEl.hasAttribute("data-hero-sentinel") - ? r.top <= stickyRevealPx - : r.bottom <= stickyRevealPx - } - return window.scrollY > stickyRevealPx - } - - const tick = () => { - if (!alive) { - return - } - if (stickyEl) { - const show = pastReveal() - stickyEl.style.opacity = show ? "1" : "0" - stickyEl.style.transform = show - ? "translate3d(0, 0, 0)" - : "translate3d(0, -100%, 0)" - stickyEl.style.pointerEvents = show ? "auto" : "none" - } - if (progressOk) { - const p = scrollProgress01() - progressFill.style.transform = `scaleX(${p})` - progressFill.style.transformOrigin = "left center" - } - rafId = requestAnimationFrame(tick) - } - - rafId = requestAnimationFrame(tick) - - return () => { - alive = false - if (rafId != null) { - cancelAnimationFrame(rafId) - } - } -} diff --git a/assets/js/landing.js b/assets/js/landing.js deleted file mode 100644 index 2b50f0b..0000000 --- a/assets/js/landing.js +++ /dev/null @@ -1,33 +0,0 @@ -import { bindLandingScrollChrome } from "./landing-scroll-chrome.js" -import { runLandingParallax } from "./landing-parallax.js" - -export function initLanding() { - const root = document.querySelector("[data-landing]") - if (!(root instanceof HTMLElement)) { - return () => {} - } - - const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)") - .matches - const stickyBar = document.querySelector("[data-sticky-bar]") - const heroBoundary = - root.querySelector("[data-hero-sentinel]") || - root.querySelector("[data-hero-boundary]") || - root.querySelector("[data-hero]") - const progressFill = document.querySelector("[data-scroll-progress-fill]") - - const stops = [] - - stops.push(bindLandingScrollChrome(stickyBar, heroBoundary, progressFill)) - stops.push(...runLandingParallax(root, reducedMotion)) - - function teardown() { - stops.forEach((fn) => { - if (typeof fn === "function") fn() - }) - } - - window.addEventListener("pagehide", teardown, { once: true }) - - return teardown -} diff --git a/assets/js/lenis.js b/assets/js/lenis.js deleted file mode 100644 index d7f38cb..0000000 --- a/assets/js/lenis.js +++ /dev/null @@ -1,37 +0,0 @@ -import Lenis from "lenis" - -const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches - -export function initLenis() { - if (reducedMotion) { - document.documentElement.classList.remove("lenis") - globalThis.__landingLenis = undefined - return - } - - const lenis = new Lenis({ - duration: 1.1, - smoothWheel: true, - easing: (t) => 1 - Math.pow(1 - t, 3), - }) - - globalThis.__landingLenis = lenis - - function raf(time) { - lenis.raf(time) - requestAnimationFrame(raf) - } - - requestAnimationFrame(raf) - - document.querySelectorAll('a[href^="#"]').forEach((link) => { - link.addEventListener("click", (event) => { - const href = link.getAttribute("href") - if (!href || href === "#") return - const target = document.querySelector(href) - if (!target) return - event.preventDefault() - lenis.scrollTo(target, { offset: 0 }) - }) - }) -} diff --git a/assets/js/pager.js b/assets/js/pager.js new file mode 100644 index 0000000..7d46cdb --- /dev/null +++ b/assets/js/pager.js @@ -0,0 +1,39 @@ +export function initPager() { + const lists = document.querySelectorAll("[data-soonex-page-list]") + + lists.forEach((list) => { + const pagerId = list.getAttribute("data-soonex-page-list") + const pageSize = pageSizeOf(list) + applyPage(list, 1, pageSize) + + if (!pagerId) { + return + } + + const pager = document.getElementById(pagerId) + if (!pager) { + return + } + + pager.addEventListener("soonex:page-change", (event) => { + const page = Number(event.detail?.page) || 1 + const size = Number(event.detail?.page_size) || pageSize + applyPage(list, page, size) + }) + }) +} + +function pageSizeOf(list) { + const size = Number(list.getAttribute("data-page-size")) + return Number.isFinite(size) && size > 0 ? size : 3 +} + +function applyPage(list, page, pageSize) { + const items = list.querySelectorAll("[data-soonex-page-item]") + const start = (page - 1) * pageSize + const end = start + pageSize + + items.forEach((item, index) => { + item.hidden = index < start || index >= end + }) +} diff --git a/assets/js/pagination.js b/assets/js/pagination.js new file mode 100644 index 0000000..4121e63 --- /dev/null +++ b/assets/js/pagination.js @@ -0,0 +1,36 @@ +function pageSizeFrom(pager, fallback) { + const raw = + pager?.dataset.pageSize || pager?.getAttribute("data-page-size") || fallback + const size = Number(raw) + return Number.isFinite(size) && size > 0 ? size : 3 +} + +function applyPage(items, page, size) { + const start = (page - 1) * size + const end = start + size + items.forEach((el, index) => { + el.hidden = index < start || index >= end + }) +} + +export function initPagination() { + document.querySelectorAll("[data-soonex-page]").forEach((root) => { + if (!(root instanceof HTMLElement)) return + const id = root.dataset.soonexPage + if (!id) return + const pager = document.getElementById(id) + const items = [...root.querySelectorAll("[data-soonex-page-item]")] + if (items.length === 0) return + const fallbackSize = root.dataset.soonexPageSize + const size = pageSizeFrom(pager, fallbackSize) + applyPage(items, 1, size) + + pager?.addEventListener("pagination-page-changed", (event) => { + const detail = event.detail || {} + const page = typeof detail.page === "number" ? detail.page : 1 + const nextSize = + typeof detail.page_size === "number" ? detail.page_size : size + applyPage(items, page, nextSize) + }) + }) +} diff --git a/assets/js/site.js b/assets/js/site.js index 42d8371..e583f38 100644 --- a/assets/js/site.js +++ b/assets/js/site.js @@ -1,5 +1,5 @@ -import "./theme.js" import "./mode.js" +import "./theme.js" import { Socket } from "phoenix" import { LiveSocket } from "phoenix_live_view" import { hooks } from "corex/hooks" @@ -7,16 +7,19 @@ import { FloatingPanel } from "corex/floating-panel" import { Toast } from "corex/toast" import { Select } from "corex/select" import { Toggle } from "corex/toggle" -import { initLenis } from "./lenis.js" -import { initLanding } from "./landing.js" +import { Dialog } from "corex/dialog" +import { ToggleGroup } from "corex/toggle-group" +import { Menu } from "corex/menu" +import { Clipboard } from "corex/clipboard" +import { Marquee } from "corex/marquee" import { initWaitlistForm } from "./waitlist.js" +import { initPager } from "./pager.js" +import "./cookie-consent.js" const csrfToken = document .querySelector("meta[name='csrf-token']") ?.getAttribute("content") -// Eager chrome (every page) — matches installer/my_app3 so Template Options is instant. -// Lazy page hooks — Timer, Marquee, Accordion, etc. only when present. const liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken }, hooks: { @@ -24,24 +27,30 @@ const liveSocket = new LiveSocket("/live", Socket, { Toast, Select, Toggle, + Dialog, + ToggleGroup, + Menu, + Clipboard, + Marquee, ...hooks({ Tabs: () => import("corex/tabs"), Timer: () => import("corex/timer"), - Marquee: () => import("corex/marquee"), Accordion: () => import("corex/accordion"), Checkbox: () => import("corex/checkbox"), - Avatar: () => import("corex/avatar"), - Clipboard: () => import("corex/clipboard"), + Pagination: () => import("corex/pagination"), + Tooltip: () => import("corex/tooltip"), + Collapsible: () => import("corex/collapsible"), + Switch: () => import("corex/switch"), + RadioGroup: () => import("corex/radio-group"), + TagsInput: () => import("corex/tags-input"), + NumberInput: () => import("corex/number-input"), + DatePicker: () => import("corex/date-picker"), }), }, }) liveSocket.disableDebug() -initLenis() liveSocket.connect() -if (document.querySelector("[data-landing]")) { - initLanding() -} - initWaitlistForm() +initPager() diff --git a/assets/js/waitlist.js b/assets/js/waitlist.js index a78afea..d8bfa6a 100644 --- a/assets/js/waitlist.js +++ b/assets/js/waitlist.js @@ -2,25 +2,31 @@ function dispatchLayoutToast(detail) { const root = document.getElementById("layout-toast") if (!(root instanceof HTMLElement)) return root.dispatchEvent( - new CustomEvent("toast:create", { bubbles: true, detail }), + new CustomEvent("corex:toast:create", { + bubbles: true, + detail: { group_id: "layout-toast", ...detail }, + }), ) } export function initWaitlistForm() { - const form = document.querySelector("form[data-waitlist-toast-title]") - if (!(form instanceof HTMLFormElement)) return + const forms = document.querySelectorAll("form[data-waitlist-toast-title]") - form.addEventListener("submit", (event) => { - event.preventDefault() - if (!form.reportValidity()) return - const title = form.dataset.waitlistToastTitle - const description = form.dataset.waitlistToastDescription - if (!title || !description) return - dispatchLayoutToast({ - title, - description, - type: "success", - duration: "6000", + forms.forEach((form) => { + if (!(form instanceof HTMLFormElement)) return + + form.addEventListener("submit", (event) => { + event.preventDefault() + if (!form.reportValidity()) return + const title = form.dataset.waitlistToastTitle + const description = form.dataset.waitlistToastDescription + if (!title || !description) return + dispatchLayoutToast({ + title, + description, + type: "success", + duration: 6000, + }) }) }) } diff --git a/assets/package-lock.json b/assets/package-lock.json deleted file mode 100644 index b283cd6..0000000 --- a/assets/package-lock.json +++ /dev/null @@ -1,1849 +0,0 @@ -{ - "name": "soonex-template", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "soonex-template", - "dependencies": { - "lenis": "^1.3.23", - "motion": "^12.38.0" - }, - "devDependencies": { - "@tokens-studio/sd-transforms": "^2.0.3", - "style-dictionary": "^5.4.0" - } - }, - "node_modules/@bundled-es-modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-Rk453EklPUPC3NRWc3VUNI/SSUjdBaFoaQvFRmNBNtMHVtOFD5AntiWg5kEE1hqcPqedYFDzxE3ZcMYPcA195w==", - "dev": true, - "license": "ISC", - "dependencies": { - "deepmerge": "^4.3.1" - } - }, - "node_modules/@bundled-es-modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-x9nR2e1pt8LF0yLPC6yz/aUoiN7qJJwZ1znLxIXCxGyH+8BI+yO/sklBdn1+QbUyWXQBM+CjfZz3IhqtgIoDVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^6.0.3", - "events": "^3.3.0", - "glob": "^13.0.6", - "path": "^0.12.7", - "stream": "^0.0.3", - "string_decoder": "^1.3.0", - "url": "^0.11.4" - } - }, - "node_modules/@bundled-es-modules/memfs": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/memfs/-/memfs-4.17.0.tgz", - "integrity": "sha512-ykdrkEmQr9BV804yd37ikXfNnvxrwYfY9Z2/EtMHFEFadEjsQXJ1zL9bVZrKNLDtm91UdUOEHso6Aweg93K6xQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "assert": "^2.1.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "memfs": "^4.17.0", - "path": "^0.12.7", - "stream": "^0.0.3", - "util": "^0.12.5" - } - }, - "node_modules/@bundled-es-modules/postcss-calc-ast-parser": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/postcss-calc-ast-parser/-/postcss-calc-ast-parser-0.1.6.tgz", - "integrity": "sha512-y65TM5zF+uaxo9OeekJ3rxwTINlQvrkbZLogYvQYVoLtxm4xEiHfZ7e/MyiWbStYyWZVZkVqsaVU6F4SUK5XUA==", - "dev": true, - "license": "ISC", - "dependencies": { - "postcss-calc-ast-parser": "^0.1.4" - } - }, - "node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/buffers": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", - "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", - "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-core": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.2.tgz", - "integrity": "sha512-SVjwklkpIV5wrynpYtuYnfYH1QF4/nDuLBX7VXdb+3miglcAgBVZb/5y0cOsehRV/9Vb+3UqhkMq3/NR3ztdkQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.2.tgz", - "integrity": "sha512-fhO8+iR2I+OCw668ISDJdn1aArc9zx033sWejIyzQ8RBeXa9bDSaUeA3ix0poYOfrj1KdOzytmYNv2/uLDfV6g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-core": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.2.tgz", - "integrity": "sha512-nX2AdL6cOFwLdju9G4/nbRnYevmCJbh7N7hvR3gGm97Cs60uEjyd0rpR+YBS7cTg175zzl22pGKXR5USaQMvKg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-core": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "@jsonjoy.com/fs-print": "4.57.2", - "@jsonjoy.com/fs-snapshot": "4.57.2", - "glob-to-regex.js": "^1.0.0", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.2.tgz", - "integrity": "sha512-xhiegylRmhw43Ki2HO1ZBL7DQ5ja/qpRsL29VtQ2xuUHiuDGbgf2uD4p9Qd8hJI5P6RCtGYD50IXHXVq/Ocjcg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.2.tgz", - "integrity": "sha512-18LmWTSONhoAPW+IWRuf8w/+zRolPFGPeGwMxlAhhfY11EKzX+5XHDBPAw67dBF5dxDErHJbl40U+3IXSDRXSQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-fsa": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.2.tgz", - "integrity": "sha512-rsPSJgekz43IlNbLyAM/Ab+ouYLWGp5DDBfYBNNEqDaSpsbXfthBn29Q4muFA9L0F+Z3mKo+CWlgSCXrf+mOyQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.2" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-print": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.2.tgz", - "integrity": "sha512-wK9NSow48i4DbDl9F1CQE5TqnyZOJ04elU3WFG5aJ76p+YxO/ulyBBQvKsessPxdo381Bc2pcEoyPujMOhcRqQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.57.2", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.2.tgz", - "integrity": "sha512-GdduDZuoP5V/QCgJkx9+BZ6SC0EZ/smXAdTS7PfMqgMTGXLlt/bH/FqMYaqB9JmLf05sJPtO0XRbAwwkEEPbVw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "@jsonjoy.com/json-pack": "^17.65.0", - "@jsonjoy.com/util": "^17.65.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", - "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", - "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", - "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/base64": "17.67.0", - "@jsonjoy.com/buffers": "17.67.0", - "@jsonjoy.com/codegen": "17.67.0", - "@jsonjoy.com/json-pointer": "17.67.0", - "@jsonjoy.com/util": "17.67.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", - "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/util": "17.67.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", - "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/buffers": "17.67.0", - "@jsonjoy.com/codegen": "17.67.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", - "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.2.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.2", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@tokens-studio/sd-transforms": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@tokens-studio/sd-transforms/-/sd-transforms-2.0.3.tgz", - "integrity": "sha512-PyrmRb7FuJBHzsbuWNk/O06hJbaZ+RL7chmK7PRbEptaSruhANai7Kxja5CiYnTcxOj373uRMDPxoFwCHaVpvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bundled-es-modules/deepmerge": "^4.3.1", - "@bundled-es-modules/postcss-calc-ast-parser": "^0.1.6", - "@tokens-studio/types": "^0.5.1", - "colorjs.io": "^0.5.2", - "expr-eval-fork": "^3.0.1", - "is-mergeable-object": "^1.1.1" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "style-dictionary": "^5.0.0" - } - }, - "node_modules/@tokens-studio/types": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@tokens-studio/types/-/types-0.5.2.tgz", - "integrity": "sha512-rzMcZP0bj2E5jaa7Fj0LGgYHysoCrbrxILVbT0ohsCUH5uCHY/u6J7Qw/TE0n6gR9Js/c9ZO9T8mOoz0HdLMbA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@zip.js/zip.js": { - "version": "2.8.26", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz", - "integrity": "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "bun": ">=0.7.0", - "deno": ">=1.0.0", - "node": ">=18.0.0" - } - }, - "node_modules/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/change-case": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", - "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/colorjs.io": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", - "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/component-emitter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-2.0.0.tgz", - "integrity": "sha512-4m5s3Me2xxlVKG9PkZpQqHQR7bgpnN7joDMJ4yvVkVXngjoITG76IaZmzmywSeRTeTpc6N6r3H3+KyUurV8OYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/expr-eval-fork": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/expr-eval-fork/-/expr-eval-fork-3.0.3.tgz", - "integrity": "sha512-BhC+hbc5lIVjygr840n5DEkW3MQq7H9o+mc1/N7Z5uIiCFVyESLL5DIE7LNq4CYUNxy+XjA+3jRrL/h0Kt2xcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/framer-motion": { - "version": "12.38.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", - "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", - "license": "MIT", - "dependencies": { - "motion-dom": "^12.38.0", - "motion-utils": "^12.36.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-to-regex.js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", - "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hyperdyperid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.18" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-mergeable-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-mergeable-object/-/is-mergeable-object-1.1.1.tgz", - "integrity": "sha512-CPduJfuGg8h8vW74WOxHtHmtQutyQBzR+3MjQ6iDHIYdbOnm1YC7jv43SqCoU8OPGTJD4nibmiryA4kmogbGrA==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lenis": { - "version": "1.3.23", - "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.23.tgz", - "integrity": "sha512-YxYq3TJqj9sJNv0V9SkyQHejt14xwyIwgDaaMK89Uf9SxQfIszu+gTQSSphh6BWlLTNVKvvXAGkg+Zf+oFIevg==", - "license": "MIT", - "workspaces": [ - "packages/*", - "playground", - "playground/*" - ], - "funding": { - "type": "github", - "url": "https://github.com/sponsors/darkroomengineering" - }, - "peerDependencies": { - "@nuxt/kit": ">=3.0.0", - "react": ">=17.0.0", - "vue": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - }, - "react": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, - "node_modules/lru-cache": { - "version": "11.3.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", - "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/memfs": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.2.tgz", - "integrity": "sha512-2nWzSsJzrukurSDna4Z0WywuScK4Id3tSKejgu74u8KCdW4uNrseKRSIDg75C6Yw5ZRqBe0F0EtMNlTbUq8bAQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-core": "4.57.2", - "@jsonjoy.com/fs-fsa": "4.57.2", - "@jsonjoy.com/fs-node": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-to-fsa": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "@jsonjoy.com/fs-print": "4.57.2", - "@jsonjoy.com/fs-snapshot": "4.57.2", - "@jsonjoy.com/json-pack": "^1.11.0", - "@jsonjoy.com/util": "^1.9.0", - "glob-to-regex.js": "^1.0.1", - "thingies": "^2.5.0", - "tree-dump": "^1.0.3", - "tslib": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/motion": { - "version": "12.38.0", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz", - "integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==", - "license": "MIT", - "dependencies": { - "framer-motion": "^12.38.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/motion-dom": { - "version": "12.38.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", - "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", - "license": "MIT", - "dependencies": { - "motion-utils": "^12.36.0" - } - }, - "node_modules/motion-utils": { - "version": "12.36.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", - "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", - "license": "MIT" - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/path": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", - "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "process": "^0.11.1", - "util": "^0.10.3" - } - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-unified": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/path-unified/-/path-unified-0.2.0.tgz", - "integrity": "sha512-MNKqvrKbbbb5p7XHXV6ZAsf/1f/yJQa13S/fcX0uua8ew58Tgc6jXV+16JyAbnR/clgCH+euKDxrF2STxMHdrg==", - "dev": true, - "license": "MIT" - }, - "node_modules/path/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true, - "license": "ISC" - }, - "node_modules/path/node_modules/util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "2.0.3" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss-calc-ast-parser": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/postcss-calc-ast-parser/-/postcss-calc-ast-parser-0.1.4.tgz", - "integrity": "sha512-CebpbHc96zgFjGgdQ6BqBy6XIUgRx1xXWCAAk6oke02RZ5nxwo9KQejTg8y7uYEeI9kv8jKQPYjoe6REsY23vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^3.3.1" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/prettier": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/stream": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.3.tgz", - "integrity": "sha512-aMsbn7VKrl4A2T7QAQQbzgN7NVc70vgF5INQrBXqn4dCXN1zy3L9HGgLO5s7PExmdrzTJ8uR/27aviW8or8/+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^2.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/style-dictionary": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/style-dictionary/-/style-dictionary-5.4.0.tgz", - "integrity": "sha512-6BzO0DV19t6KUEXYfvHJ73d3y8bBDcd0wNLfoZRX817obJ8YX5Vev8Xh3+k9601tHE8qRJ/586iLt0byuY2THw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@bundled-es-modules/deepmerge": "^4.3.1", - "@bundled-es-modules/glob": "^13.0.6", - "@bundled-es-modules/memfs": "^4.17.0", - "@zip.js/zip.js": "^2.7.44", - "chalk": "^5.3.0", - "change-case": "^5.3.0", - "colorjs.io": "^0.5.2", - "commander": "^12.1.0", - "is-plain-obj": "^4.1.0", - "json5": "^2.2.2", - "path-unified": "^0.2.0", - "prettier": "^3.3.3", - "tinycolor2": "^1.6.0" - }, - "bin": { - "style-dictionary": "bin/style-dictionary.js" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/thingies": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", - "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "^2" - } - }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tree-dump": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", - "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/url": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", - "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^1.4.1", - "qs": "^6.12.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - } - } -} diff --git a/assets/package.json b/assets/package.json deleted file mode 100644 index 0625fc3..0000000 --- a/assets/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "soonex-template", - "private": true, - "dependencies": { - "lenis": "^1.3.23", - "motion": "^12.38.0" - }, - "devDependencies": { - "@tokens-studio/sd-transforms": "^2.0.3", - "style-dictionary": "^5.4.0" - } -} diff --git a/config/config.exs b/config/config.exs index 547edae..decd4d5 100644 --- a/config/config.exs +++ b/config/config.exs @@ -49,7 +49,7 @@ config :tableau, :config, mdex: [ extension: [ table: true, - header_ids: "", + header_id_prefix: "", tasklist: true, strikethrough: true, autolink: true, @@ -76,17 +76,385 @@ config :tableau, Tableau.RSSExtension, enabled: true, title: "Soonex", description: - "Elixir static site template: Tableau, Corex, tokens, Tailwind v4, Markdown posts. Waitlist for launch updates." + "Tableau + Corex coming-soon template: accessible Phoenix UI, waitlist, journal, and config-driven design tokens." config :elixir, :time_zone_database, Tz.TimeZoneDatabase +l = fn lightness, seed -> + %{kind: :l, seed: seed, l: lightness} +end + +fill = fn lightness, seed -> + %{ + kind: :l, + seed: seed, + l: lightness, + states: %{ + muted: min(lightness + 0.03, 1.0), + default: lightness, + hover: max(lightness - 0.04, 0.0), + active: max(lightness - 0.07, 0.0) + } + } +end + +contrast = fn seed, against, target -> + %{kind: :contrast, seed: seed, against: against, target: target * 1.0} +end + +# Host color maps replace the preset mode (resolved_spec?/1). Keep *-contrast and +# *-text so solid brand/accent buttons stay readable. Do not lower these targets. +with_on = fn colors -> + colors + |> Map.merge(%{ + alert: fill.(0.44, :alert), + info: fill.(0.4, :info), + success: fill.(0.38, :success) + }) + |> Map.merge(%{ + "accent-contrast" => contrast.(:neutral, :accent, 9.5), + "brand-contrast" => contrast.(:neutral, :brand, 9.5), + "alert-contrast" => contrast.(:neutral, :alert, 9.5), + "info-contrast" => contrast.(:neutral, :info, 9.5), + "success-contrast" => contrast.(:neutral, :success, 9.5), + "accent-text" => contrast.(:accent, :ui, 4.6), + "brand-text" => contrast.(:brand, :ui, 4.6), + "alert-text" => contrast.(:alert, :ui, 4.6), + "info-text" => contrast.(:info, :ui, 4.6), + "success-text" => contrast.(:success, :ui, 4.6) + }) +end + config :corex_design, output: "assets/corex", default_theme: :neo, default_mode: :light, - themes: [:neo, :uno, :duo, :leo], + # Host overlays cannot set *_scale / duration / opacity. See Theme.Validator. + # Contrast targets stay at or above preset strength so the solver and axe stay green. + themes: %{ + neo: %{ + seeds: %{ + neutral: "#FAF9F6", + accent: "#141414", + brand: "#141414", + alert: "#B42318", + info: "#0E7490", + success: "#15803D" + }, + colors: %{ + light: + with_on.(%{ + root: l.(0.99, :neutral), + surface: l.(0.965, :neutral), + ui: fill.(0.94, :neutral), + accent: fill.(0.24, :accent), + brand: fill.(0.4, :brand), + ink: contrast.(:accent, :root, 12), + "ink-muted": contrast.(:accent, :root, 6.0), + link: contrast.(:brand, :root, 5.5), + border: contrast.(:neutral, :ui, 1.28), + focus: contrast.(:brand, :ui, 2.3), + shadow: contrast.(:accent, :root, 1.1) + }), + dark: + with_on.(%{ + root: l.(0.055, :accent), + surface: l.(0.095, :accent), + ui: fill.(0.155, :accent), + accent: fill.(0.5, :accent), + brand: fill.(0.54, :brand), + ink: contrast.(:neutral, :root, 13), + "ink-muted": contrast.(:neutral, :root, 6.6), + link: contrast.(:brand, :root, 6.2), + border: contrast.(:neutral, :ui, 1.32), + focus: contrast.(:brand, :ui, 2.4), + shadow: contrast.(:accent, :root, 1.18) + }) + }, + dimensions: %{ + radius: %{ + xs: 0.28, + sm: 0.48, + md: 0.72, + lg: 1.0, + xl: 1.25, + "2xl": 1.5, + "3xl": 1.9, + "4xl": 2.4, + full: 9999 + }, + font: %{ + sans: ["Manrope", "ui-sans-serif", "system-ui", "sans-serif"], + display: ["Outfit", "ui-sans-serif", "system-ui", "sans-serif"], + mono: ["JetBrains Mono", "ui-monospace", "monospace"], + code: ["JetBrains Mono", "ui-monospace", "monospace"], + serif: ["ui-serif", "Georgia", "serif"] + } + }, + typography: %{ + "h1" => %{ + font_family: {:font, :display}, + font_weight: {:weight, :medium}, + letter_spacing: {:tracking, :tight} + }, + "h2" => %{ + font_family: {:font, :display}, + font_weight: {:weight, :medium}, + letter_spacing: {:tracking, :tight} + }, + "h3" => %{font_family: {:font, :display}, font_weight: {:weight, :medium}}, + "h4" => %{font_family: {:font, :sans}, font_weight: {:weight, :medium}}, + "p" => %{line_height: {:leading, :relaxed}}, + "p.display" => %{ + font_family: {:font, :display}, + font_weight: {:weight, :medium}, + letter_spacing: {:tracking, :tight} + }, + "blockquote" => %{font_family: {:font, :serif}, font_style: :italic}, + "code" => %{font_family: {:font, :mono}}, + "kbd" => %{font_family: {:font, :mono}} + } + }, + uno: %{ + seeds: %{ + neutral: "#F6F6F6", + accent: "#071614", + brand: "#0F766E", + alert: "#B91C1C", + info: "#0369A1", + success: "#166534" + }, + colors: %{ + light: + with_on.(%{ + root: l.(0.99, :neutral), + surface: l.(0.97, :neutral), + ui: fill.(0.94, :neutral), + accent: fill.(0.3, :accent), + brand: fill.(0.36, :brand), + ink: contrast.(:accent, :root, 10), + "ink-muted": contrast.(:accent, :root, 5.5), + link: contrast.(:brand, :root, 5.8), + border: contrast.(:neutral, :ui, 1.22), + focus: contrast.(:brand, :ui, 2.2), + shadow: contrast.(:accent, :root, 1.06) + }), + dark: + with_on.(%{ + root: l.(0.065, :accent), + surface: l.(0.11, :accent), + ui: fill.(0.175, :accent), + accent: fill.(0.52, :accent), + brand: fill.(0.52, :brand), + ink: contrast.(:neutral, :root, 12.5), + "ink-muted": contrast.(:neutral, :root, 6.2), + link: contrast.(:brand, :root, 7.0), + border: contrast.(:neutral, :ui, 1.28), + focus: contrast.(:brand, :ui, 2.3), + shadow: contrast.(:accent, :root, 1.14) + }) + }, + dimensions: %{ + radius: %{ + xs: 0.55, + sm: 0.9, + md: 1.25, + lg: 1.7, + xl: 2.2, + "2xl": 3.0, + "3xl": 4.0, + "4xl": 5.0, + full: 9999 + }, + font: %{ + sans: ["DM Sans", "ui-sans-serif", "system-ui", "sans-serif"], + display: ["Sora", "ui-sans-serif", "system-ui", "sans-serif"], + mono: ["JetBrains Mono", "ui-monospace", "monospace"], + code: ["JetBrains Mono", "ui-monospace", "monospace"], + serif: ["Playfair Display", "Georgia", "serif"] + } + }, + typography: %{ + "h1" => %{ + font_family: {:font, :display}, + font_weight: {:weight, :semibold}, + letter_spacing: {:tracking, :tight} + }, + "h2" => %{font_family: {:font, :display}, letter_spacing: {:tracking, :tight}}, + "h3" => %{font_family: {:font, :display}, font_weight: {:weight, :medium}}, + "h4" => %{font_weight: {:weight, :medium}}, + "p" => %{line_height: {:leading, :snug}}, + "p.display" => %{font_family: {:font, :display}, font_weight: {:weight, :semibold}}, + "blockquote" => %{font_family: {:font, :serif}, font_style: :italic}, + "code" => %{font_family: {:font, :mono}}, + "kbd" => %{font_family: {:font, :mono}} + } + }, + duo: %{ + seeds: %{ + neutral: "#F7EDE4", + accent: "#3A2618", + brand: "#C2410C", + alert: "#9F1239", + info: "#3D5278", + success: "#3F6B4E" + }, + colors: %{ + light: + Map.merge( + with_on.(%{ + root: l.(0.99, :neutral), + surface: l.(0.97, :neutral), + ui: fill.(0.94, :neutral), + accent: fill.(0.34, :accent), + brand: fill.(0.42, :brand), + ink: contrast.(:accent, :root, 12.5), + "ink-muted": contrast.(:accent, :root, 6.8), + link: contrast.(:brand, :root, 7.0), + border: contrast.(:neutral, :ui, 1.22), + focus: contrast.(:brand, :ui, 2.3), + shadow: contrast.(:accent, :root, 1.28) + }), + %{"brand-text" => contrast.(:brand, :root, 7.0)} + ), + dark: + with_on.(%{ + root: l.(0.075, :accent), + surface: l.(0.125, :accent), + ui: fill.(0.195, :accent), + accent: fill.(0.5, :accent), + brand: fill.(0.56, :brand), + ink: contrast.(:neutral, :root, 12), + "ink-muted": contrast.(:neutral, :root, 6.2), + link: contrast.(:brand, :root, 7.0), + border: contrast.(:neutral, :ui, 1.18), + focus: contrast.(:brand, :ui, 2.3), + shadow: contrast.(:accent, :root, 1.32) + }) + }, + dimensions: %{ + radius: %{ + xs: 0.55, + sm: 0.9, + md: 1.3, + lg: 1.8, + xl: 2.35, + "2xl": 3.1, + "3xl": 4.0, + "4xl": 5.0, + full: 9999 + }, + font: %{ + sans: ["Work Sans", "ui-sans-serif", "system-ui", "sans-serif"], + display: ["Playfair Display", "Georgia", "serif"], + mono: ["JetBrains Mono", "ui-monospace", "monospace"], + code: ["JetBrains Mono", "ui-monospace", "monospace"], + serif: ["Playfair Display", "Georgia", "serif"] + } + }, + typography: %{ + "h1" => %{ + font_family: {:font, :display}, + font_weight: {:weight, :bold}, + letter_spacing: {:tracking, :tight} + }, + "h2" => %{font_family: {:font, :display}, font_weight: {:weight, :semibold}}, + "h3" => %{font_family: {:font, :display}}, + "h4" => %{font_family: {:font, :serif}, font_weight: {:weight, :medium}}, + "p" => %{line_height: {:leading, :relaxed}}, + "p.display" => %{ + font_family: {:font, :display}, + font_weight: {:weight, :bold}, + letter_spacing: {:tracking, :tight} + }, + "blockquote" => %{font_family: {:font, :serif}, font_style: :italic}, + "code" => %{font_family: {:font, :mono}}, + "kbd" => %{font_family: {:font, :mono}} + } + }, + leo: %{ + seeds: %{ + neutral: "#E6DCCE", + accent: "#0C0C0C", + brand: "#E2410A", + alert: "#C41E1E", + info: "#0369A1", + success: "#166534" + }, + colors: %{ + light: + with_on.(%{ + root: l.(0.985, :neutral), + surface: l.(0.96, :neutral), + ui: fill.(0.92, :neutral), + accent: fill.(0.16, :accent), + brand: fill.(0.36, :brand), + ink: contrast.(:accent, :root, 12), + "ink-muted": contrast.(:accent, :root, 5.8), + link: contrast.(:brand, :root, 5.8), + border: contrast.(:accent, :ui, 1.55), + focus: contrast.(:brand, :ui, 2.5), + shadow: contrast.(:accent, :root, 1.04) + }), + dark: + with_on.(%{ + root: l.(0.04, :accent), + surface: l.(0.085, :accent), + ui: fill.(0.145, :accent), + accent: fill.(0.48, :accent), + brand: fill.(0.5, :brand), + ink: contrast.(:neutral, :root, 14), + "ink-muted": contrast.(:neutral, :root, 6.8), + link: contrast.(:brand, :root, 7.2), + border: contrast.(:neutral, :ui, 1.42), + focus: contrast.(:brand, :ui, 2.6), + shadow: contrast.(:accent, :root, 1.06) + }) + }, + dimensions: %{ + radius: %{ + xs: 0, + sm: 0.01, + md: 0.02, + lg: 0.03, + xl: 0.04, + "2xl": 0.06, + "3xl": 0.08, + "4xl": 0.1, + full: 9999 + }, + font: %{ + sans: ["IBM Plex Sans", "ui-sans-serif", "system-ui", "sans-serif"], + display: ["IBM Plex Sans", "ui-sans-serif", "system-ui", "sans-serif"], + mono: ["IBM Plex Mono", "ui-monospace", "monospace"], + code: ["IBM Plex Mono", "ui-monospace", "monospace"], + serif: ["ui-serif", "Georgia", "serif"] + } + }, + typography: %{ + "h1" => %{font_weight: {:weight, :semibold}, letter_spacing: {:tracking, :tight}}, + "h2" => %{font_weight: {:weight, :semibold}}, + "h3" => %{font_weight: {:weight, :medium}}, + "h4" => %{ + font_weight: {:weight, :medium}, + letter_spacing: {:tracking, :widest}, + font_size: {:text, :sm} + }, + "p" => %{line_height: {:leading, :snug}}, + "p.display" => %{font_weight: {:weight, :semibold}, letter_spacing: {:tracking, :tight}}, + "blockquote" => %{font_style: :italic}, + "code" => %{font_family: {:font, :mono}}, + "kbd" => %{font_family: {:font, :mono}} + } + } + }, modes: [:light, :dark], - scales: [], + scales: [ + space: [xs: 1, sm: 2, md: 3, lg: 5, xl: 6], + size: [xs: 6, sm: 8, md: 11, lg: 13, xl: 15], + text: [md: 1.02, lg: 1.15, xl: 1.28, "2xl": 1.55], + weight: [normal: 400, medium: 500, semibold: 600, bold: 700] + ], components: [ :toast, :typo, @@ -101,6 +469,9 @@ config :corex_design, :select, :tabs, :toggle, + :"toggle-group", + :dialog, + :menu, :"native-input", :checkbox, :"data-list", @@ -109,9 +480,19 @@ config :corex_design, :accordion, :avatar, :"floating-panel", - :scrollbar + :scrollbar, + :pagination, + :carousel, + :switch, + :"data-table", + :collapsible, + :"radio-group", + :"tags-input", + :"number-input", + :"date-picker" ], - semantics: [:accent, :brand, :alert, :info, :success] + semantics: [:accent, :brand, :alert, :info, :success], + accessibility: true config :phoenix, json_library: Jason diff --git a/extra/fonts/dm-sans-latin-wght-normal.woff2 b/extra/fonts/dm-sans-latin-wght-normal.woff2 new file mode 100644 index 0000000..01383d7 Binary files /dev/null and b/extra/fonts/dm-sans-latin-wght-normal.woff2 differ diff --git a/extra/fonts/ibm-plex-mono-latin-400-normal.woff2 b/extra/fonts/ibm-plex-mono-latin-400-normal.woff2 new file mode 100644 index 0000000..0804aaf Binary files /dev/null and b/extra/fonts/ibm-plex-mono-latin-400-normal.woff2 differ diff --git a/extra/fonts/ibm-plex-mono-latin-500-normal.woff2 b/extra/fonts/ibm-plex-mono-latin-500-normal.woff2 new file mode 100644 index 0000000..090f82f Binary files /dev/null and b/extra/fonts/ibm-plex-mono-latin-500-normal.woff2 differ diff --git a/extra/fonts/ibm-plex-sans-latin-wght-normal.woff2 b/extra/fonts/ibm-plex-sans-latin-wght-normal.woff2 new file mode 100644 index 0000000..b757bc5 Binary files /dev/null and b/extra/fonts/ibm-plex-sans-latin-wght-normal.woff2 differ diff --git a/extra/fonts/jetbrains-mono-latin-wght-normal.woff2 b/extra/fonts/jetbrains-mono-latin-wght-normal.woff2 new file mode 100644 index 0000000..cd5102a Binary files /dev/null and b/extra/fonts/jetbrains-mono-latin-wght-normal.woff2 differ diff --git a/extra/fonts/manrope-latin-wght-normal.woff2 b/extra/fonts/manrope-latin-wght-normal.woff2 new file mode 100644 index 0000000..71eb731 Binary files /dev/null and b/extra/fonts/manrope-latin-wght-normal.woff2 differ diff --git a/extra/fonts/outfit-latin-wght-normal.woff2 b/extra/fonts/outfit-latin-wght-normal.woff2 new file mode 100644 index 0000000..85e3332 Binary files /dev/null and b/extra/fonts/outfit-latin-wght-normal.woff2 differ diff --git a/extra/fonts/playfair-display-latin-wght-normal.woff2 b/extra/fonts/playfair-display-latin-wght-normal.woff2 new file mode 100644 index 0000000..5a3fbbd Binary files /dev/null and b/extra/fonts/playfair-display-latin-wght-normal.woff2 differ diff --git a/extra/fonts/sora-latin-wght-normal.woff2 b/extra/fonts/sora-latin-wght-normal.woff2 new file mode 100644 index 0000000..3dbf56e Binary files /dev/null and b/extra/fonts/sora-latin-wght-normal.woff2 differ diff --git a/extra/fonts/work-sans-latin-wght-normal.woff2 b/extra/fonts/work-sans-latin-wght-normal.woff2 new file mode 100644 index 0000000..3a70b25 Binary files /dev/null and b/extra/fonts/work-sans-latin-wght-normal.woff2 differ diff --git a/extra/images/covers/current.jpg b/extra/images/covers/current.jpg new file mode 100644 index 0000000..97e453f Binary files /dev/null and b/extra/images/covers/current.jpg differ diff --git a/extra/images/covers/dusk.jpg b/extra/images/covers/dusk.jpg new file mode 100644 index 0000000..522fbf5 Binary files /dev/null and b/extra/images/covers/dusk.jpg differ diff --git a/extra/images/covers/haze.jpg b/extra/images/covers/haze.jpg new file mode 100644 index 0000000..d6a7567 Binary files /dev/null and b/extra/images/covers/haze.jpg differ diff --git a/extra/images/covers/orbit.jpg b/extra/images/covers/orbit.jpg new file mode 100644 index 0000000..9d8c4b5 Binary files /dev/null and b/extra/images/covers/orbit.jpg differ diff --git a/extra/images/covers/spectrum.jpg b/extra/images/covers/spectrum.jpg new file mode 100644 index 0000000..61ef26d Binary files /dev/null and b/extra/images/covers/spectrum.jpg differ diff --git a/extra/images/covers/waves.jpg b/extra/images/covers/waves.jpg new file mode 100644 index 0000000..990c974 Binary files /dev/null and b/extra/images/covers/waves.jpg differ diff --git a/extra/images/logo.svg b/extra/images/logo.svg new file mode 100644 index 0000000..82deb3e --- /dev/null +++ b/extra/images/logo.svg @@ -0,0 +1,7 @@ + + + diff --git a/extra/images/og.svg b/extra/images/og.svg index f9236a7..ef354f9 100644 --- a/extra/images/og.svg +++ b/extra/images/og.svg @@ -1,9 +1,15 @@ - - - - Soonex - Static HEEx, shipped by Mix - Tableau · Corex · tokens · Tailwind v4 · Markdown - soonex.example · join the waitlist + + + + + + Soonex + Lorem ipsum dolor sit amet. + Consectetur adipiscing elit · 1 September + Sed do eiusmod tempor incididunt ut labore. diff --git a/extra/images/photos/closing.jpg b/extra/images/photos/closing.jpg new file mode 100644 index 0000000..5ea8d26 Binary files /dev/null and b/extra/images/photos/closing.jpg differ diff --git a/extra/images/photos/hero.jpg b/extra/images/photos/hero.jpg new file mode 100644 index 0000000..de05f09 Binary files /dev/null and b/extra/images/photos/hero.jpg differ diff --git a/extra/images/photos/ribbons.jpg b/extra/images/photos/ribbons.jpg new file mode 100644 index 0000000..ee0f0cc Binary files /dev/null and b/extra/images/photos/ribbons.jpg differ diff --git a/extra/images/photos/texture.jpg b/extra/images/photos/texture.jpg new file mode 100644 index 0000000..f3f3781 Binary files /dev/null and b/extra/images/photos/texture.jpg differ diff --git a/extra/images/tech/css.svg b/extra/images/tech/css.svg deleted file mode 100644 index 86a3c88..0000000 --- a/extra/images/tech/css.svg +++ /dev/null @@ -1,2 +0,0 @@ - -file_type_css \ No newline at end of file diff --git a/extra/images/tech/ecto.png b/extra/images/tech/ecto.png deleted file mode 100644 index d26cd94..0000000 Binary files a/extra/images/tech/ecto.png and /dev/null differ diff --git a/extra/images/tech/elixir.svg b/extra/images/tech/elixir.svg index 0914517..e29f5cf 100644 --- a/extra/images/tech/elixir.svg +++ b/extra/images/tech/elixir.svg @@ -1,23 +1 @@ - - - - - - - - - - - - - - - file_type_elixir - - - - - - - - \ No newline at end of file +Elixir \ No newline at end of file diff --git a/extra/images/tech/figma.svg b/extra/images/tech/figma.svg deleted file mode 100644 index 92a08a2..0000000 --- a/extra/images/tech/figma.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/extra/images/tech/html5.svg b/extra/images/tech/html5.svg index 2aa78b1..4763957 100644 --- a/extra/images/tech/html5.svg +++ b/extra/images/tech/html5.svg @@ -1,6 +1 @@ - - - - - - \ No newline at end of file +HTML5 diff --git a/extra/images/tech/javascript.svg b/extra/images/tech/javascript.svg index 23243bb..6918b4e 100644 --- a/extra/images/tech/javascript.svg +++ b/extra/images/tech/javascript.svg @@ -1,8 +1 @@ - - - - - - - - +JavaScript diff --git a/extra/images/tech/phoenix.svg b/extra/images/tech/phoenix.svg deleted file mode 100644 index 534ff4c..0000000 --- a/extra/images/tech/phoenix.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/extra/images/tech/phoenixframework.svg b/extra/images/tech/phoenixframework.svg new file mode 100644 index 0000000..d17b99f --- /dev/null +++ b/extra/images/tech/phoenixframework.svg @@ -0,0 +1 @@ +Phoenix Framework \ No newline at end of file diff --git a/extra/images/tech/tailwind.svg b/extra/images/tech/tailwind.svg deleted file mode 100644 index 3e38e2f..0000000 --- a/extra/images/tech/tailwind.svg +++ /dev/null @@ -1,2 +0,0 @@ - -file_type_tailwind \ No newline at end of file diff --git a/extra/images/tech/tailwindcss.svg b/extra/images/tech/tailwindcss.svg new file mode 100644 index 0000000..e069b14 --- /dev/null +++ b/extra/images/tech/tailwindcss.svg @@ -0,0 +1 @@ +Tailwind CSS \ No newline at end of file diff --git a/extra/images/tech/typescript.svg b/extra/images/tech/typescript.svg index 91e84ac..f8bf69b 100644 --- a/extra/images/tech/typescript.svg +++ b/extra/images/tech/typescript.svg @@ -1,12 +1 @@ - - - - - - - - - - - - \ No newline at end of file +TypeScript \ No newline at end of file diff --git a/extra/images/tech/zag.webp b/extra/images/tech/zag.webp deleted file mode 100644 index d3d79ad..0000000 Binary files a/extra/images/tech/zag.webp and /dev/null differ diff --git a/lib/layouts/articles.ex b/lib/layouts/articles.ex new file mode 100644 index 0000000..205e862 --- /dev/null +++ b/lib/layouts/articles.ex @@ -0,0 +1,101 @@ +defmodule Soonex.Layouts.Articles do + @moduledoc false + + use Phoenix.Component + use Corex + + import Soonex.Layouts.Media, only: [photo: 1] + + alias Soonex.Layouts.Shell + + attr(:posts, :list, required: true) + attr(:empty, :string, default: "No posts yet.") + attr(:pager_id, :string, default: nil) + attr(:page_size, :integer, default: 3) + + def cards(assigns) do + ~H""" +
+

{@empty}

+
+
+ +
+ """ + end + + attr(:id, :string, required: true) + attr(:count, :integer, required: true) + attr(:page_size, :integer, default: 6) + + def pager(assigns) do + ~H""" +
+ <.pagination + id={@id} + class="pagination ui-brand ui-size-sm" + count={@count} + page_size={@page_size} + type={:button} + on_page_change_client="soonex:page-change" + > + <:prev_trigger> + <.heroicon name="hero-chevron-left" /> + + <:next_trigger> + <.heroicon name="hero-chevron-right" /> + + <:ellipsis> + <.heroicon name="hero-ellipsis-horizontal" /> + + +
+ """ + end + + defp cover(post) do + src = post[:image] + + if is_binary(src) and src != "" do + %{src: src, alt: post[:image_alt] || post[:title] || "Journal cover"} + end + end + + defp date_label(%{date: %DateTime{} = date}), do: Calendar.strftime(date, "%d %B %Y") + defp date_label(_), do: nil + + defp post_tags(post) do + post + |> Map.get(:tags, []) + |> List.wrap() + |> Enum.filter(&is_binary/1) + end +end diff --git a/lib/layouts/brand.ex b/lib/layouts/brand.ex new file mode 100644 index 0000000..0823689 --- /dev/null +++ b/lib/layouts/brand.ex @@ -0,0 +1,46 @@ +defmodule Soonex.Layouts.Brand do + @moduledoc false + + use Phoenix.Component + use Corex + + attr(:class, :any, default: nil) + attr(:to, :string, default: nil) + + def lockup(assigns) do + assigns = assign(assigns, :to, assigns.to || Soonex.Public.path("/")) + + ~H""" + <.navigate + to={@to} + type="href" + class={["link ui-nav ui-size-md flex items-center gap-2 no-underline", @class]} + > + + <.mark class="soonex-mark" /> + + Soonex + + """ + end + + attr(:class, :any, default: "soonex-mark") + + def mark(assigns) do + ~H""" + + """ + end +end diff --git a/lib/layouts/media.ex b/lib/layouts/media.ex new file mode 100644 index 0000000..7932652 --- /dev/null +++ b/lib/layouts/media.ex @@ -0,0 +1,32 @@ +defmodule Soonex.Layouts.Media do + @moduledoc false + + use Phoenix.Component + + attr(:src, :string, required: true) + attr(:alt, :string, required: true) + attr(:class, :any, default: nil) + attr(:width, :integer, default: nil) + attr(:height, :integer, default: nil) + attr(:loading, :string, default: "lazy") + attr(:sizes, :string, default: nil) + + def photo(assigns) do + ~H""" + {@alt} + """ + end + + def credits do + "Photography from Unsplash (Unsplash License). Tool marks from Simple Icons (CC0) and the Tableau project." + end +end diff --git a/lib/layouts/post_layout.ex b/lib/layouts/post_layout.ex index 6da7275..294ee1c 100644 --- a/lib/layouts/post_layout.ex +++ b/lib/layouts/post_layout.ex @@ -5,6 +5,8 @@ defmodule Soonex.PostLayout do use Phoenix.Component use Corex + import Soonex.Layouts.Media, only: [photo: 1] + alias Soonex.Layouts.Shell def template(assigns) do @@ -15,7 +17,7 @@ defmodule Soonex.PostLayout do date_label = case date do - %DateTime{} -> Calendar.strftime(date, "%Y-%m-%d") + %DateTime{} -> Calendar.strftime(date, "%d %B %Y") _ -> nil end @@ -31,40 +33,55 @@ defmodule Soonex.PostLayout do |> Map.put(:post_description, description) |> Map.put(:post_date_label, date_label) |> Map.put(:post_tags, tags) + |> Map.put(:post_cover, cover(page)) ~H""" -
- - -
-
-

{@post_date_label}

-

{@post_title}

-

{@post_description}

-
-
    -
  • - {tag} -
  • -
- <.navigate to={Soonex.Public.path("/tags")} class="link ui-brand ui-size-sm"> - Browse tags +
+
+ <.layout_heading class="layout-heading" subtitle_tag="p"> + <:title>{@post_title} + <:subtitle> + {@post_date_label} + · + {@post_description} + + <:actions> + <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-sm"> + <.heroicon name="hero-arrow-left" /> Log + + + +
+
    +
  • + {tag} +
  • +
+ <.navigate to={Soonex.Public.path("/tags")} class="link ui-brand ui-size-sm"> + All tags + +
+ +
+
+ <.photo src={@post_cover.src} alt={@post_cover.alt} width={1400} height={700} />
-
-
- {{:safe, render(@inner_content)}} +
+ {{:safe, render(@inner_content)}} +
""" end + + defp cover(page) do + src = page[:image] + + if is_binary(src) and src != "" do + %{src: src, alt: page[:image_alt] || page[:title] || "Post cover"} + end + end end diff --git a/lib/layouts/root/demo.ex b/lib/layouts/root/demo.ex index ec9f0b8..ad7ebf9 100644 --- a/lib/layouts/root/demo.ex +++ b/lib/layouts/root/demo.ex @@ -4,6 +4,8 @@ defmodule Soonex.Layouts.Root.Demo do use Phoenix.Component use Corex + import Soonex.Accessibility, only: [accessibility_panel: 1] + attr(:mode, :any, required: true) def demo_site_controls(assigns) do @@ -13,6 +15,7 @@ defmodule Soonex.Layouts.Root.Demo do aria-label="Demo site controls" class="fixed bottom-space end-space z-50 flex flex-col items-end gap-space" > + <.accessibility_panel /> <.floating_panel id="site-controls" class="floating-panel" @@ -46,6 +49,16 @@ defmodule Soonex.Layouts.Root.Demo do close_on_select={false} update_trigger={false} on_value_change_client="corex:set-theme" + positioning={ + %Corex.Positioning{ + strategy: "fixed", + placement: "bottom-start", + same_width: true, + gutter: 8, + slide: false, + fit_viewport: false + } + } translation={%Corex.Select.Translation{placeholder: "Theme"}} > <:label>Theme @@ -78,11 +91,11 @@ defmodule Soonex.Layouts.Root.Demo do <.navigate - to="https://corex.gigalixirapp.com/templates" + to="https://hexdocs.pm/corex" class="button ui-accent ui-solid ui-size-sm" external > - Made with Corex <.heroicon name="hero-arrow-down-tray" /> + Corex docs <.heroicon name="hero-arrow-up-right" /> """ diff --git a/lib/layouts/root/footer.ex b/lib/layouts/root/footer.ex index 7ac1c5d..53eae8c 100644 --- a/lib/layouts/root/footer.ex +++ b/lib/layouts/root/footer.ex @@ -4,6 +4,11 @@ defmodule Soonex.Layouts.Root.Footer do use Phoenix.Component use Corex + import Soonex.CookieConsent, only: [cookie_dialog: 1] + import Soonex.Layouts.Brand, only: [lockup: 1] + + alias Soonex.Layouts.Shell + @github "https://github.com/corex-ui/soonex" @hexdocs "https://hexdocs.pm/corex" @@ -16,98 +21,108 @@ defmodule Soonex.Layouts.Root.Footer do |> assign(:hexdocs, @hexdocs) ~H""" -