From c9e5cb2a83b0f24041e5f533199c06de0f1f3480 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 26 Aug 2026 02:29:33 +0000 Subject: [PATCH 01/65] Give Soonex a paper-cut identity with local fonts. Upgrade Corex to 0.2.1, self-host theme fonts, drop BEM homepage/blog CSS, and restyle the sticky countdown as a hanging paper tab. Layout now uses full-width Tailwind + Corex tokens like Netoum and Oranje, with a distinct kraft/stamp look. Co-authored-by: Karim Semmoud --- CHANGELOG.md | 6 + README.md | 6 +- assets/css/blog.css | 191 ------------------ assets/css/fonts.css | 102 ++++++++++ assets/css/home-numbers.css | 107 ---------- assets/css/home.css | 140 ------------- assets/css/paper.css | 71 +++++++ assets/css/prose.css | 2 +- assets/css/site.css | 5 +- assets/js/landing-hero.js | 30 --- assets/js/landing-parallax.js | 19 -- assets/js/landing-reveal.js | 28 --- assets/js/landing.js | 14 +- extra/fonts/dm-sans-latin-wght-normal.woff2 | Bin 0 -> 36932 bytes extra/fonts/fraunces-latin-wght-normal.woff2 | Bin 0 -> 36620 bytes .../ibm-plex-mono-latin-400-normal.woff2 | Bin 0 -> 14708 bytes .../ibm-plex-mono-latin-500-normal.woff2 | Bin 0 -> 14888 bytes .../ibm-plex-sans-latin-wght-normal.woff2 | Bin 0 -> 45712 bytes .../jetbrains-mono-latin-wght-normal.woff2 | Bin 0 -> 40404 bytes .../fonts/nunito-sans-latin-wght-normal.woff2 | Bin 0 -> 31076 bytes .../playfair-display-latin-wght-normal.woff2 | Bin 0 -> 38404 bytes extra/fonts/sora-latin-wght-normal.woff2 | Bin 0 -> 33652 bytes extra/fonts/work-sans-latin-wght-normal.woff2 | Bin 0 -> 50316 bytes lib/layouts/post_layout.ex | 46 +++-- lib/layouts/root/footer.ex | 17 +- lib/layouts/root/landing_chrome.ex | 10 +- lib/layouts/root_layout.ex | 18 +- lib/layouts/shell.ex | 21 +- lib/layouts/tag_layout.ex | 57 +++--- lib/pages/blog_index_page.ex | 84 ++++---- lib/pages/home/faq.ex | 33 +-- lib/pages/home/hero.ex | 46 ++--- lib/pages/home/highlights.ex | 76 ++++--- lib/pages/home/scale.ex | 69 ++++--- lib/pages/home/waitlist.ex | 97 ++++----- lib/pages/not_found_page.ex | 10 +- lib/pages/tags_index_page.ex | 58 +++--- lib/soonex/palette/config.ex | 10 +- mix.exs | 8 +- mix.lock | 18 +- 40 files changed, 548 insertions(+), 851 deletions(-) delete mode 100644 assets/css/blog.css create mode 100644 assets/css/fonts.css delete mode 100644 assets/css/home-numbers.css delete mode 100644 assets/css/home.css create mode 100644 assets/css/paper.css delete mode 100644 assets/js/landing-hero.js delete mode 100644 assets/js/landing-parallax.js delete mode 100644 assets/js/landing-reveal.js create mode 100644 extra/fonts/dm-sans-latin-wght-normal.woff2 create mode 100644 extra/fonts/fraunces-latin-wght-normal.woff2 create mode 100644 extra/fonts/ibm-plex-mono-latin-400-normal.woff2 create mode 100644 extra/fonts/ibm-plex-mono-latin-500-normal.woff2 create mode 100644 extra/fonts/ibm-plex-sans-latin-wght-normal.woff2 create mode 100644 extra/fonts/jetbrains-mono-latin-wght-normal.woff2 create mode 100644 extra/fonts/nunito-sans-latin-wght-normal.woff2 create mode 100644 extra/fonts/playfair-display-latin-wght-normal.woff2 create mode 100644 extra/fonts/sora-latin-wght-normal.woff2 create mode 100644 extra/fonts/work-sans-latin-wght-normal.woff2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 99ea2db..1126d5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.2.1 + +- Upgrade Corex packages to 0.2.1 (`corex`, `corex_design`, `corex_mcp`). +- Paper-cut visual identity: local fonts, full-width token layout, sticky launch tab. +- Drop homepage/blog BEM CSS in favor of inline Tailwind + Corex tokens. + ## 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..7af3c1a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ English-only **Tableau** static site: Corex components, theme/mode toggles, and - Elixir ~> 1.17 - Node.js (for `npm install` in `assets/`) -- Hex packages `corex`, `corex_design`, and `corex_mcp` (`~> 0.2.0`) +- Hex packages `corex`, `corex_design`, and `corex_mcp` (`~> 0.2.1`) ## Quick start @@ -32,6 +32,8 @@ Rebuild assets: `mix assets.build`. - **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`. +- **Fonts:** self-hosted woff2 in [`extra/fonts/`](extra/fonts/), faces in [`assets/css/fonts.css`](assets/css/fonts.css). Neo uses Fraunces + Nunito Sans; uno/duo/leo keep Corex stacks. +- **Paper-cut chrome:** [`assets/css/paper.css`](assets/css/paper.css) (tiny). Layout is Tailwind + Corex tokens in HEEx. - **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. @@ -50,7 +52,7 @@ MDX-style Tableau extras (tags, `include_dir`, sitemap) are summarized in **Tabl - `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). +- Client UI: [`assets/js/theme.js`](assets/js/theme.js), [`assets/js/mode.js`](assets/js/mode.js); sticky launch tab in [`assets/js/landing-scroll-chrome.js`](assets/js/landing-scroll-chrome.js). ## Production and hosting 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/fonts.css b/assets/css/fonts.css new file mode 100644 index 0000000..f61649a --- /dev/null +++ b/assets/css/fonts.css @@ -0,0 +1,102 @@ +/* Self-hosted latin woff2. Family names match Corex 0.2 theme stacks + (plus Soonex neo overrides in paper.css). SIL OFL. */ + +@font-face { + font-family: "Nunito Sans"; + font-style: normal; + font-display: swap; + font-weight: 200 1000; + src: url("../fonts/nunito-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: Fraunces; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url("../fonts/fraunces-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/paper.css b/assets/css/paper.css new file mode 100644 index 0000000..c389ef3 --- /dev/null +++ b/assets/css/paper.css @@ -0,0 +1,71 @@ +/* Paper-cut primitives only. Layout stays in Tailwind + Corex tokens. */ + +[data-theme="neo"] { + --font-sans: "Nunito Sans", ui-sans-serif, system-ui, sans-serif; + --font-display: Fraunces, ui-serif, Georgia, serif; + --font-mono: "IBM Plex Mono", ui-monospace, monospace; + --font-code: "IBM Plex Mono", ui-monospace, monospace; +} + +[data-theme="neo"][data-mode="light"] { + --color-root: #f6f0e4; + --color-surface: #eee4d3; + --color-ui: #e5d8c3; + --color-ui-muted: #e5d8c3; + --color-brand: #7a2430; + --color-brand-text: #7a2430; + --color-brand-contrast: #f6f0e4; + --color-accent: #6b5344; + --color-ink: #2a2118; + --color-ink-muted: #5c5146; + --color-border: #d4c4ae; + --color-link: #7a2430; + --color-success: #3e6b4f; + --color-success-text: #2f513c; +} + +[data-theme="neo"][data-mode="dark"] { + --color-root: #16110d; + --color-surface: #1f1913; + --color-ui: #2a221b; + --color-ui-muted: #2a221b; + --color-brand: #d47a84; + --color-brand-text: #e8b4b9; + --color-brand-contrast: #16110d; + --color-accent: #c4a484; + --color-ink: #f0e6d6; + --color-ink-muted: #b5a894; + --color-border: #3d3228; + --color-link: #e8b4b9; + --color-success: #7da88a; + --color-success-text: #b7d4bf; +} + +.paper-grain { + background-image: url("data:image/svg+xml;utf8,"); + background-size: 140px 140px; +} + +.paper-tab { + position: relative; +} + +.paper-tab::after { + content: ""; + position: absolute; + inset-inline: 0; + bottom: -7px; + height: 14px; + pointer-events: none; + background-image: radial-gradient( + circle at 8px 7px, + var(--color-root) 5px, + transparent 5.5px + ); + background-size: 16px 14px; + background-repeat: repeat-x; +} + +.marquee [data-scope="marquee"][data-part="edge"] { + --edge-background: var(--color-surface); +} diff --git a/assets/css/prose.css b/assets/css/prose.css index d12e214..2b153dc 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; } diff --git a/assets/css/site.css b/assets/css/site.css index 6811dea..053624c 100644 --- a/assets/css/site.css +++ b/assets/css/site.css @@ -2,14 +2,13 @@ @plugin "../vendor/heroicons"; @import "../node_modules/lenis/dist/lenis.css"; +@import "./fonts.css"; @import "../corex/corex.css"; @source "../corex"; @source "../../lib"; -@import "./blog.css"; +@import "./paper.css"; @import "./prose.css"; -@import "./home.css"; -@import "./home-numbers.css"; @custom-variant dark (&:where([data-mode=dark], [data-mode=dark] *)); 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.js b/assets/js/landing.js index 2b50f0b..ab573a4 100644 --- a/assets/js/landing.js +++ b/assets/js/landing.js @@ -1,5 +1,4 @@ import { bindLandingScrollChrome } from "./landing-scroll-chrome.js" -import { runLandingParallax } from "./landing-parallax.js" export function initLanding() { const root = document.querySelector("[data-landing]") @@ -7,8 +6,6 @@ export function initLanding() { return () => {} } - const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)") - .matches const stickyBar = document.querySelector("[data-sticky-bar]") const heroBoundary = root.querySelector("[data-hero-sentinel]") || @@ -16,16 +13,7 @@ export function initLanding() { 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() - }) - } + const teardown = bindLandingScrollChrome(stickyBar, heroBoundary, progressFill) window.addEventListener("pagehide", teardown, { once: true }) 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 0000000000000000000000000000000000000000..01383d7e5c4340847b8fe4570ad0458e6582989d GIT binary patch literal 36932 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 literal 0 HcmV?d00001 diff --git a/extra/fonts/fraunces-latin-wght-normal.woff2 b/extra/fonts/fraunces-latin-wght-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..32e1ccc852fdd25f4aaf8822584fe643c1cbf667 GIT binary patch literal 36620 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

(@n7gb``R!s+W~d}p5oY(=~HEZ>sL0}i(v-G^zY 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0804aaff92260b5359330f63e05b9ac25ccabeee GIT binary patch literal 14708 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

-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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cd5102a44c763f1cfe9f2b199fd5d7257491c0dd GIT binary patch literal 40404 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5a3fbbdae8f66414a2754017de622f3c74c536b0 GIT binary patch literal 38404 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3a70b258d0fb12cb411ab56542dcd15050a859e2 GIT binary patch literal 50316 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 literal 0 HcmV?d00001 diff --git a/lib/layouts/post_layout.ex b/lib/layouts/post_layout.ex index 6da7275..a4df1c7 100644 --- a/lib/layouts/post_layout.ex +++ b/lib/layouts/post_layout.ex @@ -34,30 +34,38 @@ defmodule Soonex.PostLayout do ~H"""

-
- <.pager id="soonex-tags-pagination" count={length(@tag_cards)} page_size={1} /> + <.pager id="soonex-tags-pagination" count={length(@tag_cards)} page_size={6} /> diff --git a/lib/soonex/accessibility.ex b/lib/soonex/accessibility.ex index 3477db8..05629c4 100644 --- a/lib/soonex/accessibility.ex +++ b/lib/soonex/accessibility.ex @@ -126,7 +126,21 @@ defmodule Soonex.Accessibility do final_focus={"dialog:#{accessibility_dialog_id()}:trigger"} > <:trigger class={@trigger_class} aria_label="Accessibility"> - <.heroicon name="hero-adjustments-horizontal" /> + <:title>Accessibility <:description> From ecf3fc0f84a5d4083f2424b8f710e1aa046b40cc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 00:30:55 +0000 Subject: [PATCH 20/65] Give each home band a unique heading for landmark names. Axe landmark-unique failed when several sections shared the same lorem title. Co-authored-by: Karim Semmoud --- lib/pages/home/capita.ex | 2 +- lib/pages/home/faq.ex | 2 +- lib/pages/home/highlights.ex | 2 +- lib/pages/home/numeri.ex | 2 +- lib/pages/home/officia.ex | 2 +- lib/pages/home/voces.ex | 2 +- lib/pages/home/waitlist.ex | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pages/home/capita.ex b/lib/pages/home/capita.ex index b6f9870..422392b 100644 --- a/lib/pages/home/capita.ex +++ b/lib/pages/home/capita.ex @@ -18,7 +18,7 @@ defmodule Soonex.HomePage.Capita do tone={:surface} > <:title> - Lorem ipsum dolor sit + Consectetur adipiscing elit <:lede> Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index 9f33e09..17dff31 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -10,7 +10,7 @@ defmodule Soonex.HomePage.Faq do ~H""" <.block id="quaestiones" labelledby="soonex-faq-heading" eyebrow="Questions" layout={:sticky}> <:title> - Lorem ipsum dolor sit + Laboris nisi ut aliquip <:lede> Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. diff --git a/lib/pages/home/highlights.ex b/lib/pages/home/highlights.ex index 108b21b..00da944 100644 --- a/lib/pages/home/highlights.ex +++ b/lib/pages/home/highlights.ex @@ -28,7 +28,7 @@ defmodule Soonex.HomePage.Highlights do tone={:root} > <:title> - Lorem ipsum dolor sit + Veniam quis nostrud <:lede> Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. diff --git a/lib/pages/home/numeri.ex b/lib/pages/home/numeri.ex index a95c89f..fcc2768 100644 --- a/lib/pages/home/numeri.ex +++ b/lib/pages/home/numeri.ex @@ -18,7 +18,7 @@ defmodule Soonex.HomePage.Numeri do compact > <:title> - Lorem ipsum dolor sit + Aliqua ut enim ad minim <:lede> Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. diff --git a/lib/pages/home/officia.ex b/lib/pages/home/officia.ex index fa358a0..0520a3c 100644 --- a/lib/pages/home/officia.ex +++ b/lib/pages/home/officia.ex @@ -17,7 +17,7 @@ defmodule Soonex.HomePage.Officia do tone={:root} > <:title> - Lorem ipsum dolor sit + Sed do eiusmod tempor <:lede> Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. diff --git a/lib/pages/home/voces.ex b/lib/pages/home/voces.ex index f8a3582..2f191da 100644 --- a/lib/pages/home/voces.ex +++ b/lib/pages/home/voces.ex @@ -20,7 +20,7 @@ defmodule Soonex.HomePage.Voces do tone={:root} > <:title> - Lorem ipsum dolor sit amet + Ut labore et dolore magna <:lede> Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. diff --git a/lib/pages/home/waitlist.ex b/lib/pages/home/waitlist.ex index e436f98..62c70c7 100644 --- a/lib/pages/home/waitlist.ex +++ b/lib/pages/home/waitlist.ex @@ -18,7 +18,7 @@ defmodule Soonex.HomePage.Waitlist do tone={:surface} > <:title> - Lorem ipsum dolor sit + Commodo consequat <:lede> Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. From d4b4f878b22d687126443c742ec7f211fb1f6a05 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 02:45:01 +0000 Subject: [PATCH 21/65] Give Soonex an editorial pass: Select nav, sticky beats, and token motion. Mobile hamburger is a Corex Select left of the logo. Marquee stays in color. Capita drops the dead collapsible. Timeline headings pin per beat. Voces and journal carousels are vertical with padding. Waitlist is a centered column with a Role select. FAQ is richer. Accessibility trigger is larger. Scroll reveal and card lift are host CSS, off when motion is reduced. Co-authored-by: Karim Semmoud --- assets/css/chrome.css | 61 +++++++++++++--- assets/css/hosts.css | 55 +++++++++++++++ assets/js/landing-scroll-chrome.js | 3 + lib/layouts/root/nav.ex | 46 +++++++----- lib/layouts/section.ex | 4 +- lib/pages/home/capita.ex | 28 +++----- lib/pages/home/faq.ex | 110 ++++++++++++++++++++++++----- lib/pages/home/hero.ex | 4 +- lib/pages/home/highlights.ex | 10 +-- lib/pages/home/kalendarium.ex | 109 +++++++++++++++++++++------- lib/pages/home/officia.ex | 9 ++- lib/pages/home/socii.ex | 3 +- lib/pages/home/voces.ex | 6 +- lib/pages/home/waitlist.ex | 50 ++++++++----- lib/soonex/accessibility.ex | 6 +- 15 files changed, 384 insertions(+), 120 deletions(-) diff --git a/assets/css/chrome.css b/assets/css/chrome.css index c37c2b0..5506b62 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -1,9 +1,62 @@ /* Tiny layout helpers that utilities cannot express. */ +html { + scroll-behavior: smooth; +} + +html[data-motion="reduce"] { + scroll-behavior: auto; +} + [data-motion="reduce"] [data-site-header] { transition: none; } +@supports (animation-timeline: view()) { + @keyframes soonex-reveal { + from { + transform: translateY(var(--spacing-space)); + } + + to { + transform: translateY(0); + } + } + + .soonex-reveal { + animation-name: soonex-reveal; + animation-duration: var(--duration-slow); + animation-timing-function: ease-out; + animation-fill-mode: both; + animation-timeline: view(); + animation-range: entry 0% entry 40%; + } + + html[data-motion="reduce"] .soonex-reveal { + animation: none; + transform: none; + } +} + +.soonex-card-motion { + transition: + transform var(--duration-fast) ease, + border-color var(--duration-fast) ease; +} + +@media (hover: hover) { + .soonex-card-motion:hover { + transform: translateY(calc(var(--spacing-space-sm) * -1)); + border-color: var(--color-brand); + } +} + +html[data-motion="reduce"] .soonex-card-motion, +html[data-motion="reduce"] .soonex-card-motion:hover { + transform: none; + transition: none; +} + [data-site-header] [data-header-bar] { min-height: var(--spacing-size); } @@ -79,14 +132,6 @@ height: 3.5rem; } -.marquee img { - filter: grayscale(1); -} - -[data-mode="dark"] .marquee img { - filter: grayscale(1) invert(1); -} - .marquee [data-scope="marquee"][data-part="edge"] { --edge-background: var(--color-root); } diff --git a/assets/css/hosts.css b/assets/css/hosts.css index ab90bc7..5631980 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -7,6 +7,7 @@ .number-input.ui-width-full, .date-picker.ui-width-full, .data-table.ui-width-full, +.select.ui-width-full, .carousel.ui-width-full, .collapsible.ui-width-full, .switch.ui-width-full { @@ -14,6 +15,11 @@ width: 100%; } +.data-table.ui-width-fit { + width: fit-content; + max-width: 100%; +} + .carousel.ui-width-full { max-height: none; isolation: isolate; @@ -29,6 +35,55 @@ z-index: 1; } +.carousel.ui-width-full + [data-scope="carousel"][data-part="root"][data-orientation="vertical"] { + max-height: min(80svh, var(--container-md)); + padding-inline-end: var(--spacing-space); +} + +.select.soonex-nav-select [data-scope="select"][data-part="trigger"] { + width: auto; + min-width: var(--ctl-size, var(--spacing-size)); + margin-inline-end: 0; + aspect-ratio: 1 / 1; + padding-inline: 0; + justify-content: center; +} + +.select.soonex-nav-select [data-scope="select"][data-part="item-text"] { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.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; +} + +.soonex-a11y-trigger { + padding: 0; +} + +.soonex-a11y-trigger svg { + width: var(--text-2xl); + height: var(--text-2xl); +} + .menu.ui-width-fit [data-scope="menu"][data-part="root"] > [data-scope="menu"][data-part="trigger"] { width: auto; min-width: var(--ctl-size, var(--spacing-size)); diff --git a/assets/js/landing-scroll-chrome.js b/assets/js/landing-scroll-chrome.js index 6e585b7..77aa5cd 100644 --- a/assets/js/landing-scroll-chrome.js +++ b/assets/js/landing-scroll-chrome.js @@ -59,6 +59,9 @@ export function bindLandingScrollChrome(header, countdown, heroBoundary, progres const p = scrollProgress01() progressFill.style.transform = `scaleX(${p})` progressFill.style.transformOrigin = "left center" + if (motionReduce()) { + progressFill.style.transition = "none" + } } rafId = requestAnimationFrame(tick) } diff --git a/lib/layouts/root/nav.ex b/lib/layouts/root/nav.ex index c2edffd..0834b45 100644 --- a/lib/layouts/root/nav.ex +++ b/lib/layouts/root/nav.ex @@ -12,7 +12,7 @@ defmodule Soonex.Layouts.Root.Nav do attr(:page_path, :string, default: "/") def site_nav(assigns) do - assigns = assign(assigns, :menu_items, menu_items()) + assigns = assign(assigns, :nav_select_items, nav_select_items()) ~H"""
- <.lockup /> +
+ <.select + id="soonex-mobile-nav" + class="select ui-size-sm ui-width-fit soonex-nav-select lg:hidden" + redirect + update_trigger={false} + positioning={ + %Corex.Positioning{ + placement: "bottom-start", + same_width: false, + gutter: 8, + fit_viewport: true, + strategy: "fixed" + } + } + translation={%Corex.Select.Translation{placeholder: "Open menu"}} + items={@nav_select_items} + > + <:trigger> + <.heroicon name="hero-bars-3" /> + + + <.lockup /> +
@@ -118,8 +126,8 @@ defmodule Soonex.Layouts.Root.Nav do ] end - defp menu_items do - Corex.Tree.new([ + defp nav_select_items do + Corex.List.new([ %{ label: "Product", value: "principia", diff --git a/lib/layouts/section.ex b/lib/layouts/section.ex index cc949cb..1d417a5 100644 --- a/lib/layouts/section.ex +++ b/lib/layouts/section.ex @@ -49,8 +49,8 @@ defmodule Soonex.Layouts.Section do """ end - defp section_class(true), do: Shell.section_compact() - defp section_class(false), do: Shell.section() + defp section_class(true), do: "#{Shell.section_compact()} soonex-reveal" + defp section_class(false), do: "#{Shell.section()} soonex-reveal" defp tone_class(:root), do: "bg-root" defp tone_class(:surface), do: "border-y border-border bg-surface" diff --git a/lib/pages/home/capita.ex b/lib/pages/home/capita.ex index 422392b..4c94899 100644 --- a/lib/pages/home/capita.ex +++ b/lib/pages/home/capita.ex @@ -2,7 +2,6 @@ defmodule Soonex.HomePage.Capita do @moduledoc false use Phoenix.Component - use Corex import Soonex.Layouts.Media, only: [photo: 1] import Soonex.Layouts.Section, only: [block: 1] @@ -24,7 +23,7 @@ defmodule Soonex.HomePage.Capita do Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
-
+
<.photo src="/images/photos/studio.jpg" alt="A calm studio interior with a low sofa, plants, and a large window" @@ -40,26 +39,9 @@ defmodule Soonex.HomePage.Capita do
-
-

Ipsum

- <.collapsible id="soonex-capita-collapsible" class="collapsible ui-width-full mt-4"> - <:trigger>Read more - <:closed> - <.heroicon name="hero-chevron-down" /> - - <:content> -

- 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. -

- - -
-

{cell.kicker}

{cell.title}

@@ -72,6 +54,12 @@ defmodule Soonex.HomePage.Capita do defp cells do [ + %{ + kicker: "Ipsum", + title: "Sed do eiusmod tempor", + body: + "Incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + }, %{ kicker: "Dolor", title: "Sit amet consectetur", diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index 17dff31..b31bb9e 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -25,7 +25,7 @@ defmodule Soonex.HomePage.Faq do class="accordion ui-accent ui-size-md mx-auto w-full max-w-none" multiple={false} collapsible={false} - value="what" + value="ships" items={faq_items()} > <:trigger :let={item}> @@ -33,6 +33,7 @@ defmodule Soonex.HomePage.Faq do <:content :let={item}>

{item.content}

+

{para}

<:indicator> <.heroicon name="hero-chevron-right" /> @@ -42,43 +43,118 @@ defmodule Soonex.HomePage.Faq do """ end + defp faq_paragraphs(%{meta: %{paragraphs: paragraphs}}) when is_list(paragraphs), do: paragraphs + defp faq_paragraphs(_), do: [] + defp faq_items do Corex.Content.new([ %{ - value: "what", - label: "Lorem ipsum dolor sit amet?", + value: "ships", + label: "What ships when I fork this template?", content: - "Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam." + "A waitlist landing page, a journal, countdown chrome, and the Corex controls already on this page. Photography, copy, and the launch date are placeholders you replace.", + meta: %{ + paragraphs: [ + "Lorem bands stay in place so the layout holds while you swap headlines. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip.", + "The stack is Tableau plus Phoenix LiveView. No extra gallery, no extra CSS framework. Keep the tokens and the page still looks like a product site." + ] + } }, %{ - value: "npm", - label: "Quis nostrud exercitation ullamco?", + value: "waitlist", + label: "Does the waitlist collect real addresses?", content: - "Laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore." + "Not in this demo. Submit still shows a toast so you can see the success path without storing mailboxes.", + meta: %{ + paragraphs: [ + "Wire the same form to your list later. Interests, role, team size, and launch notes are already named fields.", + "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. The hero card and the closing band share that pattern." + ] + } }, %{ value: "themes", - label: "Eu fugiat nulla pariatur?", + label: "How do themes change without forking Corex CSS?", content: - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + "Theme, brand, and accent live in config. The floating Template Options panel switches neo, uno, duo, and leo on this device.", + meta: %{ + paragraphs: [ + "Host CSS only retunes layout. It does not copy component recipes or set overlay scale, duration, or opacity.", + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + ] + } }, %{ - value: "i18n", - label: "Sed ut perspiciatis unde omnis?", + value: "journal", + label: "Where does the journal live, and can I add posts?", content: - "Iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore." + "Posts are Markdown under the journal route. The home carousel is a short reading list; the index paginates the rest.", + meta: %{ + paragraphs: [ + "Add a file, rebuild, and the cover, date, and title show up. Tags stay optional.", + "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + ] + } }, %{ - value: "waitlist", - label: "Veritatis et quasi architecto?", + value: "motion", + label: "How does scroll motion behave with reduced motion?", content: - "Beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit." + "Scroll reveal and card lift live in host CSS. They follow scroll and hover; they do not fight the type.", + meta: %{ + paragraphs: [ + "Set Motion to Reduced in Accessibility and those animations stop. The condensed header and progress bar stay as chrome.", + "Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores." + ] + } }, %{ value: "a11y", - label: "Sed quia consequuntur magni?", + label: "What do the accessibility controls change?", + content: + "The person-in-circle control at the corner opens zoom, contrast, motion, cursor, focus, and link underline for this device.", + meta: %{ + paragraphs: [ + "Choices persist in local storage and write data attributes on the document. Reset returns the defaults.", + "Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit." + ] + } + }, + %{ + value: "toolchain", + label: "Do I need npm or a JavaScript bundler to start?", + content: + "No. Mix, Tailwind, and esbuild already in the project are enough. There is no package.json and no Lenis.", + meta: %{ + paragraphs: [ + "Select, marquee, carousel, and accordion are Corex hooks. Host scripts only bind header chrome and the waitlist toast.", + "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam." + ] + } + }, + %{ + value: "countdown", + label: "How is the countdown wired to a launch date?", + content: + "The hero timer counts down to 1 September in this demo. The header timer stays in the bar and appears after the hero.", + meta: %{ + paragraphs: [ + "Change the target in the home template when you have a real date. Days, hours, minutes, and seconds stay on one row.", + "Nisi ut aliquid ex ea commodi consequatur. Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse." + ] + } + }, + %{ + value: "brand", + label: "Can I keep the photography and marquee after a rebrand?", content: - "Dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet." + "Yes. Swap the stills and the lockup. The marquee is Corex as-is, with color marks and a fade to the page root.", + meta: %{ + paragraphs: [ + "Grey filters are gone on purpose so logos stay on-brand. Pause on hover still holds a mark in place.", + "Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat." + ] + } } ]) end diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index a0e7fdf..0734c78 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -97,7 +97,7 @@ defmodule Soonex.HomePage.Hero do placeholder="you@studio.dev" class="native-input ui-size-md ui-width-full" > - <:label>Email + <:label class="sr-only">Email <.radio_group @@ -111,7 +111,7 @@ defmodule Soonex.HomePage.Hero do %{value: "designer", label: "Designer"} ]} > - <:label>I am a + <:label>I am a <.switch diff --git a/lib/pages/home/highlights.ex b/lib/pages/home/highlights.ex index 00da944..345cdfd 100644 --- a/lib/pages/home/highlights.ex +++ b/lib/pages/home/highlights.ex @@ -46,7 +46,9 @@ defmodule Soonex.HomePage.Highlights do id="soonex-acta-carousel" class="carousel ui-brand ui-width-full max-w-none" items={@highlight_posts} + orientation="vertical" spacing="1.5rem" + padding="2.5rem" > <:item :let={post}>
@@ -68,18 +70,18 @@ defmodule Soonex.HomePage.Highlights do

<.navigate to={Soonex.Public.path(post.permalink)} - class="link ui-brand mt-6 w-fit" + class="button ui-ghost ui-brand ui-size-sm mt-6 w-fit" > - Read <.heroicon name="hero-arrow-up-right" /> + Read more <.heroicon name="hero-arrow-up-right" />
<:prev_trigger> - <.heroicon name="hero-arrow-left" /> + <.heroicon name="hero-chevron-up" /> <:next_trigger> - <.heroicon name="hero-arrow-right" /> + <.heroicon name="hero-chevron-down" /> diff --git a/lib/pages/home/kalendarium.ex b/lib/pages/home/kalendarium.ex index d64a2e0..12b7bfd 100644 --- a/lib/pages/home/kalendarium.ex +++ b/lib/pages/home/kalendarium.ex @@ -2,7 +2,6 @@ defmodule Soonex.HomePage.Kalendarium do @moduledoc false use Phoenix.Component - use Corex import Soonex.Layouts.Media, only: [photo: 1] import Soonex.Layouts.Section, only: [block: 1] @@ -16,7 +15,6 @@ defmodule Soonex.HomePage.Kalendarium do labelledby="soonex-kalendarium-heading" eyebrow="Calendar" tone={:surface} - layout={:sticky} > <:title> Road to 1 September @@ -25,7 +23,7 @@ defmodule Soonex.HomePage.Kalendarium do Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -
+
<.photo src="/images/covers/shipping.jpg" alt="A laptop on a wooden desk with a code editor open" @@ -35,11 +33,32 @@ defmodule Soonex.HomePage.Kalendarium do />
    -
  1. - -

    {beat.moment}

    -

    {beat.title}

    -

    {beat.body}

    +
  2. + +
    +

    {beat.moment}

    +

    + {beat.title} +

    +
    +
    +

    {para}

    +
      +
    • {point}
    • +
    +
    + <.photo + src={beat.image.src} + alt={beat.image.alt} + width={1400} + height={900} + class="absolute inset-0 size-full" + /> +
    +
@@ -51,50 +70,92 @@ defmodule Soonex.HomePage.Kalendarium do %{ moment: "12 March 2026", title: "Lorem ipsum dolor", - body: - "Sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + paragraphs: [ + "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.", + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident." + ], + points: [ + "Lorem checklist item one", + "Ipsum notes for the studio", + "Dolor review with the team" + ], + image: nil }, %{ moment: "8 April 2026", title: "Ut enim ad minim", - body: - "Veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor." + paragraphs: [ + "Veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.", + "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam." + ], + points: ["Aliqua draft", "Minima veniam pass"], + image: nil }, %{ moment: "8 May 2026", title: "In reprehenderit voluptate", - body: - "Velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident." + paragraphs: [ + "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.", + "Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos." + ], + points: [], + image: %{ + src: "/images/photos/studio.jpg", + alt: "A calm studio interior with a low sofa, plants, and a large window" + } }, %{ moment: "1 June 2026", title: "Sunt in culpa qui", - body: - "Officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem." + paragraphs: [ + "Officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.", + "Eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo." + ], + points: ["Voluptatem first pass", "Architecto second pass"], + image: nil }, %{ moment: "14 July 2026", title: "Nemo enim ipsam", - body: - "Voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos." + paragraphs: [ + "Voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.", + "Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit." + ], + points: ["Magni dolores", "Ratione sequi"], + image: nil }, %{ moment: "2 August 2026", title: "Neque porro quisquam", - body: - "Est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit. Nam libero tempore, cum soluta nobis." + paragraphs: [ + "Est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit.", + "Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae." + ], + points: [], + image: %{ + src: "/images/photos/orbit.jpg", + alt: "Earth from orbit against a dark sky" + } }, %{ moment: "18 August 2026", title: "Temporibus autem quibusdam", - body: - "Et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae." + paragraphs: [ + "Et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae.", + "Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur." + ], + points: ["Debitis check", "Voluptates pass"], + image: nil }, %{ moment: "1 September 2026", title: "Itaque earum rerum", - body: - "Hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus." + paragraphs: [ + "Hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.", + "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti." + ], + points: ["Launch day", "Public notes"], + image: nil } ] end diff --git a/lib/pages/home/officia.ex b/lib/pages/home/officia.ex index 0520a3c..9ed835e 100644 --- a/lib/pages/home/officia.ex +++ b/lib/pages/home/officia.ex @@ -23,16 +23,19 @@ defmodule Soonex.HomePage.Officia do Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
-
+

{plan.name}

{plan.headline}

{plan.body}

-
+
<.data_table id="soonex-plan-table" - class="data-table ui-brand ui-size-sm ui-width-full max-w-none max-h-none" + class="data-table ui-brand ui-size-sm ui-width-fit max-h-none" rows={plan_rows()} > <:col :let={row} label="Feature">{row.feature} diff --git a/lib/pages/home/socii.ex b/lib/pages/home/socii.ex index 8b9142d..5ce41ac 100644 --- a/lib/pages/home/socii.ex +++ b/lib/pages/home/socii.ex @@ -12,9 +12,10 @@ defmodule Soonex.HomePage.Socii do <.marquee id="soonex-tech-marquee" class="marquee ui-accent ui-width-full max-w-none" - duration={32} + duration={24} spacing="3.5rem" pause_on_interaction={true} + auto_fill items={@tech} > <:item :let={item}> diff --git a/lib/pages/home/voces.ex b/lib/pages/home/voces.ex index 2f191da..34cd297 100644 --- a/lib/pages/home/voces.ex +++ b/lib/pages/home/voces.ex @@ -30,7 +30,9 @@ defmodule Soonex.HomePage.Voces do class="carousel ui-brand ui-width-full max-w-none" items={@voices} loop + orientation="vertical" spacing="1.5rem" + padding="2.5rem" > <:item :let={voice}>
@@ -55,10 +57,10 @@ defmodule Soonex.HomePage.Voces do
<:prev_trigger> - <.heroicon name="hero-arrow-left" /> + <.heroicon name="hero-chevron-up" /> <:next_trigger> - <.heroicon name="hero-arrow-right" /> + <.heroicon name="hero-chevron-down" /> diff --git a/lib/pages/home/waitlist.ex b/lib/pages/home/waitlist.ex index 62c70c7..7683034 100644 --- a/lib/pages/home/waitlist.ex +++ b/lib/pages/home/waitlist.ex @@ -10,12 +10,15 @@ defmodule Soonex.HomePage.Waitlist do alias Soonex.Layouts.Shell def waitlist(assigns) do + assigns = assign(assigns, :role_items, role_items()) + ~H""" <.block id="epistula" labelledby="soonex-waitlist-heading" eyebrow="Waitlist" tone={:surface} + align={:center} > <:title> Commodo consequat @@ -23,8 +26,8 @@ defmodule Soonex.HomePage.Waitlist do <:lede> Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -
-
+
+
<.photo src="/images/photos/orbit.jpg" alt="Earth from orbit against a dark sky" @@ -35,7 +38,7 @@ defmodule Soonex.HomePage.Waitlist do
@@ -49,9 +52,28 @@ defmodule Soonex.HomePage.Waitlist do placeholder="you@studio.dev" class="native-input ui-size-md ui-width-full" > - <:label>Email + <:label class="sr-only">Email + <.select + id="soonex-waitlist-role" + name="waitlist[role]" + class="select ui-brand ui-size-md ui-width-full" + value={["founder"]} + positioning={ + %Corex.Positioning{ + placement: "bottom-start", + same_width: true + } + } + items={@role_items} + > + <:label>Role + <:trigger> + <.heroicon name="hero-chevron-down" /> + + + <.tags_input id="soonex-waitlist-interests" name="waitlist[interests]" @@ -96,18 +118,6 @@ defmodule Soonex.HomePage.Waitlist do <:label>Email me launch notes - <.checkbox - id="soonex-waitlist-updates" - name="waitlist[updates]" - checked={true} - class="checkbox ui-accent ui-size-md" - > - <:indicator> - <.heroicon name="hero-check" /> - - <:label>I understand this is a demo form - - @@ -117,4 +127,12 @@ defmodule Soonex.HomePage.Waitlist do """ end + + defp role_items do + Corex.List.new([ + %{label: "Founder", value: "founder"}, + %{label: "Engineer", value: "engineer"}, + %{label: "Designer", value: "designer"} + ]) + end end diff --git a/lib/soonex/accessibility.ex b/lib/soonex/accessibility.ex index 05629c4..dce474b 100644 --- a/lib/soonex/accessibility.ex +++ b/lib/soonex/accessibility.ex @@ -110,7 +110,9 @@ defmodule Soonex.Accessibility do def accessibility_dialog_id, do: "a11y-dialog" - attr(:trigger_class, :string, default: "button ui-ghost ui-size-sm ui-trigger--circle") + attr(:trigger_class, :string, + default: "button ui-ghost ui-size-md ui-trigger--circle p-0 soonex-a11y-trigger" + ) def accessibility_panel(assigns) do assigns = assign(assigns, :axes, Accessibility.axes()) @@ -134,7 +136,7 @@ defmodule Soonex.Accessibility do stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" - class="size-5" + class="size-6" aria-hidden="true" > From 5dadb3ed0cb2b0c9fb5efcd181b7188d1f47a42c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 03:18:31 +0000 Subject: [PATCH 22/65] Show mobile nav Select labels; only clip the trigger text. The hamburger clip rule targeted every item-text node, so the open list was effectively empty. Scope the clip to the trigger and give the panel a min-width so Product / Journal / Questions stay readable. Co-authored-by: Karim Semmoud --- assets/css/hosts.css | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/assets/css/hosts.css b/assets/css/hosts.css index 5631980..4ae74bd 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -50,7 +50,7 @@ justify-content: center; } -.select.soonex-nav-select [data-scope="select"][data-part="item-text"] { +.select.soonex-nav-select [data-scope="select"][data-part="trigger"] > [data-part="item-text"] { position: absolute; width: 1px; height: 1px; @@ -62,6 +62,12 @@ border-width: 0; } +.select.soonex-nav-select [data-scope="select"][data-part="content"] { + z-index: 60; + min-width: var(--container-3xs); + width: max-content; +} + .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; From 39542d80c92843dcaf79523de4c60fda3d1615c8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 15:13:06 +0000 Subject: [PATCH 23/65] Give the four Corex themes a quieter professional motion language. Neo uses a deeper ink blue and tighter radius. Duo shifts to amber so the theme switcher shows the Filament-adjacent warmth. Host CSS shares one deceleration curve, cards hover on border only, and the header border uses token duration instead of Tailwind duration-200. Co-authored-by: Karim Semmoud --- assets/css/chrome.css | 12 ++++++------ assets/css/hosts.css | 9 +++++++++ config/config.exs | 26 +++++++++++++------------- lib/layouts/root/nav.ex | 2 +- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/assets/css/chrome.css b/assets/css/chrome.css index 5506b62..a09cff1 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -26,7 +26,7 @@ html[data-motion="reduce"] { .soonex-reveal { animation-name: soonex-reveal; animation-duration: var(--duration-slow); - animation-timing-function: ease-out; + animation-timing-function: var(--soonex-ease); animation-fill-mode: both; animation-timeline: view(); animation-range: entry 0% entry 40%; @@ -39,24 +39,24 @@ html[data-motion="reduce"] { } .soonex-card-motion { - transition: - transform var(--duration-fast) ease, - border-color var(--duration-fast) ease; + transition: border-color var(--duration-fast) var(--soonex-ease); } @media (hover: hover) { .soonex-card-motion:hover { - transform: translateY(calc(var(--spacing-space-sm) * -1)); border-color: var(--color-brand); } } html[data-motion="reduce"] .soonex-card-motion, html[data-motion="reduce"] .soonex-card-motion:hover { - transform: none; transition: none; } +[data-site-header] { + transition: border-color var(--duration-normal) var(--soonex-ease); +} + [data-site-header] [data-header-bar] { min-height: var(--spacing-size); } diff --git a/assets/css/hosts.css b/assets/css/hosts.css index 4ae74bd..2eca770 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -1,5 +1,14 @@ /* 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, diff --git a/config/config.exs b/config/config.exs index d88762a..9138e34 100644 --- a/config/config.exs +++ b/config/config.exs @@ -134,9 +134,9 @@ config :corex_design, themes: %{ neo: %{ seeds: %{ - neutral: "#F3EDE3", - accent: "#191614", - brand: "#2A4DFF", + neutral: "#EEEAE4", + accent: "#161618", + brand: "#1D4ED8", alert: "#B42318", info: "#0E7490", success: "#15803D" @@ -173,14 +173,14 @@ config :corex_design, }, dimensions: %{ radius: %{ - xs: 0.42, - sm: 0.72, - md: 1.05, - lg: 1.45, - xl: 1.9, - "2xl": 2.5, - "3xl": 3.2, - "4xl": 4.0, + 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: %{ @@ -219,7 +219,7 @@ config :corex_design, seeds: %{ neutral: "#D7E8E3", accent: "#071614", - brand: "#0B8F7B", + brand: "#0F766E", alert: "#B91C1C", info: "#0369A1", success: "#166534" @@ -294,7 +294,7 @@ config :corex_design, seeds: %{ neutral: "#F7EDE4", accent: "#3A2618", - brand: "#7C3AED", + brand: "#C2410C", alert: "#9F1239", info: "#3D5278", success: "#3F6B4E" diff --git a/lib/layouts/root/nav.ex b/lib/layouts/root/nav.ex index 0834b45..cc30d2f 100644 --- a/lib/layouts/root/nav.ex +++ b/lib/layouts/root/nav.ex @@ -17,7 +17,7 @@ defmodule Soonex.Layouts.Root.Nav do ~H"""
Date: Thu, 27 Aug 2026 15:34:50 +0000 Subject: [PATCH 24/65] Make card hover a token shadow, not a lift. Border-color alone was too easy to miss against the frame. Use --shadow-sm with the shared easing so plans and bento still feel still, not bouncy. Co-authored-by: Karim Semmoud --- assets/css/chrome.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/assets/css/chrome.css b/assets/css/chrome.css index a09cff1..0bc25ab 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -39,12 +39,15 @@ html[data-motion="reduce"] { } .soonex-card-motion { - transition: border-color var(--duration-fast) var(--soonex-ease); + transition: + border-color var(--duration-fast) var(--soonex-ease), + box-shadow var(--duration-fast) var(--soonex-ease); } @media (hover: hover) { .soonex-card-motion:hover { border-color: var(--color-brand); + box-shadow: var(--shadow-sm); } } From e5fe846a07a97f0266876b08076a771f8c80751e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 16:46:46 +0000 Subject: [PATCH 25/65] Give the toolchain marquee brand colors and Corex demo motion. Match the Corex marquee demo (duration 20, 2rem spacing, pause on hover, no ui-accent), color the Simple Icon fills, and lock duration/edges in host CSS so the strip no longer greys out or jumps. Co-authored-by: Karim Semmoud --- assets/css/chrome.css | 27 ++++++++++++++++++++++++++ assets/css/hosts.css | 1 + extra/images/tech/ecto-mark.svg | 2 +- extra/images/tech/elixir.svg | 2 +- extra/images/tech/phoenixframework.svg | 2 +- extra/images/tech/tableau.svg | 2 +- extra/images/tech/tailwindcss.svg | 2 +- extra/images/tech/typescript.svg | 2 +- extra/images/tech/zag.svg | 2 +- lib/pages/home/socii.ex | 18 ++++++++--------- 10 files changed, 43 insertions(+), 17 deletions(-) diff --git a/assets/css/chrome.css b/assets/css/chrome.css index 0bc25ab..8bb9813 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -135,10 +135,37 @@ html[data-motion="reduce"] .soonex-card-motion:hover { height: 3.5rem; } +/* Corex demo: duration={20} spacing="2rem" pause_on_interaction, no ui-accent. + Zag writes a speed-derived --marquee-duration on every render, then Corex + overwrites it from data-duration. That flicker restarts the CSS animation. */ +#soonex-tech-marquee.marquee [data-scope="marquee"][data-part="root"] { + --marquee-duration: 20s !important; +} + +#soonex-tech-marquee.marquee[data-loading], +#soonex-tech-marquee.marquee:not([data-loading]) { + opacity: 1; + transition: none; +} + +.marquee [data-scope="marquee"][data-part="content"] { + align-items: center; +} + .marquee [data-scope="marquee"][data-part="edge"] { --edge-background: var(--color-root); } +.marquee [data-scope="marquee"][data-part="edge"][data-side="start"], +.marquee [data-scope="marquee"][data-part="edge"][data-side="end"] { + width: 8%; + max-width: 5rem; +} + +html[data-motion="reduce"] .marquee [data-scope="marquee"][data-part="content"] { + animation: none; +} + .data-list.data-list--wide, .tabs.tabs--wide { max-width: none; diff --git a/assets/css/hosts.css b/assets/css/hosts.css index 2eca770..9404788 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -17,6 +17,7 @@ .date-picker.ui-width-full, .data-table.ui-width-full, .select.ui-width-full, +.marquee.ui-width-full, .carousel.ui-width-full, .collapsible.ui-width-full, .switch.ui-width-full { diff --git a/extra/images/tech/ecto-mark.svg b/extra/images/tech/ecto-mark.svg index a8a4937..fd3d8cf 100644 --- a/extra/images/tech/ecto-mark.svg +++ b/extra/images/tech/ecto-mark.svg @@ -1,3 +1,3 @@ - + diff --git a/extra/images/tech/elixir.svg b/extra/images/tech/elixir.svg index 5bd304d..e29f5cf 100644 --- a/extra/images/tech/elixir.svg +++ b/extra/images/tech/elixir.svg @@ -1 +1 @@ -Elixir \ No newline at end of file +Elixir \ No newline at end of file diff --git a/extra/images/tech/phoenixframework.svg b/extra/images/tech/phoenixframework.svg index 4f9cfe8..d17b99f 100644 --- a/extra/images/tech/phoenixframework.svg +++ b/extra/images/tech/phoenixframework.svg @@ -1 +1 @@ -Phoenix Framework \ No newline at end of file +Phoenix Framework \ No newline at end of file diff --git a/extra/images/tech/tableau.svg b/extra/images/tech/tableau.svg index 0c24b37..4f22553 100644 --- a/extra/images/tech/tableau.svg +++ b/extra/images/tech/tableau.svg @@ -1 +1 @@ -Tableau \ No newline at end of file +Tableau \ No newline at end of file diff --git a/extra/images/tech/tailwindcss.svg b/extra/images/tech/tailwindcss.svg index 38b493f..e069b14 100644 --- a/extra/images/tech/tailwindcss.svg +++ b/extra/images/tech/tailwindcss.svg @@ -1 +1 @@ -Tailwind CSS \ No newline at end of file +Tailwind CSS \ No newline at end of file diff --git a/extra/images/tech/typescript.svg b/extra/images/tech/typescript.svg index e8beb74..f8bf69b 100644 --- a/extra/images/tech/typescript.svg +++ b/extra/images/tech/typescript.svg @@ -1 +1 @@ -TypeScript \ No newline at end of file +TypeScript \ No newline at end of file diff --git a/extra/images/tech/zag.svg b/extra/images/tech/zag.svg index 2cacd75..534f334 100644 --- a/extra/images/tech/zag.svg +++ b/extra/images/tech/zag.svg @@ -1,3 +1,3 @@ - + diff --git a/lib/pages/home/socii.ex b/lib/pages/home/socii.ex index 5ce41ac..6514cd1 100644 --- a/lib/pages/home/socii.ex +++ b/lib/pages/home/socii.ex @@ -11,22 +11,20 @@ defmodule Soonex.HomePage.Socii do
<.marquee id="soonex-tech-marquee" - class="marquee ui-accent ui-width-full max-w-none" - duration={24} - spacing="3.5rem" - pause_on_interaction={true} - auto_fill + class="marquee ui-width-full max-w-none" + duration={20} + spacing="2rem" + pause_on_interaction items={@tech} > <:item :let={item}> - + {item.name} From 0b8fc4cc0d93b6023eac23b15cff1ef735dee7b3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 16:57:04 +0000 Subject: [PATCH 26/65] Stop clipping header countdown digits. Keep Corex's 2em flip window and compact the header timer with text-sm, so the digit strip is no longer cropped to 1.25em. Co-authored-by: Karim Semmoud --- assets/css/chrome.css | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/assets/css/chrome.css b/assets/css/chrome.css index 8bb9813..0728a11 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -64,15 +64,20 @@ html[data-motion="reduce"] .soonex-card-motion:hover { min-height: var(--spacing-size); } -/* Header timer stays in flow (invisible until reveal) so the bar never jumps. */ +/* Header timer stays in flow (invisible until reveal) so the bar never jumps. + Corex digits flip in a 2em window (`top: calc(var(--value) * -2em)`). Do not + shrink that window — it clips the strip. Compact the type instead. */ [data-site-header] .timer.ui-size-sm { --timer-digit: var(--text-sm); - --timer-digit-leading: var(--text-sm--line-height); + --timer-digit-leading: 2; +} + +[data-site-header] .timer [data-scope="timer"][data-part="area"] { + align-items: center; } -[data-site-header] .timer [data-scope="timer"][data-part="item"], [data-site-header] .timer [data-scope="timer"][data-part="separator"] { - height: 1.25em; + align-self: center; } [data-site-header] .timer [data-scope="timer"][data-part="item-label"] { From 44f92a6a8b925cf13411a0a533fc534f841cf143 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 17:09:47 +0000 Subject: [PATCH 27/65] Turn the Product strip into a Corex clipboard clone command. Replace the sparse lorem + icon-only copy with the clipboard demo (label, input, Copy/Copied) filling the frame, using the real git clone URL. Co-authored-by: Karim Semmoud --- lib/pages/home/contents.ex | 43 +++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/lib/pages/home/contents.ex b/lib/pages/home/contents.ex index 602f85a..bc13aa6 100644 --- a/lib/pages/home/contents.ex +++ b/lib/pages/home/contents.ex @@ -42,25 +42,30 @@ defmodule Soonex.HomePage.Contents do -
-
-

Lorem ipsum

-
{@snippet}
+
+
+
+

Quick start

+

+ Clone the template, then run mix setup and mix soonex.server. +

+
+ <.clipboard + id="soonex-theme-snippet" + class="clipboard ui-brand ui-width-full ui-size-md min-w-0 flex-1" + value={@snippet} + > + <:label>Clone + <:copy> + <.heroicon name="hero-clipboard" /> + Copy + + <:copied> + <.heroicon name="hero-check" /> + Copied + +
- <.clipboard - id="soonex-theme-snippet" - class="clipboard ui-brand ui-size-sm shrink-0" - value={@snippet} - input={false} - trigger_aria_label="Copy" - > - <:copy> - <.heroicon name="hero-clipboard" /> - - <:copied> - <.heroicon name="hero-check" /> - -
""" @@ -96,6 +101,6 @@ defmodule Soonex.HomePage.Contents do end defp snippet do - "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + "git clone https://github.com/corex-ui/soonex.git" end end From 29793abbbf058e2267ea8091d3485e1c983e289f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 17:11:15 +0000 Subject: [PATCH 28/65] Give the Product clone control a solid Copy trigger. Co-authored-by: Karim Semmoud --- lib/pages/home/contents.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/home/contents.ex b/lib/pages/home/contents.ex index bc13aa6..d581c65 100644 --- a/lib/pages/home/contents.ex +++ b/lib/pages/home/contents.ex @@ -52,7 +52,7 @@ defmodule Soonex.HomePage.Contents do
<.clipboard id="soonex-theme-snippet" - class="clipboard ui-brand ui-width-full ui-size-md min-w-0 flex-1" + class="clipboard ui-brand ui-solid ui-width-full ui-size-md min-w-0 flex-1" value={@snippet} > <:label>Clone From 4e2ab0ee9546be04459598b05b85cc79a8bebc6f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 17:14:27 +0000 Subject: [PATCH 29/65] Dock calendar beat titles flush under the sticky header. Measure the header height into --soonex-header-offset so lg sticky titles sit against the bar instead of top-28. Co-authored-by: Karim Semmoud --- assets/css/chrome.css | 8 ++++++++ assets/css/hosts.css | 1 + assets/js/landing-scroll-chrome.js | 6 ++++++ lib/pages/home/kalendarium.ex | 2 +- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/assets/css/chrome.css b/assets/css/chrome.css index 0728a11..fadf1ef 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -64,6 +64,14 @@ html[data-motion="reduce"] .soonex-card-motion:hover { min-height: var(--spacing-size); } +@media (width >= 64rem) { + .soonex-sticky-beat { + position: sticky; + top: var(--soonex-header-offset); + z-index: 1; + } +} + /* Header timer stays in flow (invisible until reveal) so the bar never jumps. Corex digits flip in a 2em window (`top: calc(var(--value) * -2em)`). Do not shrink that window — it clips the strip. Compact the type instead. */ diff --git a/assets/css/hosts.css b/assets/css/hosts.css index 9404788..7effa84 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -2,6 +2,7 @@ [data-theme] { --soonex-ease: cubic-bezier(0.22, 1, 0.36, 1); + --soonex-header-offset: calc(var(--spacing-size) + 1.5rem); } .button, diff --git a/assets/js/landing-scroll-chrome.js b/assets/js/landing-scroll-chrome.js index 77aa5cd..cb75bc0 100644 --- a/assets/js/landing-scroll-chrome.js +++ b/assets/js/landing-scroll-chrome.js @@ -23,6 +23,7 @@ export function bindLandingScrollChrome(header, countdown, heroBoundary, progres let alive = true let rafId = null + let lastHeaderOffset = "" const pastReveal = () => { if (boundaryEl) { @@ -53,6 +54,11 @@ export function bindLandingScrollChrome(header, countdown, heroBoundary, progres if (motionReduce()) { headerEl.style.transition = "none" } + const offset = `${headerEl.getBoundingClientRect().height}px` + if (offset !== lastHeaderOffset) { + lastHeaderOffset = offset + document.documentElement.style.setProperty("--soonex-header-offset", offset) + } } showCountdown(pastReveal()) if (progressOk) { diff --git a/lib/pages/home/kalendarium.ex b/lib/pages/home/kalendarium.ex index 12b7bfd..bdaf33f 100644 --- a/lib/pages/home/kalendarium.ex +++ b/lib/pages/home/kalendarium.ex @@ -35,7 +35,7 @@ defmodule Soonex.HomePage.Kalendarium do
  1. -
    +

    {beat.moment}

    {beat.title} From 463b7c094f8da5e1738ec77517399994de06fbc4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 17:18:02 +0000 Subject: [PATCH 30/65] Drop leftover reveal transforms so calendar titles can stick. translateY(0) on .soonex-reveal kept a containing block and left a gap under the header. Co-authored-by: Karim Semmoud --- assets/css/chrome.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/css/chrome.css b/assets/css/chrome.css index fadf1ef..1a9cefd 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -19,7 +19,7 @@ html[data-motion="reduce"] { } to { - transform: translateY(0); + transform: none; } } From cb987b5212aecb498fab3b5a70efe608f959e8b1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 17:19:01 +0000 Subject: [PATCH 31/65] Do not hold reveal transforms after the calendar enters. fill-mode both left a translate on the section, so sticky beat titles sat 12px below the header. Co-authored-by: Karim Semmoud --- assets/css/chrome.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/css/chrome.css b/assets/css/chrome.css index 1a9cefd..4d7d6c7 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -27,7 +27,7 @@ html[data-motion="reduce"] { animation-name: soonex-reveal; animation-duration: var(--duration-slow); animation-timing-function: var(--soonex-ease); - animation-fill-mode: both; + animation-fill-mode: none; animation-timeline: view(); animation-range: entry 0% entry 40%; } From 4ff50479794d75ef6b574d32bacf4947da3e5a7d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 17:22:40 +0000 Subject: [PATCH 32/65] Fill calendar beats with quotes, cards, links, and data lists. Keep the 70svh sticky cells, but use a two-column layout so April and the other beats are no longer empty paper. Co-authored-by: Karim Semmoud --- lib/pages/home/kalendarium.ex | 261 ++++++++++++++++++++++++++++++---- 1 file changed, 237 insertions(+), 24 deletions(-) diff --git a/lib/pages/home/kalendarium.ex b/lib/pages/home/kalendarium.ex index bdaf33f..7359cc6 100644 --- a/lib/pages/home/kalendarium.ex +++ b/lib/pages/home/kalendarium.ex @@ -2,6 +2,7 @@ defmodule Soonex.HomePage.Kalendarium do @moduledoc false use Phoenix.Component + use Corex import Soonex.Layouts.Media, only: [photo: 1] import Soonex.Layouts.Section, only: [block: 1] @@ -41,23 +42,60 @@ defmodule Soonex.HomePage.Kalendarium do {beat.title}

    -
    -

    {para}

    -
      -
    • {point}
    • -
    -
    - <.photo - src={beat.image.src} - alt={beat.image.alt} - width={1400} - height={900} - class="absolute inset-0 size-full" - /> +
    +
    +

    {para}

    +
      +
    • {point}
    • +
    +
    + <.photo + src={beat.image.src} + alt={beat.image.alt} + width={1400} + height={900} + class="absolute inset-0 size-full" + /> +
    +
    +
@@ -66,6 +104,11 @@ defmodule Soonex.HomePage.Kalendarium do end defp beats do + product = Soonex.Public.path("/") <> "#principia" + waitlist = Soonex.Public.path("/") <> "#epistula" + journal = Soonex.Public.path("/blog") + questions = Soonex.Public.path("/") <> "#quaestiones" + [ %{ moment: "12 March 2026", @@ -79,7 +122,32 @@ defmodule Soonex.HomePage.Kalendarium do "Ipsum notes for the studio", "Dolor review with the team" ], - image: nil + image: nil, + quote: %{ + text: "Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit.", + cite: "Lorem, founding note" + }, + cards: [ + %{ + kicker: "Product", + title: "Sit amet consectetur", + body: "Adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna.", + to: product, + link: "See the product" + }, + %{ + kicker: "Journal", + title: "Ut labore et dolore", + body: "Magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco.", + to: journal, + link: "Read the journal" + } + ], + links: [ + %{label: "Join waitlist", to: waitlist}, + %{label: "Questions", to: questions} + ], + facts: [] }, %{ moment: "8 April 2026", @@ -89,7 +157,37 @@ defmodule Soonex.HomePage.Kalendarium do "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam." ], points: ["Aliqua draft", "Minima veniam pass"], - image: nil + image: nil, + quote: %{ + text: "Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit.", + cite: "Ipsum, April studio" + }, + cards: [ + %{ + kicker: "Plans", + title: "Aliqua ut enim", + body: "Ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip.", + to: Soonex.Public.path("/") <> "#officia", + link: "Compare plans" + }, + %{ + kicker: "Inside", + title: "Dolor sit amet", + body: "Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.", + to: Soonex.Public.path("/") <> "#capita", + link: "Look inside" + } + ], + links: [ + %{label: "See the product", to: product}, + %{label: "Read the journal", to: journal}, + %{label: "Join waitlist", to: waitlist} + ], + facts: [ + %{label: "Status", content: "Aliqua draft"}, + %{label: "Owner", content: "Studio pass"}, + %{label: "Next", content: "8 May review"} + ] }, %{ moment: "8 May 2026", @@ -102,7 +200,27 @@ defmodule Soonex.HomePage.Kalendarium do image: %{ src: "/images/photos/studio.jpg", alt: "A calm studio interior with a low sofa, plants, and a large window" - } + }, + quote: nil, + cards: [ + %{ + kicker: "Studio", + title: "Velit esse cillum", + body: + "Dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident.", + to: product, + link: "Open product" + }, + %{ + kicker: "Questions", + title: "Sunt in culpa qui", + body: "Officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis.", + to: questions, + link: "Read questions" + } + ], + links: [%{label: "Join waitlist", to: waitlist}], + facts: [] }, %{ moment: "1 June 2026", @@ -112,7 +230,21 @@ defmodule Soonex.HomePage.Kalendarium do "Eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo." ], points: ["Voluptatem first pass", "Architecto second pass"], - image: nil + image: nil, + quote: %{ + text: "Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur.", + cite: "Dolor, June note" + }, + cards: [], + links: [ + %{label: "Compare plans", to: Soonex.Public.path("/") <> "#officia"}, + %{label: "Read the journal", to: journal} + ], + facts: [ + %{label: "Pass", content: "Architecto"}, + %{label: "Review", content: "14 July"}, + %{label: "Owner", content: "Agency"} + ] }, %{ moment: "14 July 2026", @@ -122,7 +254,29 @@ defmodule Soonex.HomePage.Kalendarium do "Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit." ], points: ["Magni dolores", "Ratione sequi"], - image: nil + image: nil, + quote: nil, + cards: [ + %{ + kicker: "Numbers", + title: "Magni dolores eos", + body: "Qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem.", + to: Soonex.Public.path("/") <> "#numeri", + link: "See the numbers" + }, + %{ + kicker: "Voices", + title: "Ipsum quia dolor", + body: "Sit amet, consectetur, adipisci velit. Nam libero tempore, cum soluta nobis.", + to: Soonex.Public.path("/") <> "#voces", + link: "Hear voices" + } + ], + links: [%{label: "Questions", to: questions}, %{label: "Join waitlist", to: waitlist}], + facts: [ + %{label: "Theme", content: "Magni"}, + %{label: "Check", content: "Ratione sequi"} + ] }, %{ moment: "2 August 2026", @@ -135,7 +289,18 @@ defmodule Soonex.HomePage.Kalendarium do image: %{ src: "/images/photos/orbit.jpg", alt: "Earth from orbit against a dark sky" - } + }, + quote: %{ + text: + "Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus.", + cite: "Sit, August orbit" + }, + cards: [], + links: [ + %{label: "See the product", to: product}, + %{label: "Join waitlist", to: waitlist} + ], + facts: [] }, %{ moment: "18 August 2026", @@ -145,7 +310,31 @@ defmodule Soonex.HomePage.Kalendarium do "Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur." ], points: ["Debitis check", "Voluptates pass"], - image: nil + image: nil, + quote: nil, + cards: [ + %{ + kicker: "Journal", + title: "Debitis aut rerum", + body: "Necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae.", + to: journal, + link: "Open the journal" + }, + %{ + kicker: "Waitlist", + title: "Maiores alias", + body: + "Consequatur aut perferendis doloribus asperiores repellat. At vero eos et accusamus.", + to: waitlist, + link: "Join waitlist" + } + ], + links: [%{label: "See the product", to: product}], + facts: [ + %{label: "Check", content: "Debitis"}, + %{label: "Pass", content: "Voluptates"}, + %{label: "Launch", content: "1 September"} + ] }, %{ moment: "1 September 2026", @@ -155,7 +344,31 @@ defmodule Soonex.HomePage.Kalendarium do "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti." ], points: ["Launch day", "Public notes"], - image: nil + image: nil, + quote: %{ + text: + "Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus.", + cite: "Earum, launch morning" + }, + cards: [ + %{ + kicker: "Launch", + title: "Public notes", + body: + "Blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas.", + to: waitlist, + link: "Join waitlist" + } + ], + links: [ + %{label: "See the product", to: product}, + %{label: "Read the journal", to: journal} + ], + facts: [ + %{label: "Day", content: "1 September"}, + %{label: "Status", content: "Public"}, + %{label: "Notes", content: "Live"} + ] } ] end From f9e65805429f0ed29d49d870a4a7164c59fd8287 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 17:26:39 +0000 Subject: [PATCH 33/65] Put calendar pull quotes in the story column. Keeps cards and the data list on the side so the 70svh beat is filled on both columns. Co-authored-by: Karim Semmoud --- lib/pages/home/kalendarium.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/pages/home/kalendarium.ex b/lib/pages/home/kalendarium.ex index 7359cc6..33aeb55 100644 --- a/lib/pages/home/kalendarium.ex +++ b/lib/pages/home/kalendarium.ex @@ -51,6 +51,12 @@ defmodule Soonex.HomePage.Kalendarium do >
  • {point}
  • +
    +

    + “{beat.quote.text}” +

    +
    {beat.quote.cite}
    +
    <.photo src={beat.image.src} @@ -71,12 +77,6 @@ defmodule Soonex.HomePage.Kalendarium do
    From d37e71e86dfc9015e8b01e59ebcedeaecf3e7ad3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 17:32:57 +0000 Subject: [PATCH 35/65] Drop the waitlist photo and center the form. The closing band is copy plus the form only, so the orbit still is gone and the panel sits on mx-auto max-w-lg under the centered intro. Co-authored-by: Karim Semmoud --- lib/pages/home/waitlist.ex | 172 +++++++++++++++++-------------------- 1 file changed, 80 insertions(+), 92 deletions(-) diff --git a/lib/pages/home/waitlist.ex b/lib/pages/home/waitlist.ex index 7683034..24cbe26 100644 --- a/lib/pages/home/waitlist.ex +++ b/lib/pages/home/waitlist.ex @@ -4,7 +4,6 @@ defmodule Soonex.HomePage.Waitlist do use Phoenix.Component use Corex - import Soonex.Layouts.Media, only: [photo: 1] import Soonex.Layouts.Section, only: [block: 1] alias Soonex.Layouts.Shell @@ -26,104 +25,93 @@ defmodule Soonex.HomePage.Waitlist do <:lede> Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -
    -
    - <.photo - src="/images/photos/orbit.jpg" - alt="Earth from orbit against a dark sky" - width={1400} - height={900} - class="absolute inset-0 size-full" - /> -
    - -
    - <.native_input - type="email" - name="waitlist[email]" - id="soonex-waitlist-email" - required - autocomplete="email" - placeholder="you@studio.dev" - class="native-input ui-size-md ui-width-full" - > - <:label class="sr-only">Email - + +
    + <.native_input + type="email" + name="waitlist[email]" + id="soonex-waitlist-email" + required + autocomplete="email" + placeholder="you@studio.dev" + class="native-input ui-size-md ui-width-full" + > + <:label class="sr-only">Email + - <.select - id="soonex-waitlist-role" - name="waitlist[role]" - class="select ui-brand ui-size-md ui-width-full" - value={["founder"]} - positioning={ - %Corex.Positioning{ - placement: "bottom-start", - same_width: true - } + <.select + id="soonex-waitlist-role" + name="waitlist[role]" + class="select ui-brand ui-size-md ui-width-full" + value={["founder"]} + positioning={ + %Corex.Positioning{ + placement: "bottom-start", + same_width: true } - items={@role_items} - > - <:label>Role - <:trigger> - <.heroicon name="hero-chevron-down" /> - - + } + items={@role_items} + > + <:label>Role + <:trigger> + <.heroicon name="hero-chevron-down" /> + + - <.tags_input - id="soonex-waitlist-interests" - name="waitlist[interests]" - class="tags-input ui-brand ui-width-full" - value={[]} - translation={ - %Corex.TagsInput.Translation{ - placeholder: "Add an interest and press Enter" - } + <.tags_input + id="soonex-waitlist-interests" + name="waitlist[interests]" + class="tags-input ui-brand ui-width-full" + value={[]} + translation={ + %Corex.TagsInput.Translation{ + placeholder: "Add an interest and press Enter" } - > - <:label>Interests - <:close> - <.heroicon name="hero-x-mark" /> - - + } + > + <:label>Interests + <:close> + <.heroicon name="hero-x-mark" /> + + - <.number_input - id="soonex-waitlist-team" - name="waitlist[team_size]" - class="number-input ui-brand ui-width-full" - min={1.0} - max={200.0} - step={1.0} - value="2" - > - <:label>Team size - <:decrement_trigger> - <.heroicon name="hero-chevron-down" class="icon" /> - - <:increment_trigger> - <.heroicon name="hero-chevron-up" class="icon" /> - - + <.number_input + id="soonex-waitlist-team" + name="waitlist[team_size]" + class="number-input ui-brand ui-width-full" + min={1.0} + max={200.0} + step={1.0} + value="2" + > + <:label>Team size + <:decrement_trigger> + <.heroicon name="hero-chevron-down" class="icon" /> + + <:increment_trigger> + <.heroicon name="hero-chevron-up" class="icon" /> + + - <.switch - id="soonex-waitlist-notes" - name="waitlist[notes]" - checked - class="switch ui-brand" - > - <:label>Email me launch notes - + <.switch + id="soonex-waitlist-notes" + name="waitlist[notes]" + checked + class="switch ui-brand" + > + <:label>Email me launch notes + - -
    - -
    + +
    + """ end From 4956b56805170f2bad8ff9fac84a7ab49b8b6c94 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 17:35:29 +0000 Subject: [PATCH 36/65] Scale the header and footer lockup to ui-size-md. The shared Brand.lockup used ui-size-sm with a size-7 mark. Header, footer, and 404 now use the md control size, a size-8 mark, and text-lg wordmark. Co-authored-by: Karim Semmoud --- lib/layouts/brand.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/layouts/brand.ex b/lib/layouts/brand.ex index 5a42eb4..883a850 100644 --- a/lib/layouts/brand.ex +++ b/lib/layouts/brand.ex @@ -14,15 +14,15 @@ defmodule Soonex.Layouts.Brand do <.navigate to={@to} type="href" - class={["link ui-nav ui-size-sm flex items-center gap-2 no-underline", @class]} + class={["link ui-nav ui-size-md flex items-center gap-2 no-underline", @class]} > - <.mark class="size-7 shrink-0" /> - Soonex + <.mark class="size-8 shrink-0" /> + Soonex """ end - attr(:class, :any, default: "size-7") + attr(:class, :any, default: "size-8") def mark(assigns) do ~H""" From 016bf03b422dda4d1f0d71654fe444c28ee0b5c8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 17:38:51 +0000 Subject: [PATCH 37/65] Fill the accessibility trigger icon to the control size. Corex pins button SVGs to 1em, so p-0 on the circle still left a 16px glyph in a 44px hit target. Host CSS now sizes the mark to --ctl-size. Co-authored-by: Karim Semmoud --- assets/css/hosts.css | 5 +++-- lib/soonex/accessibility.ex | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/css/hosts.css b/assets/css/hosts.css index 7effa84..1804af8 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -97,8 +97,9 @@ } .soonex-a11y-trigger svg { - width: var(--text-2xl); - height: var(--text-2xl); + /* Corex buttons pin child SVGs to 1em !important; fill the circle instead. */ + width: var(--ctl-size, var(--spacing-size)) !important; + height: var(--ctl-size, var(--spacing-size)) !important; } .menu.ui-width-fit [data-scope="menu"][data-part="root"] > [data-scope="menu"][data-part="trigger"] { diff --git a/lib/soonex/accessibility.ex b/lib/soonex/accessibility.ex index dce474b..2b53cdb 100644 --- a/lib/soonex/accessibility.ex +++ b/lib/soonex/accessibility.ex @@ -136,7 +136,6 @@ defmodule Soonex.Accessibility do stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" - class="size-6" aria-hidden="true" > From 188a73562ada4f9570929e3920709cce7c38b1f5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 17:40:52 +0000 Subject: [PATCH 38/65] Put the hero countdown above the headline. Replace the Launching badge with the Corex timer in the copy column, and move the avatars plus proof line onto the photograph panel under the waitlist card. Co-authored-by: Karim Semmoud --- lib/pages/home/hero.ex | 170 ++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index 0734c78..dec8c0f 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -20,7 +20,18 @@ defmodule Soonex.HomePage.Hero do
    - Launching 1 September + <.timer + id="soonex-hero-countdown" + countdown + start_ms={@countdown_ms} + target_ms={0} + class="timer ui-success ui-size-lg w-full max-w-lg" + > + <:day_label>Days + <:hour_label>Hours + <:minute_label>Min + <:second_label>Sec +

    - -
    -
      -
    • - <.photo - src={person.src} - alt={person.alt} - width={72} - height={72} - class="soonex-avatar" - loading="eager" - /> -
    • -
    -

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor. -

    -
    @@ -72,79 +65,86 @@ defmodule Soonex.HomePage.Hero do class="absolute inset-0 size-full" loading="eager" /> -
    -
    -

    Waitlist

    -

    - Lorem ipsum dolor sit -

    -

    - Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. -

    - -
    - <.native_input - type="email" - name="waitlist[email]" - id="soonex-hero-waitlist-email" - required - autocomplete="email" - placeholder="you@studio.dev" - class="native-input ui-size-md ui-width-full" +
    +
    +
    +

    Waitlist

    +

    + Lorem ipsum dolor sit +

    +

    + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. +

    + + - <:label class="sr-only">Email - - - <.radio_group - id="soonex-hero-role" - name="waitlist[role]" - class="radio-group ui-brand ui-width-full" - value="founder" - items={[ - %{value: "founder", label: "Founder"}, - %{value: "engineer", label: "Engineer"}, - %{value: "designer", label: "Designer"} - ]} - > - <:label>I am a - - - <.switch - id="soonex-hero-notes" - name="waitlist[notes]" - checked - class="switch ui-brand" - > - <:label>Email me launch notes - + <.native_input + type="email" + name="waitlist[email]" + id="soonex-hero-waitlist-email" + required + autocomplete="email" + placeholder="you@studio.dev" + class="native-input ui-size-md ui-width-full" + > + <:label class="sr-only">Email + + + <.radio_group + id="soonex-hero-role" + name="waitlist[role]" + class="radio-group ui-brand ui-width-full" + value="founder" + items={[ + %{value: "founder", label: "Founder"}, + %{value: "engineer", label: "Engineer"}, + %{value: "designer", label: "Designer"} + ]} + > + <:label>I am a + + + <.switch + id="soonex-hero-notes" + name="waitlist[notes]" + checked + class="switch ui-brand" + > + <:label>Email me launch notes + + + + +
    +
    - - +
    +
      +
    • + <.photo + src={person.src} + alt={person.alt} + width={72} + height={72} + class="soonex-avatar" + loading="eager" + /> +
    • +
    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor. +

    - - <.timer - id="soonex-hero-countdown" - countdown - start_ms={@countdown_ms} - target_ms={0} - class="timer ui-success ui-size-lg mt-14 w-full" - > - <:day_label>Days - <:hour_label>Hours - <:minute_label>Min - <:second_label>Sec -
    From 1d5caac9b6b5b5097537003a54143fb969d96820 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 18:40:52 +0000 Subject: [PATCH 39/65] Give Soonex a premium Corex identity and real product copy. Bind journal pagination to Corex client events, drop the marquee and plans table, split frame vs panel, and recompose every home band with existing Corex controls. Co-authored-by: Karim Semmoud --- _posts/2026-03-12-duis-aute.md | 28 +-- _posts/2026-04-08-excepteur.md | 39 ++-- _posts/2026-05-08-docs.md | 97 ++++----- _posts/2026-06-01-shipping.md | 32 +-- _posts/2026-07-14-nemo-enim.md | 24 +- _posts/2026-08-02-temporibus.md | 26 +-- assets/css/chrome.css | 50 ++--- assets/css/hosts.css | 54 ++++- assets/js/pagination.js | 36 +++ assets/js/site.js | 6 +- config/config.exs | 42 ++-- lib/layouts/articles.ex | 12 +- lib/layouts/root/footer.ex | 27 ++- lib/layouts/root/nav.ex | 2 +- lib/layouts/root_layout.ex | 10 +- lib/layouts/shell.ex | 2 +- lib/layouts/tag_layout.ex | 6 +- lib/pages/blog_index_page.ex | 5 +- lib/pages/home/capita.ex | 64 +++--- lib/pages/home/contents.ex | 67 +++--- lib/pages/home/faq.ex | 14 +- lib/pages/home/hero.ex | 60 +++-- lib/pages/home/highlights.ex | 12 +- lib/pages/home/kalendarium.ex | 373 ++++++++------------------------ lib/pages/home/numeri.ex | 32 +-- lib/pages/home/officia.ex | 98 +++------ lib/pages/home/socii.ex | 48 ---- lib/pages/home/voces.ex | 54 ++--- lib/pages/home/waitlist.ex | 9 +- lib/pages/home_page.ex | 2 - lib/pages/not_found_page.ex | 2 +- lib/pages/tags_index_page.ex | 13 +- 32 files changed, 586 insertions(+), 760 deletions(-) create mode 100644 assets/js/pagination.js delete mode 100644 lib/pages/home/socii.ex diff --git a/_posts/2026-03-12-duis-aute.md b/_posts/2026-03-12-duis-aute.md index 3e5d83f..ee3f9be 100644 --- a/_posts/2026-03-12-duis-aute.md +++ b/_posts/2026-03-12-duis-aute.md @@ -1,9 +1,9 @@ --- layout: Soonex.PostLayout -title: Duis aute irure dolor in reprehenderit +title: Fork day — Tableau plus Corex date: 2026-03-12 12:00:00 +0000 permalink: /blog/duis-aute-irure/ -description: Velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident. +description: Why Soonex is a static Tableau site with Corex controls, not a LiveView app and not an npm project. image: /images/photos/hero.jpg image_alt: Sunlit studio with long work tables and hanging task lamps tags: @@ -14,23 +14,25 @@ sitemap: 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**. +Soonex starts as a **static Tableau** site. Mix, Tailwind, and esbuild are already in the project. There is no `package.json`, no Lenis, and no LiveView pager. -## Magna aliqua +The first pass put a waitlist on the page and a countdown aimed at **1 September**. Copy and stills are placeholders you replace after the fork. -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. +## What we kept upstream + +Corex owns the controls. Host CSS retunes layout. Theme overlays may set seeds, colors, radius, fonts, and typography — not scale, duration, or opacity. ### Lists -- Lorem ipsum dolor sit amet -- Consectetur adipiscing elit -- Sed do eiusmod tempor incididunt +- Clone, then `mix setup` and `mix soonex.server` +- Waitlist fields are already named +- The header timer appears after the hero -1. Ut labore et dolore magna -2. Aliqua ut enim ad minim -3. Veniam quis nostrud +1. Fork the repo +2. Swap the lockup and stills +3. Point the countdown at your date -> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore. +> The clone strip on Product is the same git URL you run locally. > [!NOTE] -> Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip. +> This demo waitlist does not collect addresses. Submit still shows the launch toast so you can see the success path. diff --git a/_posts/2026-04-08-excepteur.md b/_posts/2026-04-08-excepteur.md index 48ac630..c1d7948 100644 --- a/_posts/2026-04-08-excepteur.md +++ b/_posts/2026-04-08-excepteur.md @@ -1,9 +1,9 @@ --- layout: Soonex.PostLayout -title: Excepteur sint occaecat cupidatat +title: Four themes without forking Corex CSS date: 2026-04-08 09:00:00 +0000 permalink: /blog/excepteur-sint-occaecat/ -description: Sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis. +description: neo, uno, duo, and leo live in config. Template Options switches them on this device so they read as four products. image: /images/photos/studio.jpg image_alt: A calm studio interior with a low sofa, plants, and a large window tags: @@ -14,32 +14,31 @@ sitemap: changefreq: monthly --- -Sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium. +Theme, brand, and accent live in config. The floating **Template Options** panel switches neo, uno, duo, and leo on this device. Host CSS does not copy component recipes. -## Totam rem aperiam +## What each theme is for -Eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. +neo is a cool gallery. uno is mint and tight. duo is salon cream with round corners. leo is kraft paper and a hard edge. -### Table +### Review table -| Lorem | Ipsum | Dolor | -| --- | --- | --- | -| Sit amet | Consectetur | Adipiscing | -| Elit sed | Do eiusmod | Tempor | -| Incididunt | Ut labore | Et dolore | +| Theme | Paper | Corner | Type | +| --- | --- | --- | --- | +| neo | Gallery | Soft | Outfit | +| uno | Mint | Tight | Sora | +| duo | Salon | Round | Playfair | +| leo | Kraft | None | IBM Plex | -### Task list +### Studio checklist -- [x] Lorem ipsum checked item -- [x] Dolor sit amet done -- [ ] Consectetur adipiscing unfinished +- [x] Seeds and light/dark colors in overlays +- [x] Radius and fonts per theme +- [ ] Your photography in the frames > [!TIP] -> Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit. +> There is no `ui-input` utility in Corex. Soonex treats triggers and field hosts as the control language via `--ctl-radius`, `--ctl-space`, `--ctl-size`, and `--ctl-text`. ```elixir -defmodule Lorem.Studio do - def ipsum(dolor) when is_binary(dolor), do: String.trim(dolor) - def ipsum(_), do: :error -end +# Theme overlays must not set *_scale, duration, or opacity. +config :corex_design, default_theme: :neo ``` diff --git a/_posts/2026-05-08-docs.md b/_posts/2026-05-08-docs.md index a76bd12..88cf14a 100644 --- a/_posts/2026-05-08-docs.md +++ b/_posts/2026-05-08-docs.md @@ -1,9 +1,9 @@ --- layout: Soonex.PostLayout -title: Ut enim ad minim veniam +title: How to clone, theme, and ship Soonex date: 2026-05-08 12:00:00 +0000 permalink: /blog/using-the-soonex-template/ -description: Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +description: Clone the waitlist template, switch neo/uno/duo/leo in config, and rebuild the Markdown journal. image: /images/covers/overlay.jpg image_alt: Soft abstract color field in warm paper tones tags: @@ -14,17 +14,15 @@ sitemap: 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**. +Soonex is the first **Corex plus Tableau** waitlist template. Fork it, theme it in config, keep the journal and countdown, and launch with accessible controls. -## Primis in faucibus +## Start here -### Vestibulum ante ipsum +### Clone -#### Aenean lacinia bibendum +#### Then mix setup -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. +The repo already has Mix, Tailwind, and esbuild. There is no npm install step. Use **bold**, *italic*, ***combined***, `inline code`, and ~~strikethrough~~ in the same paragraph when you need emphasis variety. ### Links and autolink @@ -36,34 +34,34 @@ Markdown link: [Hexdocs Corex](https://hexdocs.pm/corex). Bare URL with autolink ### Unordered -- Lorem ipsum dolor sit amet - - Consectetur adipiscing elit - - Sed do eiusmod tempor -- Ut labore et dolore magna aliqua -- Quis nostrud exercitation ullamco +- Clone `https://github.com/corex-ui/soonex.git` + - Run `mix setup` + - Run `mix soonex.server` +- Swap stills and the lockup +- Point the countdown at your date ### Ordered -1. Incididunt ut labore et dolore -2. Magna aliqua ut enim ad minim -3. Veniam quis nostrud exercitation +1. Theme overlays in `config/config.exs` +2. Journal posts under `_posts/` +3. Waitlist fields wired to your list ### Task list -- [x] Lorem ipsum checked item -- [ ] Dolor sit amet pending -- [ ] Consectetur adipiscing unfinished +- [x] Waitlist toast on submit +- [ ] Real mailbox backend +- [ ] Your launch photography --- ## Table -| Lorem | Ipsum | Dolor | +| Band | Corex | Note | | --- | --- | --- | -| Sit amet | Consectetur | Adipiscing | -| Elit sed | Do eiusmod | Tempor | -| Incididunt | Ut labore | Et dolore | -| Magna aliqua | Ut enim | Ad minim | +| Product | Tabs | Overview / Themes / Mix | +| Plans | Data list | Launch / Studio / Agency | +| Voices | Carousel | Horizontal quotes | +| Journal | Pagination | Client events only | | Feature | Status | Note | | --- | :---: | --- | @@ -76,9 +74,9 @@ Markdown link: [Hexdocs Corex](https://hexdocs.pm/corex). Bare URL with autolink ## 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. +> Fork once. Theme in config. The controls stay upstream. -> Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat. +> Host CSS retunes layout. It does not copy Corex recipes or set overlay scale, duration, or opacity. --- @@ -86,9 +84,9 @@ Markdown link: [Hexdocs Corex](https://hexdocs.pm/corex). Bare URL with autolink Default Open Graph asset: -![Lorem Open Graph placeholder](/images/og.svg) +![Soonex Open Graph](/images/og.svg) -Tech marks used elsewhere on the site: +Tech marks used in the footer: ![Elixir](/images/tech/elixir.svg) @@ -103,28 +101,28 @@ Tech marks used elsewhere on the site: ## GitHub-style alerts > [!NOTE] -> Lorem ipsum dolor sit amet—neutral context readers should notice without panic. +> The waitlist demo does not collect addresses. Submit still shows the launch toast. > [!TIP] -> Sed do eiusmod tempor: shortcuts, conventions, and quality-of-life details belong here. +> Template Options switches neo, uno, duo, and leo on this device without a rebuild. > [!IMPORTANT] -> Ut enim ad minim veniam—call out security, data loss, or breaking changes. +> Do not fork Corex CSS. Theme overlays must not set `*_scale`, duration, or opacity. > [!WARNING] -> Quis nostrud exercitation may surprise operators if ordering differs by environment. +> `mix soonex.server` fails if port 4999 is already bound. Stop the occupant first. > [!CAUTION] -> Duis aute irure dolor: outcome depends on timing, locale, or host configuration. +> Journal pagination is a Corex client event. There is no LiveView `handle_event`. --- ## Footnotes -Lorem ipsum dolor sit amet, consectetur adipiscing elit[^alpha]. Sed do eiusmod tempor incididunt ut labore[^beta]. +The journal index paginates with the Corex pager on the client[^alpha]. Reduced motion lives in Accessibility[^beta]. -[^alpha]: Lorem footnote: first reference collected at the end of the document. -[^beta]: Ipsum footnote: second definition for MDEx `footnotes: true`. +[^alpha]: Host script listens for `pagination-page-changed` and hides the other SSR cards. +[^beta]: Scroll reveal and card lift stop under `data-motion=reduce`. --- @@ -133,16 +131,12 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit[^alpha]. Sed do eiusmod Fenced Elixir: ```elixir -defmodule Lorem.Ipsum do - @moduledoc "Dolor sit amet consectetur." +defmodule Soonex.HomePage do + @moduledoc "Waitlist landing for the Corex template." - def dolor(sit) when is_binary(sit) do - sit - |> String.trim() - |> String.upcase() + def launch_day do + ~D[2026-09-01] end - - def dolor(_), do: :error end ``` @@ -158,12 +152,13 @@ mix tableau.build Fenced JavaScript: ```javascript -export function lorem(ipsum = []) { - return ipsum.map((item) => String(item).trim()).filter(Boolean) -} +el.addEventListener("pagination-page-changed", (e) => { + const { page, page_size } = e.detail + // show SSR cards for that 1-based slice +}) ``` -Inline: run `mix tableau.server` then open `http://localhost:4999`. +Inline: run `mix soonex.server` then open `http://localhost:4999`. --- @@ -172,8 +167,8 @@ Inline: run `mix tableau.server` then open `http://localhost:4999`. Trusted raw HTML with `render: [unsafe: true]`:
    -Lorem ipsum details / summary -

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

    +Keyboard shortcuts in docs +

    Press Ctrl+K style shortcuts when documenting keyboards. The template does not bind that chord.

    --- diff --git a/_posts/2026-06-01-shipping.md b/_posts/2026-06-01-shipping.md index 3bb6012..f48ed27 100644 --- a/_posts/2026-06-01-shipping.md +++ b/_posts/2026-06-01-shipping.md @@ -1,9 +1,9 @@ --- layout: Soonex.PostLayout -title: Lorem ipsum dolor sit amet +title: Shipping notes — mix test before Pages date: 2026-06-01 12:00:00 +0000 permalink: /blog/shipping-tableau-corex/ -description: Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +description: Keep CI green before Pages deploys. Axe on the home document is the gate; the countdown stays aimed at 1 September. image: /images/covers/shipping.jpg image_alt: A laptop on a wooden desk with a code editor open tags: @@ -14,37 +14,37 @@ sitemap: 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**. +Keep **CI green** before Pages deploys so a broken push never reaches the public CDN. Wallaby axe on the home document is the test gate. -## Magna aliqua +## Before you push -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +Stop `mix soonex.server` if port 4999 is bound. Then `MIX_ENV=test mix test`. ### Checklist -- [x] Lorem build pipeline -- [x] Ipsum asset alias -- [ ] Dolor deploy gate +- [x] Mix, Tailwind, and esbuild in the project +- [x] No `package.json` +- [ ] Your mailbox wired to the waitlist fields ### Quick table -| Lorem | Ipsum | Dolor | +| Check | Command | Note | | --- | --- | --- | -| Sit | Amet | Consectetur | -| Elit | Sed | Eiusmod | -| Tempor | Incididunt | Labore | +| Axe | `mix test` | Home document | +| Build | `mix tableau.build` | Static `_site/` | +| Serve | `mix soonex.server` | 4999 + Corex MCP | ![Soonex Open Graph](/images/og.svg) > [!TIP] -> Lorem ipsum: keep CI green before Pages deploys so broken pushes never reach the public CDN. +> Keep CI green before Pages deploys so broken pushes never reach the public CDN. ```elixir -defmodule Shipping.Demo do +defmodule Soonex.Shipping 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]. +The countdown target in this demo is 1 September. Change it in the home template when you have a real date[^ship]. -[^ship]: Footnote lorem: ship notes stay short so the kitchen-sink post can carry the full gallery. +[^ship]: Days, hours, minutes, and seconds stay on one row. The header timer appears after the hero. diff --git a/_posts/2026-07-14-nemo-enim.md b/_posts/2026-07-14-nemo-enim.md index a9a00c0..b6f4212 100644 --- a/_posts/2026-07-14-nemo-enim.md +++ b/_posts/2026-07-14-nemo-enim.md @@ -1,9 +1,9 @@ --- layout: Soonex.PostLayout -title: Nemo enim ipsam voluptatem +title: Calendar beats and the 1 September target date: 2026-07-14 15:00:00 +0000 permalink: /blog/nemo-enim-ipsam/ -description: Quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos. +description: Sticky calendar beats, a client journal pager, and a countdown aimed at 1 September. image: /images/photos/orbit.jpg image_alt: Earth from orbit against a dark sky tags: @@ -14,23 +14,23 @@ sitemap: changefreq: monthly --- -Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. +The home calendar is a stack of **sticky beats**, not a wall of cards. Open a note when you need the extra pass. The countdown in the hero is aimed at **1 September**. -## Neque porro quisquam +## What the beats cover -Est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt. +Fork day, journal, four themes, the accessibility pass, and launch morning. -- Ut labore et dolore magnam -- Aliquam quaerat voluptatem -- Ut enim ad minima veniam +- Tableau plus Corex, no npm +- Journal pagination on the client +- Accessibility choices persist in local storage > [!IMPORTANT] -> Quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur. +> Change the target in the home template when you have a real date. Days, hours, minutes, and seconds stay on one row. -Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus[^orbit]. +The header timer stays in the bar and appears after the hero[^orbit]. -[^orbit]: Lorem footnote: collected at the end of the document. +[^orbit]: The condensed header and progress bar stay as chrome when motion is reduced. ```bash -echo "lorem ipsum dolor sit amet" +mix soonex.server ``` diff --git a/_posts/2026-08-02-temporibus.md b/_posts/2026-08-02-temporibus.md index ad27a37..fd12d13 100644 --- a/_posts/2026-08-02-temporibus.md +++ b/_posts/2026-08-02-temporibus.md @@ -1,9 +1,9 @@ --- layout: Soonex.PostLayout -title: Temporibus autem quibusdam et aut +title: Accessible controls that persist on device date: 2026-08-02 11:00:00 +0000 permalink: /blog/temporibus-autem/ -description: Officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint. +description: Zoom, contrast, motion, cursor, focus, and link underline — Corex accessibility, stored in local storage. image: /images/covers/overlay.jpg image_alt: Soft abstract color field in warm paper tones tags: @@ -14,23 +14,23 @@ sitemap: changefreq: monthly --- -Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. +The person-in-circle control at the corner opens zoom, contrast, motion, cursor, focus, and link underline for this device. Choices persist in **local storage** and write data attributes on the document. -## Itaque earum rerum +## What reduced motion does -Hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat. +Scroll reveal and card lift live in host CSS. Set Motion to Reduced in Accessibility and those animations stop. -> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. +> Reset returns the defaults. The condensed header and progress bar stay as chrome. -### Ordered +### Order of operations -1. At vero eos et accusamus -2. Et iusto odio dignissimos ducimus -3. Qui blanditiis praesentium voluptatum +1. Open Accessibility +2. Set Motion to Reduced +3. Confirm the hero and journal still read > [!WARNING] -> Deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident. +> Host CSS must not set overlay scale, duration, or opacity. Theme.Validator will reject those keys. -Use **bold**, *italic*, and `inline code` in the same paragraph when you need emphasis variety. +Use **bold**, *italic*, and `inline code` in the same paragraph when you document the dialog. -![Lorem Open Graph placeholder](/images/og.svg) +![Soonex Open Graph](/images/og.svg) diff --git a/assets/css/chrome.css b/assets/css/chrome.css index 4d7d6c7..c53fae9 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -131,52 +131,28 @@ html[data-motion="reduce"] .soonex-card-motion:hover { align-items: center; } -.soonex-avatars li + li { +.soonex-avatars .avatar + .avatar { margin-inline-start: calc(var(--spacing-space-sm) * -1); } -.soonex-avatar { - width: 2.25rem; - height: 2.25rem; - object-fit: cover; - border-radius: var(--radius-full); +.soonex-avatars .avatar [data-scope="avatar"][data-part="root"] { border: 2px solid var(--color-root); } -.soonex-avatar-lg { - width: 3.5rem; - height: 3.5rem; -} - -/* Corex demo: duration={20} spacing="2rem" pause_on_interaction, no ui-accent. - Zag writes a speed-derived --marquee-duration on every render, then Corex - overwrites it from data-duration. That flicker restarts the CSS animation. */ -#soonex-tech-marquee.marquee [data-scope="marquee"][data-part="root"] { - --marquee-duration: 20s !important; -} - -#soonex-tech-marquee.marquee[data-loading], -#soonex-tech-marquee.marquee:not([data-loading]) { - opacity: 1; - transition: none; -} - -.marquee [data-scope="marquee"][data-part="content"] { +.soonex-toolchain { + display: flex; + flex-wrap: wrap; align-items: center; + gap: var(--spacing-space); + margin: 0; + padding: 0; + list-style: none; } -.marquee [data-scope="marquee"][data-part="edge"] { - --edge-background: var(--color-root); -} - -.marquee [data-scope="marquee"][data-part="edge"][data-side="start"], -.marquee [data-scope="marquee"][data-part="edge"][data-side="end"] { - width: 8%; - max-width: 5rem; -} - -html[data-motion="reduce"] .marquee [data-scope="marquee"][data-part="content"] { - animation: none; +.soonex-toolchain img { + display: block; + height: var(--spacing-size); + width: auto; } .data-list.data-list--wide, diff --git a/assets/css/hosts.css b/assets/css/hosts.css index 1804af8..42141c0 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -47,9 +47,9 @@ } .carousel.ui-width-full - [data-scope="carousel"][data-part="root"][data-orientation="vertical"] { - max-height: min(80svh, var(--container-md)); - padding-inline-end: var(--spacing-space); + [data-scope="carousel"][data-part="root"][data-orientation="horizontal"] { + max-height: none; + aspect-ratio: auto; } .select.soonex-nav-select [data-scope="select"][data-part="trigger"] { @@ -115,10 +115,56 @@ padding-block: var(--spacing-space); } +.collapsible.ui-width-full [data-scope="collapsible"][data-part="trigger"] { + max-width: none; + width: 100%; +} + +.tabs.tabs--wide [data-scope="tabs"][data-part="list"] { + width: 100%; + max-width: none; + justify-content: flex-start; +} + +.carousel.ui-width-full [data-scope="carousel"][data-part="item"] { + align-items: stretch; +} + +.carousel.ui-width-full [data-scope="carousel"][data-part="item"] > * { + width: 100%; + height: 100%; +} + pre.code.code--wide { max-width: none; } +/* + Soonex control language. There is no ui-input utility — triggers and field + hosts share --ctl-* so header CTA, waitlist submit, and inputs read as one family. +*/ +:is( + .button.ui-trigger--circle, + .button.ui-trigger--square, + .native-input, + .select, + .tags-input, + .number-input +) { + --ctl-space: var(--spacing-space); + --ctl-size: var(--spacing-size); + --ctl-text: var(--text-base); +} + +.select [data-scope="select"][data-part="trigger"], +.menu [data-scope="menu"][data-part="trigger"], +.dialog [data-scope="dialog"][data-part="trigger"] { + --ctl-radius: inherit; + --ctl-size: var(--ctl-size, var(--spacing-size)); + --ctl-space: var(--ctl-space, var(--spacing-space)); + --ctl-text: var(--ctl-text, var(--text-base)); +} + /* Per-theme ui-* personality. Corex recipes read --ctl-radius from ui-rounded-*; these host rules retune that axis so neo/uno/duo/leo @@ -208,7 +254,7 @@ pre.code.code--wide { .dialog, .floating-panel ) { - --ctl-radius: var(--radius-2xl); + --ctl-radius: var(--radius-3xl); } [data-theme="leo"] :is( 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 050613e..67ae42a 100644 --- a/assets/js/site.js +++ b/assets/js/site.js @@ -13,6 +13,7 @@ import { Menu } from "corex/menu" import { Clipboard } from "corex/clipboard" import { initLanding } from "./landing.js" import { initWaitlistForm } from "./waitlist.js" +import { initPagination } from "./pagination.js" const csrfToken = document .querySelector("meta[name='csrf-token']") @@ -51,11 +52,12 @@ const liveSocket = new LiveSocket("/live", Socket, { }, }) +initWaitlistForm() +initPagination() + liveSocket.disableDebug() liveSocket.connect() if (document.querySelector("[data-landing]")) { initLanding() } - -initWaitlistForm() diff --git a/config/config.exs b/config/config.exs index 9138e34..a0f4c2d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -134,9 +134,9 @@ config :corex_design, themes: %{ neo: %{ seeds: %{ - neutral: "#EEEAE4", - accent: "#161618", - brand: "#1D4ED8", + neutral: "#F3F0E8", + accent: "#12141C", + brand: "#1E40AF", alert: "#B42318", info: "#0E7490", success: "#15803D" @@ -217,9 +217,9 @@ config :corex_design, }, uno: %{ seeds: %{ - neutral: "#D7E8E3", - accent: "#071614", - brand: "#0F766E", + neutral: "#B9E0D4", + accent: "#031814", + brand: "#0D9488", alert: "#B91C1C", info: "#0369A1", success: "#166534" @@ -292,9 +292,9 @@ config :corex_design, }, duo: %{ seeds: %{ - neutral: "#F7EDE4", - accent: "#3A2618", - brand: "#C2410C", + neutral: "#F6E2CC", + accent: "#3F1F0F", + brand: "#B45309", alert: "#9F1239", info: "#3D5278", success: "#3F6B4E" @@ -331,14 +331,14 @@ config :corex_design, }, 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, + xs: 0.7, + sm: 1.1, + md: 1.6, + lg: 2.2, + xl: 2.9, + "2xl": 3.8, + "3xl": 5.0, + "4xl": 6.5, full: 9999 }, font: %{ @@ -371,9 +371,9 @@ config :corex_design, }, leo: %{ seeds: %{ - neutral: "#E6DCCE", - accent: "#0C0C0C", - brand: "#E2410A", + neutral: "#D2C19A", + accent: "#050505", + brand: "#EA580C", alert: "#C41E1E", info: "#0369A1", success: "#166534" @@ -389,7 +389,7 @@ config :corex_design, ink: contrast.(:accent, :root, 12), "ink-muted": contrast.(:accent, :root, 5.8), link: contrast.(:brand, :root, 5.8), - border: contrast.(:accent, :ui, 1.55), + border: contrast.(:accent, :ui, 1.75), focus: contrast.(:brand, :ui, 2.5), shadow: contrast.(:accent, :root, 1.04) }), diff --git a/lib/layouts/articles.ex b/lib/layouts/articles.ex index 1faed67..bc25c88 100644 --- a/lib/layouts/articles.ex +++ b/lib/layouts/articles.ex @@ -16,10 +16,15 @@ defmodule Soonex.Layouts.Articles do

    {@empty}

    -
    +
    <.photo src={cover(post).src} alt={cover(post).alt} width={1400} height={900} /> @@ -49,13 +54,14 @@ defmodule Soonex.Layouts.Articles do def pager(assigns) do ~H""" -
    +
    @page_size} class="mt-12 flex justify-center"> <.pagination id={@id} class="pagination ui-brand ui-size-sm" count={@count} page_size={@page_size} type={:button} + on_page_change_client="pagination-page-changed" > <:prev_trigger> <.heroicon name="hero-chevron-left" /> diff --git a/lib/layouts/root/footer.ex b/lib/layouts/root/footer.ex index 0b9fe6c..654c2d1 100644 --- a/lib/layouts/root/footer.ex +++ b/lib/layouts/root/footer.ex @@ -18,6 +18,7 @@ defmodule Soonex.Layouts.Root.Footer do assigns |> assign(:github, @github) |> assign(:hexdocs, @hexdocs) + |> assign(:toolchain, toolchain()) ~H"""
    @@ -26,9 +27,18 @@ defmodule Soonex.Layouts.Root.Footer do
    <.lockup />

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - incididunt ut labore et dolore magna aliqua. + The first Corex plus Tableau waitlist template. Fork, theme, and ship 1 September.

    +
      +
    • + {mark.alt} +
    • +
    @@ -112,7 +122,7 @@ defmodule Soonex.Layouts.Root.Footer do
    <.navigate to={Soonex.Public.path("/") <> "#epistula"} - class="button ui-brand ui-solid ui-size-sm w-fit" + class="button ui-brand ui-solid ui-size-md w-fit" > Join waitlist @@ -121,4 +131,15 @@ defmodule Soonex.Layouts.Root.Footer do
    """ end + + defp toolchain do + [ + %{src: "/images/tech/elixir.svg", alt: "Elixir"}, + %{src: "/images/tech/phoenix.svg", alt: "Phoenix"}, + %{src: "/images/tech/tableau.svg", alt: "Tableau"}, + %{src: "/images/tech/zag.svg", alt: "Zag"}, + %{src: "/images/tech/tailwind.svg", alt: "Tailwind CSS"}, + %{src: "/images/tech/hex.svg", alt: "Hex"} + ] + end end diff --git a/lib/layouts/root/nav.ex b/lib/layouts/root/nav.ex index cc30d2f..051ed11 100644 --- a/lib/layouts/root/nav.ex +++ b/lib/layouts/root/nav.ex @@ -93,7 +93,7 @@ defmodule Soonex.Layouts.Root.Nav do
    <.navigate to={Soonex.Public.path("/") <> "#epistula"} - class="button ui-brand ui-solid ui-size-sm" + class="button ui-brand ui-solid ui-size-md" > Join waitlist diff --git a/lib/layouts/root_layout.ex b/lib/layouts/root_layout.ex index fb91705..2aa0acb 100644 --- a/lib/layouts/root_layout.ex +++ b/lib/layouts/root_layout.ex @@ -221,16 +221,16 @@ defmodule Soonex.RootLayout do defp meta_description(page, site_name) do cond do page[:page_kind] == :home -> - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt." + "Soonex is the first Corex plus Tableau waitlist template. Fork, theme, journal, countdown, and ship 1 September." page[:page_kind] == :blog_index -> - "Lorem ipsum dolor sit amet from #{site_name}." + "Shipping notes from #{site_name}: themes, countdown, clone steps, and the road to 1 September." page[:page_kind] == :not_found -> - "That page is not on #{site_name}." + "That page is not on #{site_name}. Head home or read the shipping log." page[:page_kind] == :tags_index -> - "Browse journal tags on #{site_name}." + "Browse the #{site_name} journal by topic — themes, launch, studio, and notes." tag_page?(page) -> "Journal posts tagged #{page[:tag]} on #{site_name}." @@ -239,7 +239,7 @@ defmodule Soonex.RootLayout do page[:description] true -> - "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + "Soonex is the first Corex plus Tableau waitlist template. Fork, theme, and ship 1 September." end end diff --git a/lib/layouts/shell.ex b/lib/layouts/shell.ex index 83df763..be9163a 100644 --- a/lib/layouts/shell.ex +++ b/lib/layouts/shell.ex @@ -48,7 +48,7 @@ defmodule Soonex.Layouts.Shell do def panel, do: "soonex-frame relative overflow-hidden border border-border bg-surface" - def frame, do: "soonex-frame relative overflow-hidden border border-border bg-surface" + def frame, do: "soonex-frame relative overflow-hidden" def data_list, do: "data-list data-list--wide ui-accent ui-size-md w-full max-w-none" end diff --git a/lib/layouts/tag_layout.ex b/lib/layouts/tag_layout.ex index 97a8d61..eb213f4 100644 --- a/lib/layouts/tag_layout.ex +++ b/lib/layouts/tag_layout.ex @@ -26,7 +26,7 @@ defmodule Soonex.TagLayout do <.layout_heading class="layout-heading" subtitle_tag="p"> <:title>{@tag_label} <:subtitle> - {@tag_count} {if @tag_count == 1, do: "post", else: "posts"} tagged in the journal + {@tag_count} {if @tag_count == 1, do: "shipping-log post", else: "shipping-log posts"} tagged in the journal <:actions> <.navigate to={Soonex.Public.path("/tags")} class="button ui-ghost ui-size-sm"> @@ -38,9 +38,9 @@ defmodule Soonex.TagLayout do -
    +
    <.cards posts={@tag_posts} /> - <.pager id="soonex-tag-pagination" count={@tag_count} page_size={1} /> + <.pager id="soonex-tag-pagination" count={@tag_count} page_size={3} />
    diff --git a/lib/pages/blog_index_page.ex b/lib/pages/blog_index_page.ex index 64b4ed8..0c3d119 100644 --- a/lib/pages/blog_index_page.ex +++ b/lib/pages/blog_index_page.ex @@ -32,7 +32,8 @@ defmodule Soonex.BlogIndexPage do <.layout_heading class="layout-heading" subtitle_tag="p"> <:title>Journal <:subtitle> - {@blog_count} {if @blog_count == 1, do: "post", else: "posts"} + Shipping notes for the Corex waitlist template — {@blog_count} + {if @blog_count == 1, do: "post", else: "posts"}. <:actions> <.navigate to={Soonex.Public.path("/")} class="button ui-ghost ui-size-sm"> @@ -44,7 +45,7 @@ defmodule Soonex.BlogIndexPage do -
    +
    <.cards posts={@sorted_posts} /> <.pager id="soonex-blog-pagination" count={@blog_count} page_size={3} />
    diff --git a/lib/pages/home/capita.ex b/lib/pages/home/capita.ex index 4c94899..4978b45 100644 --- a/lib/pages/home/capita.ex +++ b/lib/pages/home/capita.ex @@ -2,6 +2,7 @@ defmodule Soonex.HomePage.Capita do @moduledoc false use Phoenix.Component + use Corex import Soonex.Layouts.Media, only: [photo: 1] import Soonex.Layouts.Section, only: [block: 1] @@ -17,13 +18,14 @@ defmodule Soonex.HomePage.Capita do tone={:surface} > <:title> - Consectetur adipiscing elit + What you actually ship <:lede> - Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + One landing, one journal, four themes, and chrome that already knows the date. The rest is + copy and stills you own. -
    -
    +
    +
    <.photo src="/images/photos/studio.jpg" alt="A calm studio interior with a low sofa, plants, and a large window" @@ -32,22 +34,25 @@ defmodule Soonex.HomePage.Capita do class="absolute inset-0 size-full" />
    -

    Lorem

    +

    Template

    - Ipsum dolor sit amet consectetur adipiscing elit + Waitlist, journal, and a countdown aimed at 1 September

    -
    -

    {cell.kicker}

    -

    {cell.title}

    -

    {cell.body}

    -
    +
    +
    +

    {cell.kicker}

    +

    {cell.title}

    +

    {cell.body}

    +
    +
    + <.data_list class={"#{Shell.data_list()} mt-4"} items={ships()} /> """ end @@ -55,27 +60,26 @@ defmodule Soonex.HomePage.Capita do defp cells do [ %{ - kicker: "Ipsum", - title: "Sed do eiusmod tempor", + kicker: "Journal", + title: "Markdown posts, covers, tags", body: - "Incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + "Add a file under the journal route, rebuild, and the cover, date, and title show up. The index paginates on the client." }, %{ - kicker: "Dolor", - title: "Sit amet consectetur", + kicker: "Controls", + title: "Corex, not a custom kit", body: - "Adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - }, - %{ - kicker: "Sit", - title: "Ut enim ad minim", - body: "Veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo." - }, - %{ - kicker: "Amet", - title: "Duis aute irure dolor", - body: "In reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + "Select, accordion, tabs, carousel, and the waitlist fields are upstream Corex. Host CSS only retunes layout tokens." } ] end + + defp ships do + Corex.Content.new([ + %{label: "Waitlist", content: "Email, role, interests, team size, launch notes, toast"}, + %{label: "Themes", content: "neo, uno, duo, and leo from config overlays"}, + %{label: "Journal", content: "Six shipping-log posts and tag pages"}, + %{label: "Chrome", content: "Countdown, accessibility, Template Options"} + ]) + end end diff --git a/lib/pages/home/contents.ex b/lib/pages/home/contents.ex index d581c65..71ec306 100644 --- a/lib/pages/home/contents.ex +++ b/lib/pages/home/contents.ex @@ -19,30 +19,21 @@ defmodule Soonex.HomePage.Contents do tone={:root} > <:title> - Lorem ipsum dolor sit amet + Fork once. Theme in config. <:lede> - Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Soonex is a static Tableau site with Corex controls already on the page. No LiveView events, + no npm, no forked component CSS. -
      -
    • - <.tooltip id={feature.tooltip_id} class="tooltip"> - <:trigger> - - {feature.index} - - {feature.title} - - <:content>{feature.tip} - -

      - {feature.title} -

      -

      {feature.body}

      -
    • -
    + <.tabs + id="soonex-product-tabs" + class="tabs tabs--wide ui-brand ui-size-md" + indicator + value="overview" + items={product_tabs()} + /> -
    +

    Quick start

    @@ -71,33 +62,27 @@ defmodule Soonex.HomePage.Contents do """ end - defp features do - [ + defp product_tabs do + Corex.Content.new([ %{ - index: "01", - tooltip_id: "soonex-feature-tokens", - tip: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - title: "Lorem ipsum dolor", - body: - "Sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + value: "overview", + label: "Overview", + content: + "A waitlist landing page, a Markdown journal, countdown chrome, and the Corex controls already wired on this page. Photography, copy, and the launch date are yours to replace." }, %{ - index: "02", - tooltip_id: "soonex-feature-themes", - tip: "Ut enim ad minim veniam, quis nostrud exercitation ullamco.", - title: "Ut enim ad minim", - body: - "Veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + value: "themes", + label: "Themes", + content: + "neo, uno, duo, and leo live in config. Template Options switches them on this device so they read as four products, not four tints. Host CSS retunes layout; it does not copy Corex recipes." }, %{ - index: "03", - tooltip_id: "soonex-feature-mix", - tip: "Duis aute irure dolor in reprehenderit in voluptate velit.", - title: "Duis aute irure", - body: - "Dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + value: "mix", + label: "Mix", + content: + "Mix, Tailwind, and esbuild already in the project are enough. There is no package.json and no Lenis. Host scripts bind the header, the waitlist toast, and journal pagination on the client." } - ] + ]) end defp snippet do diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index 9b86c05..a81f5aa 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -10,10 +10,10 @@ defmodule Soonex.HomePage.Faq do ~H""" <.block id="quaestiones" labelledby="soonex-faq-heading" eyebrow="Questions" layout={:sticky}> <:title> - Laboris nisi ut aliquip + Before you clone <:lede> - Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + What ships, what is demo-only, and how themes, motion, and the journal behave after you fork. <:actions> <.navigate to="#epistula" class="link ui-brand"> @@ -24,7 +24,7 @@ defmodule Soonex.HomePage.Faq do id="soonex-faq" class="accordion ui-accent ui-size-md mx-auto w-full max-w-none" multiple={false} - collapsible={false} + collapsible={true} value="ships" items={faq_items()} > @@ -66,7 +66,7 @@ defmodule Soonex.HomePage.Faq do value: "journal", label: "Where does the journal live, and can I add posts?", content: - "Posts are Markdown under the journal route. The home carousel is a short reading list; the index paginates the rest. Add a file, rebuild, and the cover, date, and title show up. Tags stay optional." + "Posts are Markdown under the journal route. The home carousel is a short reading list; the index paginates the rest with the Corex pager on the client — no LiveView round trip. Add a file, rebuild, and the cover, date, and title show up. Tags stay optional." }, %{ value: "motion", @@ -84,7 +84,7 @@ defmodule Soonex.HomePage.Faq do value: "toolchain", label: "Do I need npm or a JavaScript bundler to start?", content: - "No. Mix, Tailwind, and esbuild already in the project are enough. There is no package.json and no Lenis. Select, marquee, carousel, and accordion are Corex hooks. Host scripts only bind header chrome and the waitlist toast." + "No. Mix, Tailwind, and esbuild already in the project are enough. There is no package.json and no Lenis. Tabs, carousel, and accordion are Corex hooks. Host scripts bind header chrome, the waitlist toast, and journal pagination." }, %{ value: "countdown", @@ -94,9 +94,9 @@ defmodule Soonex.HomePage.Faq do }, %{ value: "brand", - label: "Can I keep the photography and marquee after a rebrand?", + label: "Can I keep the photography after a rebrand?", content: - "Yes. Swap the stills and the lockup. The marquee is Corex as-is, with color marks and a fade to the page root. Grey filters are gone on purpose so logos stay on-brand. Pause on hover still holds a mark in place." + "Yes. Swap the stills and the lockup. Toolchain marks live in the footer as static SVGs, not a marquee. Host CSS does not own the photos." } ]) end diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index dec8c0f..60e43ec 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -37,19 +37,19 @@ defmodule Soonex.HomePage.Hero do id="soonex-headline" class="display mt-6 text-pretty text-5xl font-semibold tracking-tight text-ink sm:text-6xl lg:text-7xl xl:text-8xl" > - Lorem ipsum dolor sit amet. + Soonex ships 1 September.

    - Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna - aliqua. Ut enim ad minim veniam, quis nostrud exercitation. + The first Corex plus Tableau waitlist template. Fork it, theme it in config, keep the + journal and countdown, and launch with accessible controls.

    - <.navigate to="#epistula" class="button ui-brand ui-solid ui-size-lg"> + <.navigate to="#epistula" class="button ui-brand ui-solid ui-size-md"> Join waitlist - <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-lg"> + <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-md"> Read the log <.heroicon name="hero-arrow-up-right" />
    @@ -70,10 +70,10 @@ defmodule Soonex.HomePage.Hero do

    Waitlist

    - Lorem ipsum dolor sit + Reserve a September seat

    - Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. + This demo does not collect addresses. Submit still shows the launch toast.

    -
      -
    • - <.photo - src={person.src} - alt={person.alt} - width={72} - height={72} - class="soonex-avatar" - loading="eager" - /> -
    • -
    +
    + <.avatar + :for={person <- people()} + id={person.id} + src={Soonex.Public.path(person.src)} + alt={person.alt} + class="avatar ui-size-sm" + > + <:fallback>{person.initials} + +

    - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor. + Studios already cloning the repo for a 1 September launch.

    @@ -154,9 +153,24 @@ defmodule Soonex.HomePage.Hero do defp people do [ - %{src: "/images/people/amira.jpg", alt: "Amira N."}, - %{src: "/images/people/jonas.jpg", alt: "Jonas K."}, - %{src: "/images/people/leah.jpg", alt: "Leah M."} + %{ + id: "soonex-hero-avatar-amira", + src: "/images/people/amira.jpg", + alt: "Amira N.", + initials: "AN" + }, + %{ + id: "soonex-hero-avatar-jonas", + src: "/images/people/jonas.jpg", + alt: "Jonas K.", + initials: "JK" + }, + %{ + id: "soonex-hero-avatar-leah", + src: "/images/people/leah.jpg", + alt: "Leah M.", + initials: "LM" + } ] end end diff --git a/lib/pages/home/highlights.ex b/lib/pages/home/highlights.ex index 345cdfd..0e103bb 100644 --- a/lib/pages/home/highlights.ex +++ b/lib/pages/home/highlights.ex @@ -28,10 +28,10 @@ defmodule Soonex.HomePage.Highlights do tone={:root} > <:title> - Veniam quis nostrud + From the shipping log <:lede> - Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Short notes on the fork, the four themes, and the September target. The index paginates the rest. <:actions> <.navigate to={Soonex.Public.path("/blog")} class="link ui-brand"> @@ -46,12 +46,12 @@ defmodule Soonex.HomePage.Highlights do id="soonex-acta-carousel" class="carousel ui-brand ui-width-full max-w-none" items={@highlight_posts} - orientation="vertical" + orientation="horizontal" spacing="1.5rem" padding="2.5rem" > <:item :let={post}> -
    +
    <.photo src={cover(post).src} @@ -78,10 +78,10 @@ defmodule Soonex.HomePage.Highlights do
    <:prev_trigger> - <.heroicon name="hero-chevron-up" /> + <.heroicon name="hero-chevron-left" /> <:next_trigger> - <.heroicon name="hero-chevron-down" /> + <.heroicon name="hero-chevron-right" /> diff --git a/lib/pages/home/kalendarium.ex b/lib/pages/home/kalendarium.ex index a4c431b..abcf44f 100644 --- a/lib/pages/home/kalendarium.ex +++ b/lib/pages/home/kalendarium.ex @@ -21,10 +21,9 @@ defmodule Soonex.HomePage.Kalendarium do Road to 1 September <:lede> - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. + Sticky beats, not a wall of cards. Open a note when you need the extra pass. -
    +
    <.photo src="/images/covers/shipping.jpg" alt="A laptop on a wooden desk with a code editor open" @@ -34,7 +33,7 @@ defmodule Soonex.HomePage.Kalendarium do />
      -
    1. +
    2. {beat.moment}

      @@ -42,60 +41,34 @@ defmodule Soonex.HomePage.Kalendarium do {beat.title}
      -
      -
      -

      {para}

      -
        -
      • {point}
      • -
      -
      -

      - “{beat.quote.text}” -

      -
      {beat.quote.cite}
      -
      -
      - <.photo - src={beat.image.src} - alt={beat.image.alt} - width={1400} - height={900} - class="absolute inset-0 size-full" - /> -
      - -
      -
      -
      -
      +

      {beat.lede}

      + <.data_list + :if={beat.facts != []} + class={Shell.data_list()} + items={Corex.Content.new(beat.facts)} + /> + <.collapsible + id={beat.note_id} + class="collapsible ui-brand ui-size-sm ui-width-full" + > + <:trigger>Notes from this pass + <:closed> + <.heroicon name="hero-chevron-right" /> + + <:content> +

      {para}

      +
      -
      - <.data_list - :if={beat.facts != []} - class={"#{Shell.data_list()}"} - items={Corex.Content.new(beat.facts)} - /> -
      + <.navigate :for={link <- beat.links} to={link.to} class="link ui-brand"> + {link.label} + + + +
    @@ -111,263 +84,103 @@ defmodule Soonex.HomePage.Kalendarium do [ %{ + note_id: "soonex-kal-fork", moment: "12 March 2026", - title: "Lorem ipsum dolor", - paragraphs: [ - "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.", - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident." - ], - points: [ - "Lorem checklist item one", - "Ipsum notes for the studio", - "Dolor review with the team" - ], - image: nil, - quote: %{ - text: "Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit.", - cite: "Lorem, founding note" - }, - cards: [ - %{ - kicker: "Product", - title: "Sit amet consectetur", - body: "Adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna.", - to: product, - link: "See the product" - }, - %{ - kicker: "Journal", - title: "Ut labore et dolore", - body: "Magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco.", - to: journal, - link: "Read the journal" - } - ], - links: [ - %{label: "Join waitlist", to: waitlist}, - %{label: "Questions", to: questions} - ], - facts: [] - }, - %{ - moment: "8 April 2026", - title: "Ut enim ad minim", - paragraphs: [ - "Veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.", - "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam." + title: "Fork Tableau, keep Corex upstream", + lede: + "Soonex starts as a static Tableau site. Mix, Tailwind, and esbuild are already in the project. The waitlist and countdown land first so the page has a date.", + facts: [ + %{label: "Stack", content: "Tableau plus Corex"}, + %{label: "npm", content: "None"}, + %{label: "Target", content: "1 September"} ], - points: ["Aliqua draft", "Minima veniam pass"], - image: nil, - quote: %{ - text: "Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit.", - cite: "Ipsum, April studio" - }, - cards: [ - %{ - kicker: "Plans", - title: "Aliqua ut enim", - body: "Ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip.", - to: Soonex.Public.path("/") <> "#officia", - link: "Compare plans" - }, - %{ - kicker: "Inside", - title: "Dolor sit amet", - body: "Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.", - to: Soonex.Public.path("/") <> "#capita", - link: "Look inside" - } + notes: [ + "Host CSS only retunes layout. Theme overlays may set seeds, colors, radius, fonts, and typography — not scale, duration, or opacity.", + "The clone strip on Product is the same git URL you run locally after mix setup." ], links: [ %{label: "See the product", to: product}, - %{label: "Read the journal", to: journal}, %{label: "Join waitlist", to: waitlist} - ], - facts: [ - %{label: "Status", content: "Aliqua draft"}, - %{label: "Owner", content: "Studio pass"}, - %{label: "Next", content: "8 May review"} ] }, %{ + note_id: "soonex-kal-journal", moment: "8 May 2026", - title: "In reprehenderit voluptate", - paragraphs: [ - "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.", - "Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos." - ], - points: [], - image: %{ - src: "/images/photos/studio.jpg", - alt: "A calm studio interior with a low sofa, plants, and a large window" - }, - quote: nil, - cards: [ - %{ - kicker: "Studio", - title: "Velit esse cillum", - body: - "Dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident.", - to: product, - link: "Open product" - }, - %{ - kicker: "Questions", - title: "Sunt in culpa qui", - body: "Officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis.", - to: questions, - link: "Read questions" - } + title: "Journal, tags, and covers", + lede: + "Posts are Markdown. Add a file, rebuild, and the cover, date, and title show up. Tags stay optional. The index pager slices cards in the browser.", + facts: [ + %{label: "Posts", content: "Six shipping-log entries"}, + %{label: "Pager", content: "Corex client events"}, + %{label: "LiveView", content: "Not used"} ], - links: [%{label: "Join waitlist", to: waitlist}], - facts: [] - }, - %{ - moment: "1 June 2026", - title: "Sunt in culpa qui", - paragraphs: [ - "Officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.", - "Eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo." + notes: [ + "The home journal band is a short reading list. The index paginates the rest without a full-page reload.", + "Keep permalinks stable when you rewrite titles so existing links still resolve." ], - points: ["Voluptatem first pass", "Architecto second pass"], - image: nil, - quote: %{ - text: "Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur.", - cite: "Dolor, June note" - }, - cards: [], links: [ - %{label: "Compare plans", to: Soonex.Public.path("/") <> "#officia"}, - %{label: "Read the journal", to: journal} - ], - facts: [ - %{label: "Pass", content: "Architecto"}, - %{label: "Review", content: "14 July"}, - %{label: "Owner", content: "Agency"} + %{label: "Read the journal", to: journal}, + %{label: "Questions", to: questions} ] }, %{ + note_id: "soonex-kal-themes", moment: "14 July 2026", - title: "Nemo enim ipsam", - paragraphs: [ - "Voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.", - "Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit." - ], - points: ["Magni dolores", "Ratione sequi"], - image: nil, - quote: nil, - cards: [ - %{ - kicker: "Numbers", - title: "Magni dolores eos", - body: "Qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem.", - to: Soonex.Public.path("/") <> "#numeri", - link: "See the numbers" - }, - %{ - kicker: "Voices", - title: "Ipsum quia dolor", - body: "Sit amet, consectetur, adipisci velit. Nam libero tempore, cum soluta nobis.", - to: Soonex.Public.path("/") <> "#voces", - link: "Hear voices" - } - ], - links: [%{label: "Questions", to: questions}, %{label: "Join waitlist", to: waitlist}], + title: "Four themes that feel like four products", + lede: + "neo, uno, duo, and leo push paper, ink, brand, and radius apart. Template Options switches them on this device. Controls share one height and pick up a Soonex corner language.", facts: [ - %{label: "Theme", content: "Magni"}, - %{label: "Check", content: "Ratione sequi"} - ] - }, - %{ - moment: "2 August 2026", - title: "Neque porro quisquam", - paragraphs: [ - "Est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit.", - "Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae." + %{label: "Themes", content: "neo, uno, duo, leo"}, + %{label: "CSS fork", content: "None"}, + %{label: "Tokens", content: "--ctl-radius, --ctl-size"} + ], + notes: [ + "Triggers and fields host --ctl-radius, --ctl-space, --ctl-size, and --ctl-text. There is no ui-input utility in Corex.", + "Reduced motion lives in Accessibility. Scroll reveal and card lift stop under data-motion=reduce." ], - points: [], - image: %{ - src: "/images/photos/orbit.jpg", - alt: "Earth from orbit against a dark sky" - }, - quote: %{ - text: - "Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus.", - cite: "Sit, August orbit" - }, - cards: [], links: [ %{label: "See the product", to: product}, - %{label: "Join waitlist", to: waitlist} - ], - facts: [] + %{label: "Compare plans", to: Soonex.Public.path("/") <> "#officia"} + ] }, %{ + note_id: "soonex-kal-a11y", moment: "18 August 2026", - title: "Temporibus autem quibusdam", - paragraphs: [ - "Et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae.", - "Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur." + title: "Accessibility pass before launch", + lede: + "The person-in-circle control opens zoom, contrast, motion, cursor, focus, and link underline for this device. Choices persist in local storage.", + facts: [ + %{label: "Dialog", content: "Corex accessibility"}, + %{label: "Persist", content: "localStorage"}, + %{label: "Reset", content: "Returns defaults"} ], - points: ["Debitis check", "Voluptates pass"], - image: nil, - quote: nil, - cards: [ - %{ - kicker: "Journal", - title: "Debitis aut rerum", - body: "Necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae.", - to: journal, - link: "Open the journal" - }, - %{ - kicker: "Waitlist", - title: "Maiores alias", - body: - "Consequatur aut perferendis doloribus asperiores repellat. At vero eos et accusamus.", - to: waitlist, - link: "Join waitlist" - } + notes: [ + "The condensed header and progress bar stay as chrome when motion is reduced.", + "axe on the home document is the test gate before Pages deploy." ], - links: [%{label: "See the product", to: product}], - facts: [ - %{label: "Check", content: "Debitis"}, - %{label: "Pass", content: "Voluptates"}, - %{label: "Launch", content: "1 September"} + links: [ + %{label: "Questions", to: questions}, + %{label: "Join waitlist", to: waitlist} ] }, %{ + note_id: "soonex-kal-launch", moment: "1 September 2026", - title: "Itaque earum rerum", - paragraphs: [ - "Hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.", - "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti." + title: "Public notes, public template", + lede: + "Launch day is the countdown target in this demo. Change the date in the home template when you have a real one. Days, hours, minutes, and seconds stay on one row.", + facts: [ + %{label: "Day", content: "1 September"}, + %{label: "Status", content: "Public"}, + %{label: "Notes", content: "Journal"} ], - points: ["Launch day", "Public notes"], - image: nil, - quote: %{ - text: - "Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus.", - cite: "Earum, launch morning" - }, - cards: [ - %{ - kicker: "Launch", - title: "Public notes", - body: - "Blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas.", - to: waitlist, - link: "Join waitlist" - } + notes: [ + "Wire the waitlist fields to your list later — email, role, interests, team size, and launch notes are already named.", + "Keep CI green before Pages deploys so a broken push never reaches the public CDN." ], links: [ - %{label: "See the product", to: product}, + %{label: "Join waitlist", to: waitlist}, %{label: "Read the journal", to: journal} - ], - facts: [ - %{label: "Day", content: "1 September"}, - %{label: "Status", content: "Public"}, - %{label: "Notes", content: "Live"} ] } ] diff --git a/lib/pages/home/numeri.ex b/lib/pages/home/numeri.ex index fcc2768..a8c6dc8 100644 --- a/lib/pages/home/numeri.ex +++ b/lib/pages/home/numeri.ex @@ -2,6 +2,7 @@ defmodule Soonex.HomePage.Numeri do @moduledoc false use Phoenix.Component + use Corex import Soonex.Layouts.Section, only: [block: 1] @@ -12,36 +13,27 @@ defmodule Soonex.HomePage.Numeri do <.block id="numeri" labelledby="soonex-numeri-heading" - eyebrow="Numbers" + eyebrow="Facts" tone={:surface} - align={:center} compact > <:title> - Aliqua ut enim ad minim + What is actually true <:lede> - Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + No vanity counters. These are the numbers this template ships with. -
    -
    -
    {caption}
    -
    {stat}
    -
    -
    + <.data_list class={Shell.data_list()} items={facts()} /> """ end - defp stats do - [ - {"128", "Lorem ipsum"}, - {"24", "Dolor sit amet"}, - {"1 Sep", "Consectetur"}, - {"6", "Adipiscing elit"} - ] + defp facts do + Corex.Content.new([ + %{label: "Themes", content: "4 — neo, uno, duo, leo"}, + %{label: "Journal", content: "6 shipping-log posts"}, + %{label: "npm", content: "0 — Mix, Tailwind, esbuild"}, + %{label: "Launch", content: "1 September"} + ]) end end diff --git a/lib/pages/home/officia.ex b/lib/pages/home/officia.ex index 9ed835e..6eae8f7 100644 --- a/lib/pages/home/officia.ex +++ b/lib/pages/home/officia.ex @@ -17,33 +17,26 @@ defmodule Soonex.HomePage.Officia do tone={:root} > <:title> - Sed do eiusmod tempor + Pick a lane. Ship the same core. <:lede> - Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Every plan is the same Soonex fork. The columns describe how far you take the template before + 1 September. -
    +

    {plan.name}

    {plan.headline}

    -

    {plan.body}

    +

    {plan.body}

    + <.data_list class={"#{Shell.data_list()} mt-6"} items={Corex.Content.new(plan.items)} /> + <.navigate to="#epistula" class={"#{plan.cta_class} mt-8 w-full"}> + {plan.cta} +
    -
    - <.data_table - id="soonex-plan-table" - class="data-table ui-brand ui-size-sm ui-width-fit max-h-none" - rows={plan_rows()} - > - <:col :let={row} label="Feature">{row.feature} - <:col :let={row} label="Launch">{row.launch} - <:col :let={row} label="Studio">{row.studio} - <:col :let={row} label="Agency">{row.agency} - -
    """ end @@ -52,58 +45,39 @@ defmodule Soonex.HomePage.Officia do [ %{ name: "Launch", - headline: "Lorem ipsum dolor", - body: "Sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore." + headline: "Fork and replace the copy", + body: "One theme, the waitlist, and the journal. Enough to put a date on the page.", + cta: "Join waitlist", + cta_class: "button ui-ghost ui-size-md ui-width-full", + items: [ + %{label: "Site", content: "Waitlist landing and Markdown journal"}, + %{label: "Theme", content: "Keep neo, or switch once in config"}, + %{label: "Date", content: "Countdown aimed at 1 September"} + ] }, %{ name: "Studio", - headline: "Ut enim ad minim", - body: "Veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo." + headline: "Four themes, real stills", + body: "Treat Template Options as a client review. Photography and the shipping log stay.", + cta: "Join waitlist", + cta_class: "button ui-brand ui-solid ui-size-md ui-width-full", + items: [ + %{label: "Themes", content: "neo, uno, duo, and leo on this device"}, + %{label: "Journal", content: "Covers, tags, and client pagination"}, + %{label: "Chrome", content: "Header timer, toast, and clone strip"} + ] }, %{ name: "Agency", - headline: "Duis aute irure", - body: "Dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla." - } - ] - end - - defp plan_rows do - [ - %{ - id: "lorem", - feature: "Lorem ipsum", - launch: "Dolor", - studio: "Sit amet", - agency: "Consectetur" - }, - %{ - id: "ipsum", - feature: "Adipiscing elit", - launch: "Sed do", - studio: "Eiusmod", - agency: "Tempor" - }, - %{ - id: "dolor", - feature: "Incididunt ut", - launch: "Labore", - studio: "Et dolore", - agency: "Magna aliqua" - }, - %{ - id: "sit", - feature: "Ut enim ad", - launch: "Minim", - studio: "Veniam", - agency: "Quis nostrud" - }, - %{ - id: "amet", - feature: "Exercitation", - launch: "Ullamco", - studio: "Laboris", - agency: "Nisi ut" + headline: "Accessible by default", + body: "Ship the a11y dialog, reduced motion, and a clone path your team can repeat.", + cta: "Join waitlist", + cta_class: "button ui-ghost ui-size-md ui-width-full", + items: [ + %{label: "A11y", content: "Zoom, contrast, motion, focus, underline"}, + %{label: "Stack", content: "Mix, Tailwind, esbuild — no npm"}, + %{label: "Handoff", content: "Clone, mix setup, mix soonex.server"} + ] } ] end diff --git a/lib/pages/home/socii.ex b/lib/pages/home/socii.ex deleted file mode 100644 index 6514cd1..0000000 --- a/lib/pages/home/socii.ex +++ /dev/null @@ -1,48 +0,0 @@ -defmodule Soonex.HomePage.Socii do - @moduledoc false - - use Phoenix.Component - use Corex - - def socii(assigns) do - assigns = assign(assigns, :tech, tech_items()) - - ~H""" -
    - <.marquee - id="soonex-tech-marquee" - class="marquee ui-width-full max-w-none" - duration={20} - spacing="2rem" - pause_on_interaction - items={@tech} - > - <:item :let={item}> - - {item.name} - - - -
    - """ - end - - defp tech_items do - [ - %{name: "Elixir", src: "/images/tech/elixir.svg"}, - %{name: "Phoenix", src: "/images/tech/phoenixframework.svg"}, - %{name: "Tableau", src: "/images/tech/tableau.svg"}, - %{name: "Tailwind", src: "/images/tech/tailwindcss.svg"}, - %{name: "Hex", src: "/images/tech/hex.svg"}, - %{name: "TypeScript", src: "/images/tech/typescript.svg"}, - %{name: "Zag", src: "/images/tech/zag.svg"}, - %{name: "Ecto", src: "/images/tech/ecto-mark.svg"} - ] - end -end diff --git a/lib/pages/home/voces.ex b/lib/pages/home/voces.ex index 34cd297..0143b91 100644 --- a/lib/pages/home/voces.ex +++ b/lib/pages/home/voces.ex @@ -4,7 +4,6 @@ defmodule Soonex.HomePage.Voces do use Phoenix.Component use Corex - import Soonex.Layouts.Media, only: [photo: 1] import Soonex.Layouts.Section, only: [block: 1] alias Soonex.Layouts.Shell @@ -20,47 +19,46 @@ defmodule Soonex.HomePage.Voces do tone={:root} > <:title> - Ut labore et dolore magna + From studios already forking <:lede> - Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Quotes from the people putting Soonex on a September calendar — not a component gallery. <.carousel id="soonex-voces-carousel" class="carousel ui-brand ui-width-full max-w-none" items={@voices} loop - orientation="vertical" + orientation="horizontal" spacing="1.5rem" padding="2.5rem" > <:item :let={voice}> -
    -
    - <.photo - src={voice.src} +
    +

    + “{voice.quote}” +

    +
    + <.avatar + id={voice.id} + src={Soonex.Public.path(voice.src)} alt={voice.name} - width={800} - height={800} - class="absolute inset-0 size-full" - /> -
    -
    -

    - “{voice.quote}” -

    -
    + class="avatar ui-size-md" + > + <:fallback>{voice.initials} + +
    {voice.name} {voice.role} -
    -
    +
    + <:prev_trigger> - <.heroicon name="hero-chevron-up" /> + <.heroicon name="hero-chevron-left" /> <:next_trigger> - <.heroicon name="hero-chevron-down" /> + <.heroicon name="hero-chevron-right" /> @@ -70,23 +68,29 @@ defmodule Soonex.HomePage.Voces do defp voices do [ %{ + id: "soonex-voice-amira", src: "/images/people/amira.jpg", + initials: "AN", quote: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "We forked on a Thursday, swapped the stills Friday, and the waitlist toast was already the one we wanted to ship.", name: "Amira N.", role: "Founder, studio launch" }, %{ + id: "soonex-voice-jonas", src: "/images/people/jonas.jpg", + initials: "JK", quote: - "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + "No package.json, no LiveView pager, no forked Corex CSS. Mix setup and the controls are the upstream ones.", name: "Jonas K.", role: "Staff engineer" }, %{ + id: "soonex-voice-leah", src: "/images/people/leah.jpg", + initials: "LM", quote: - "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + "neo, uno, duo, and leo finally feel like four products. Template Options is how we review with clients.", name: "Leah M.", role: "Product designer" } diff --git a/lib/pages/home/waitlist.ex b/lib/pages/home/waitlist.ex index 24cbe26..149f8c3 100644 --- a/lib/pages/home/waitlist.ex +++ b/lib/pages/home/waitlist.ex @@ -20,10 +20,11 @@ defmodule Soonex.HomePage.Waitlist do align={:center} > <:title> - Commodo consequat + Join the September list <:lede> - Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Tell us your role, interests, and team size. This demo does not store addresses — submit still + shows the launch toast. <.hero countdown_ms={@countdown_ms} /> - <.socii /> <.contents /> <.capita /> <.officia /> diff --git a/lib/pages/not_found_page.ex b/lib/pages/not_found_page.ex index c36f076..7289ff4 100644 --- a/lib/pages/not_found_page.ex +++ b/lib/pages/not_found_page.ex @@ -30,7 +30,7 @@ defmodule Soonex.NotFoundPage do Page not found

    - That URL is not on this site. Head home or read the shipping log. + That URL is not on Soonex. Head home for the waitlist, or read the shipping log.

    <.navigate to={Soonex.Public.path("/")} class="button ui-brand ui-solid ui-size-md"> diff --git a/lib/pages/tags_index_page.ex b/lib/pages/tags_index_page.ex index 29c73d3..c7bd902 100644 --- a/lib/pages/tags_index_page.ex +++ b/lib/pages/tags_index_page.ex @@ -37,7 +37,7 @@ defmodule Soonex.TagsIndexPage do
    <.layout_heading class="layout-heading" subtitle_tag="p"> <:title>Tags - <:subtitle>Browse the journal by topic + <:subtitle>Browse the shipping log by theme, launch, studio, and notes. <:actions> <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-sm"> <.heroicon name="hero-arrow-left" /> Journal @@ -45,15 +45,20 @@ defmodule Soonex.TagsIndexPage do -
    +

    No tags yet.

    -
    +

    <.navigate to={tag.href} class="link ui-nav">{tag.label}

    From af9ec89cf25221add418aa94f780e69a06800757 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 03:06:59 +0000 Subject: [PATCH 40/65] Give tabs full-width triggers and drop repeated data lists. Expand product tabs across the row, replace spec-sheet data lists with stats and badges, and retune theme paper, brand, and radius plus ui-trigger utilities so Soonex has its own corner language. Co-authored-by: Karim Semmoud --- assets/css/chrome.css | 43 +--------- assets/css/hosts.css | 145 +++++++++++++--------------------- config/config.exs | 110 +++++++++++++------------- lib/layouts/shell.ex | 8 +- lib/pages/home/capita.ex | 11 --- lib/pages/home/contents.ex | 2 +- lib/pages/home/kalendarium.ex | 10 +-- lib/pages/home/numeri.ex | 23 ++++-- lib/pages/home/officia.ex | 7 +- 9 files changed, 146 insertions(+), 213 deletions(-) diff --git a/assets/css/chrome.css b/assets/css/chrome.css index c53fae9..45e8a42 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -116,7 +116,7 @@ html[data-motion="reduce"] .soonex-card-motion:hover { } .soonex-frame { - border-radius: var(--radius-2xl); + border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); } .soonex-photo { @@ -154,44 +154,3 @@ html[data-motion="reduce"] .soonex-card-motion:hover { height: var(--spacing-size); width: auto; } - -.data-list.data-list--wide, -.tabs.tabs--wide { - max-width: none; -} - -.data-list.data-list--wide - [data-scope="data-list"][data-part="root"][data-orientation="horizontal"] - [data-scope="data-list"][data-part="item"] { - display: flex; - width: 100%; - flex-direction: row; - align-items: center; -} - -.data-list.data-list--wide - [data-scope="data-list"][data-part="root"][data-orientation="horizontal"] - [data-scope="data-list"][data-part="label"] { - min-width: 4.5rem; - max-width: 8rem; - width: auto; - flex: 0 0 auto; -} - -.data-list.data-list--wide - [data-scope="data-list"][data-part="root"][data-orientation="horizontal"] - [data-scope="data-list"][data-part="content"] { - display: flex; - flex: 1 1 0%; - width: auto; - min-width: 0; - align-items: center; -} - -.data-list.data-list--wide - [data-scope="data-list"][data-part="root"][data-orientation="horizontal"] - [data-scope="data-list"][data-part="content"] - .link { - max-width: none; - width: 100%; -} diff --git a/assets/css/hosts.css b/assets/css/hosts.css index 42141c0..cac68d5 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -3,6 +3,7 @@ [data-theme] { --soonex-ease: cubic-bezier(0.22, 1, 0.36, 1); --soonex-header-offset: calc(var(--spacing-size) + 1.5rem); + --soonex-ctl-radius: var(--radius-lg); } .button, @@ -120,10 +121,22 @@ width: 100%; } -.tabs.tabs--wide [data-scope="tabs"][data-part="list"] { +.tabs.ui-width-full [data-scope="tabs"][data-part="list"] { width: 100%; max-width: none; - justify-content: flex-start; + flex-wrap: nowrap; +} + +.tabs.ui-width-full [data-scope="tabs"][data-part="trigger"] { + flex: 1 1 0%; + width: auto; + min-width: 0; + justify-content: center; +} + +.tabs.ui-width-full [data-scope="tabs"][data-part="content"][data-orientation="horizontal"] { + border-end-start-radius: var(--soonex-ctl-radius, var(--radius-md)); + border-end-end-radius: var(--soonex-ctl-radius, var(--radius-md)); } .carousel.ui-width-full [data-scope="carousel"][data-part="item"] { @@ -166,98 +179,26 @@ pre.code.code--wide { } /* - Per-theme ui-* personality. Corex recipes read --ctl-radius from - ui-rounded-*; these host rules retune that axis so neo/uno/duo/leo - do not share one corner language. + Shared Soonex radius. :where() keeps specificity at 0 so ui-rounded-* + on a host still wins. Circles stay full. */ -[data-theme="neo"] :is( - .button, - .badge, - .native-input, - .avatar, - .menu, - .select, - .tabs, - .toast, - .pagination, - .clipboard, - .accordion, - .collapsible, - .carousel, - .date-picker, - .tags-input, - .number-input, - .radio-group, - .data-table, - .switch, - .checkbox, - .timer, - .tooltip, - .dialog, - .floating-panel -) { - --ctl-radius: var(--radius-xl); +[data-theme="neo"] { + --soonex-ctl-radius: var(--radius-xl); } -[data-theme="uno"] :is( - .button, - .badge, - .native-input, - .avatar, - .menu, - .select, - .tabs, - .toast, - .pagination, - .clipboard, - .accordion, - .collapsible, - .carousel, - .date-picker, - .tags-input, - .number-input, - .radio-group, - .data-table, - .switch, - .checkbox, - .timer, - .tooltip, - .dialog, - .floating-panel -) { - --ctl-radius: var(--radius-sm); +[data-theme="uno"] { + --soonex-ctl-radius: var(--radius-sm); } -[data-theme="duo"] :is( - .button, - .badge, - .native-input, - .avatar, - .menu, - .select, - .tabs, - .toast, - .pagination, - .clipboard, - .accordion, - .collapsible, - .carousel, - .date-picker, - .tags-input, - .number-input, - .radio-group, - .data-table, - .switch, - .checkbox, - .timer, - .tooltip, - .dialog, - .floating-panel -) { - --ctl-radius: var(--radius-3xl); +[data-theme="duo"] { + --soonex-ctl-radius: var(--radius-3xl); } -[data-theme="leo"] :is( +[data-theme="leo"] { + --soonex-ctl-radius: var(--radius-none); +} + +:where( .button, .badge, .native-input, @@ -282,8 +223,8 @@ pre.code.code--wide { .tooltip, .dialog, .floating-panel -) { - --ctl-radius: var(--radius-none); +):not(.ui-trigger--circle) { + --ctl-radius: var(--soonex-ctl-radius); } [data-theme="leo"] :is(.button, .badge) { @@ -299,3 +240,29 @@ pre.code.code--wide { [data-theme="duo"] .soonex-quote { font-family: var(--font-serif); } + +/* + Corex pins ui-trigger--square to radius-sm. Map square triggers (and the + same recipe on button/badge/toggle) onto Soonex's per-theme corner. +*/ +@layer utilities { + .ui-trigger--square { + border-radius: var(--soonex-ctl-radius, var(--radius-md)); + --ctl-radius: var(--soonex-ctl-radius, var(--radius-md)); + } + + .button.ui-trigger--square, + .badge.ui-trigger--square { + border-radius: var(--soonex-ctl-radius, var(--radius-md)); + --ctl-radius: var(--soonex-ctl-radius, var(--radius-md)); + } + + .toggle.ui-trigger--square [data-scope="toggle"][data-part="root"] { + border-radius: var(--soonex-ctl-radius, var(--radius-md)); + --ctl-radius: var(--soonex-ctl-radius, var(--radius-md)); + } + + .ui-solid:not(.ui-trigger--square):not(.ui-trigger--circle) { + --ctl-radius: var(--soonex-ctl-radius, var(--radius-lg)); + } +} diff --git a/config/config.exs b/config/config.exs index a0f4c2d..75c4ef0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -134,9 +134,9 @@ config :corex_design, themes: %{ neo: %{ seeds: %{ - neutral: "#F3F0E8", - accent: "#12141C", - brand: "#1E40AF", + neutral: "#D5DCE6", + accent: "#0E1218", + brand: "#1D4ED8", alert: "#B42318", info: "#0E7490", success: "#15803D" @@ -144,11 +144,11 @@ config :corex_design, colors: %{ light: with_on.(%{ - root: l.(0.99, :neutral), - surface: l.(0.965, :neutral), - ui: fill.(0.93, :neutral), - accent: fill.(0.24, :accent), - brand: fill.(0.4, :brand), + root: l.(0.945, :neutral), + surface: l.(0.91, :neutral), + ui: fill.(0.865, :neutral), + accent: fill.(0.22, :accent), + brand: fill.(0.42, :brand), ink: contrast.(:accent, :root, 12), "ink-muted": contrast.(:accent, :root, 6.0), link: contrast.(:brand, :root, 5.5), @@ -173,14 +173,14 @@ config :corex_design, }, 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, + xs: 0.35, + sm: 0.6, + md: 0.9, + lg: 1.2, + xl: 1.55, + "2xl": 1.95, + "3xl": 2.5, + "4xl": 3.2, full: 9999 }, font: %{ @@ -217,9 +217,9 @@ config :corex_design, }, uno: %{ seeds: %{ - neutral: "#B9E0D4", - accent: "#031814", - brand: "#0D9488", + neutral: "#9FD4C4", + accent: "#04221C", + brand: "#0F766E", alert: "#B91C1C", info: "#0369A1", success: "#166534" @@ -227,11 +227,11 @@ config :corex_design, 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), + root: l.(0.935, :neutral), + surface: l.(0.9, :neutral), + ui: fill.(0.85, :neutral), + accent: fill.(0.28, :accent), + brand: fill.(0.34, :brand), ink: contrast.(:accent, :root, 10), "ink-muted": contrast.(:accent, :root, 5.5), link: contrast.(:brand, :root, 5.8), @@ -256,14 +256,14 @@ config :corex_design, }, dimensions: %{ radius: %{ - xs: 0.08, - sm: 0.14, - md: 0.22, - lg: 0.32, - xl: 0.44, - "2xl": 0.58, - "3xl": 0.74, - "4xl": 0.92, + xs: 0.04, + sm: 0.08, + md: 0.14, + lg: 0.22, + xl: 0.32, + "2xl": 0.44, + "3xl": 0.58, + "4xl": 0.74, full: 9999 }, font: %{ @@ -292,9 +292,9 @@ config :corex_design, }, duo: %{ seeds: %{ - neutral: "#F6E2CC", - accent: "#3F1F0F", - brand: "#B45309", + neutral: "#E8C4A0", + accent: "#3A1C0F", + brand: "#C2410C", alert: "#9F1239", info: "#3D5278", success: "#3F6B4E" @@ -302,11 +302,11 @@ config :corex_design, colors: %{ light: 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), + root: l.(0.94, :neutral), + surface: l.(0.905, :neutral), + ui: fill.(0.86, :neutral), + accent: fill.(0.32, :accent), + brand: fill.(0.44, :brand), ink: contrast.(:accent, :root, 9.5), "ink-muted": contrast.(:accent, :root, 5.2), link: contrast.(:brand, :root, 5.6), @@ -331,14 +331,14 @@ config :corex_design, }, dimensions: %{ radius: %{ - xs: 0.7, - sm: 1.1, - md: 1.6, - lg: 2.2, - xl: 2.9, - "2xl": 3.8, - "3xl": 5.0, - "4xl": 6.5, + xs: 0.9, + sm: 1.4, + md: 2.0, + lg: 2.8, + xl: 3.6, + "2xl": 4.6, + "3xl": 6.0, + "4xl": 7.5, full: 9999 }, font: %{ @@ -371,8 +371,8 @@ config :corex_design, }, leo: %{ seeds: %{ - neutral: "#D2C19A", - accent: "#050505", + neutral: "#CDB889", + accent: "#111111", brand: "#EA580C", alert: "#C41E1E", info: "#0369A1", @@ -381,11 +381,11 @@ config :corex_design, 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), + root: l.(0.925, :neutral), + surface: l.(0.88, :neutral), + ui: fill.(0.82, :neutral), + accent: fill.(0.14, :accent), + brand: fill.(0.38, :brand), ink: contrast.(:accent, :root, 12), "ink-muted": contrast.(:accent, :root, 5.8), link: contrast.(:brand, :root, 5.8), diff --git a/lib/layouts/shell.ex b/lib/layouts/shell.ex index be9163a..8cd1032 100644 --- a/lib/layouts/shell.ex +++ b/lib/layouts/shell.ex @@ -50,5 +50,11 @@ defmodule Soonex.Layouts.Shell do def frame, do: "soonex-frame relative overflow-hidden" - def data_list, do: "data-list data-list--wide ui-accent ui-size-md w-full max-w-none" + def stat_grid, do: "grid grid-cols-2 gap-4 lg:grid-cols-4" + + def stat, do: "#{panel()} flex min-h-36 flex-col justify-between gap-6 p-6 sm:p-8" + + def feature_list, do: "m-0 mt-6 flex list-none flex-col gap-3 p-0" + + def chip_row, do: "flex flex-wrap items-center gap-2" end diff --git a/lib/pages/home/capita.ex b/lib/pages/home/capita.ex index 4978b45..2ea73b6 100644 --- a/lib/pages/home/capita.ex +++ b/lib/pages/home/capita.ex @@ -2,7 +2,6 @@ defmodule Soonex.HomePage.Capita do @moduledoc false use Phoenix.Component - use Corex import Soonex.Layouts.Media, only: [photo: 1] import Soonex.Layouts.Section, only: [block: 1] @@ -52,7 +51,6 @@ defmodule Soonex.HomePage.Capita do
    - <.data_list class={"#{Shell.data_list()} mt-4"} items={ships()} /> """ end @@ -73,13 +71,4 @@ defmodule Soonex.HomePage.Capita do } ] end - - defp ships do - Corex.Content.new([ - %{label: "Waitlist", content: "Email, role, interests, team size, launch notes, toast"}, - %{label: "Themes", content: "neo, uno, duo, and leo from config overlays"}, - %{label: "Journal", content: "Six shipping-log posts and tag pages"}, - %{label: "Chrome", content: "Countdown, accessibility, Template Options"} - ]) - end end diff --git a/lib/pages/home/contents.ex b/lib/pages/home/contents.ex index 71ec306..db65ab2 100644 --- a/lib/pages/home/contents.ex +++ b/lib/pages/home/contents.ex @@ -27,7 +27,7 @@ defmodule Soonex.HomePage.Contents do <.tabs id="soonex-product-tabs" - class="tabs tabs--wide ui-brand ui-size-md" + class="tabs ui-brand ui-size-md ui-width-full" indicator value="overview" items={product_tabs()} diff --git a/lib/pages/home/kalendarium.ex b/lib/pages/home/kalendarium.ex index abcf44f..188b193 100644 --- a/lib/pages/home/kalendarium.ex +++ b/lib/pages/home/kalendarium.ex @@ -43,11 +43,11 @@ defmodule Soonex.HomePage.Kalendarium do

    {beat.lede}

    - <.data_list - :if={beat.facts != []} - class={Shell.data_list()} - items={Corex.Content.new(beat.facts)} - /> +
    + + {fact.label}: {fact.content} + +
    <.collapsible id={beat.note_id} class="collapsible ui-brand ui-size-sm ui-width-full" diff --git a/lib/pages/home/numeri.ex b/lib/pages/home/numeri.ex index a8c6dc8..d828024 100644 --- a/lib/pages/home/numeri.ex +++ b/lib/pages/home/numeri.ex @@ -2,7 +2,6 @@ defmodule Soonex.HomePage.Numeri do @moduledoc false use Phoenix.Component - use Corex import Soonex.Layouts.Section, only: [block: 1] @@ -23,17 +22,25 @@ defmodule Soonex.HomePage.Numeri do <:lede> No vanity counters. These are the numbers this template ships with. - <.data_list class={Shell.data_list()} items={facts()} /> +
    +
    +

    {fact.label}

    +

    + {fact.value} +

    +

    {fact.detail}

    +
    +
    """ end defp facts do - Corex.Content.new([ - %{label: "Themes", content: "4 — neo, uno, duo, leo"}, - %{label: "Journal", content: "6 shipping-log posts"}, - %{label: "npm", content: "0 — Mix, Tailwind, esbuild"}, - %{label: "Launch", content: "1 September"} - ]) + [ + %{label: "Themes", value: "4", detail: "neo, uno, duo, leo"}, + %{label: "Journal", value: "6", detail: "Shipping-log posts"}, + %{label: "npm", value: "0", detail: "Mix, Tailwind, esbuild"}, + %{label: "Launch", value: "1 Sep", detail: "Countdown target"} + ] end end diff --git a/lib/pages/home/officia.ex b/lib/pages/home/officia.ex index 6eae8f7..1e5a21b 100644 --- a/lib/pages/home/officia.ex +++ b/lib/pages/home/officia.ex @@ -31,7 +31,12 @@ defmodule Soonex.HomePage.Officia do

    {plan.name}

    {plan.headline}

    {plan.body}

    - <.data_list class={"#{Shell.data_list()} mt-6"} items={Corex.Content.new(plan.items)} /> +
      +
    • + {item.label} + {item.content} +
    • +
    <.navigate to="#epistula" class={"#{plan.cta_class} mt-8 w-full"}> {plan.cta} From 82e213e2254434e6a088dfaf2bfef24415165f94 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 03:36:24 +0000 Subject: [PATCH 41/65] Force product tabs into equal columns and chip the plan labels. Tab triggers use a zero-basis flex row so Overview, Themes, and Mix share the full width. Plan and calendar labels use filled badges instead of ghost text. Co-authored-by: Karim Semmoud --- assets/css/hosts.css | 39 +++++++++++++++++++++++++++++++++-- lib/pages/home/kalendarium.ex | 2 +- lib/pages/home/officia.ex | 2 +- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/assets/css/hosts.css b/assets/css/hosts.css index cac68d5..a0286d8 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -121,17 +121,31 @@ width: 100%; } +.tabs.ui-width-full { + max-width: none; +} + +.tabs.ui-width-full [data-scope="tabs"][data-part="root"] { + width: 100%; +} + .tabs.ui-width-full [data-scope="tabs"][data-part="list"] { + display: flex; width: 100%; max-width: none; flex-wrap: nowrap; + justify-content: stretch; + align-items: stretch; } .tabs.ui-width-full [data-scope="tabs"][data-part="trigger"] { - flex: 1 1 0%; - width: auto; + display: flex; + flex: 1 1 0; + width: 0; min-width: 0; + max-width: none; justify-content: center; + text-align: center; } .tabs.ui-width-full [data-scope="tabs"][data-part="content"][data-orientation="horizontal"] { @@ -246,6 +260,27 @@ pre.code.code--wide { same recipe on button/badge/toggle) onto Soonex's per-theme corner. */ @layer utilities { + .tabs.ui-width-full { + max-width: none; + width: 100%; + } + + .tabs.ui-width-full [data-scope="tabs"][data-part="list"] { + display: flex; + width: 100%; + max-width: none; + flex-wrap: nowrap; + } + + .tabs.ui-width-full [data-scope="tabs"][data-part="trigger"] { + display: flex; + flex: 1 1 0; + width: 0; + min-width: 0; + max-width: none; + justify-content: center; + } + .ui-trigger--square { border-radius: var(--soonex-ctl-radius, var(--radius-md)); --ctl-radius: var(--soonex-ctl-radius, var(--radius-md)); diff --git a/lib/pages/home/kalendarium.ex b/lib/pages/home/kalendarium.ex index 188b193..ce6ac1b 100644 --- a/lib/pages/home/kalendarium.ex +++ b/lib/pages/home/kalendarium.ex @@ -44,7 +44,7 @@ defmodule Soonex.HomePage.Kalendarium do

    {beat.lede}

    - + {fact.label}: {fact.content}
    diff --git a/lib/pages/home/officia.ex b/lib/pages/home/officia.ex index 1e5a21b..d2439b8 100644 --- a/lib/pages/home/officia.ex +++ b/lib/pages/home/officia.ex @@ -33,7 +33,7 @@ defmodule Soonex.HomePage.Officia do

    {plan.body}

    • - {item.label} + {item.label} {item.content}
    From 456d5815fc814f47c826afe205db5986d156f095 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 03:51:34 +0000 Subject: [PATCH 42/65] Let theme typography own headlines and rewrite the shipping-log copy. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-theme maps now set h1/h2 size, case, and family so neo/uno/duo/leo change how type sits, not only color. Headings drop Tailwind recipes; section intros use eyebrow/lede tags. Home order is product → inside → voices → plans, with manifesto tabs and a shorter FAQ. Co-authored-by: Karim Semmoud --- assets/css/hosts.css | 57 ++++++++++++++++- config/config.exs | 112 +++++++++++++++++++++++++++------- lib/layouts/articles.ex | 6 +- lib/layouts/brand.ex | 2 +- lib/layouts/root/footer.ex | 4 +- lib/layouts/root_layout.ex | 2 +- lib/layouts/section.ex | 2 +- lib/layouts/shell.ex | 12 ++-- lib/pages/home/capita.ex | 10 +-- lib/pages/home/contents.ex | 14 ++--- lib/pages/home/faq.ex | 61 ++++++++---------- lib/pages/home/hero.ex | 15 ++--- lib/pages/home/highlights.ex | 6 +- lib/pages/home/kalendarium.ex | 4 +- lib/pages/home/numeri.ex | 8 +-- lib/pages/home/officia.ex | 6 +- lib/pages/home/voces.ex | 11 ++-- lib/pages/home_page.ex | 2 +- lib/pages/not_found_page.ex | 6 +- lib/pages/tags_index_page.ex | 4 +- 20 files changed, 226 insertions(+), 118 deletions(-) diff --git a/assets/css/hosts.css b/assets/css/hosts.css index a0286d8..1901f52 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -246,13 +246,68 @@ pre.code.code--wide { text-transform: uppercase; } -[data-theme="uno"] .display { +[data-theme="uno"] h1, +[data-theme="uno"] h2, +[data-theme="uno"] .soonex-quote, +[data-theme="uno"] .soonex-stat-value { letter-spacing: -0.03em; } [data-theme="duo"] blockquote, [data-theme="duo"] .soonex-quote { font-family: var(--font-serif); + font-style: italic; +} + +.soonex-lockup-word { + font-family: var(--font-display); + font-size: var(--text-lg); + font-weight: var(--font-weight-semibold); + letter-spacing: var(--tracking-tight); + color: var(--color-ink); +} + +.soonex-card-title { + font-family: var(--font-display); + font-size: var(--text-xl); + font-weight: var(--font-weight-semibold); + letter-spacing: var(--tracking-tight); + line-height: var(--text-xl--line-height); + color: var(--color-ink); +} + +.soonex-quote { + font-family: var(--font-display); + font-size: var(--text-2xl); + font-weight: var(--font-weight-semibold); + line-height: var(--leading-snug); + color: var(--color-ink); +} + +@media (min-width: 64rem) { + .soonex-quote { + font-size: var(--text-4xl); + } +} + +.soonex-stat-value { + font-family: var(--font-display); + font-size: var(--text-4xl); + font-weight: var(--font-weight-semibold); + letter-spacing: var(--tracking-tight); + line-height: 1; + color: var(--color-ink); +} + +small.eyebrow { + font-weight: var(--font-weight-semibold); + color: var(--color-brand-text); +} + +[data-theme="leo"] small.eyebrow { + font-size: var(--text-sm); + letter-spacing: var(--tracking-widest); + text-transform: uppercase; } /* diff --git a/config/config.exs b/config/config.exs index 75c4ef0..1bd00ca 100644 --- a/config/config.exs +++ b/config/config.exs @@ -76,7 +76,7 @@ config :tableau, Tableau.RSSExtension, enabled: true, title: "Soonex", description: - "Tableau + Corex launch template: waitlist, countdown, and themes you customize in config." + "A waitlist you can fork. Four themes in config. A journal. A countdown aimed at 1 September." config :elixir, :time_zone_database, Tz.TimeZoneDatabase @@ -195,20 +195,34 @@ config :corex_design, "h1" => %{ font_family: {:font, :display}, font_weight: {:weight, :bold}, - letter_spacing: {:tracking, :tighter} + letter_spacing: {:tracking, :tighter}, + font_size: {:text, :"5xl"}, + sm: %{font_size: {:text, :"6xl"}}, + lg: %{font_size: {:text, :"8xl"}} }, "h2" => %{ font_family: {:font, :display}, font_weight: {:weight, :bold}, - letter_spacing: {:tracking, :tight} + letter_spacing: {:tracking, :tight}, + font_size: {:text, :"4xl"}, + sm: %{font_size: {:text, :"5xl"}} + }, + "h3" => %{ + font_family: {:font, :display}, + font_weight: {:weight, :semibold}, + font_size: {:text, :xl} }, - "h3" => %{font_family: {:font, :display}, font_weight: {:weight, :semibold}}, "h4" => %{font_family: {:font, :sans}, font_weight: {:weight, :semibold}}, "p" => %{line_height: {:leading, :relaxed}}, - "p.display" => %{ - font_family: {:font, :display}, - font_weight: {:weight, :bold}, - letter_spacing: {:tracking, :tighter} + "p.lede" => %{ + font_size: {:text, :lg}, + line_height: {:leading, :relaxed}, + color: "var(--color-ink-muted)" + }, + "small.eyebrow" => %{ + font_weight: {:weight, :semibold}, + font_size: {:text, :base}, + color: "var(--color-brand-text)" }, "blockquote" => %{font_family: {:font, :serif}, font_style: :italic}, "code" => %{font_family: {:font, :mono}}, @@ -278,13 +292,32 @@ config :corex_design, "h1" => %{ font_family: {:font, :display}, font_weight: {:weight, :semibold}, - letter_spacing: {:tracking, :tight} + letter_spacing: {:tracking, :tight}, + font_size: {:text, :"4xl"}, + sm: %{font_size: {:text, :"5xl"}}, + lg: %{font_size: {:text, :"6xl"}} + }, + "h2" => %{ + font_family: {:font, :display}, + font_weight: {:weight, :semibold}, + letter_spacing: {:tracking, :tight}, + font_size: {:text, :"3xl"}, + sm: %{font_size: {:text, :"4xl"}} }, - "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}}, + "p.lede" => %{ + font_size: {:text, :lg}, + line_height: {:leading, :snug}, + color: "var(--color-ink-muted)" + }, + "small.eyebrow" => %{ + font_weight: {:weight, :medium}, + font_size: {:text, :sm}, + letter_spacing: {:tracking, :wide}, + color: "var(--color-brand-text)" + }, "blockquote" => %{font_style: :italic}, "code" => %{font_family: {:font, :mono}}, "kbd" => %{font_family: {:font, :mono}} @@ -353,16 +386,31 @@ config :corex_design, "h1" => %{ font_family: {:font, :display}, font_weight: {:weight, :bold}, - letter_spacing: {:tracking, :tight} + letter_spacing: {:tracking, :tight}, + font_size: {:text, :"5xl"}, + sm: %{font_size: {:text, :"6xl"}}, + lg: %{font_size: {:text, :"7xl"}} + }, + "h2" => %{ + font_family: {:font, :display}, + font_weight: {:weight, :semibold}, + font_size: {:text, :"4xl"}, + sm: %{font_size: {:text, :"5xl"}} }, - "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} + "p.lede" => %{ + font_family: {:font, :serif}, + font_style: :italic, + font_size: {:text, :lg}, + line_height: {:leading, :relaxed}, + color: "var(--color-ink-muted)" + }, + "small.eyebrow" => %{ + font_family: {:font, :sans}, + font_weight: {:weight, :semibold}, + color: "var(--color-brand-text)" }, "blockquote" => %{font_family: {:font, :serif}, font_style: :italic}, "code" => %{font_family: {:font, :mono}}, @@ -429,16 +477,38 @@ config :corex_design, } }, typography: %{ - "h1" => %{font_weight: {:weight, :semibold}, letter_spacing: {:tracking, :tight}}, - "h2" => %{font_weight: {:weight, :semibold}}, + "h1" => %{ + font_weight: {:weight, :semibold}, + letter_spacing: {:tracking, :tight}, + font_size: {:text, :"4xl"}, + sm: %{font_size: {:text, :"5xl"}}, + lg: %{font_size: {:text, :"6xl"}} + }, + "h2" => %{ + font_weight: {:weight, :semibold}, + font_size: {:text, :"3xl"}, + sm: %{font_size: {:text, :"4xl"}} + }, "h3" => %{font_weight: {:weight, :medium}}, "h4" => %{ font_weight: {:weight, :medium}, letter_spacing: {:tracking, :widest}, - font_size: {:text, :sm} + font_size: {:text, :sm}, + text_transform: "uppercase" }, "p" => %{line_height: {:leading, :snug}}, - "p.display" => %{font_weight: {:weight, :semibold}, letter_spacing: {:tracking, :tight}}, + "p.lede" => %{ + font_size: {:text, :lg}, + line_height: {:leading, :snug}, + color: "var(--color-ink-muted)" + }, + "small.eyebrow" => %{ + font_weight: {:weight, :medium}, + font_size: {:text, :sm}, + letter_spacing: {:tracking, :widest}, + text_transform: "uppercase", + color: "var(--color-brand-text)" + }, "blockquote" => %{font_style: :italic}, "code" => %{font_family: {:font, :mono}}, "kbd" => %{font_family: {:font, :mono}} diff --git a/lib/layouts/articles.ex b/lib/layouts/articles.ex index bc25c88..8211ab3 100644 --- a/lib/layouts/articles.ex +++ b/lib/layouts/articles.ex @@ -30,13 +30,13 @@ defmodule Soonex.Layouts.Articles do <.photo src={cover(post).src} alt={cover(post).alt} width={1400} height={900} />
    -

    {date_label(post)}

    -

    + {date_label(post)} +

    <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> {post[:title] || "Untitled"}

    -

    +

    {post[:description]}

    diff --git a/lib/layouts/brand.ex b/lib/layouts/brand.ex index 883a850..04fe220 100644 --- a/lib/layouts/brand.ex +++ b/lib/layouts/brand.ex @@ -17,7 +17,7 @@ defmodule Soonex.Layouts.Brand do class={["link ui-nav ui-size-md flex items-center gap-2 no-underline", @class]} > <.mark class="size-8 shrink-0" /> - Soonex + Soonex """ end diff --git a/lib/layouts/root/footer.ex b/lib/layouts/root/footer.ex index 654c2d1..adb5ede 100644 --- a/lib/layouts/root/footer.ex +++ b/lib/layouts/root/footer.ex @@ -26,8 +26,8 @@ defmodule Soonex.Layouts.Root.Footer do
    <.lockup /> -

    - The first Corex plus Tableau waitlist template. Fork, theme, and ship 1 September. +

    + A waitlist you can fork. Four themes. A journal. Ship 1 September.

    • diff --git a/lib/layouts/root_layout.ex b/lib/layouts/root_layout.ex index 2aa0acb..41382b4 100644 --- a/lib/layouts/root_layout.ex +++ b/lib/layouts/root_layout.ex @@ -221,7 +221,7 @@ defmodule Soonex.RootLayout do defp meta_description(page, site_name) do cond do page[:page_kind] == :home -> - "Soonex is the first Corex plus Tableau waitlist template. Fork, theme, journal, countdown, and ship 1 September." + "A waitlist you can fork. Four themes in config. A journal. A countdown. Ship 1 September." page[:page_kind] == :blog_index -> "Shipping notes from #{site_name}: themes, countdown, clone steps, and the road to 1 September." diff --git a/lib/layouts/section.ex b/lib/layouts/section.ex index 1d417a5..efb1388 100644 --- a/lib/layouts/section.ex +++ b/lib/layouts/section.ex @@ -27,7 +27,7 @@ defmodule Soonex.Layouts.Section do >
      -

      {@eyebrow}

      + {@eyebrow}

      {render_slot(@title)}

      diff --git a/lib/layouts/shell.ex b/lib/layouts/shell.ex index 8cd1032..1a52a28 100644 --- a/lib/layouts/shell.ex +++ b/lib/layouts/shell.ex @@ -35,15 +35,15 @@ defmodule Soonex.Layouts.Shell do def sticky_body, do: "w-full min-w-0 lg:col-span-7" - def eyebrow, do: "m-0 text-base/7 font-semibold text-brand-text" + def eyebrow, do: "eyebrow m-0" - def section_heading, - do: "display mt-2 text-pretty text-4xl font-semibold tracking-tight text-ink sm:text-5xl" + def section_heading, do: "mt-2 text-pretty" - def page_heading, - do: "display mt-2 text-pretty text-4xl font-semibold tracking-tight text-ink sm:text-5xl" + def page_heading, do: "mt-2 text-pretty" - def lede, do: "mt-6 max-w-2xl text-pretty text-lg/8 text-ink-muted" + def lede, do: "lede mt-6 max-w-2xl text-pretty" + + def card_title, do: "soonex-card-title mt-2" def panel, do: "soonex-frame relative overflow-hidden border border-border bg-surface" diff --git a/lib/pages/home/capita.ex b/lib/pages/home/capita.ex index 2ea73b6..df5eabd 100644 --- a/lib/pages/home/capita.ex +++ b/lib/pages/home/capita.ex @@ -33,8 +33,8 @@ defmodule Soonex.HomePage.Capita do class="absolute inset-0 size-full" />
      -

      Template

      -

      + Template +

      Waitlist, journal, and a countdown aimed at 1 September

      @@ -45,9 +45,9 @@ defmodule Soonex.HomePage.Capita do :for={cell <- cells()} class={"#{Shell.panel()} soonex-card-motion flex flex-1 flex-col p-6 sm:p-8"} > -

      {cell.kicker}

      -

      {cell.title}

      -

      {cell.body}

      + {cell.kicker} +

      {cell.title}

      +

      {cell.body}

    diff --git a/lib/pages/home/contents.ex b/lib/pages/home/contents.ex index db65ab2..29a4ddf 100644 --- a/lib/pages/home/contents.ex +++ b/lib/pages/home/contents.ex @@ -22,8 +22,8 @@ defmodule Soonex.HomePage.Contents do Fork once. Theme in config. <:lede> - Soonex is a static Tableau site with Corex controls already on the page. No LiveView events, - no npm, no forked component CSS. + Static on purpose. Tableau builds the pages. Corex runs the controls. You replace the stills + and the date. <.tabs id="soonex-product-tabs" @@ -36,8 +36,8 @@ defmodule Soonex.HomePage.Contents do
    -

    Quick start

    -

    + Quick start +

    Clone the template, then run mix setup and mix soonex.server.

    @@ -68,19 +68,19 @@ defmodule Soonex.HomePage.Contents do value: "overview", label: "Overview", content: - "A waitlist landing page, a Markdown journal, countdown chrome, and the Corex controls already wired on this page. Photography, copy, and the launch date are yours to replace." + "Static on purpose. Tableau builds the pages. Corex runs the controls. You replace the stills and the date." }, %{ value: "themes", label: "Themes", content: - "neo, uno, duo, and leo live in config. Template Options switches them on this device so they read as four products, not four tints. Host CSS retunes layout; it does not copy Corex recipes." + "neo is a gallery. uno is a spa. duo is a salon. leo is a print shop. Template Options is the client review." }, %{ value: "mix", label: "Mix", content: - "Mix, Tailwind, and esbuild already in the project are enough. There is no package.json and no Lenis. Host scripts bind the header, the waitlist toast, and journal pagination on the client." + "Mix, Tailwind, esbuild. There is no package.json. Host CSS retunes layout. Recipes stay upstream." } ]) end diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index a81f5aa..6ae895a 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -13,7 +13,7 @@ defmodule Soonex.HomePage.Faq do Before you clone <:lede> - What ships, what is demo-only, and how themes, motion, and the journal behave after you fork. + Opinionated answers. Still true after you fork. <:actions> <.navigate to="#epistula" class="link ui-brand"> @@ -25,7 +25,7 @@ defmodule Soonex.HomePage.Faq do class="accordion ui-accent ui-size-md mx-auto w-full max-w-none" multiple={false} collapsible={true} - value="ships" + value="gallery" items={faq_items()} > <:trigger :let={item}> @@ -45,58 +45,47 @@ defmodule Soonex.HomePage.Faq do defp faq_items do Corex.Content.new([ %{ - value: "ships", - label: "What ships when I fork this template?", - content: - "A waitlist landing page, a journal, countdown chrome, and the Corex controls already on this page. Photography, copy, and the launch date are placeholders you replace." + value: "gallery", + label: "Is this a component gallery?", + content: "No. It’s a waitlist with a date. The controls happen to be Corex." }, %{ - value: "waitlist", - label: "Does the waitlist collect real addresses?", + value: "email", + label: "Will you email me?", content: - "Not in this demo. Submit still shows a toast so you can see the success path without storing mailboxes. Wire the same form to your list later — interests, role, team size, and launch notes are already named fields." + "Not from this demo. Submit anyway — the toast is the whole point until you wire a list." }, %{ value: "themes", - label: "How do themes change without forking Corex CSS?", - content: - "Theme, brand, and accent live in config. The floating Template Options panel switches neo, uno, duo, and leo on this device. Host CSS only retunes layout; it does not copy component recipes or set overlay scale, duration, or opacity." - }, - %{ - value: "journal", - label: "Where does the journal live, and can I add posts?", - content: - "Posts are Markdown under the journal route. The home carousel is a short reading list; the index paginates the rest with the Corex pager on the client — no LiveView round trip. Add a file, rebuild, and the cover, date, and title show up. Tags stay optional." + label: "Why four themes?", + content: "So a client can sit on the sofa and say ‘that one’ without a rebuild." }, %{ - value: "motion", - label: "How does scroll motion behave with reduced motion?", - content: - "Scroll reveal and card lift live in host CSS. They follow scroll and hover; they do not fight the type. Set Motion to Reduced in Accessibility and those animations stop. The condensed header and progress bar stay as chrome." + value: "npm", + label: "Do I need npm?", + content: "No. If you miss it, this isn’t your template." }, %{ - value: "a11y", - label: "What do the accessibility controls change?", - content: - "The person-in-circle control at the corner opens zoom, contrast, motion, cursor, focus, and link underline for this device. Choices persist in local storage and write data attributes on the document. Reset returns the defaults." + value: "posts", + label: "Where do posts go?", + content: "Markdown. Rebuild. The pager is a Corex event in the browser, not LiveView." }, %{ - value: "toolchain", - label: "Do I need npm or a JavaScript bundler to start?", + value: "motion", + label: "What if motion makes me sick?", content: - "No. Mix, Tailwind, and esbuild already in the project are enough. There is no package.json and no Lenis. Tabs, carousel, and accordion are Corex hooks. Host scripts bind header chrome, the waitlist toast, and journal pagination." + "Accessibility → Motion → Reduced. The header still condenses. The type still holds." }, %{ - value: "countdown", - label: "How is the countdown wired to a launch date?", - content: - "The hero timer counts down to 1 September in this demo. The header timer stays in the bar and appears after the hero. Change the target in the home template when you have a real date. Days, hours, minutes, and seconds stay on one row." + value: "photos", + label: "Can I keep the photos?", + content: "Swap them. They’re placeholders with a license, not the brand." }, %{ - value: "brand", - label: "Can I keep the photography after a rebrand?", + value: "september", + label: "When is September?", content: - "Yes. Swap the stills and the lockup. Toolchain marks live in the footer as static SVGs, not a marquee. Host CSS does not own the photos." + "Whenever you change the timestamp in the home template. The digits stay on one row." } ]) end diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index 60e43ec..2d96447 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -33,16 +33,13 @@ defmodule Soonex.HomePage.Hero do <:second_label>Sec -

    +

    Soonex ships 1 September.

    - The first Corex plus Tableau waitlist template. Fork it, theme it in config, keep the - journal and countdown, and launch with accessible controls. + A waitlist you can fork. Four themes in config. A journal. A countdown. Controls you + didn’t have to invent.

    @@ -68,11 +65,11 @@ defmodule Soonex.HomePage.Hero do
    -

    Waitlist

    -

    + Waitlist +

    Reserve a September seat

    -

    +

    This demo does not collect addresses. Submit still shows the launch toast.

    diff --git a/lib/pages/home/highlights.ex b/lib/pages/home/highlights.ex index 0e103bb..bc6cfaf 100644 --- a/lib/pages/home/highlights.ex +++ b/lib/pages/home/highlights.ex @@ -61,11 +61,11 @@ defmodule Soonex.HomePage.Highlights do />
    -

    {date_label(post)}

    -

    + {date_label(post)} +

    {post[:title] || "Untitled"}

    -

    +

    {post[:description]}

    <.navigate diff --git a/lib/pages/home/kalendarium.ex b/lib/pages/home/kalendarium.ex index ce6ac1b..b75419d 100644 --- a/lib/pages/home/kalendarium.ex +++ b/lib/pages/home/kalendarium.ex @@ -37,12 +37,12 @@ defmodule Soonex.HomePage.Kalendarium do

    {beat.moment}

    -

    +

    {beat.title}

    -

    {beat.lede}

    +

    {beat.lede}

    {fact.label}: {fact.content} diff --git a/lib/pages/home/numeri.ex b/lib/pages/home/numeri.ex index d828024..535d8b7 100644 --- a/lib/pages/home/numeri.ex +++ b/lib/pages/home/numeri.ex @@ -24,11 +24,11 @@ defmodule Soonex.HomePage.Numeri do
    -

    {fact.label}

    -

    - {fact.value} +

    + {fact.label}

    -

    {fact.detail}

    +

    {fact.value}

    +

    {fact.detail}

    diff --git a/lib/pages/home/officia.ex b/lib/pages/home/officia.ex index d2439b8..e72ca64 100644 --- a/lib/pages/home/officia.ex +++ b/lib/pages/home/officia.ex @@ -28,9 +28,9 @@ defmodule Soonex.HomePage.Officia do :for={plan <- plans()} class={"#{Shell.panel()} soonex-card-motion flex flex-col p-6 sm:p-8"} > -

    {plan.name}

    -

    {plan.headline}

    -

    {plan.body}

    + {plan.name} +

    {plan.headline}

    +

    {plan.body}

    • {item.label} diff --git a/lib/pages/home/voces.ex b/lib/pages/home/voces.ex index 0143b91..f26b004 100644 --- a/lib/pages/home/voces.ex +++ b/lib/pages/home/voces.ex @@ -35,7 +35,7 @@ defmodule Soonex.HomePage.Voces do > <:item :let={voice}>
      -

      +

      “{voice.quote}”

      @@ -71,8 +71,7 @@ defmodule Soonex.HomePage.Voces do id: "soonex-voice-amira", src: "/images/people/amira.jpg", initials: "AN", - quote: - "We forked on a Thursday, swapped the stills Friday, and the waitlist toast was already the one we wanted to ship.", + quote: "We didn’t need a design system. We needed a date on the page by Friday.", name: "Amira N.", role: "Founder, studio launch" }, @@ -80,8 +79,7 @@ defmodule Soonex.HomePage.Voces do id: "soonex-voice-jonas", src: "/images/people/jonas.jpg", initials: "JK", - quote: - "No package.json, no LiveView pager, no forked Corex CSS. Mix setup and the controls are the upstream ones.", + quote: "I grepped for package.json. Then I made coffee.", name: "Jonas K.", role: "Staff engineer" }, @@ -89,8 +87,7 @@ defmodule Soonex.HomePage.Voces do id: "soonex-voice-leah", src: "/images/people/leah.jpg", initials: "LM", - quote: - "neo, uno, duo, and leo finally feel like four products. Template Options is how we review with clients.", + quote: "I review with Template Options now. Four rooms, same building.", name: "Leah M.", role: "Product designer" } diff --git a/lib/pages/home_page.ex b/lib/pages/home_page.ex index 5d33490..ed8bc71 100644 --- a/lib/pages/home_page.ex +++ b/lib/pages/home_page.ex @@ -34,9 +34,9 @@ defmodule Soonex.HomePage do <.hero countdown_ms={@countdown_ms} /> <.contents /> <.capita /> + <.voces /> <.officia /> <.kalendarium /> - <.voces /> <.numeri /> <.highlights posts={@posts} /> <.faq /> diff --git a/lib/pages/not_found_page.ex b/lib/pages/not_found_page.ex index 7289ff4..e6fee1a 100644 --- a/lib/pages/not_found_page.ex +++ b/lib/pages/not_found_page.ex @@ -25,11 +25,11 @@ defmodule Soonex.NotFoundPage do
      <.lockup /> -

      404

      -

      + 404 +

      Page not found

      -

      +

      That URL is not on Soonex. Head home for the waitlist, or read the shipping log.

      diff --git a/lib/pages/tags_index_page.ex b/lib/pages/tags_index_page.ex index c7bd902..f3bed2b 100644 --- a/lib/pages/tags_index_page.ex +++ b/lib/pages/tags_index_page.ex @@ -59,10 +59,10 @@ defmodule Soonex.TagsIndexPage do data-soonex-page-item class={"#{Shell.panel()} flex flex-col p-8"} > -

      +

      <.navigate to={tag.href} class="link ui-nav">{tag.label}

      -

      +

      {tag.count} {if tag.count == 1, do: "post", else: "posts"}

    From e2c5c6b82c0978a8f3a858bb714d891ff333c7ff Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 04:06:59 +0000 Subject: [PATCH 43/65] Keep mobile timer digits on a 2em strip so they do not clip. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Narrow viewports still compact the digit type, but leading stays 2 so Zag’s flip window is not cut off. Co-authored-by: Karim Semmoud --- assets/css/chrome.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/css/chrome.css b/assets/css/chrome.css index 45e8a42..5f8ca44 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -105,7 +105,7 @@ html[data-motion="reduce"] .soonex-card-motion:hover { .timer.ui-size-md, .timer.ui-size-lg { --timer-digit: var(--text-xl); - --timer-digit-leading: var(--text-xl--line-height); + --timer-digit-leading: 2; } .timer [data-scope="timer"][data-part="area"] { From 9fc9cb1764a151d925f2de24b0c305b1b6b10a75 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 04:33:39 +0000 Subject: [PATCH 44/65] Restage Soonex as a near-white product landing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lift every theme’s paper to near-white so identity lives in type, radius, and brand. The hero is a sentence plus a still; the waitlist is one form; calendar, facts, and the journal carousel leave the home. Co-authored-by: Karim Semmoud --- assets/css/hosts.css | 13 +---- config/config.exs | 42 +++++++------- lib/pages/home/capita.ex | 2 +- lib/pages/home/contents.ex | 8 +-- lib/pages/home/faq.ex | 3 +- lib/pages/home/hero.ex | 114 +++++++------------------------------ lib/pages/home/officia.ex | 6 +- lib/pages/home/voces.ex | 63 ++++++++------------ lib/pages/home_page.ex | 21 +------ 9 files changed, 78 insertions(+), 194 deletions(-) diff --git a/assets/css/hosts.css b/assets/css/hosts.css index 1901f52..c056e03 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -241,11 +241,6 @@ pre.code.code--wide { --ctl-radius: var(--soonex-ctl-radius); } -[data-theme="leo"] :is(.button, .badge) { - letter-spacing: 0.08em; - text-transform: uppercase; -} - [data-theme="uno"] h1, [data-theme="uno"] h2, [data-theme="uno"] .soonex-quote, @@ -278,18 +273,12 @@ pre.code.code--wide { .soonex-quote { font-family: var(--font-display); - font-size: var(--text-2xl); + font-size: var(--text-xl); font-weight: var(--font-weight-semibold); line-height: var(--leading-snug); color: var(--color-ink); } -@media (min-width: 64rem) { - .soonex-quote { - font-size: var(--text-4xl); - } -} - .soonex-stat-value { font-family: var(--font-display); font-size: var(--text-4xl); diff --git a/config/config.exs b/config/config.exs index 1bd00ca..4c77840 100644 --- a/config/config.exs +++ b/config/config.exs @@ -134,7 +134,7 @@ config :corex_design, themes: %{ neo: %{ seeds: %{ - neutral: "#D5DCE6", + neutral: "#F3F5F8", accent: "#0E1218", brand: "#1D4ED8", alert: "#B42318", @@ -144,15 +144,15 @@ config :corex_design, colors: %{ light: with_on.(%{ - root: l.(0.945, :neutral), - surface: l.(0.91, :neutral), - ui: fill.(0.865, :neutral), + root: l.(0.99, :neutral), + surface: l.(0.985, :neutral), + ui: fill.(0.96, :neutral), accent: fill.(0.22, :accent), brand: fill.(0.42, :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), + border: contrast.(:neutral, :ui, 1.2), focus: contrast.(:brand, :ui, 2.3), shadow: contrast.(:accent, :root, 1.1) }), @@ -198,7 +198,7 @@ config :corex_design, letter_spacing: {:tracking, :tighter}, font_size: {:text, :"5xl"}, sm: %{font_size: {:text, :"6xl"}}, - lg: %{font_size: {:text, :"8xl"}} + lg: %{font_size: {:text, :"7xl"}} }, "h2" => %{ font_family: {:font, :display}, @@ -231,7 +231,7 @@ config :corex_design, }, uno: %{ seeds: %{ - neutral: "#9FD4C4", + neutral: "#F4F8F6", accent: "#04221C", brand: "#0F766E", alert: "#B91C1C", @@ -241,15 +241,15 @@ config :corex_design, colors: %{ light: with_on.(%{ - root: l.(0.935, :neutral), - surface: l.(0.9, :neutral), - ui: fill.(0.85, :neutral), + root: l.(0.99, :neutral), + surface: l.(0.985, :neutral), + ui: fill.(0.96, :neutral), accent: fill.(0.28, :accent), brand: fill.(0.34, :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), + border: contrast.(:neutral, :ui, 1.18), focus: contrast.(:brand, :ui, 2.2), shadow: contrast.(:accent, :root, 1.06) }), @@ -325,7 +325,7 @@ config :corex_design, }, duo: %{ seeds: %{ - neutral: "#E8C4A0", + neutral: "#F8F6F3", accent: "#3A1C0F", brand: "#C2410C", alert: "#9F1239", @@ -335,15 +335,15 @@ config :corex_design, colors: %{ light: with_on.(%{ - root: l.(0.94, :neutral), - surface: l.(0.905, :neutral), - ui: fill.(0.86, :neutral), + root: l.(0.99, :neutral), + surface: l.(0.985, :neutral), + ui: fill.(0.96, :neutral), accent: fill.(0.32, :accent), brand: fill.(0.44, :brand), ink: contrast.(:accent, :root, 9.5), "ink-muted": contrast.(:accent, :root, 5.2), link: contrast.(:brand, :root, 5.6), - border: contrast.(:neutral, :ui, 1.14), + border: contrast.(:neutral, :ui, 1.16), focus: contrast.(:brand, :ui, 2.15), shadow: contrast.(:accent, :root, 1.28) }), @@ -419,7 +419,7 @@ config :corex_design, }, leo: %{ seeds: %{ - neutral: "#CDB889", + neutral: "#F6F5F2", accent: "#111111", brand: "#EA580C", alert: "#C41E1E", @@ -429,15 +429,15 @@ config :corex_design, colors: %{ light: with_on.(%{ - root: l.(0.925, :neutral), - surface: l.(0.88, :neutral), - ui: fill.(0.82, :neutral), + root: l.(0.99, :neutral), + surface: l.(0.985, :neutral), + ui: fill.(0.96, :neutral), accent: fill.(0.14, :accent), brand: fill.(0.38, :brand), ink: contrast.(:accent, :root, 12), "ink-muted": contrast.(:accent, :root, 5.8), link: contrast.(:brand, :root, 5.8), - border: contrast.(:accent, :ui, 1.75), + border: contrast.(:accent, :ui, 1.22), focus: contrast.(:brand, :ui, 2.5), shadow: contrast.(:accent, :root, 1.04) }), diff --git a/lib/pages/home/capita.ex b/lib/pages/home/capita.ex index df5eabd..1057e56 100644 --- a/lib/pages/home/capita.ex +++ b/lib/pages/home/capita.ex @@ -67,7 +67,7 @@ defmodule Soonex.HomePage.Capita do kicker: "Controls", title: "Corex, not a custom kit", body: - "Select, accordion, tabs, carousel, and the waitlist fields are upstream Corex. Host CSS only retunes layout tokens." + "Select, accordion, tabs, and the waitlist fields are upstream Corex. Host CSS only retunes layout tokens." } ] end diff --git a/lib/pages/home/contents.ex b/lib/pages/home/contents.ex index 29a4ddf..5a73381 100644 --- a/lib/pages/home/contents.ex +++ b/lib/pages/home/contents.ex @@ -35,10 +35,10 @@ defmodule Soonex.HomePage.Contents do
    -
    - Quick start +
    + Clone

    - Clone the template, then run mix setup and mix soonex.server. + mix setup, then mix soonex.server.

    <.clipboard @@ -74,7 +74,7 @@ defmodule Soonex.HomePage.Contents do value: "themes", label: "Themes", content: - "neo is a gallery. uno is a spa. duo is a salon. leo is a print shop. Template Options is the client review." + "neo, uno, duo, and leo share one near-white field. Type, radius, and brand do the rest. Template Options is the client review." }, %{ value: "mix", diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index 6ae895a..1fd7688 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -58,7 +58,8 @@ defmodule Soonex.HomePage.Faq do %{ value: "themes", label: "Why four themes?", - content: "So a client can sit on the sofa and say ‘that one’ without a rebuild." + content: + "Type, radius, and brand. Paper stays near-white. Template Options is the review." }, %{ value: "npm", diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index 2d96447..7fa07f9 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -8,8 +8,6 @@ defmodule Soonex.HomePage.Hero do alias Soonex.Layouts.Shell - attr(:countdown_ms, :integer, required: true) - def hero(assigns) do ~H"""
    -
    +
    - <.timer - id="soonex-hero-countdown" - countdown - start_ms={@countdown_ms} - target_ms={0} - class="timer ui-success ui-size-lg w-full max-w-lg" - > - <:day_label>Days - <:hour_label>Hours - <:minute_label>Min - <:second_label>Sec - - -

    +

    Soonex ships 1 September.

    @@ -50,10 +35,27 @@ defmodule Soonex.HomePage.Hero do Read the log <.heroicon name="hero-arrow-up-right" />
    + +
    +
    + <.avatar + :for={person <- people()} + id={person.id} + src={Soonex.Public.path(person.src)} + alt={person.alt} + class="avatar ui-size-sm" + > + <:fallback>{person.initials} + +
    +

    + Studios already cloning the repo for a 1 September launch. +

    +
    -
    +
    <.photo src="/images/photos/hero.jpg" alt="Sunlit studio with long work tables and hanging task lamps" @@ -62,82 +64,6 @@ defmodule Soonex.HomePage.Hero do class="absolute inset-0 size-full" loading="eager" /> -
    -
    -
    - Waitlist -

    - Reserve a September seat -

    -

    - This demo does not collect addresses. Submit still shows the launch toast. -

    - - - <.native_input - type="email" - name="waitlist[email]" - id="soonex-hero-waitlist-email" - required - autocomplete="email" - placeholder="you@studio.dev" - class="native-input ui-size-md ui-width-full" - > - <:label class="sr-only">Email - - - <.radio_group - id="soonex-hero-role" - name="waitlist[role]" - class="radio-group ui-brand ui-width-full" - value="founder" - items={[ - %{value: "founder", label: "Founder"}, - %{value: "engineer", label: "Engineer"}, - %{value: "designer", label: "Designer"} - ]} - > - <:label>I am a - - - <.switch - id="soonex-hero-notes" - name="waitlist[notes]" - checked - class="switch ui-brand" - > - <:label>Email me launch notes - - - - -
    -
    - -
    -
    - <.avatar - :for={person <- people()} - id={person.id} - src={Soonex.Public.path(person.src)} - alt={person.alt} - class="avatar ui-size-sm" - > - <:fallback>{person.initials} - -
    -

    - Studios already cloning the repo for a 1 September launch. -

    -
    -
    diff --git a/lib/pages/home/officia.ex b/lib/pages/home/officia.ex index e72ca64..d718378 100644 --- a/lib/pages/home/officia.ex +++ b/lib/pages/home/officia.ex @@ -32,9 +32,9 @@ defmodule Soonex.HomePage.Officia do

    {plan.headline}

    {plan.body}

      -
    • - {item.label} - {item.content} +
    • + {item.label} + {item.content}
    <.navigate to="#epistula" class={"#{plan.cta_class} mt-8 w-full"}> diff --git a/lib/pages/home/voces.ex b/lib/pages/home/voces.ex index f26b004..217d829 100644 --- a/lib/pages/home/voces.ex +++ b/lib/pages/home/voces.ex @@ -22,45 +22,32 @@ defmodule Soonex.HomePage.Voces do From studios already forking <:lede> - Quotes from the people putting Soonex on a September calendar — not a component gallery. + Short notes from people putting Soonex on a September calendar. - <.carousel - id="soonex-voces-carousel" - class="carousel ui-brand ui-width-full max-w-none" - items={@voices} - loop - orientation="horizontal" - spacing="1.5rem" - padding="2.5rem" - > - <:item :let={voice}> -
    -

    - “{voice.quote}” -

    -
    - <.avatar - id={voice.id} - src={Soonex.Public.path(voice.src)} - alt={voice.name} - class="avatar ui-size-md" - > - <:fallback>{voice.initials} - -
    - {voice.name} - {voice.role} -
    -
    -
    - - <:prev_trigger> - <.heroicon name="hero-chevron-left" /> - - <:next_trigger> - <.heroicon name="hero-chevron-right" /> - - +
    +
    +

    + “{voice.quote}” +

    +
    + <.avatar + id={voice.id} + src={Soonex.Public.path(voice.src)} + alt={voice.name} + class="avatar ui-size-md" + > + <:fallback>{voice.initials} + +
    + {voice.name} + {voice.role} +
    +
    +
    +
    """ end diff --git a/lib/pages/home_page.ex b/lib/pages/home_page.ex index ed8bc71..e42ae34 100644 --- a/lib/pages/home_page.ex +++ b/lib/pages/home_page.ex @@ -8,37 +8,18 @@ defmodule Soonex.HomePage do import Soonex.HomePage.Contents, only: [contents: 1] import Soonex.HomePage.Capita, only: [capita: 1] import Soonex.HomePage.Officia, only: [officia: 1] - import Soonex.HomePage.Kalendarium, only: [kalendarium: 1] import Soonex.HomePage.Voces, only: [voces: 1] - import Soonex.HomePage.Numeri, only: [numeri: 1] - import Soonex.HomePage.Highlights, only: [highlights: 1] import Soonex.HomePage.Faq, only: [faq: 1] import Soonex.HomePage.Waitlist, only: [waitlist: 1] def template(assigns) do - assigns = - assigns - |> Map.put( - :countdown_ms, - max(DateTime.diff(~U[2026-09-01 00:00:00Z], DateTime.utc_now(), :millisecond), 0) - ) - |> Map.put( - :posts, - assigns - |> Map.get(:posts, []) - |> List.wrap() - ) - ~H"""
    - <.hero countdown_ms={@countdown_ms} /> + <.hero /> <.contents /> <.capita /> <.voces /> <.officia /> - <.kalendarium /> - <.numeri /> - <.highlights posts={@posts} /> <.faq /> <.waitlist />
    From 9caa914e761bb1d351eaab3c9c72535536296ae7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 04:42:15 +0000 Subject: [PATCH 45/65] Restore the hero waitlist card on the studio still. The right column is photography plus the reserve-a-seat form and proof strip again. The left column stays a product sentence and CTAs. Co-authored-by: Karim Semmoud --- lib/pages/home/hero.ex | 97 +++++++++++++++++++++++++++++++++--------- 1 file changed, 78 insertions(+), 19 deletions(-) diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index 7fa07f9..a9b6a8d 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -16,7 +16,7 @@ defmodule Soonex.HomePage.Hero do data-hero-boundary >
    -
    +

    Soonex ships 1 September. @@ -35,27 +35,10 @@ defmodule Soonex.HomePage.Hero do Read the log <.heroicon name="hero-arrow-up-right" />

    - -
    -
    - <.avatar - :for={person <- people()} - id={person.id} - src={Soonex.Public.path(person.src)} - alt={person.alt} - class="avatar ui-size-sm" - > - <:fallback>{person.initials} - -
    -

    - Studios already cloning the repo for a 1 September launch. -

    -
    -
    +
    <.photo src="/images/photos/hero.jpg" alt="Sunlit studio with long work tables and hanging task lamps" @@ -64,6 +47,82 @@ defmodule Soonex.HomePage.Hero do class="absolute inset-0 size-full" loading="eager" /> +
    +
    +
    + Waitlist +

    + Reserve a September seat +

    +

    + This demo does not collect addresses. Submit still shows the launch toast. +

    + +
    + <.native_input + type="email" + name="waitlist[email]" + id="soonex-hero-waitlist-email" + required + autocomplete="email" + placeholder="you@studio.dev" + class="native-input ui-size-md ui-width-full" + > + <:label class="sr-only">Email + + + <.radio_group + id="soonex-hero-role" + name="waitlist[role]" + class="radio-group ui-brand ui-width-full" + value="founder" + items={[ + %{value: "founder", label: "Founder"}, + %{value: "engineer", label: "Engineer"}, + %{value: "designer", label: "Designer"} + ]} + > + <:label>I am a + + + <.switch + id="soonex-hero-notes" + name="waitlist[notes]" + checked + class="switch ui-brand" + > + <:label>Email me launch notes + + + +
    +
    +
    + +
    +
    + <.avatar + :for={person <- people()} + id={person.id} + src={Soonex.Public.path(person.src)} + alt={person.alt} + class="avatar ui-size-sm" + > + <:fallback>{person.initials} + +
    +

    + Studios already cloning the repo for a 1 September launch. +

    +
    +
    From 5fd7f0993ec4415fa9bcb83ef3ca1cd897367cf3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 05:29:27 +0000 Subject: [PATCH 46/65] Keep 1 September on one line when the headline wraps. The launch date is a nowrap phrase in the hero and other UI copy so the day and month cannot split. Co-authored-by: Karim Semmoud --- lib/layouts/root/footer.ex | 2 +- lib/pages/home/capita.ex | 3 ++- lib/pages/home/hero.ex | 7 +++++-- lib/pages/home/officia.ex | 5 ++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/layouts/root/footer.ex b/lib/layouts/root/footer.ex index adb5ede..4e15388 100644 --- a/lib/layouts/root/footer.ex +++ b/lib/layouts/root/footer.ex @@ -27,7 +27,7 @@ defmodule Soonex.Layouts.Root.Footer do
    <.lockup />

    - A waitlist you can fork. Four themes. A journal. Ship 1 September. + A waitlist you can fork. Four themes. A journal. Ship 1 September.

    • diff --git a/lib/pages/home/capita.ex b/lib/pages/home/capita.ex index 1057e56..8d5ba2a 100644 --- a/lib/pages/home/capita.ex +++ b/lib/pages/home/capita.ex @@ -35,7 +35,8 @@ defmodule Soonex.HomePage.Capita do
      Template

      - Waitlist, journal, and a countdown aimed at 1 September + Waitlist, journal, and a countdown aimed at + 1 September

    diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index a9b6a8d..bfcd1c8 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -19,7 +19,8 @@ defmodule Soonex.HomePage.Hero do

    - Soonex ships 1 September. + Soonex + ships 1 September.

    @@ -119,7 +120,9 @@ defmodule Soonex.HomePage.Hero do

    - Studios already cloning the repo for a 1 September launch. + Studios already cloning the repo for a + 1 September + launch.

    diff --git a/lib/pages/home/officia.ex b/lib/pages/home/officia.ex index d718378..e4e51ad 100644 --- a/lib/pages/home/officia.ex +++ b/lib/pages/home/officia.ex @@ -20,8 +20,7 @@ defmodule Soonex.HomePage.Officia do Pick a lane. Ship the same core. <:lede> - Every plan is the same Soonex fork. The columns describe how far you take the template before - 1 September. + Every plan is the same Soonex fork. The columns describe how far you take the template before 1 September.
    Date: Fri, 28 Aug 2026 08:41:16 +0000 Subject: [PATCH 47/65] Make the accessibility trigger a normal filled button. Drop the ghost circle and oversized SVG. The control is a standard .button with a background and a Corex icon, same family as Template Options. Co-authored-by: Karim Semmoud --- assets/css/hosts.css | 10 ---------- lib/soonex/accessibility.ex | 19 ++----------------- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/assets/css/hosts.css b/assets/css/hosts.css index c056e03..935aa32 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -93,16 +93,6 @@ border-width: 0; } -.soonex-a11y-trigger { - padding: 0; -} - -.soonex-a11y-trigger svg { - /* Corex buttons pin child SVGs to 1em !important; fill the circle instead. */ - width: var(--ctl-size, var(--spacing-size)) !important; - height: var(--ctl-size, var(--spacing-size)) !important; -} - .menu.ui-width-fit [data-scope="menu"][data-part="root"] > [data-scope="menu"][data-part="trigger"] { width: auto; min-width: var(--ctl-size, var(--spacing-size)); diff --git a/lib/soonex/accessibility.ex b/lib/soonex/accessibility.ex index 2b53cdb..2832689 100644 --- a/lib/soonex/accessibility.ex +++ b/lib/soonex/accessibility.ex @@ -110,9 +110,7 @@ defmodule Soonex.Accessibility do def accessibility_dialog_id, do: "a11y-dialog" - attr(:trigger_class, :string, - default: "button ui-ghost ui-size-md ui-trigger--circle p-0 soonex-a11y-trigger" - ) + attr(:trigger_class, :string, default: "button ui-size-sm") def accessibility_panel(assigns) do assigns = assign(assigns, :axes, Accessibility.axes()) @@ -128,20 +126,7 @@ defmodule Soonex.Accessibility do final_focus={"dialog:#{accessibility_dialog_id()}:trigger"} > <:trigger class={@trigger_class} aria_label="Accessibility"> - + <.heroicon name="hero-user-circle" /> <:title>Accessibility <:description> From ba0a9e4e33b8a7f133c1f81ca1a8ff57838bb457 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 08:43:02 +0000 Subject: [PATCH 48/65] Label the accessibility control as a normal filled button. Match Template Options: icon plus text on a standard .button with a background. Co-authored-by: Karim Semmoud --- lib/soonex/accessibility.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/soonex/accessibility.ex b/lib/soonex/accessibility.ex index 2832689..be2416e 100644 --- a/lib/soonex/accessibility.ex +++ b/lib/soonex/accessibility.ex @@ -126,7 +126,7 @@ defmodule Soonex.Accessibility do final_focus={"dialog:#{accessibility_dialog_id()}:trigger"} > <:trigger class={@trigger_class} aria_label="Accessibility"> - <.heroicon name="hero-user-circle" /> + <.heroicon name="hero-user-circle" /> Accessibility <:title>Accessibility <:description> From 81d96a994f18022061ac8785419a36a407ac5168 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 29 Aug 2026 01:08:13 +0000 Subject: [PATCH 49/65] Size the accessibility button with the Template Options cluster. Dialog triggers were inheriting the default --ctl-size, so Accessibility rendered at md while Template Options stayed ui-size-sm. Keep radius, drop the size override, and let the floating-panel trigger use --ctl-size too. Co-authored-by: Karim Semmoud --- assets/css/hosts.css | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/assets/css/hosts.css b/assets/css/hosts.css index 935aa32..9368219 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -174,14 +174,25 @@ pre.code.code--wide { } .select [data-scope="select"][data-part="trigger"], -.menu [data-scope="menu"][data-part="trigger"], -.dialog [data-scope="dialog"][data-part="trigger"] { +.menu [data-scope="menu"][data-part="trigger"] { --ctl-radius: inherit; --ctl-size: var(--ctl-size, var(--spacing-size)); --ctl-space: var(--ctl-space, var(--spacing-space)); --ctl-text: var(--ctl-text, var(--text-base)); } +/* Dialog triggers are .button hosts. Keep ui-size-* (the Template Options + cluster is ui-size-sm). Only inherit radius so corners stay Soonex. */ +.dialog [data-scope="dialog"][data-part="trigger"] { + --ctl-radius: inherit; +} + +/* Corex pins floating-panel triggers to 0.8 * --spacing-size (md). Let + ui-size-sm on the demo cluster set one height with Accessibility. */ +.floating-panel [data-scope="floating-panel"][data-part="trigger"] { + min-height: var(--ctl-size, var(--spacing-size)); +} + /* Shared Soonex radius. :where() keeps specificity at 0 so ui-rounded-* on a host still wins. Circles stay full. From a1550d395e9fa6b4f355eccf1e77565cc1a476d4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 29 Aug 2026 18:22:19 +0000 Subject: [PATCH 50/65] Restage Soonex as a production Corex launch landing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Break the Tailwind-UI band recipe, put a waitlist object on a full-bleed still, and speak like a September launch — English ids, four rooms, a shipping-log tease, and no demo apology above the fold. Co-authored-by: Karim Semmoud --- CHANGELOG.md | 2 + README.md | 6 +- ...12-duis-aute.md => 2026-03-12-fork-day.md} | 12 +- ...-excepteur.md => 2026-04-08-four-rooms.md} | 20 +- ...docs.md => 2026-05-08-clone-theme-ship.md} | 40 ++-- ...pping.md => 2026-06-01-ci-before-pages.md} | 6 +- _posts/2026-07-14-header-timer.md | 36 ++++ _posts/2026-07-14-nemo-enim.md | 36 ---- ... => 2026-08-02-accessibility-on-device.md} | 12 +- assets/css/chrome.css | 32 +++ assets/css/hosts.css | 60 ++++++ config/config.exs | 19 +- extra/images/grain.svg | 6 + lib/layouts/articles.ex | 53 ++--- lib/layouts/post_layout.ex | 4 +- lib/layouts/root/footer.ex | 46 ++--- lib/layouts/root/nav.ex | 24 +-- lib/layouts/root_layout.ex | 10 +- lib/layouts/section.ex | 29 ++- lib/layouts/shell.ex | 38 ++-- lib/layouts/tag_layout.ex | 4 +- lib/pages/blog_index_page.ex | 8 +- lib/pages/home/capita.ex | 75 ------- lib/pages/home/contents.ex | 91 --------- lib/pages/home/faq.ex | 52 ++--- lib/pages/home/hero.ex | 167 +++++++--------- lib/pages/home/highlights.ex | 101 ---------- lib/pages/home/kalendarium.ex | 188 ------------------ lib/pages/home/log.ex | 55 +++++ lib/pages/home/notes.ex | 95 +++++++++ lib/pages/home/numeri.ex | 46 ----- lib/pages/home/officia.ex | 88 -------- lib/pages/home/product.ex | 125 ++++++++++++ lib/pages/home/themes.ex | 42 ++++ lib/pages/home/voces.ex | 83 -------- lib/pages/home/waitlist.ex | 11 +- lib/pages/home_page.ex | 26 ++- lib/pages/not_found_page.ex | 2 +- lib/pages/tags_index_page.ex | 18 +- 39 files changed, 736 insertions(+), 1032 deletions(-) rename _posts/{2026-03-12-duis-aute.md => 2026-03-12-fork-day.md} (54%) rename _posts/{2026-04-08-excepteur.md => 2026-04-08-four-rooms.md} (62%) rename _posts/{2026-05-08-docs.md => 2026-05-08-clone-theme-ship.md} (72%) rename _posts/{2026-06-01-shipping.md => 2026-06-01-ci-before-pages.md} (90%) create mode 100644 _posts/2026-07-14-header-timer.md delete mode 100644 _posts/2026-07-14-nemo-enim.md rename _posts/{2026-08-02-temporibus.md => 2026-08-02-accessibility-on-device.md} (63%) create mode 100644 extra/images/grain.svg delete mode 100644 lib/pages/home/capita.ex delete mode 100644 lib/pages/home/contents.ex delete mode 100644 lib/pages/home/highlights.ex delete mode 100644 lib/pages/home/kalendarium.ex create mode 100644 lib/pages/home/log.ex create mode 100644 lib/pages/home/notes.ex delete mode 100644 lib/pages/home/numeri.ex delete mode 100644 lib/pages/home/officia.ex create mode 100644 lib/pages/home/product.ex create mode 100644 lib/pages/home/themes.ex delete mode 100644 lib/pages/home/voces.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index 08f30f1..9309f21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 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. diff --git a/README.md b/README.md index c3ca79b..b9d53f8 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,9 @@ Rebuild assets: `mix assets.build`. - **Themes:** overlay **all allowed keys** per theme in [`config/config.exs`](config/config.exs) (`seeds`, `colors.light` / `colors.dark`, `dimensions.radius`, `dimensions.font`, `typography`) plus top-level `scales:`. Contrast stays calculated; `*_scale` / duration / opacity keys are rejected. Then `mix corex.design.build`. [`lib/soonex/theme.ex`](lib/soonex/theme.ex) must list the same ids as `data-theme`. - **Accessibility:** Corex `--a11y` dialog in the demo FAB ([`lib/soonex/accessibility.ex`](lib/soonex/accessibility.ex)). Preferences live in `localStorage` (`phx:a11y`); `corex_design` must stay a **runtime** dep. - **Fonts:** self-hosted woff2 in [`extra/fonts/`](extra/fonts/), faces in [`assets/css/fonts.css`](assets/css/fonts.css). Stacks follow Corex 0.2 (neo = Outfit + Manrope; uno/duo/leo keep their Corex families). -- **Chrome:** sticky header in [`lib/layouts/root/nav.ex`](lib/layouts/root/nav.ex); condensed + countdown `hidden`/`inert` in [`assets/js/landing-scroll-chrome.js`](assets/js/landing-scroll-chrome.js). [`assets/css/hosts.css`](assets/css/hosts.css) is tiny, after `corex.css`. No token opacity (`bg-surface/90`). -- **Content:** home sections in [`lib/pages/home/`](lib/pages/home/), composed by [`lib/pages/home_page.ex`](lib/pages/home_page.ex). Only FAQ uses the sticky split (`layout={:sticky}` in [`lib/layouts/section.ex`](lib/layouts/section.ex)). -- **Blog:** index at [`/blog`](lib/pages/blog_index_page.ex) (`layout_heading`, cards, pagination); posts under [`_posts/`](_posts/) with `Soonex.PostLayout`; tags at [`/tags`](lib/pages/tags_index_page.ex). +- **Chrome:** sticky header in [`lib/layouts/root/nav.ex`](lib/layouts/root/nav.ex); condensed + countdown `hidden`/`inert` in [`assets/js/landing-scroll-chrome.js`](assets/js/landing-scroll-chrome.js). [`assets/css/hosts.css`](assets/css/hosts.css) is after `corex.css`. No token opacity (`bg-surface/90`). +- **Content:** home sections in [`lib/pages/home/`](lib/pages/home/), composed by [`lib/pages/home_page.ex`](lib/pages/home_page.ex). Band skeletons live in [`lib/layouts/section.ex`](lib/layouts/section.ex) (`:open`, `:editorial`, `:sticky`). FAQ is the sticky split. Anchors: `#product`, `#notes`, `#themes`, `#log`, `#questions`, `#waitlist`. +- **Log:** index at [`/blog`](lib/pages/blog_index_page.ex) (`layout_heading`, list, pagination); 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. MDX-style Tableau extras (tags, `include_dir`, sitemap) are summarized in **Tableau data, tags, and static extras** in the longer notes below. diff --git a/_posts/2026-03-12-duis-aute.md b/_posts/2026-03-12-fork-day.md similarity index 54% rename from _posts/2026-03-12-duis-aute.md rename to _posts/2026-03-12-fork-day.md index ee3f9be..170830e 100644 --- a/_posts/2026-03-12-duis-aute.md +++ b/_posts/2026-03-12-fork-day.md @@ -1,9 +1,9 @@ --- layout: Soonex.PostLayout -title: Fork day — Tableau plus Corex +title: Fork day date: 2026-03-12 12:00:00 +0000 -permalink: /blog/duis-aute-irure/ -description: Why Soonex is a static Tableau site with Corex controls, not a LiveView app and not an npm project. +permalink: /blog/fork-day/ +description: Soonex starts as a static launch page you can fork — waitlist, log, countdown, four themes in config. image: /images/photos/hero.jpg image_alt: Sunlit studio with long work tables and hanging task lamps tags: @@ -14,9 +14,7 @@ sitemap: changefreq: monthly --- -Soonex starts as a **static Tableau** site. Mix, Tailwind, and esbuild are already in the project. There is no `package.json`, no Lenis, and no LiveView pager. - -The first pass put a waitlist on the page and a countdown aimed at **1 September**. Copy and stills are placeholders you replace after the fork. +Soonex is an **open-source launch template** from Corex. The first pass put a waitlist on the page and a countdown aimed at **1 September**. Copy and stills are placeholders you replace after the fork. ## What we kept upstream @@ -35,4 +33,4 @@ Corex owns the controls. Host CSS retunes layout. Theme overlays may set seeds, > The clone strip on Product is the same git URL you run locally. > [!NOTE] -> This demo waitlist does not collect addresses. Submit still shows the launch toast so you can see the success path. +> The demo waitlist does not collect addresses. Submit still shows the launch toast so you can see the success path. diff --git a/_posts/2026-04-08-excepteur.md b/_posts/2026-04-08-four-rooms.md similarity index 62% rename from _posts/2026-04-08-excepteur.md rename to _posts/2026-04-08-four-rooms.md index c1d7948..0f171c0 100644 --- a/_posts/2026-04-08-excepteur.md +++ b/_posts/2026-04-08-four-rooms.md @@ -1,9 +1,9 @@ --- layout: Soonex.PostLayout -title: Four themes without forking Corex CSS +title: Four rooms on near-white paper date: 2026-04-08 09:00:00 +0000 -permalink: /blog/excepteur-sint-occaecat/ -description: neo, uno, duo, and leo live in config. Template Options switches them on this device so they read as four products. +permalink: /blog/four-rooms/ +description: neo, uno, duo, and leo live in config. Type, radius, and brand change the room. Paper stays near-white. image: /images/photos/studio.jpg image_alt: A calm studio interior with a low sofa, plants, and a large window tags: @@ -16,18 +16,18 @@ sitemap: Theme, brand, and accent live in config. The floating **Template Options** panel switches neo, uno, duo, and leo on this device. Host CSS does not copy component recipes. -## What each theme is for +## What each room is for -neo is a cool gallery. uno is mint and tight. duo is salon cream with round corners. leo is kraft paper and a hard edge. +neo is a product studio. uno is calm ops. duo is an editorial atelier. leo is a signal poster. Paper stays near-white in every room. ### Review table -| Theme | Paper | Corner | Type | +| Theme | Room | Corner | Type | | --- | --- | --- | --- | -| neo | Gallery | Soft | Outfit | -| uno | Mint | Tight | Sora | -| duo | Salon | Round | Playfair | -| leo | Kraft | None | IBM Plex | +| neo | Product studio | Open | Outfit | +| uno | Calm ops | Tight | Sora | +| duo | Editorial atelier | Soft | Playfair | +| leo | Signal poster | Square | IBM Plex | ### Studio checklist diff --git a/_posts/2026-05-08-docs.md b/_posts/2026-05-08-clone-theme-ship.md similarity index 72% rename from _posts/2026-05-08-docs.md rename to _posts/2026-05-08-clone-theme-ship.md index 88cf14a..4520934 100644 --- a/_posts/2026-05-08-docs.md +++ b/_posts/2026-05-08-clone-theme-ship.md @@ -1,9 +1,9 @@ --- layout: Soonex.PostLayout -title: How to clone, theme, and ship Soonex +title: Clone, theme, and ship date: 2026-05-08 12:00:00 +0000 -permalink: /blog/using-the-soonex-template/ -description: Clone the waitlist template, switch neo/uno/duo/leo in config, and rebuild the Markdown journal. +permalink: /blog/clone-theme-ship/ +description: Clone the launch template, switch neo/uno/duo/leo in config, and rebuild the Markdown log. image: /images/covers/overlay.jpg image_alt: Soft abstract color field in warm paper tones tags: @@ -14,7 +14,7 @@ sitemap: changefreq: monthly --- -Soonex is the first **Corex plus Tableau** waitlist template. Fork it, theme it in config, keep the journal and countdown, and launch with accessible controls. +Soonex is an **open-source launch template** from Corex. Fork it, theme it in config, keep the log and countdown, and launch with accessible controls. ## Start here @@ -43,7 +43,7 @@ Markdown link: [Hexdocs Corex](https://hexdocs.pm/corex). Bare URL with autolink ### Ordered 1. Theme overlays in `config/config.exs` -2. Journal posts under `_posts/` +2. Log posts under `_posts/` 3. Waitlist fields wired to your list ### Task list @@ -56,12 +56,12 @@ Markdown link: [Hexdocs Corex](https://hexdocs.pm/corex). Bare URL with autolink ## Table -| Band | Corex | Note | +| Band | Job | Note | | --- | --- | --- | -| Product | Tabs | Overview / Themes / Mix | -| Plans | Data list | Launch / Studio / Agency | -| Voices | Carousel | Horizontal quotes | -| Journal | Pagination | Client events only | +| Product | What you fork | Tabs + clone strip | +| Rooms | Four themes | Type, radius, brand | +| Log | Shipping notes | Client pager | +| Waitlist | Canonical form | Toast on submit | | Feature | Status | Note | | --- | :---: | --- | @@ -86,16 +86,6 @@ Default Open Graph asset: ![Soonex Open Graph](/images/og.svg) -Tech marks used in the footer: - -![Elixir](/images/tech/elixir.svg) - -![Phoenix](/images/tech/phoenix.svg) - -![Tailwind](/images/tech/tailwind.svg) - -![Hex](/images/tech/hex.svg) - --- ## GitHub-style alerts @@ -113,16 +103,16 @@ Tech marks used in the footer: > `mix soonex.server` fails if port 4999 is already bound. Stop the occupant first. > [!CAUTION] -> Journal pagination is a Corex client event. There is no LiveView `handle_event`. +> Log pagination is a Corex client event. There is no LiveView `handle_event`. --- ## Footnotes -The journal index paginates with the Corex pager on the client[^alpha]. Reduced motion lives in Accessibility[^beta]. +The log index paginates with the Corex pager on the client[^alpha]. Reduced motion lives in Accessibility[^beta]. -[^alpha]: Host script listens for `pagination-page-changed` and hides the other SSR cards. -[^beta]: Scroll reveal and card lift stop under `data-motion=reduce`. +[^alpha]: Host script listens for `pagination-page-changed` and hides the other SSR entries. +[^beta]: Scroll reveal stops under `data-motion=reduce`. --- @@ -154,7 +144,7 @@ Fenced JavaScript: ```javascript el.addEventListener("pagination-page-changed", (e) => { const { page, page_size } = e.detail - // show SSR cards for that 1-based slice + // show SSR entries for that 1-based slice }) ``` diff --git a/_posts/2026-06-01-shipping.md b/_posts/2026-06-01-ci-before-pages.md similarity index 90% rename from _posts/2026-06-01-shipping.md rename to _posts/2026-06-01-ci-before-pages.md index f48ed27..e9997e5 100644 --- a/_posts/2026-06-01-shipping.md +++ b/_posts/2026-06-01-ci-before-pages.md @@ -1,8 +1,8 @@ --- layout: Soonex.PostLayout -title: Shipping notes — mix test before Pages +title: CI before Pages date: 2026-06-01 12:00:00 +0000 -permalink: /blog/shipping-tableau-corex/ +permalink: /blog/ci-before-pages/ description: Keep CI green before Pages deploys. Axe on the home document is the gate; the countdown stays aimed at 1 September. image: /images/covers/shipping.jpg image_alt: A laptop on a wooden desk with a code editor open @@ -45,6 +45,6 @@ defmodule Soonex.Shipping do end ``` -The countdown target in this demo is 1 September. Change it in the home template when you have a real date[^ship]. +The countdown target in this demo is 1 September. Change it in the root layout when you have a real date[^ship]. [^ship]: Days, hours, minutes, and seconds stay on one row. The header timer appears after the hero. diff --git a/_posts/2026-07-14-header-timer.md b/_posts/2026-07-14-header-timer.md new file mode 100644 index 0000000..88b2800 --- /dev/null +++ b/_posts/2026-07-14-header-timer.md @@ -0,0 +1,36 @@ +--- +layout: Soonex.PostLayout +title: The header timer and 1 September +date: 2026-07-14 15:00:00 +0000 +permalink: /blog/header-timer/ +description: The countdown lives in the header, stays on one row, and aims at 1 September until you change the date. +image: /images/photos/orbit.jpg +image_alt: Earth from orbit against a dark sky +tags: + - Launch + - Notes +sitemap: + priority: 0.6 + changefreq: monthly +--- + +The countdown is a **header control**, not a hero gadget. It stays hidden and inert until you pass the hero, then it sits on one row aimed at **1 September**. + +## What to change + +The target lives in the root layout. Swap the timestamp when you have a real date. Days, hours, minutes, and seconds stay on one row. + +- Header timer appears after the hero +- Digits flip in a 2em window — do not shrink it +- Reduced motion still condenses the bar + +> [!IMPORTANT] +> Change the target when you have a real date. The digits stay on one row. + +The condensed header and progress bar stay as chrome when motion is reduced[^orbit]. + +[^orbit]: Accessibility → Motion → Reduced. The type still holds. + +```bash +mix soonex.server +``` diff --git a/_posts/2026-07-14-nemo-enim.md b/_posts/2026-07-14-nemo-enim.md deleted file mode 100644 index b6f4212..0000000 --- a/_posts/2026-07-14-nemo-enim.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -layout: Soonex.PostLayout -title: Calendar beats and the 1 September target -date: 2026-07-14 15:00:00 +0000 -permalink: /blog/nemo-enim-ipsam/ -description: Sticky calendar beats, a client journal pager, and a countdown aimed at 1 September. -image: /images/photos/orbit.jpg -image_alt: Earth from orbit against a dark sky -tags: - - Launch - - Calendar -sitemap: - priority: 0.6 - changefreq: monthly ---- - -The home calendar is a stack of **sticky beats**, not a wall of cards. Open a note when you need the extra pass. The countdown in the hero is aimed at **1 September**. - -## What the beats cover - -Fork day, journal, four themes, the accessibility pass, and launch morning. - -- Tableau plus Corex, no npm -- Journal pagination on the client -- Accessibility choices persist in local storage - -> [!IMPORTANT] -> Change the target in the home template when you have a real date. Days, hours, minutes, and seconds stay on one row. - -The header timer stays in the bar and appears after the hero[^orbit]. - -[^orbit]: The condensed header and progress bar stay as chrome when motion is reduced. - -```bash -mix soonex.server -``` diff --git a/_posts/2026-08-02-temporibus.md b/_posts/2026-08-02-accessibility-on-device.md similarity index 63% rename from _posts/2026-08-02-temporibus.md rename to _posts/2026-08-02-accessibility-on-device.md index fd12d13..7705a20 100644 --- a/_posts/2026-08-02-temporibus.md +++ b/_posts/2026-08-02-accessibility-on-device.md @@ -1,24 +1,24 @@ --- layout: Soonex.PostLayout -title: Accessible controls that persist on device +title: Accessibility on this device date: 2026-08-02 11:00:00 +0000 -permalink: /blog/temporibus-autem/ +permalink: /blog/accessibility-on-device/ description: Zoom, contrast, motion, cursor, focus, and link underline — Corex accessibility, stored in local storage. image: /images/covers/overlay.jpg image_alt: Soft abstract color field in warm paper tones tags: - - Voices - Notes + - Launch sitemap: priority: 0.6 changefreq: monthly --- -The person-in-circle control at the corner opens zoom, contrast, motion, cursor, focus, and link underline for this device. Choices persist in **local storage** and write data attributes on the document. +The Accessibility control opens zoom, contrast, motion, cursor, focus, and link underline for this device. Choices persist in **local storage** and write data attributes on the document. ## What reduced motion does -Scroll reveal and card lift live in host CSS. Set Motion to Reduced in Accessibility and those animations stop. +Scroll reveal lives in host CSS. Set Motion to Reduced in Accessibility and those animations stop. > Reset returns the defaults. The condensed header and progress bar stay as chrome. @@ -26,7 +26,7 @@ Scroll reveal and card lift live in host CSS. Set Motion to Reduced in Accessibi 1. Open Accessibility 2. Set Motion to Reduced -3. Confirm the hero and journal still read +3. Confirm the hero and log still read > [!WARNING] > Host CSS must not set overlay scale, duration, or opacity. Theme.Validator will reject those keys. diff --git a/assets/css/chrome.css b/assets/css/chrome.css index 5f8ca44..047c024 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -119,6 +119,38 @@ html[data-motion="reduce"] .soonex-card-motion:hover { border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); } +.soonex-hero-wash { + background: linear-gradient( + 180deg, + color-mix(in oklab, var(--color-root) 18%, transparent) 0%, + var(--color-root) 86% + ); +} + +@media (width >= 64rem) { + .soonex-hero-wash { + background: linear-gradient( + 105deg, + var(--color-root) 0%, + color-mix(in oklab, var(--color-root) 86%, transparent) 42%, + color-mix(in oklab, var(--color-accent) 24%, transparent) 100% + ); + } +} + +.soonex-waitlist-object { + border: 1px solid var(--color-border); + background-color: var(--color-root); + border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); + padding: var(--spacing-space); +} + +@media (width >= 40rem) { + .soonex-waitlist-object { + padding: calc(var(--spacing-space) * 1.25); + } +} + .soonex-photo { display: block; width: 100%; diff --git a/assets/css/hosts.css b/assets/css/hosts.css index 9368219..da09570 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -6,6 +6,44 @@ --soonex-ctl-radius: var(--radius-lg); } +body.layout { + background-image: + url("../images/grain.svg"), + radial-gradient( + ellipse 80% 50% at 110% -10%, + color-mix(in oklab, var(--color-brand) 10%, transparent), + transparent 55% + ), + radial-gradient( + ellipse 45% 40% at -8% 110%, + color-mix(in oklab, var(--color-accent) 7%, transparent), + transparent 50% + ); + background-repeat: repeat, no-repeat, no-repeat; + background-attachment: scroll, fixed, fixed; + background-blend-mode: multiply, normal, normal; +} + +[data-mode="dark"] body.layout { + background-image: + url("../images/grain.svg"), + radial-gradient( + ellipse 80% 50% at 110% -10%, + color-mix(in oklab, var(--color-brand) 16%, transparent), + transparent 55% + ), + radial-gradient( + ellipse 45% 40% at -8% 110%, + color-mix(in oklab, var(--color-accent) 10%, transparent), + transparent 50% + ); + background-blend-mode: overlay, normal, normal; +} + +html[data-motion="reduce"] body.layout { + background-attachment: scroll; +} + .button, .link.ui-nav { transition-timing-function: var(--soonex-ease); @@ -289,6 +327,28 @@ pre.code.code--wide { color: var(--color-ink); } +.soonex-room-specimen { + font-family: var(--font-display); + font-size: var(--text-6xl); + font-weight: var(--font-weight-bold); + letter-spacing: var(--tracking-tighter); + line-height: 0.95; + color: var(--color-ink); +} + +.soonex-room-name { + font-family: var(--font-display); + font-size: var(--text-4xl); + font-weight: var(--font-weight-semibold); + letter-spacing: var(--tracking-tight); + line-height: 1; + color: var(--color-ink); +} + +.soonex-room { + color: var(--color-ink); +} + small.eyebrow { font-weight: var(--font-weight-semibold); color: var(--color-brand-text); diff --git a/config/config.exs b/config/config.exs index 4c77840..34caf86 100644 --- a/config/config.exs +++ b/config/config.exs @@ -76,7 +76,7 @@ config :tableau, Tableau.RSSExtension, enabled: true, title: "Soonex", description: - "A waitlist you can fork. Four themes in config. A journal. A countdown aimed at 1 September." + "Soonex is an open-source launch template from Corex — waitlist, journal, countdown, four themes in config." config :elixir, :time_zone_database, Tz.TimeZoneDatabase @@ -292,7 +292,7 @@ config :corex_design, "h1" => %{ font_family: {:font, :display}, font_weight: {:weight, :semibold}, - letter_spacing: {:tracking, :tight}, + letter_spacing: {:tracking, :tighter}, font_size: {:text, :"4xl"}, sm: %{font_size: {:text, :"5xl"}}, lg: %{font_size: {:text, :"6xl"}} @@ -479,15 +479,18 @@ config :corex_design, typography: %{ "h1" => %{ font_weight: {:weight, :semibold}, - letter_spacing: {:tracking, :tight}, - font_size: {:text, :"4xl"}, - sm: %{font_size: {:text, :"5xl"}}, - lg: %{font_size: {:text, :"6xl"}} + letter_spacing: {:tracking, :tighter}, + font_size: {:text, :"5xl"}, + text_transform: "uppercase", + sm: %{font_size: {:text, :"6xl"}}, + lg: %{font_size: {:text, :"7xl"}} }, "h2" => %{ font_weight: {:weight, :semibold}, - font_size: {:text, :"3xl"}, - sm: %{font_size: {:text, :"4xl"}} + letter_spacing: {:tracking, :tight}, + font_size: {:text, :"4xl"}, + text_transform: "uppercase", + sm: %{font_size: {:text, :"5xl"}} }, "h3" => %{font_weight: {:weight, :medium}}, "h4" => %{ diff --git a/extra/images/grain.svg b/extra/images/grain.svg new file mode 100644 index 0000000..3cfb81d --- /dev/null +++ b/extra/images/grain.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lib/layouts/articles.ex b/lib/layouts/articles.ex index 8211ab3..81722c7 100644 --- a/lib/layouts/articles.ex +++ b/lib/layouts/articles.ex @@ -4,8 +4,6 @@ defmodule Soonex.Layouts.Articles do use Phoenix.Component use Corex - import Soonex.Layouts.Media, only: [photo: 1] - alias Soonex.Layouts.Shell attr(:posts, :list, required: true) @@ -13,38 +11,31 @@ defmodule Soonex.Layouts.Articles do def cards(assigns) do ~H""" -
    -

    {@empty}

    -
    -
    {@empty}

    +
      -
      -
      - <.photo src={cover(post).src} alt={cover(post).alt} width={1400} height={900} /> + {date_label(post)} +

      + <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> + {post[:title] || "Untitled"} + +

      +

      + {post[:description]} +

      +
      + {tag}
      -
      - {date_label(post)} -

      - <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> - {post[:title] || "Untitled"} - -

      -

      - {post[:description]} -

      -
      - {tag} -
      -
      -
      -
    + + """ end @@ -77,14 +68,6 @@ defmodule Soonex.Layouts.Articles do """ 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 diff --git a/lib/layouts/post_layout.ex b/lib/layouts/post_layout.ex index ec53234..33406e2 100644 --- a/lib/layouts/post_layout.ex +++ b/lib/layouts/post_layout.ex @@ -47,7 +47,7 @@ defmodule Soonex.PostLayout do <:actions> <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-sm"> - <.heroicon name="hero-arrow-left" /> Journal + <.heroicon name="hero-arrow-left" /> Log @@ -81,7 +81,7 @@ defmodule Soonex.PostLayout do src = page[:image] if is_binary(src) and src != "" do - %{src: src, alt: page[:image_alt] || page[:title] || "Journal cover"} + %{src: src, alt: page[:image_alt] || page[:title] || "Log cover"} end end end diff --git a/lib/layouts/root/footer.ex b/lib/layouts/root/footer.ex index 4e15388..ed08e4a 100644 --- a/lib/layouts/root/footer.ex +++ b/lib/layouts/root/footer.ex @@ -18,7 +18,6 @@ defmodule Soonex.Layouts.Root.Footer do assigns |> assign(:github, @github) |> assign(:hexdocs, @hexdocs) - |> assign(:toolchain, toolchain()) ~H"""
    @@ -27,18 +26,9 @@ defmodule Soonex.Layouts.Root.Footer do
    <.lockup />

    - A waitlist you can fork. Four themes. A journal. Ship 1 September. + An open-source launch template from Corex — waitlist, journal, countdown, four themes in + config. Ship 1 September.

    -
      -
    • - {mark.alt} -
    • -
    @@ -46,20 +36,23 @@ defmodule Soonex.Layouts.Root.Footer do
    • <.navigate - to={Soonex.Public.path("/") <> "#principia"} + to={Soonex.Public.path("/") <> "#product"} class="link ui-nav ui-size-sm" > - Features + Product
    • - <.navigate to={Soonex.Public.path("/") <> "#officia"} class="link ui-nav ui-size-sm"> - Plans + <.navigate + to={Soonex.Public.path("/") <> "#themes"} + class="link ui-nav ui-size-sm" + > + Rooms
    • <.navigate - to={Soonex.Public.path("/") <> "#quaestiones"} + to={Soonex.Public.path("/") <> "#questions"} class="link ui-nav ui-size-sm" > Questions @@ -67,7 +60,7 @@ defmodule Soonex.Layouts.Root.Footer do
    • <.navigate - to={Soonex.Public.path("/") <> "#epistula"} + to={Soonex.Public.path("/") <> "#waitlist"} class="link ui-nav ui-size-sm" > Waitlist @@ -76,11 +69,11 @@ defmodule Soonex.Layouts.Root.Footer do
    -

    Journal

    +

    Log

    • <.navigate to={Soonex.Public.path("/blog")} class="link ui-nav ui-size-sm"> - All posts + All entries
    • @@ -121,7 +114,7 @@ defmodule Soonex.Layouts.Root.Footer do

      {Soonex.Layouts.Media.credits()}

    <.navigate - to={Soonex.Public.path("/") <> "#epistula"} + to={Soonex.Public.path("/") <> "#waitlist"} class="button ui-brand ui-solid ui-size-md w-fit" > Join waitlist @@ -131,15 +124,4 @@ defmodule Soonex.Layouts.Root.Footer do
    """ end - - defp toolchain do - [ - %{src: "/images/tech/elixir.svg", alt: "Elixir"}, - %{src: "/images/tech/phoenix.svg", alt: "Phoenix"}, - %{src: "/images/tech/tableau.svg", alt: "Tableau"}, - %{src: "/images/tech/zag.svg", alt: "Zag"}, - %{src: "/images/tech/tailwind.svg", alt: "Tailwind CSS"}, - %{src: "/images/tech/hex.svg", alt: "Hex"} - ] - end end diff --git a/lib/layouts/root/nav.ex b/lib/layouts/root/nav.ex index 051ed11..de24269 100644 --- a/lib/layouts/root/nav.ex +++ b/lib/layouts/root/nav.ex @@ -92,7 +92,7 @@ defmodule Soonex.Layouts.Root.Nav do
    <.navigate - to={Soonex.Public.path("/") <> "#epistula"} + to={Soonex.Public.path("/") <> "#waitlist"} class="button ui-brand ui-solid ui-size-md" > Join waitlist @@ -112,7 +112,7 @@ defmodule Soonex.Layouts.Root.Nav do ] end - defp nav_current?(page_path, %{id: :journal}) do + defp nav_current?(page_path, %{id: :log}) do String.starts_with?(page_path, "/blog") or String.starts_with?(page_path, "/tags") end @@ -120,9 +120,9 @@ defmodule Soonex.Layouts.Root.Nav do defp desktop_links do [ - %{id: :product, label: "Product", to: Soonex.Public.path("/") <> "#principia"}, - %{id: :journal, label: "Journal", to: Soonex.Public.path("/blog")}, - %{id: :questions, label: "Questions", to: Soonex.Public.path("/") <> "#quaestiones"} + %{id: :product, label: "Product", to: Soonex.Public.path("/") <> "#product"}, + %{id: :log, label: "Log", to: Soonex.Public.path("/blog")}, + %{id: :questions, label: "Questions", to: Soonex.Public.path("/") <> "#questions"} ] end @@ -130,26 +130,26 @@ defmodule Soonex.Layouts.Root.Nav do Corex.List.new([ %{ label: "Product", - value: "principia", - to: Soonex.Public.path("/") <> "#principia", + value: "product", + to: Soonex.Public.path("/") <> "#product", redirect: :href }, %{ - label: "Journal", + label: "Log", value: "blog", to: Soonex.Public.path("/blog"), redirect: :href }, %{ label: "Questions", - value: "quaestiones", - to: Soonex.Public.path("/") <> "#quaestiones", + value: "questions", + to: Soonex.Public.path("/") <> "#questions", redirect: :href }, %{ label: "Join waitlist", - value: "epistula", - to: Soonex.Public.path("/") <> "#epistula", + value: "waitlist", + to: Soonex.Public.path("/") <> "#waitlist", redirect: :href } ]) diff --git a/lib/layouts/root_layout.ex b/lib/layouts/root_layout.ex index 41382b4..9aa6d0a 100644 --- a/lib/layouts/root_layout.ex +++ b/lib/layouts/root_layout.ex @@ -202,7 +202,7 @@ defmodule Soonex.RootLayout do "#{site_name} · Launch 1 September" page[:page_kind] == :blog_index -> - "Journal · #{site_name}" + "Log · #{site_name}" page[:page_kind] == :not_found -> "Page not found · #{site_name}" @@ -221,10 +221,10 @@ defmodule Soonex.RootLayout do defp meta_description(page, site_name) do cond do page[:page_kind] == :home -> - "A waitlist you can fork. Four themes in config. A journal. A countdown. Ship 1 September." + "Soonex is an open-source launch template from Corex — waitlist, journal, countdown, four themes in config." page[:page_kind] == :blog_index -> - "Shipping notes from #{site_name}: themes, countdown, clone steps, and the road to 1 September." + "Shipping notes from #{site_name}: four rooms, the countdown, and the road to 1 September." page[:page_kind] == :not_found -> "That page is not on #{site_name}. Head home or read the shipping log." @@ -233,13 +233,13 @@ defmodule Soonex.RootLayout do "Browse the #{site_name} journal by topic — themes, launch, studio, and notes." tag_page?(page) -> - "Journal posts tagged #{page[:tag]} on #{site_name}." + "Log entries tagged #{page[:tag]} on #{site_name}." present_string?(page[:description]) -> page[:description] true -> - "Soonex is the first Corex plus Tableau waitlist template. Fork, theme, and ship 1 September." + "Soonex is an open-source launch template from Corex — waitlist, journal, countdown, four themes in config." end end diff --git a/lib/layouts/section.ex b/lib/layouts/section.ex index efb1388..4109384 100644 --- a/lib/layouts/section.ex +++ b/lib/layouts/section.ex @@ -10,7 +10,7 @@ defmodule Soonex.Layouts.Section do attr(:eyebrow, :string, required: true) attr(:tone, :atom, default: :root) attr(:align, :atom, default: :start) - attr(:layout, :atom, default: :stack, values: [:stack, :sticky]) + attr(:layout, :atom, default: :open, values: [:bleed, :open, :editorial, :sticky, :stack]) attr(:compact, :boolean, default: false) slot(:title, required: true) @@ -22,7 +22,7 @@ defmodule Soonex.Layouts.Section do ~H"""
    @@ -49,28 +49,35 @@ defmodule Soonex.Layouts.Section do """ end - defp section_class(true), do: "#{Shell.section_compact()} soonex-reveal" - defp section_class(false), do: "#{Shell.section()} soonex-reveal" + defp section_class(:bleed, _compact), do: "#{Shell.section_hero()} soonex-reveal" + defp section_class(_layout, true), do: "#{Shell.section_compact()} soonex-reveal" + defp section_class(_layout, false), do: "#{Shell.section()} soonex-reveal" defp tone_class(:root), do: "bg-root" defp tone_class(:surface), do: "border-y border-border bg-surface" + defp tone_class(:clear), do: "" + defp stage_class(:bleed), do: "#{Shell.stage()} relative z-[1]" defp stage_class(:sticky), do: "#{Shell.stage()} #{Shell.sticky_grid()}" - defp stage_class(:stack), do: Shell.stage() + defp stage_class(:editorial), do: "#{Shell.stage()} #{Shell.editorial_grid()}" + defp stage_class(_layout), do: Shell.stage() defp intro_class(:sticky, _align), do: Shell.sticky_intro() - defp intro_class(:stack, :center), do: Shell.intro_center() - defp intro_class(:stack, :start), do: Shell.intro() + defp intro_class(:editorial, _align), do: Shell.editorial_intro() + defp intro_class(_layout, :center), do: Shell.intro_center() + defp intro_class(_layout, :start), do: Shell.intro() - defp lede_align(:sticky, _align), do: "" defp lede_align(:stack, :center), do: "mx-auto" - defp lede_align(:stack, :start), do: "" + defp lede_align(_layout, _align), do: "" - defp actions_align(:sticky, _align), do: "" defp actions_align(:stack, :center), do: "justify-center" - defp actions_align(:stack, :start), do: "" + defp actions_align(_layout, :center), do: "justify-center" + defp actions_align(_layout, _align), do: "" defp body_class(:sticky, _compact), do: Shell.sticky_body() + defp body_class(:editorial, _compact), do: Shell.editorial_body() + defp body_class(:open, _compact), do: Shell.body_tight() + defp body_class(:bleed, _compact), do: Shell.body_tight() defp body_class(:stack, true), do: Shell.body_tight() defp body_class(:stack, false), do: Shell.body() end diff --git a/lib/layouts/shell.ex b/lib/layouts/shell.ex index 1a52a28..4fe2c85 100644 --- a/lib/layouts/shell.ex +++ b/lib/layouts/shell.ex @@ -1,32 +1,31 @@ defmodule Soonex.Layouts.Shell do @moduledoc false - # Marketing band used across Tailwind UI-style landings: - #
    - #
    - #
    heading
    - #
    content
    - #
    - #
    + # Named skeletons — not the Tailwind-UI eyebrow → h2 → card-grid band. + # :bleed full-bleed media (hero). Stage is overlay only. + # :open heading cluster, then type — no panels. + # :editorial heading + supporting column. + # :sticky FAQ split. + # :stack last-resort marketing band. def section, do: "scroll-mt-24 w-full py-24 sm:py-32" def section_compact, do: "scroll-mt-24 w-full py-16 sm:py-20" def section_hero, - do: "relative flex min-h-svh w-full flex-col justify-center overflow-x-clip py-16 sm:py-24" + do: + "relative isolate flex min-h-svh w-full flex-col justify-end overflow-x-clip lg:justify-center" def stage, do: "mx-auto w-full max-w-[90rem] px-6 lg:px-8" - def intro, do: "mx-auto max-w-2xl lg:mx-0" + def intro, do: "mx-auto max-w-3xl lg:mx-0" def intro_center, do: "mx-auto max-w-2xl lg:text-center" - def body, do: "mx-auto mt-16 w-full sm:mt-20 lg:mt-24" + def body, do: "mx-auto mt-12 w-full sm:mt-16" - def body_tight, do: "mx-auto mt-10 w-full sm:mt-12" + def body_tight, do: "mx-auto mt-8 w-full sm:mt-10" - # FAQ-style split: sticky intro column, content beside it. def sticky_grid, do: "grid grid-cols-1 items-start gap-16 lg:grid-cols-12 lg:gap-12" @@ -35,6 +34,13 @@ defmodule Soonex.Layouts.Shell do def sticky_body, do: "w-full min-w-0 lg:col-span-7" + def editorial_grid, + do: "grid grid-cols-1 items-start gap-12 lg:grid-cols-12 lg:gap-16" + + def editorial_intro, do: "mx-auto max-w-2xl lg:col-span-5 lg:mx-0" + + def editorial_body, do: "w-full min-w-0 lg:col-span-7" + def eyebrow, do: "eyebrow m-0" def section_heading, do: "mt-2 text-pretty" @@ -50,11 +56,11 @@ defmodule Soonex.Layouts.Shell do def frame, do: "soonex-frame relative overflow-hidden" - def stat_grid, do: "grid grid-cols-2 gap-4 lg:grid-cols-4" - - def stat, do: "#{panel()} flex min-h-36 flex-col justify-between gap-6 p-6 sm:p-8" - def feature_list, do: "m-0 mt-6 flex list-none flex-col gap-3 p-0" def chip_row, do: "flex flex-wrap items-center gap-2" + + def lanes, do: "m-0 flex list-none flex-col gap-8 p-0" + + def log_list, do: "m-0 flex list-none flex-col divide-y divide-border p-0" end diff --git a/lib/layouts/tag_layout.ex b/lib/layouts/tag_layout.ex index eb213f4..4a2526e 100644 --- a/lib/layouts/tag_layout.ex +++ b/lib/layouts/tag_layout.ex @@ -26,14 +26,14 @@ defmodule Soonex.TagLayout do <.layout_heading class="layout-heading" subtitle_tag="p"> <:title>{@tag_label} <:subtitle> - {@tag_count} {if @tag_count == 1, do: "shipping-log post", else: "shipping-log posts"} tagged in the journal + {@tag_count} {if @tag_count == 1, do: "shipping-log entry", else: "shipping-log entries"} <:actions> <.navigate to={Soonex.Public.path("/tags")} class="button ui-ghost ui-size-sm"> <.heroicon name="hero-arrow-left" /> All tags <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-sm"> - Journal + Log diff --git a/lib/pages/blog_index_page.ex b/lib/pages/blog_index_page.ex index 0c3d119..7af81d9 100644 --- a/lib/pages/blog_index_page.ex +++ b/lib/pages/blog_index_page.ex @@ -4,7 +4,7 @@ defmodule Soonex.BlogIndexPage do use Tableau.Page, layout: Soonex.RootLayout, permalink: "/blog", - title: "Journal", + title: "Log", page_kind: :blog_index, sitemap: %{priority: 0.7, changefreq: "weekly"} @@ -30,10 +30,10 @@ defmodule Soonex.BlogIndexPage do
    <.layout_heading class="layout-heading" subtitle_tag="p"> - <:title>Journal + <:title>Log <:subtitle> - Shipping notes for the Corex waitlist template — {@blog_count} - {if @blog_count == 1, do: "post", else: "posts"}. + Shipping notes for the Corex launch template — {@blog_count} + {if @blog_count == 1, do: "entry", else: "entries"}. <:actions> <.navigate to={Soonex.Public.path("/")} class="button ui-ghost ui-size-sm"> diff --git a/lib/pages/home/capita.ex b/lib/pages/home/capita.ex deleted file mode 100644 index 8d5ba2a..0000000 --- a/lib/pages/home/capita.ex +++ /dev/null @@ -1,75 +0,0 @@ -defmodule Soonex.HomePage.Capita do - @moduledoc false - - use Phoenix.Component - - import Soonex.Layouts.Media, only: [photo: 1] - import Soonex.Layouts.Section, only: [block: 1] - - alias Soonex.Layouts.Shell - - def capita(assigns) do - ~H""" - <.block - id="capita" - labelledby="soonex-capita-heading" - eyebrow="Inside" - tone={:surface} - > - <:title> - What you actually ship - - <:lede> - One landing, one journal, four themes, and chrome that already knows the date. The rest is - copy and stills you own. - -
    -
    - <.photo - src="/images/photos/studio.jpg" - alt="A calm studio interior with a low sofa, plants, and a large window" - width={1400} - height={1600} - class="absolute inset-0 size-full" - /> -
    - Template -

    - Waitlist, journal, and a countdown aimed at - 1 September -

    -
    -
    - -
    -
    - {cell.kicker} -

    {cell.title}

    -

    {cell.body}

    -
    -
    -
    - - """ - end - - defp cells do - [ - %{ - kicker: "Journal", - title: "Markdown posts, covers, tags", - body: - "Add a file under the journal route, rebuild, and the cover, date, and title show up. The index paginates on the client." - }, - %{ - kicker: "Controls", - title: "Corex, not a custom kit", - body: - "Select, accordion, tabs, and the waitlist fields are upstream Corex. Host CSS only retunes layout tokens." - } - ] - end -end diff --git a/lib/pages/home/contents.ex b/lib/pages/home/contents.ex deleted file mode 100644 index 5a73381..0000000 --- a/lib/pages/home/contents.ex +++ /dev/null @@ -1,91 +0,0 @@ -defmodule Soonex.HomePage.Contents do - @moduledoc false - - use Phoenix.Component - use Corex - - import Soonex.Layouts.Section, only: [block: 1] - - alias Soonex.Layouts.Shell - - def contents(assigns) do - assigns = assign(assigns, :snippet, snippet()) - - ~H""" - <.block - id="principia" - labelledby="soonex-principia-heading" - eyebrow="Product" - tone={:root} - > - <:title> - Fork once. Theme in config. - - <:lede> - Static on purpose. Tableau builds the pages. Corex runs the controls. You replace the stills - and the date. - - <.tabs - id="soonex-product-tabs" - class="tabs ui-brand ui-size-md ui-width-full" - indicator - value="overview" - items={product_tabs()} - /> - -
    -
    -
    - Clone -

    - mix setup, then mix soonex.server. -

    -
    - <.clipboard - id="soonex-theme-snippet" - class="clipboard ui-brand ui-solid ui-width-full ui-size-md min-w-0 flex-1" - value={@snippet} - > - <:label>Clone - <:copy> - <.heroicon name="hero-clipboard" /> - Copy - - <:copied> - <.heroicon name="hero-check" /> - Copied - - -
    -
    - - """ - end - - defp product_tabs do - Corex.Content.new([ - %{ - value: "overview", - label: "Overview", - content: - "Static on purpose. Tableau builds the pages. Corex runs the controls. You replace the stills and the date." - }, - %{ - value: "themes", - label: "Themes", - content: - "neo, uno, duo, and leo share one near-white field. Type, radius, and brand do the rest. Template Options is the client review." - }, - %{ - value: "mix", - label: "Mix", - content: - "Mix, Tailwind, esbuild. There is no package.json. Host CSS retunes layout. Recipes stay upstream." - } - ]) - end - - defp snippet do - "git clone https://github.com/corex-ui/soonex.git" - end -end diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index 1fd7688..70f5b2b 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -8,15 +8,15 @@ defmodule Soonex.HomePage.Faq do def faq(assigns) do ~H""" - <.block id="quaestiones" labelledby="soonex-faq-heading" eyebrow="Questions" layout={:sticky}> + <.block id="questions" labelledby="soonex-faq-heading" eyebrow="Questions" layout={:sticky}> <:title> Before you clone <:lede> - Opinionated answers. Still true after you fork. + What Soonex is, what ships, and what you replace. <:actions> - <.navigate to="#epistula" class="link ui-brand"> + <.navigate to="#waitlist" class="link ui-brand"> Join waitlist <.heroicon name="hero-arrow-down" /> @@ -25,7 +25,7 @@ defmodule Soonex.HomePage.Faq do class="accordion ui-accent ui-size-md mx-auto w-full max-w-none" multiple={false} collapsible={true} - value="gallery" + value="what" items={faq_items()} > <:trigger :let={item}> @@ -45,48 +45,40 @@ defmodule Soonex.HomePage.Faq do defp faq_items do Corex.Content.new([ %{ - value: "gallery", - label: "Is this a component gallery?", - content: "No. It’s a waitlist with a date. The controls happen to be Corex." + value: "what", + label: "What is Soonex?", + content: + "The open-source waitlist and launch template that ships with Corex. Fork it for a September launch — or any date you set." }, %{ - value: "email", - label: "Will you email me?", + value: "ships", + label: "What ships on 1 September?", content: - "Not from this demo. Submit anyway — the toast is the whole point until you wire a list." + "The landing, the waitlist fields, the shipping log, the header countdown, and four themes. Copy and stills are yours to replace." }, %{ value: "themes", label: "Why four themes?", content: - "Type, radius, and brand. Paper stays near-white. Template Options is the review." - }, - %{ - value: "npm", - label: "Do I need npm?", - content: "No. If you miss it, this isn’t your template." + "neo, uno, duo, and leo are four rooms on the same near-white paper. Type, radius, and brand change. Template Options switches them on this device." }, %{ - value: "posts", - label: "Where do posts go?", - content: "Markdown. Rebuild. The pager is a Corex event in the browser, not LiveView." - }, - %{ - value: "motion", - label: "What if motion makes me sick?", + value: "form", + label: "Does the form collect email?", content: - "Accessibility → Motion → Reduced. The header still condenses. The type still holds." + "Not on this demo. Submit still shows the launch toast so you can see the success path. Wire your provider after you fork." }, %{ - value: "photos", - label: "Can I keep the photos?", - content: "Swap them. They’re placeholders with a license, not the brand." + value: "a11y", + label: "How does accessibility work?", + content: + "The Accessibility control opens zoom, contrast, motion, cursor, focus, and underline. Choices persist in local storage." }, %{ - value: "september", - label: "When is September?", + value: "clone", + label: "How do I clone it?", content: - "Whenever you change the timestamp in the home template. The digits stay on one row." + "git clone the repo, then mix setup and mix soonex.server. There is no package.json." } ]) end diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index bfcd1c8..bac1d8d 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -11,25 +11,39 @@ defmodule Soonex.HomePage.Hero do def hero(assigns) do ~H"""
    -
    -
    -
    +
    + <.photo + src="/images/photos/hero.jpg" + alt="Sunlit studio with long work tables and hanging task lamps" + width={1600} + height={1200} + class="absolute inset-0 size-full" + loading="eager" + /> +
    +
    + +
    +
    +

    Soonex - ships 1 September. + + ships 1 September. +

    - A waitlist you can fork. Four themes in config. A journal. A countdown. Controls you - didn’t have to invent. + An open-source launch template from Corex — waitlist, journal, countdown, four themes in + config.

    - <.navigate to="#epistula" class="button ui-brand ui-solid ui-size-md"> + <.navigate to="#waitlist" class="button ui-brand ui-solid ui-size-md"> Join waitlist <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-md"> @@ -38,93 +52,64 @@ defmodule Soonex.HomePage.Hero do
    -
    -
    - <.photo - src="/images/photos/hero.jpg" - alt="Sunlit studio with long work tables and hanging task lamps" - width={1600} - height={1200} - class="absolute inset-0 size-full" - loading="eager" - /> -
    -
    -
    - Waitlist -

    - Reserve a September seat -

    -

    - This demo does not collect addresses. Submit still shows the launch toast. -

    - -
    - <.native_input - type="email" - name="waitlist[email]" - id="soonex-hero-waitlist-email" - required - autocomplete="email" - placeholder="you@studio.dev" - class="native-input ui-size-md ui-width-full" - > - <:label class="sr-only">Email - +
    +
    + +
    + <.native_input + type="email" + name="waitlist[email]" + id="soonex-hero-waitlist-email" + required + autocomplete="email" + placeholder="you@studio.dev" + class="native-input ui-size-md ui-width-full min-w-0 flex-1" + > + <:label class="sr-only">Email + - <.radio_group - id="soonex-hero-role" - name="waitlist[role]" - class="radio-group ui-brand ui-width-full" - value="founder" - items={[ - %{value: "founder", label: "Founder"}, - %{value: "engineer", label: "Engineer"}, - %{value: "designer", label: "Designer"} - ]} - > - <:label>I am a - - - <.switch - id="soonex-hero-notes" - name="waitlist[notes]" - checked - class="switch ui-brand" - > - <:label>Email me launch notes - - - - -
    +
    -
    -
    - <.avatar - :for={person <- people()} - id={person.id} - src={Soonex.Public.path(person.src)} - alt={person.alt} - class="avatar ui-size-sm" - > - <:fallback>{person.initials} - -
    -

    - Studios already cloning the repo for a - 1 September - launch. -

    + <.radio_group + id="soonex-hero-role" + name="waitlist[role]" + class="radio-group ui-brand ui-width-full" + value="founder" + items={[ + %{value: "founder", label: "Founder"}, + %{value: "engineer", label: "Engineer"}, + %{value: "designer", label: "Designer"} + ]} + > + <:label>I am a + + + +
    +
    + <.avatar + :for={person <- people()} + id={person.id} + src={Soonex.Public.path(person.src)} + alt={person.alt} + class="avatar ui-size-sm" + > + <:fallback>{person.initials} +
    +

    + Studios already cloning the repo for a + 1 September + launch. +

    diff --git a/lib/pages/home/highlights.ex b/lib/pages/home/highlights.ex deleted file mode 100644 index bc6cfaf..0000000 --- a/lib/pages/home/highlights.ex +++ /dev/null @@ -1,101 +0,0 @@ -defmodule Soonex.HomePage.Highlights do - @moduledoc false - - use Phoenix.Component - use Corex - - import Soonex.Layouts.Media, only: [photo: 1] - import Soonex.Layouts.Section, only: [block: 1] - - alias Soonex.Layouts.Shell - - attr(:posts, :list, default: []) - - def highlights(assigns) do - posts = - assigns.posts - |> List.wrap() - |> Enum.sort_by(& &1[:date], {:desc, DateTime}) - |> Enum.take(6) - - assigns = assign(assigns, :highlight_posts, posts) - - ~H""" - <.block - id="acta" - labelledby="soonex-highlights-heading" - eyebrow="Journal" - tone={:root} - > - <:title> - From the shipping log - - <:lede> - Short notes on the fork, the four themes, and the September target. The index paginates the rest. - - <:actions> - <.navigate to={Soonex.Public.path("/blog")} class="link ui-brand"> - All posts <.heroicon name="hero-arrow-up-right" /> - - -
    -

    No posts yet.

    -
    - <.carousel - :if={@highlight_posts != []} - id="soonex-acta-carousel" - class="carousel ui-brand ui-width-full max-w-none" - items={@highlight_posts} - orientation="horizontal" - spacing="1.5rem" - padding="2.5rem" - > - <:item :let={post}> -
    -
    - <.photo - src={cover(post).src} - alt={cover(post).alt} - width={1400} - height={900} - /> -
    -
    - {date_label(post)} -

    - {post[:title] || "Untitled"} -

    -

    - {post[:description]} -

    - <.navigate - to={Soonex.Public.path(post.permalink)} - class="button ui-ghost ui-brand ui-size-sm mt-6 w-fit" - > - Read more <.heroicon name="hero-arrow-up-right" /> - -
    -
    - - <:prev_trigger> - <.heroicon name="hero-chevron-left" /> - - <:next_trigger> - <.heroicon name="hero-chevron-right" /> - - - - """ - 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 -end diff --git a/lib/pages/home/kalendarium.ex b/lib/pages/home/kalendarium.ex deleted file mode 100644 index b75419d..0000000 --- a/lib/pages/home/kalendarium.ex +++ /dev/null @@ -1,188 +0,0 @@ -defmodule Soonex.HomePage.Kalendarium do - @moduledoc false - - use Phoenix.Component - use Corex - - import Soonex.Layouts.Media, only: [photo: 1] - import Soonex.Layouts.Section, only: [block: 1] - - alias Soonex.Layouts.Shell - - def kalendarium(assigns) do - ~H""" - <.block - id="kalendarium" - labelledby="soonex-kalendarium-heading" - eyebrow="Calendar" - tone={:surface} - > - <:title> - Road to 1 September - - <:lede> - Sticky beats, not a wall of cards. Open a note when you need the extra pass. - -
    - <.photo - src="/images/covers/shipping.jpg" - alt="A laptop on a wooden desk with a code editor open" - width={1400} - height={900} - class="absolute inset-0 size-full" - /> -
    -
      -
    1. - -
      -

      {beat.moment}

      -

      - {beat.title} -

      -
      -
      -

      {beat.lede}

      -
      - - {fact.label}: {fact.content} - -
      - <.collapsible - id={beat.note_id} - class="collapsible ui-brand ui-size-sm ui-width-full" - > - <:trigger>Notes from this pass - <:closed> - <.heroicon name="hero-chevron-right" /> - - <:content> -

      {para}

      - - - -
      -
    2. -
    - - """ - end - - defp beats do - product = Soonex.Public.path("/") <> "#principia" - waitlist = Soonex.Public.path("/") <> "#epistula" - journal = Soonex.Public.path("/blog") - questions = Soonex.Public.path("/") <> "#quaestiones" - - [ - %{ - note_id: "soonex-kal-fork", - moment: "12 March 2026", - title: "Fork Tableau, keep Corex upstream", - lede: - "Soonex starts as a static Tableau site. Mix, Tailwind, and esbuild are already in the project. The waitlist and countdown land first so the page has a date.", - facts: [ - %{label: "Stack", content: "Tableau plus Corex"}, - %{label: "npm", content: "None"}, - %{label: "Target", content: "1 September"} - ], - notes: [ - "Host CSS only retunes layout. Theme overlays may set seeds, colors, radius, fonts, and typography — not scale, duration, or opacity.", - "The clone strip on Product is the same git URL you run locally after mix setup." - ], - links: [ - %{label: "See the product", to: product}, - %{label: "Join waitlist", to: waitlist} - ] - }, - %{ - note_id: "soonex-kal-journal", - moment: "8 May 2026", - title: "Journal, tags, and covers", - lede: - "Posts are Markdown. Add a file, rebuild, and the cover, date, and title show up. Tags stay optional. The index pager slices cards in the browser.", - facts: [ - %{label: "Posts", content: "Six shipping-log entries"}, - %{label: "Pager", content: "Corex client events"}, - %{label: "LiveView", content: "Not used"} - ], - notes: [ - "The home journal band is a short reading list. The index paginates the rest without a full-page reload.", - "Keep permalinks stable when you rewrite titles so existing links still resolve." - ], - links: [ - %{label: "Read the journal", to: journal}, - %{label: "Questions", to: questions} - ] - }, - %{ - note_id: "soonex-kal-themes", - moment: "14 July 2026", - title: "Four themes that feel like four products", - lede: - "neo, uno, duo, and leo push paper, ink, brand, and radius apart. Template Options switches them on this device. Controls share one height and pick up a Soonex corner language.", - facts: [ - %{label: "Themes", content: "neo, uno, duo, leo"}, - %{label: "CSS fork", content: "None"}, - %{label: "Tokens", content: "--ctl-radius, --ctl-size"} - ], - notes: [ - "Triggers and fields host --ctl-radius, --ctl-space, --ctl-size, and --ctl-text. There is no ui-input utility in Corex.", - "Reduced motion lives in Accessibility. Scroll reveal and card lift stop under data-motion=reduce." - ], - links: [ - %{label: "See the product", to: product}, - %{label: "Compare plans", to: Soonex.Public.path("/") <> "#officia"} - ] - }, - %{ - note_id: "soonex-kal-a11y", - moment: "18 August 2026", - title: "Accessibility pass before launch", - lede: - "The person-in-circle control opens zoom, contrast, motion, cursor, focus, and link underline for this device. Choices persist in local storage.", - facts: [ - %{label: "Dialog", content: "Corex accessibility"}, - %{label: "Persist", content: "localStorage"}, - %{label: "Reset", content: "Returns defaults"} - ], - notes: [ - "The condensed header and progress bar stay as chrome when motion is reduced.", - "axe on the home document is the test gate before Pages deploy." - ], - links: [ - %{label: "Questions", to: questions}, - %{label: "Join waitlist", to: waitlist} - ] - }, - %{ - note_id: "soonex-kal-launch", - moment: "1 September 2026", - title: "Public notes, public template", - lede: - "Launch day is the countdown target in this demo. Change the date in the home template when you have a real one. Days, hours, minutes, and seconds stay on one row.", - facts: [ - %{label: "Day", content: "1 September"}, - %{label: "Status", content: "Public"}, - %{label: "Notes", content: "Journal"} - ], - notes: [ - "Wire the waitlist fields to your list later — email, role, interests, team size, and launch notes are already named.", - "Keep CI green before Pages deploys so a broken push never reaches the public CDN." - ], - links: [ - %{label: "Join waitlist", to: waitlist}, - %{label: "Read the journal", to: journal} - ] - } - ] - end -end diff --git a/lib/pages/home/log.ex b/lib/pages/home/log.ex new file mode 100644 index 0000000..486af03 --- /dev/null +++ b/lib/pages/home/log.ex @@ -0,0 +1,55 @@ +defmodule Soonex.HomePage.Log do + @moduledoc false + + use Phoenix.Component + use Corex + + import Soonex.Layouts.Section, only: [block: 1] + + alias Soonex.Layouts.Shell + + attr(:posts, :list, default: []) + + def log(assigns) do + posts = + assigns.posts + |> List.wrap() + |> Enum.sort_by(& &1[:date], {:desc, DateTime}) + |> Enum.take(3) + + assigns = assign(assigns, :entries, posts) + + ~H""" + <.block id="log" labelledby="soonex-log-heading" eyebrow="Log" layout={:open}> + <:title> + From the shipping log + + <:lede> + Notes on the fork, the four rooms, and the September target. + + <:actions> + <.navigate to={Soonex.Public.path("/blog")} class="link ui-brand"> + All entries <.heroicon name="hero-arrow-up-right" /> + + +

    No posts yet.

    +
      +
    1. + {date_label(post)} +

      + <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> + {post[:title] || "Untitled"} + +

      +

      + {post[:description]} +

      +
    2. +
    + + """ + end + + defp date_label(%{date: %DateTime{} = date}), do: Calendar.strftime(date, "%d %B %Y") + defp date_label(_), do: nil +end diff --git a/lib/pages/home/notes.ex b/lib/pages/home/notes.ex new file mode 100644 index 0000000..286decf --- /dev/null +++ b/lib/pages/home/notes.ex @@ -0,0 +1,95 @@ +defmodule Soonex.HomePage.Notes do + @moduledoc false + + use Phoenix.Component + use Corex + + import Soonex.Layouts.Section, only: [block: 1] + + def notes(assigns) do + assigns = assign(assigns, :featured, featured()) + assigns = assign(assigns, :supporting, supporting()) + + ~H""" + <.block id="notes" labelledby="soonex-notes-heading" eyebrow="Studio" layout={:editorial}> + <:title> + Notes from studios already forking + + <:lede> + Short lines from people putting Soonex on a September calendar. + +
    +

    “{@featured.quote}”

    +
    + <.avatar + id={@featured.id} + src={Soonex.Public.path(@featured.src)} + alt={@featured.name} + class="avatar ui-size-md" + > + <:fallback>{@featured.initials} + +
    + {@featured.name} + {@featured.role} +
    +
    +
    + +
      +
    • +
      +

      {voice.quote}

      +
      + <.avatar + id={voice.id} + src={Soonex.Public.path(voice.src)} + alt={voice.name} + class="avatar ui-size-sm" + > + <:fallback>{voice.initials} + +
      + {voice.name} + {voice.role} +
      +
      +
      +
    • +
    + + """ + end + + defp featured do + %{ + id: "soonex-voice-amira", + src: "/images/people/amira.jpg", + initials: "AN", + quote: "We needed a date on the page before the deck was done.", + name: "Amira N.", + role: "Founder" + } + end + + defp supporting do + [ + %{ + id: "soonex-voice-jonas", + src: "/images/people/jonas.jpg", + initials: "JK", + quote: "Static site, controls included, one clone path.", + name: "Jonas K.", + role: "Staff engineer" + }, + %{ + id: "soonex-voice-leah", + src: "/images/people/leah.jpg", + initials: "LM", + quote: "Four rooms for a client review without a second build.", + name: "Leah M.", + role: "Product designer" + } + ] + end +end diff --git a/lib/pages/home/numeri.ex b/lib/pages/home/numeri.ex deleted file mode 100644 index 535d8b7..0000000 --- a/lib/pages/home/numeri.ex +++ /dev/null @@ -1,46 +0,0 @@ -defmodule Soonex.HomePage.Numeri do - @moduledoc false - - use Phoenix.Component - - import Soonex.Layouts.Section, only: [block: 1] - - alias Soonex.Layouts.Shell - - def numeri(assigns) do - ~H""" - <.block - id="numeri" - labelledby="soonex-numeri-heading" - eyebrow="Facts" - tone={:surface} - compact - > - <:title> - What is actually true - - <:lede> - No vanity counters. These are the numbers this template ships with. - -
    -
    -

    - {fact.label} -

    -

    {fact.value}

    -

    {fact.detail}

    -
    -
    - - """ - end - - defp facts do - [ - %{label: "Themes", value: "4", detail: "neo, uno, duo, leo"}, - %{label: "Journal", value: "6", detail: "Shipping-log posts"}, - %{label: "npm", value: "0", detail: "Mix, Tailwind, esbuild"}, - %{label: "Launch", value: "1 Sep", detail: "Countdown target"} - ] - end -end diff --git a/lib/pages/home/officia.ex b/lib/pages/home/officia.ex deleted file mode 100644 index e4e51ad..0000000 --- a/lib/pages/home/officia.ex +++ /dev/null @@ -1,88 +0,0 @@ -defmodule Soonex.HomePage.Officia do - @moduledoc false - - use Phoenix.Component - use Corex - - import Soonex.Layouts.Section, only: [block: 1] - - alias Soonex.Layouts.Shell - - def officia(assigns) do - ~H""" - <.block - id="officia" - labelledby="soonex-officia-heading" - eyebrow="Plans" - tone={:root} - > - <:title> - Pick a lane. Ship the same core. - - <:lede> - Every plan is the same Soonex fork. The columns describe how far you take the template before 1 September. - -
    -
    - {plan.name} -

    {plan.headline}

    -

    {plan.body}

    -
      -
    • - {item.label} - {item.content} -
    • -
    - <.navigate to="#epistula" class={"#{plan.cta_class} mt-8 w-full"}> - {plan.cta} - -
    -
    - - """ - end - - defp plans do - [ - %{ - name: "Launch", - headline: "Fork and replace the copy", - body: "One theme, the waitlist, and the journal. Enough to put a date on the page.", - cta: "Join waitlist", - cta_class: "button ui-ghost ui-size-md ui-width-full", - items: [ - %{label: "Site", content: "Waitlist landing and Markdown journal"}, - %{label: "Theme", content: "Keep neo, or switch once in config"}, - %{label: "Date", content: "Countdown aimed at 1\u00A0September"} - ] - }, - %{ - name: "Studio", - headline: "Four themes, real stills", - body: "Treat Template Options as a client review. Photography and the shipping log stay.", - cta: "Join waitlist", - cta_class: "button ui-brand ui-solid ui-size-md ui-width-full", - items: [ - %{label: "Themes", content: "neo, uno, duo, and leo on this device"}, - %{label: "Journal", content: "Covers, tags, and client pagination"}, - %{label: "Chrome", content: "Header timer, toast, and clone strip"} - ] - }, - %{ - name: "Agency", - headline: "Accessible by default", - body: "Ship the a11y dialog, reduced motion, and a clone path your team can repeat.", - cta: "Join waitlist", - cta_class: "button ui-ghost ui-size-md ui-width-full", - items: [ - %{label: "A11y", content: "Zoom, contrast, motion, focus, underline"}, - %{label: "Stack", content: "Mix, Tailwind, esbuild — no npm"}, - %{label: "Handoff", content: "Clone, mix setup, mix soonex.server"} - ] - } - ] - end -end diff --git a/lib/pages/home/product.ex b/lib/pages/home/product.ex new file mode 100644 index 0000000..fe9a9ad --- /dev/null +++ b/lib/pages/home/product.ex @@ -0,0 +1,125 @@ +defmodule Soonex.HomePage.Product do + @moduledoc false + + use Phoenix.Component + use Corex + + import Soonex.Layouts.Section, only: [block: 1] + + alias Soonex.Layouts.Shell + + def product(assigns) do + assigns = assign(assigns, :snippet, snippet()) + + ~H""" + <.block id="product" labelledby="soonex-product-heading" eyebrow="Product" layout={:open}> + <:title> + What you fork + + <:lede> + A waitlist landing, a shipping log, a countdown, and four themes. You replace the stills and + the date. + + <.tabs + id="soonex-product-tabs" + class="tabs ui-brand ui-size-md ui-width-full" + indicator + value="waitlist" + items={product_tabs()} + /> + +
    +
    + Clone +

    + mix setup, then mix soonex.server. +

    +
    + <.clipboard + id="soonex-theme-snippet" + class="clipboard ui-brand ui-solid ui-width-full ui-size-md min-w-0 flex-1" + value={@snippet} + > + <:label>Clone + <:copy> + <.heroicon name="hero-clipboard" /> + Copy + + <:copied> + <.heroicon name="hero-check" /> + Copied + + +
    + +
    +
    +
    {lane.name}
    +
    +

    {lane.body}

    + <.navigate :if={lane.cta} to="#waitlist" class={lane.cta_class}> + {lane.cta} + +
    +
    +
    + + """ + end + + defp product_tabs do + Corex.Content.new([ + %{ + value: "waitlist", + label: "Waitlist", + content: + "Email, role, interests, team size, and a launch toast. The same fields sit on the hero still and at the end of the page." + }, + %{ + value: "journal", + label: "Journal", + content: + "Markdown posts with covers, tags, and a client pager. Add a file, rebuild, and the log updates." + }, + %{ + value: "themes", + label: "Themes", + content: + "neo, uno, duo, and leo share one near-white field. Type, radius, and brand do the rest." + }, + %{ + value: "a11y", + label: "Accessibility", + content: + "Zoom, contrast, motion, focus, and underline — stored on this device. The dialog ships with the template." + } + ]) + end + + defp lanes do + [ + %{ + name: "Solo", + body: "One theme, the waitlist, and the log. Enough to put a date on the page.", + cta: nil, + cta_class: nil + }, + %{ + name: "Studio", + body: "Four rooms, real stills, and a shipping log you can hand to a client.", + cta: "Join waitlist", + cta_class: "button ui-brand ui-solid ui-size-sm w-fit shrink-0" + }, + %{ + name: "Agency", + body: "The same fork, with accessibility on and a clone path the team can repeat.", + cta: nil, + cta_class: nil + } + ] + end + + defp snippet do + "git clone https://github.com/corex-ui/soonex.git" + end +end diff --git a/lib/pages/home/themes.ex b/lib/pages/home/themes.ex new file mode 100644 index 0000000..d476168 --- /dev/null +++ b/lib/pages/home/themes.ex @@ -0,0 +1,42 @@ +defmodule Soonex.HomePage.Themes do + @moduledoc false + + use Phoenix.Component + + import Soonex.Layouts.Section, only: [block: 1] + + def themes(assigns) do + ~H""" + <.block id="themes" labelledby="soonex-themes-heading" eyebrow="Rooms" layout={:open}> + <:title> + Four rooms. One building. + + <:lede> + Paper stays near-white. Type, radius, and brand change the weather. Template Options is the + live review — not the headline. + +

    Soonex

    +

    + The active room sets this display. Switch neo, uno, duo, or leo in the corner and this line + moves with it. +

    + +
      +
    1. +

      {room.id}

      +

      {room.line}

      +
    2. +
    + + """ + end + + defp rooms do + [ + %{id: "neo", line: "Product studio — geometric display, open corners, cobalt."}, + %{id: "uno", line: "Calm ops — tight tracking, small radius, teal."}, + %{id: "duo", line: "Editorial atelier — serif display, soft corners, rust."}, + %{id: "leo", line: "Signal poster — strong display, square corners, orange."} + ] + end +end diff --git a/lib/pages/home/voces.ex b/lib/pages/home/voces.ex deleted file mode 100644 index 217d829..0000000 --- a/lib/pages/home/voces.ex +++ /dev/null @@ -1,83 +0,0 @@ -defmodule Soonex.HomePage.Voces do - @moduledoc false - - use Phoenix.Component - use Corex - - import Soonex.Layouts.Section, only: [block: 1] - - alias Soonex.Layouts.Shell - - def voces(assigns) do - assigns = assign(assigns, :voices, voices()) - - ~H""" - <.block - id="voces" - labelledby="soonex-voces-heading" - eyebrow="Voices" - tone={:root} - > - <:title> - From studios already forking - - <:lede> - Short notes from people putting Soonex on a September calendar. - -
    -
    -

    - “{voice.quote}” -

    -
    - <.avatar - id={voice.id} - src={Soonex.Public.path(voice.src)} - alt={voice.name} - class="avatar ui-size-md" - > - <:fallback>{voice.initials} - -
    - {voice.name} - {voice.role} -
    -
    -
    -
    - - """ - end - - defp voices do - [ - %{ - id: "soonex-voice-amira", - src: "/images/people/amira.jpg", - initials: "AN", - quote: "We didn’t need a design system. We needed a date on the page by Friday.", - name: "Amira N.", - role: "Founder, studio launch" - }, - %{ - id: "soonex-voice-jonas", - src: "/images/people/jonas.jpg", - initials: "JK", - quote: "I grepped for package.json. Then I made coffee.", - name: "Jonas K.", - role: "Staff engineer" - }, - %{ - id: "soonex-voice-leah", - src: "/images/people/leah.jpg", - initials: "LM", - quote: "I review with Template Options now. Four rooms, same building.", - name: "Leah M.", - role: "Product designer" - } - ] - end -end diff --git a/lib/pages/home/waitlist.ex b/lib/pages/home/waitlist.ex index 149f8c3..080a008 100644 --- a/lib/pages/home/waitlist.ex +++ b/lib/pages/home/waitlist.ex @@ -13,24 +13,24 @@ defmodule Soonex.HomePage.Waitlist do ~H""" <.block - id="epistula" + id="waitlist" labelledby="soonex-waitlist-heading" eyebrow="Waitlist" tone={:surface} align={:center} + layout={:open} > <:title> Join the September list <:lede> - Tell us your role, interests, and team size. This demo does not store addresses — submit still - shows the launch toast. + Role, interests, and team size. We will write when the date hits.
    <.native_input @@ -113,6 +113,9 @@ defmodule Soonex.HomePage.Waitlist do
    +

    + Demo form — toast only. Wire your provider after you fork. +

    """ end diff --git a/lib/pages/home_page.ex b/lib/pages/home_page.ex index e42ae34..f57308a 100644 --- a/lib/pages/home_page.ex +++ b/lib/pages/home_page.ex @@ -5,24 +5,34 @@ defmodule Soonex.HomePage do use Corex import Soonex.HomePage.Hero, only: [hero: 1] - import Soonex.HomePage.Contents, only: [contents: 1] - import Soonex.HomePage.Capita, only: [capita: 1] - import Soonex.HomePage.Officia, only: [officia: 1] - import Soonex.HomePage.Voces, only: [voces: 1] + import Soonex.HomePage.Product, only: [product: 1] + import Soonex.HomePage.Notes, only: [notes: 1] + import Soonex.HomePage.Themes, only: [themes: 1] + import Soonex.HomePage.Log, only: [log: 1] import Soonex.HomePage.Faq, only: [faq: 1] import Soonex.HomePage.Waitlist, only: [waitlist: 1] def template(assigns) do + assigns = Map.put(assigns, :log_posts, posts(assigns)) + ~H"""
    <.hero /> - <.contents /> - <.capita /> - <.voces /> - <.officia /> + <.product /> + <.notes /> + <.themes /> + <.log posts={@log_posts} /> <.faq /> <.waitlist />
    """ end + + defp posts(assigns) do + cond do + is_list(assigns[:posts]) -> assigns.posts + is_map(assigns[:page]) and is_list(assigns.page[:posts]) -> assigns.page.posts + true -> [] + end + end end diff --git a/lib/pages/not_found_page.ex b/lib/pages/not_found_page.ex index e6fee1a..e3e0aaa 100644 --- a/lib/pages/not_found_page.ex +++ b/lib/pages/not_found_page.ex @@ -37,7 +37,7 @@ defmodule Soonex.NotFoundPage do Home <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-md"> - Journal + Log
    diff --git a/lib/pages/tags_index_page.ex b/lib/pages/tags_index_page.ex index f3bed2b..7d107f1 100644 --- a/lib/pages/tags_index_page.ex +++ b/lib/pages/tags_index_page.ex @@ -40,7 +40,7 @@ defmodule Soonex.TagsIndexPage do <:subtitle>Browse the shipping log by theme, launch, studio, and notes. <:actions> <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-sm"> - <.heroicon name="hero-arrow-left" /> Journal + <.heroicon name="hero-arrow-left" /> Log @@ -49,24 +49,24 @@ defmodule Soonex.TagsIndexPage do

    No tags yet.

    -
    -

    <.navigate to={tag.href} class="link ui-nav">{tag.label}

    -

    - {tag.count} {if tag.count == 1, do: "post", else: "posts"} +

    + {tag.count} {if tag.count == 1, do: "entry", else: "entries"}

    -
    -
    + + <.pager id="soonex-tags-pagination" count={length(@tag_cards)} page_size={6} />
    From 88e46a58fb26b93cbc5bff944744b77cec83a2da Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 30 Aug 2026 03:13:05 +0000 Subject: [PATCH 51/65] Restage Soonex as a September product launch. Rewrite product fiction across hero, Product tabs, Looks, log, FAQ, and posts; ship rich slotted tab panels with real pricing; densify control chrome and theme overlays for neo/uno/duo/leo. Co-authored-by: Karim Semmoud --- _posts/2026-03-12-fork-day.md | 20 +- _posts/2026-04-08-four-rooms.md | 23 +- _posts/2026-05-08-clone-theme-ship.md | 40 +-- _posts/2026-06-01-ci-before-pages.md | 4 +- _posts/2026-08-02-accessibility-on-device.md | 6 +- assets/css/chrome.css | 134 +++++++++- assets/css/hosts.css | 31 ++- config/config.exs | 27 +- lib/layouts/root/footer.ex | 10 +- lib/layouts/root/nav.ex | 7 + lib/layouts/root_layout.ex | 8 +- lib/layouts/shell.ex | 3 + lib/pages/blog_index_page.ex | 2 +- lib/pages/home/faq.ex | 28 +- lib/pages/home/hero.ex | 25 +- lib/pages/home/log.ex | 56 +++- lib/pages/home/notes.ex | 23 +- lib/pages/home/product.ex | 262 +++++++++++++++---- lib/pages/home/themes.ex | 48 +++- lib/pages/home/waitlist.ex | 6 +- lib/pages/not_found_page.ex | 2 +- 21 files changed, 564 insertions(+), 201 deletions(-) diff --git a/_posts/2026-03-12-fork-day.md b/_posts/2026-03-12-fork-day.md index 170830e..1e7050f 100644 --- a/_posts/2026-03-12-fork-day.md +++ b/_posts/2026-03-12-fork-day.md @@ -3,7 +3,7 @@ layout: Soonex.PostLayout title: Fork day date: 2026-03-12 12:00:00 +0000 permalink: /blog/fork-day/ -description: Soonex starts as a static launch page you can fork — waitlist, log, countdown, four themes in config. +description: Day one on the September list — waitlist on the page, countdown aimed at launch, stills ready to swap. image: /images/photos/hero.jpg image_alt: Sunlit studio with long work tables and hanging task lamps tags: @@ -14,23 +14,19 @@ sitemap: changefreq: monthly --- -Soonex is an **open-source launch template** from Corex. The first pass put a waitlist on the page and a countdown aimed at **1 September**. Copy and stills are placeholders you replace after the fork. +Today we put **Soonex** on a calendar. The waitlist sits on the hero still. The countdown aims at **1 September**. Lockup and photography are yours to replace after you fork. -## What we kept upstream +## What shipped -Corex owns the controls. Host CSS retunes layout. Theme overlays may set seeds, colors, radius, fonts, and typography — not scale, duration, or opacity. - -### Lists - -- Clone, then `mix setup` and `mix soonex.server` -- Waitlist fields are already named -- The header timer appears after the hero +- Waitlist email field with a launch toast on submit +- Public shipping log under `/blog` +- Header timer that appears after the hero 1. Fork the repo 2. Swap the lockup and stills 3. Point the countdown at your date -> The clone strip on Product is the same git URL you run locally. +> Reserve a seat. Launch when the date hits. > [!NOTE] -> The demo waitlist does not collect addresses. Submit still shows the launch toast so you can see the success path. +> This demo does not collect addresses. Submit still shows the launch toast so you can see the success path. diff --git a/_posts/2026-04-08-four-rooms.md b/_posts/2026-04-08-four-rooms.md index 0f171c0..908c48c 100644 --- a/_posts/2026-04-08-four-rooms.md +++ b/_posts/2026-04-08-four-rooms.md @@ -1,9 +1,9 @@ --- layout: Soonex.PostLayout -title: Four rooms on near-white paper +title: Four looks for client review date: 2026-04-08 09:00:00 +0000 permalink: /blog/four-rooms/ -description: neo, uno, duo, and leo live in config. Type, radius, and brand change the room. Paper stays near-white. +description: neo, uno, duo, and leo on one launch — type, corners, and brand change the room; paper stays calm. image: /images/photos/studio.jpg image_alt: A calm studio interior with a low sofa, plants, and a large window tags: @@ -14,15 +14,15 @@ sitemap: changefreq: monthly --- -Theme, brand, and accent live in config. The floating **Template Options** panel switches neo, uno, duo, and leo on this device. Host CSS does not copy component recipes. +Client review used to mean a second build. Now **Template Options** switches neo, uno, duo, and leo on this device so the studio and the client share one page. -## What each room is for +## What each look is for -neo is a product studio. uno is calm ops. duo is an editorial atelier. leo is a signal poster. Paper stays near-white in every room. +neo is the product studio. uno is calm ops. duo is the editorial atelier. leo is the signal poster. Paper stays near-white in every room. ### Review table -| Theme | Room | Corner | Type | +| Look | Room | Corner | Type | | --- | --- | --- | --- | | neo | Product studio | Open | Outfit | | uno | Calm ops | Tight | Sora | @@ -31,14 +31,9 @@ neo is a product studio. uno is calm ops. duo is an editorial atelier. leo is a ### Studio checklist -- [x] Seeds and light/dark colors in overlays -- [x] Radius and fonts per theme +- [x] Four looks live on the landing +- [x] Specimen updates with the active room - [ ] Your photography in the frames > [!TIP] -> There is no `ui-input` utility in Corex. Soonex treats triggers and field hosts as the control language via `--ctl-radius`, `--ctl-space`, `--ctl-size`, and `--ctl-text`. - -```elixir -# Theme overlays must not set *_scale, duration, or opacity. -config :corex_design, default_theme: :neo -``` +> Pick a look with your client before you rewrite the stills. The waitlist and countdown stay the same. diff --git a/_posts/2026-05-08-clone-theme-ship.md b/_posts/2026-05-08-clone-theme-ship.md index 4520934..67fcc6b 100644 --- a/_posts/2026-05-08-clone-theme-ship.md +++ b/_posts/2026-05-08-clone-theme-ship.md @@ -1,20 +1,20 @@ --- layout: Soonex.PostLayout -title: Clone, theme, and ship +title: Clone, look, and ship date: 2026-05-08 12:00:00 +0000 permalink: /blog/clone-theme-ship/ -description: Clone the launch template, switch neo/uno/duo/leo in config, and rebuild the Markdown log. +description: Clone Soonex, pick a look for review, keep the waitlist and log, and aim the countdown at your date. image: /images/covers/overlay.jpg image_alt: Soft abstract color field in warm paper tones tags: - - Themes + - Launch - Template sitemap: priority: 0.9 changefreq: monthly --- -Soonex is an **open-source launch template** from Corex. Fork it, theme it in config, keep the log and countdown, and launch with accessible controls. +Soonex is the **September launch** we are shipping: waitlist, public log, countdown, and four looks. Fork it, rename the brand, and keep the controls. ## Start here @@ -22,11 +22,11 @@ Soonex is an **open-source launch template** from Corex. Fork it, theme it in co #### Then mix setup -The repo already has Mix, Tailwind, and esbuild. There is no npm install step. Use **bold**, *italic*, ***combined***, `inline code`, and ~~strikethrough~~ in the same paragraph when you need emphasis variety. +The repo already has Mix, Tailwind, and esbuild. There is no npm install step. Use **bold**, *italic*, ***combined***, `inline code`, and ~~strikethrough~~ when you need emphasis variety in a shipping note. -### Links and autolink +### Links -Markdown link: [Hexdocs Corex](https://hexdocs.pm/corex). Bare URL with autolink: https://hexdocs.pm/mdex/MDEx.html. +Markdown link: [Soonex on GitHub](https://github.com/corex-ui/soonex). Bare URL: https://github.com/corex-ui/soonex. --- @@ -42,9 +42,9 @@ Markdown link: [Hexdocs Corex](https://hexdocs.pm/corex). Bare URL with autolink ### Ordered -1. Theme overlays in `config/config.exs` -2. Log posts under `_posts/` -3. Waitlist fields wired to your list +1. Pick a look in Template Options +2. Add log posts under `_posts/` +3. Wire waitlist fields to your list ### Task list @@ -58,14 +58,14 @@ Markdown link: [Hexdocs Corex](https://hexdocs.pm/corex). Bare URL with autolink | Band | Job | Note | | --- | --- | --- | -| Product | What you fork | Tabs + clone strip | -| Rooms | Four themes | Type, radius, brand | +| Product | What you launch | Tabs + clone strip | +| Looks | Four rooms | Type, radius, brand | | Log | Shipping notes | Client pager | | Waitlist | Canonical form | Toast on submit | | Feature | Status | Note | | --- | :---: | --- | -| Tables | Ready | `extension: [table: true]` | +| Tables | Ready | In the log renderer | | Task lists | Ready | GitHub-style checkboxes | | Alerts | Ready | NOTE / TIP / WARNING | | Footnotes | Ready | Collected at document end | @@ -74,9 +74,9 @@ Markdown link: [Hexdocs Corex](https://hexdocs.pm/corex). Bare URL with autolink ## Blockquote -> Fork once. Theme in config. The controls stay upstream. +> Fork once. Pick a look. Ship on the date. -> Host CSS retunes layout. It does not copy Corex recipes or set overlay scale, duration, or opacity. +> The waitlist and countdown stay. Rename Soonex when you are ready. --- @@ -97,19 +97,19 @@ Default Open Graph asset: > Template Options switches neo, uno, duo, and leo on this device without a rebuild. > [!IMPORTANT] -> Do not fork Corex CSS. Theme overlays must not set `*_scale`, duration, or opacity. +> Swap the lockup, stills, and launch date before you call it your product. > [!WARNING] > `mix soonex.server` fails if port 4999 is already bound. Stop the occupant first. > [!CAUTION] -> Log pagination is a Corex client event. There is no LiveView `handle_event`. +> Log pagination is a client event. There is no LiveView `handle_event`. --- ## Footnotes -The log index paginates with the Corex pager on the client[^alpha]. Reduced motion lives in Accessibility[^beta]. +The log index paginates on the client[^alpha]. Reduced motion lives in Accessibility[^beta]. [^alpha]: Host script listens for `pagination-page-changed` and hides the other SSR entries. [^beta]: Scroll reveal stops under `data-motion=reduce`. @@ -122,7 +122,7 @@ Fenced Elixir: ```elixir defmodule Soonex.HomePage do - @moduledoc "Waitlist landing for the Corex template." + @moduledoc "September launch landing." def launch_day do ~D[2026-09-01] @@ -158,7 +158,7 @@ Trusted raw HTML with `render: [unsafe: true]`:
    Keyboard shortcuts in docs -

    Press Ctrl+K style shortcuts when documenting keyboards. The template does not bind that chord.

    +

    Press Ctrl+K style shortcuts when documenting keyboards. The product does not bind that chord.

    --- diff --git a/_posts/2026-06-01-ci-before-pages.md b/_posts/2026-06-01-ci-before-pages.md index e9997e5..c46e20e 100644 --- a/_posts/2026-06-01-ci-before-pages.md +++ b/_posts/2026-06-01-ci-before-pages.md @@ -14,7 +14,7 @@ sitemap: changefreq: monthly --- -Keep **CI green** before Pages deploys so a broken push never reaches the public CDN. Wallaby axe on the home document is the test gate. +We do not ship a broken landing. Keep **CI green** before Pages deploys so a bad push never reaches the public CDN. Axe on the home document is the gate. ## Before you push @@ -32,7 +32,7 @@ Stop `mix soonex.server` if port 4999 is bound. Then `MIX_ENV=test mix test`. | --- | --- | --- | | Axe | `mix test` | Home document | | Build | `mix tableau.build` | Static `_site/` | -| Serve | `mix soonex.server` | 4999 + Corex MCP | +| Serve | `mix soonex.server` | Local preview | ![Soonex Open Graph](/images/og.svg) diff --git a/_posts/2026-08-02-accessibility-on-device.md b/_posts/2026-08-02-accessibility-on-device.md index 7705a20..02c34f2 100644 --- a/_posts/2026-08-02-accessibility-on-device.md +++ b/_posts/2026-08-02-accessibility-on-device.md @@ -3,7 +3,7 @@ layout: Soonex.PostLayout title: Accessibility on this device date: 2026-08-02 11:00:00 +0000 permalink: /blog/accessibility-on-device/ -description: Zoom, contrast, motion, cursor, focus, and link underline — Corex accessibility, stored in local storage. +description: Zoom, contrast, motion, cursor, focus, and link underline — preferences stay on this device in local storage. image: /images/covers/overlay.jpg image_alt: Soft abstract color field in warm paper tones tags: @@ -14,7 +14,7 @@ sitemap: changefreq: monthly --- -The Accessibility control opens zoom, contrast, motion, cursor, focus, and link underline for this device. Choices persist in **local storage** and write data attributes on the document. +Accessibility opens zoom, contrast, motion, cursor, focus, and link underline for this device. Choices persist in **local storage** and write data attributes on the document. ## What reduced motion does @@ -29,7 +29,7 @@ Scroll reveal lives in host CSS. Set Motion to Reduced in Accessibility and thos 3. Confirm the hero and log still read > [!WARNING] -> Host CSS must not set overlay scale, duration, or opacity. Theme.Validator will reject those keys. +> Prefer Accessibility controls over custom motion overrides. Visitors should be able to quiet the page on this device. Use **bold**, *italic*, and `inline code` in the same paragraph when you document the dialog. diff --git a/assets/css/chrome.css b/assets/css/chrome.css index 047c024..8160013 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -142,15 +142,145 @@ html[data-motion="reduce"] .soonex-card-motion:hover { border: 1px solid var(--color-border); background-color: var(--color-root); border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); - padding: var(--spacing-space); + padding: calc(var(--spacing-space) * 1.15); + box-shadow: var(--shadow-sm); } @media (width >= 40rem) { .soonex-waitlist-object { - padding: calc(var(--spacing-space) * 1.25); + padding: calc(var(--spacing-space) * 1.45); } } +/* Hero email + CTA read as one joined control on sm+. */ +.soonex-waitlist-join { + gap: var(--spacing-space-sm); +} + +@media (width >= 40rem) { + .soonex-waitlist-join { + align-items: stretch; + gap: 0; + } + + .soonex-waitlist-join .native-input [data-scope="native-input"][data-part="input"] { + border-end-end-radius: 0; + border-start-end-radius: 0; + } + + .soonex-waitlist-join > button { + border-start-start-radius: 0; + border-end-start-radius: 0; + margin-inline-start: -1px; + } +} + +.soonex-tab-panel { + min-height: 18rem; +} + +.tabs.soonex-product-tabs [data-scope="tabs"][data-part="content"] { + padding: calc(var(--spacing-space) * 1.25); + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-block-start: 0; +} + +.soonex-tab-workspace { + max-width: 48rem; +} + +.soonex-tab-workspace .soonex-room-specimen { + font-size: var(--text-5xl); +} + +.soonex-check-list { + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + gap: var(--spacing-space); +} + +.soonex-check-list > li { + display: grid; + grid-template-columns: auto 1fr; + gap: var(--spacing-space); + align-items: start; +} + +.soonex-check-mark { + display: block; + width: 0.85rem; + height: 0.85rem; + margin-top: 0.35rem; + border-radius: var(--soonex-ctl-radius, var(--radius-sm)); + background-color: var(--color-brand); + box-shadow: inset 0 0 0 2px color-mix(in oklab, var(--color-brand) 35%, transparent); +} + +.soonex-feature-bullets { + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + gap: calc(var(--spacing-space) * 0.55); +} + +.soonex-feature-bullets > li { + position: relative; + padding-inline-start: 1.1rem; + color: var(--color-ink-muted); + font-size: var(--text-sm); + line-height: var(--leading-relaxed); +} + +.soonex-feature-bullets > li::before { + content: ""; + position: absolute; + inset-inline-start: 0; + top: 0.55em; + width: 0.4rem; + height: 0.4rem; + border-radius: var(--soonex-ctl-radius, 9999px); + background-color: var(--color-brand); +} + +.soonex-mini-form, +.soonex-sample-entry { + border-radius: var(--soonex-ctl-radius, var(--radius-xl)); +} + +.soonex-field-ghost { + display: flex; + align-items: center; + min-height: var(--spacing-size); + padding-inline: var(--spacing-space); + border: 1px solid var(--color-border); + border-radius: var(--soonex-ctl-radius, var(--radius-md)); + background-color: var(--color-ui); + color: var(--color-ink-muted); + font-size: var(--text-sm); +} + +.soonex-pricing { + display: flex; + flex-direction: column; + gap: 0; + border-block: 1px solid var(--color-border); +} + +.soonex-pricing-tier { + padding-block: calc(var(--spacing-space) * 1.35); + border-block-end: 1px solid var(--color-border); +} + +.soonex-pricing-tier:last-child { + border-block-end: 0; +} + .soonex-photo { display: block; width: 100%; diff --git a/assets/css/hosts.css b/assets/css/hosts.css index da09570..c0d701a 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -181,6 +181,14 @@ html[data-motion="reduce"] body.layout { border-end-end-radius: var(--soonex-ctl-radius, var(--radius-md)); } +.tabs.soonex-product-tabs [data-scope="tabs"][data-part="list"] { + gap: 0; +} + +.tabs.soonex-product-tabs [data-scope="tabs"][data-part="trigger"] { + font-weight: var(--font-weight-semibold); +} + .carousel.ui-width-full [data-scope="carousel"][data-part="item"] { align-items: stretch; } @@ -283,16 +291,33 @@ pre.code.code--wide { [data-theme="uno"] h1, [data-theme="uno"] h2, [data-theme="uno"] .soonex-quote, -[data-theme="uno"] .soonex-stat-value { - letter-spacing: -0.03em; +[data-theme="uno"] .soonex-stat-value, +[data-theme="uno"] .soonex-room-specimen, +[data-theme="uno"] .soonex-room-name, +[data-theme="uno"] .soonex-card-title { + letter-spacing: -0.04em; } [data-theme="duo"] blockquote, -[data-theme="duo"] .soonex-quote { +[data-theme="duo"] .soonex-quote, +[data-theme="duo"] .soonex-tab-workspace > .lede, +[data-theme="duo"] .soonex-room .lede { font-family: var(--font-serif); font-style: italic; } +[data-theme="leo"] .soonex-room-specimen, +[data-theme="leo"] .soonex-room-name, +[data-theme="leo"] .soonex-card-title, +[data-theme="leo"] .soonex-lockup-word { + text-transform: uppercase; + letter-spacing: -0.02em; +} + +[data-theme="neo"] .soonex-room-specimen { + letter-spacing: -0.06em; +} + .soonex-lockup-word { font-family: var(--font-display); font-size: var(--text-lg); diff --git a/config/config.exs b/config/config.exs index 34caf86..89ea6c7 100644 --- a/config/config.exs +++ b/config/config.exs @@ -76,7 +76,7 @@ config :tableau, Tableau.RSSExtension, enabled: true, title: "Soonex", description: - "Soonex is an open-source launch template from Corex — waitlist, journal, countdown, four themes in config." + "Soonex ships 1 September — waitlist, shipping log, countdown, and four looks for client review." config :elixir, :time_zone_database, Tz.TimeZoneDatabase @@ -198,7 +198,7 @@ config :corex_design, letter_spacing: {:tracking, :tighter}, font_size: {:text, :"5xl"}, sm: %{font_size: {:text, :"6xl"}}, - lg: %{font_size: {:text, :"7xl"}} + lg: %{font_size: {:text, :"8xl"}} }, "h2" => %{ font_family: {:font, :display}, @@ -300,16 +300,21 @@ config :corex_design, "h2" => %{ font_family: {:font, :display}, font_weight: {:weight, :semibold}, - letter_spacing: {:tracking, :tight}, + letter_spacing: {:tracking, :tighter}, font_size: {:text, :"3xl"}, sm: %{font_size: {:text, :"4xl"}} }, - "h3" => %{font_family: {:font, :display}, font_weight: {:weight, :medium}}, - "h4" => %{font_weight: {:weight, :medium}}, + "h3" => %{ + font_family: {:font, :display}, + font_weight: {:weight, :medium}, + letter_spacing: {:tracking, :tight} + }, + "h4" => %{font_weight: {:weight, :medium}, letter_spacing: {:tracking, :tight}}, "p" => %{line_height: {:leading, :snug}}, "p.lede" => %{ - font_size: {:text, :lg}, + font_size: {:text, :base}, line_height: {:leading, :snug}, + letter_spacing: {:tracking, :tight}, color: "var(--color-ink-muted)" }, "small.eyebrow" => %{ @@ -389,7 +394,7 @@ config :corex_design, letter_spacing: {:tracking, :tight}, font_size: {:text, :"5xl"}, sm: %{font_size: {:text, :"6xl"}}, - lg: %{font_size: {:text, :"7xl"}} + lg: %{font_size: {:text, :"8xl"}} }, "h2" => %{ font_family: {:font, :display}, @@ -403,7 +408,7 @@ config :corex_design, "p.lede" => %{ font_family: {:font, :serif}, font_style: :italic, - font_size: {:text, :lg}, + font_size: {:text, :xl}, line_height: {:leading, :relaxed}, color: "var(--color-ink-muted)" }, @@ -478,15 +483,15 @@ config :corex_design, }, typography: %{ "h1" => %{ - font_weight: {:weight, :semibold}, + font_weight: {:weight, :bold}, letter_spacing: {:tracking, :tighter}, font_size: {:text, :"5xl"}, text_transform: "uppercase", sm: %{font_size: {:text, :"6xl"}}, - lg: %{font_size: {:text, :"7xl"}} + lg: %{font_size: {:text, :"8xl"}} }, "h2" => %{ - font_weight: {:weight, :semibold}, + font_weight: {:weight, :bold}, letter_spacing: {:tracking, :tight}, font_size: {:text, :"4xl"}, text_transform: "uppercase", diff --git a/lib/layouts/root/footer.ex b/lib/layouts/root/footer.ex index ed08e4a..3fc96f9 100644 --- a/lib/layouts/root/footer.ex +++ b/lib/layouts/root/footer.ex @@ -26,8 +26,7 @@ defmodule Soonex.Layouts.Root.Footer do
    <.lockup />

    - An open-source launch template from Corex — waitlist, journal, countdown, four themes in - config. Ship 1 September. + Waitlist, shipping log, countdown, and four looks. Ship 1 September.

    @@ -43,11 +42,8 @@ defmodule Soonex.Layouts.Root.Footer do
  • - <.navigate - to={Soonex.Public.path("/") <> "#themes"} - class="link ui-nav ui-size-sm" - > - Rooms + <.navigate to={Soonex.Public.path("/") <> "#looks"} class="link ui-nav ui-size-sm"> + Looks
  • diff --git a/lib/layouts/root/nav.ex b/lib/layouts/root/nav.ex index de24269..5b91d2d 100644 --- a/lib/layouts/root/nav.ex +++ b/lib/layouts/root/nav.ex @@ -121,6 +121,7 @@ defmodule Soonex.Layouts.Root.Nav do defp desktop_links do [ %{id: :product, label: "Product", to: Soonex.Public.path("/") <> "#product"}, + %{id: :looks, label: "Looks", to: Soonex.Public.path("/") <> "#looks"}, %{id: :log, label: "Log", to: Soonex.Public.path("/blog")}, %{id: :questions, label: "Questions", to: Soonex.Public.path("/") <> "#questions"} ] @@ -134,6 +135,12 @@ defmodule Soonex.Layouts.Root.Nav do to: Soonex.Public.path("/") <> "#product", redirect: :href }, + %{ + label: "Looks", + value: "looks", + to: Soonex.Public.path("/") <> "#looks", + redirect: :href + }, %{ label: "Log", value: "blog", diff --git a/lib/layouts/root_layout.ex b/lib/layouts/root_layout.ex index 9aa6d0a..f3d7dc2 100644 --- a/lib/layouts/root_layout.ex +++ b/lib/layouts/root_layout.ex @@ -221,16 +221,16 @@ defmodule Soonex.RootLayout do defp meta_description(page, site_name) do cond do page[:page_kind] == :home -> - "Soonex is an open-source launch template from Corex — waitlist, journal, countdown, four themes in config." + "Soonex ships 1 September — waitlist, shipping log, countdown, and four looks for client review." page[:page_kind] == :blog_index -> - "Shipping notes from #{site_name}: four rooms, the countdown, and the road to 1 September." + "Shipping notes from #{site_name}: waitlist, looks, countdown, and the road to 1 September." page[:page_kind] == :not_found -> "That page is not on #{site_name}. Head home or read the shipping log." page[:page_kind] == :tags_index -> - "Browse the #{site_name} journal by topic — themes, launch, studio, and notes." + "Browse the #{site_name} shipping log by topic — launch, looks, studio, and notes." tag_page?(page) -> "Log entries tagged #{page[:tag]} on #{site_name}." @@ -239,7 +239,7 @@ defmodule Soonex.RootLayout do page[:description] true -> - "Soonex is an open-source launch template from Corex — waitlist, journal, countdown, four themes in config." + "Soonex ships 1 September — waitlist, shipping log, countdown, and four looks for client review." end end diff --git a/lib/layouts/shell.ex b/lib/layouts/shell.ex index 4fe2c85..6d4ac2c 100644 --- a/lib/layouts/shell.ex +++ b/lib/layouts/shell.ex @@ -63,4 +63,7 @@ defmodule Soonex.Layouts.Shell do def lanes, do: "m-0 flex list-none flex-col gap-8 p-0" def log_list, do: "m-0 flex list-none flex-col divide-y divide-border p-0" + + def feature_bullets, + do: "soonex-feature-bullets m-0 flex list-none flex-col gap-2 p-0" end diff --git a/lib/pages/blog_index_page.ex b/lib/pages/blog_index_page.ex index 7af81d9..09f7c08 100644 --- a/lib/pages/blog_index_page.ex +++ b/lib/pages/blog_index_page.ex @@ -32,7 +32,7 @@ defmodule Soonex.BlogIndexPage do <.layout_heading class="layout-heading" subtitle_tag="p"> <:title>Log <:subtitle> - Shipping notes for the Corex launch template — {@blog_count} + Public notes on the road to 1 September — {@blog_count} {if @blog_count == 1, do: "entry", else: "entries"}. <:actions> diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index 70f5b2b..cad57ab 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -10,10 +10,10 @@ defmodule Soonex.HomePage.Faq do ~H""" <.block id="questions" labelledby="soonex-faq-heading" eyebrow="Questions" layout={:sticky}> <:title> - Before you clone + Before you join <:lede> - What Soonex is, what ships, and what you replace. + Straight answers about the launch, the waitlist, and what you get on day one. <:actions> <.navigate to="#waitlist" class="link ui-brand"> @@ -48,37 +48,37 @@ defmodule Soonex.HomePage.Faq do value: "what", label: "What is Soonex?", content: - "The open-source waitlist and launch template that ships with Corex. Fork it for a September launch — or any date you set." + "A launch workspace shipping 1 September — waitlist, public shipping log, countdown, and four looks for client review." }, %{ value: "ships", - label: "What ships on 1 September?", + label: "What do I get on launch day?", content: - "The landing, the waitlist fields, the shipping log, the header countdown, and four themes. Copy and stills are yours to replace." + "The landing, waitlist fields, shipping log, header countdown, and four looks. Swap the stills, lockup, and date for your brand." }, %{ - value: "themes", - label: "Why four themes?", + value: "looks", + label: "What are the four looks?", content: - "neo, uno, duo, and leo are four rooms on the same near-white paper. Type, radius, and brand change. Template Options switches them on this device." + "neo, uno, duo, and leo. Same page, different type, corners, and brand. Switch them live in Template Options for a client review." }, %{ value: "form", - label: "Does the form collect email?", + label: "Will you email me after I join?", content: - "Not on this demo. Submit still shows the launch toast so you can see the success path. Wire your provider after you fork." + "Yes — when you wire a provider. On this demo page, submit shows the launch toast so you can see the success path." }, %{ value: "a11y", label: "How does accessibility work?", content: - "The Accessibility control opens zoom, contrast, motion, cursor, focus, and underline. Choices persist in local storage." + "Open Accessibility for zoom, contrast, motion, cursor, focus, and underline. Choices stay on this device in local storage." }, %{ - value: "clone", - label: "How do I clone it?", + value: "open", + label: "Is this open source?", content: - "git clone the repo, then mix setup and mix soonex.server. There is no package.json." + "Yes. Clone the repo, run mix setup and mix soonex.server. There is no package.json — Mix, Tailwind, and esbuild are already in the project." } ]) end diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index bac1d8d..19d716a 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -10,11 +10,7 @@ defmodule Soonex.HomePage.Hero do def hero(assigns) do ~H""" -
    +
    <.photo src="/images/photos/hero.jpg" @@ -38,8 +34,8 @@ defmodule Soonex.HomePage.Hero do

    - An open-source launch template from Corex — waitlist, journal, countdown, four themes in - config. + Reserve a seat for launch day. Waitlist, shipping log, countdown, and four looks — + ready when the date hits.

    @@ -54,11 +50,16 @@ defmodule Soonex.HomePage.Hero do
    +

    Reserve a September seat

    +

    + Tell us who you are. We write when the countdown ends. +

    +
    <.native_input @@ -93,7 +94,7 @@ defmodule Soonex.HomePage.Hero do -
    +
    <.avatar :for={person <- people()} @@ -106,9 +107,7 @@ defmodule Soonex.HomePage.Hero do

    - Studios already cloning the repo for a - 1 September - launch. + 240 studios already on the list for 1 September.

    diff --git a/lib/pages/home/log.ex b/lib/pages/home/log.ex index 486af03..c346852 100644 --- a/lib/pages/home/log.ex +++ b/lib/pages/home/log.ex @@ -4,6 +4,7 @@ defmodule Soonex.HomePage.Log do use Phoenix.Component use Corex + import Soonex.Layouts.Media, only: [photo: 1] import Soonex.Layouts.Section, only: [block: 1] alias Soonex.Layouts.Shell @@ -20,12 +21,13 @@ defmodule Soonex.HomePage.Log do assigns = assign(assigns, :entries, posts) ~H""" - <.block id="log" labelledby="soonex-log-heading" eyebrow="Log" layout={:open}> + <.block id="log" labelledby="soonex-log-heading" eyebrow="Shipping log" layout={:open}> <:title> - From the shipping log + What we shipped on the road to September <:lede> - Notes on the fork, the four rooms, and the September target. + Public notes on the waitlist, the four looks, and the countdown. The full index lives on the + log. <:actions> <.navigate to={Soonex.Public.path("/blog")} class="link ui-brand"> @@ -34,22 +36,52 @@ defmodule Soonex.HomePage.Log do

    No posts yet.

      -
    1. - {date_label(post)} -

      - <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> - {post[:title] || "Untitled"} +
    2. +
      + <.photo + src={cover(post).src} + alt={cover(post).alt} + width={800} + height={500} + /> +
      +
      + {date_label(post)} +

      + <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> + {post[:title] || "Untitled"} + +

      +

      + {post[:description]} +

      + <.navigate + to={Soonex.Public.path(post.permalink)} + class="button ui-ghost ui-brand ui-size-sm mt-4 w-fit" + > + Read more <.heroicon name="hero-arrow-up-right" /> -
    3. -

      - {post[:description]} -

      +
  • """ end + defp cover(post) do + src = post[:image] + + if is_binary(src) and src != "" do + %{src: src, alt: post[:image_alt] || post[:title] || "Log cover"} + end + end + defp date_label(%{date: %DateTime{} = date}), do: Calendar.strftime(date, "%d %B %Y") defp date_label(_), do: nil end diff --git a/lib/pages/home/notes.ex b/lib/pages/home/notes.ex index 286decf..4e32505 100644 --- a/lib/pages/home/notes.ex +++ b/lib/pages/home/notes.ex @@ -11,12 +11,12 @@ defmodule Soonex.HomePage.Notes do assigns = assign(assigns, :supporting, supporting()) ~H""" - <.block id="notes" labelledby="soonex-notes-heading" eyebrow="Studio" layout={:editorial}> + <.block id="notes" labelledby="soonex-notes-heading" eyebrow="Studios" layout={:editorial}> <:title> - Notes from studios already forking + Already on the September list <:lede> - Short lines from people putting Soonex on a September calendar. + Founders, engineers, and designers putting Soonex on a calendar — not a backlog.

    “{@featured.quote}”

    @@ -39,7 +39,7 @@ defmodule Soonex.HomePage.Notes do
    • -

      {voice.quote}

      +

      “{voice.quote}”

      <.avatar id={voice.id} @@ -66,9 +66,10 @@ defmodule Soonex.HomePage.Notes do id: "soonex-voice-amira", src: "/images/people/amira.jpg", initials: "AN", - quote: "We needed a date on the page before the deck was done.", + quote: + "We had a date before we had a deck. Soonex put the waitlist and the countdown on the page the same afternoon.", name: "Amira N.", - role: "Founder" + role: "Founder, Northline Studio" } end @@ -78,17 +79,19 @@ defmodule Soonex.HomePage.Notes do id: "soonex-voice-jonas", src: "/images/people/jonas.jpg", initials: "JK", - quote: "Static site, controls included, one clone path.", + quote: + "One clone path, controls included, no second design system. We pointed the date and shipped the stills.", name: "Jonas K.", - role: "Staff engineer" + role: "Staff engineer, Relay" }, %{ id: "soonex-voice-leah", src: "/images/people/leah.jpg", initials: "LM", - quote: "Four rooms for a client review without a second build.", + quote: + "Four looks for the client review without rebuilding. They picked duo; we kept shipping.", name: "Leah M.", - role: "Product designer" + role: "Product designer, Fieldwork" } ] end diff --git a/lib/pages/home/product.ex b/lib/pages/home/product.ex index fe9a9ad..a508796 100644 --- a/lib/pages/home/product.ex +++ b/lib/pages/home/product.ex @@ -14,25 +14,161 @@ defmodule Soonex.HomePage.Product do ~H""" <.block id="product" labelledby="soonex-product-heading" eyebrow="Product" layout={:open}> <:title> - What you fork + Everything you need to launch <:lede> - A waitlist landing, a shipping log, a countdown, and four themes. You replace the stills and - the date. + A waitlist that converts, a public shipping log, a countdown aimed at your date, and four + looks for client review — one product, ready for 1 September. + <.tabs id="soonex-product-tabs" - class="tabs ui-brand ui-size-md ui-width-full" + class="tabs ui-brand ui-size-md ui-width-full soonex-product-tabs" indicator - value="waitlist" - items={product_tabs()} - /> + value="overview" + > + <:trigger value="overview">Overview + <:content value="overview" class="soonex-tab-panel"> +
      +

      + Soonex is a launch workspace for studios shipping on a hard date. Put the waitlist on + the page, keep a public log, and review four looks with your client before go-live. +

      +
        +
      • + +
        +

        Waitlist on day one

        +

        + Email, role, interests, and team size — with a launch toast on submit. +

        +
        +
      • +
      • + +
        +

        Public shipping log

        +

        + Markdown entries with covers and tags. The index paginates in the browser. +

        +
        +
      • +
      • + +
        +

        Countdown that stays honest

        +

        + Aimed at 1 September until you change the + date. Digits stay on one row. +

        +
        +
      • +
      +
      + <.navigate to="#waitlist" class="button ui-brand ui-solid ui-size-md"> + Join waitlist + +
      +
      + + + <:trigger value="waitlist">Waitlist + <:content value="waitlist" class="soonex-tab-panel"> +
      +
      +
      +

      Capture who is coming

      +

      + The same fields live on the hero still and at the end of the page. Collect email, + role, interests, and team size — then show a clear success path. +

      +
        +
      • Email with validation
      • +
      • Role select or radios
      • +
      • Interests as tags
      • +
      • Team size stepper
      • +
      • Launch notes switch
      • +
      +
      +
      + Preview +

      Join the list

      +
      +
      you@studio.dev
      +
      Founder
      +
      Add an interest…
      +
      + Join waitlist +
      +
      +

      + Wire your provider after launch. The toast already ships. +

      +
      +
      +
      + + + <:trigger value="log">Log + <:content value="log" class="soonex-tab-panel"> +
      +

      + Keep a public trail of what shipped. Posts live as Markdown with covers and tags — + rebuild and the index updates. +

      +
      + 02 August 2026 +

      + Accessibility on this device +

      +

      + Zoom, contrast, motion, cursor, focus, and link underline — stored in local storage + so preferences follow the visitor. +

      + <.navigate + to={Soonex.Public.path("/blog/accessibility-on-device/")} + class="button ui-ghost ui-brand ui-size-sm mt-6 w-fit" + > + Read entry <.heroicon name="hero-arrow-up-right" /> + +
      +
      + <.navigate to={Soonex.Public.path("/blog")} class="link ui-brand"> + All shipping notes <.heroicon name="hero-arrow-up-right" /> + +
      +
      + + + <:trigger value="looks">Looks + <:content value="looks" class="soonex-tab-panel"> +
      +

      Soonex

      +

      + Four looks for the same launch. Switch rooms in Template Options and review with your + client without a second build. +

      +
        +
      • +

        {room.name}

        +

        {room.line}

        +
      • +
      + <.navigate to="#looks" class="link ui-brand mt-6 inline-flex"> + See all four looks <.heroicon name="hero-arrow-down" /> + +
      + + -
      +
      - Clone + Get Soonex

      - mix setup, then mix soonex.server. + Clone once. mix setup, then mix soonex.server.

      <.clipboard @@ -52,69 +188,83 @@ defmodule Soonex.HomePage.Product do
      -
      -
      -
      {lane.name}
      -
      -

      {lane.body}

      - <.navigate :if={lane.cta} to="#waitlist" class={lane.cta_class}> - {lane.cta} - -
      +
      + Plans +

      + Pick a lane. Ship the same core. +

      +

      + Every plan is the same Soonex launch. The columns describe how far you take it before 1 September. +

      + +
      +
      +
      +
      + {plan.name} +

      {plan.headline}

      +

      {plan.body}

      +
      + <.navigate to="#waitlist" class={"#{plan.cta_class} shrink-0"}> + {plan.cta} + +
      +
        +
      • {item}
      • +
      +
      -
      +
      """ end - defp product_tabs do - Corex.Content.new([ - %{ - value: "waitlist", - label: "Waitlist", - content: - "Email, role, interests, team size, and a launch toast. The same fields sit on the hero still and at the end of the page." - }, - %{ - value: "journal", - label: "Journal", - content: - "Markdown posts with covers, tags, and a client pager. Add a file, rebuild, and the log updates." - }, - %{ - value: "themes", - label: "Themes", - content: - "neo, uno, duo, and leo share one near-white field. Type, radius, and brand do the rest." - }, - %{ - value: "a11y", - label: "Accessibility", - content: - "Zoom, contrast, motion, focus, and underline — stored on this device. The dialog ships with the template." - } - ]) + defp looks do + [ + %{name: "neo", line: "Product studio — geometric display, open corners, cobalt."}, + %{name: "uno", line: "Calm ops — tight tracking, small radius, teal."}, + %{name: "duo", line: "Editorial atelier — serif display, soft corners, rust."}, + %{name: "leo", line: "Signal poster — strong display, square corners, orange."} + ] end - defp lanes do + defp plans do [ %{ name: "Solo", - body: "One theme, the waitlist, and the log. Enough to put a date on the page.", - cta: nil, - cta_class: nil + headline: "Fork and put a date on the page", + body: "One look, the waitlist, and the log. Enough for an independent launch.", + cta: "Join waitlist", + cta_class: "button ui-ghost ui-size-sm w-fit", + items: [ + "Waitlist landing and shipping log", + "Keep neo, or switch once for your brand", + "Countdown aimed at 1 September" + ] }, %{ name: "Studio", - body: "Four rooms, real stills, and a shipping log you can hand to a client.", + headline: "Four looks, real stills, client review", + body: "Treat Template Options as the review. Photography and the shipping log stay.", cta: "Join waitlist", - cta_class: "button ui-brand ui-solid ui-size-sm w-fit shrink-0" + cta_class: "button ui-brand ui-solid ui-size-sm w-fit", + items: [ + "neo, uno, duo, and leo on this device", + "Covers, tags, and client pagination", + "Header timer, toast, and clone strip" + ] }, %{ name: "Agency", - body: "The same fork, with accessibility on and a clone path the team can repeat.", - cta: nil, - cta_class: nil + headline: "Accessible handoff your team can repeat", + body: "Ship with accessibility on and a clone path the whole studio can run.", + cta: "Join waitlist", + cta_class: "button ui-ghost ui-size-sm w-fit", + items: [ + "Zoom, contrast, motion, focus, underline", + "Same fork for every client engagement", + "Clone, mix setup, mix soonex.server" + ] } ] end diff --git a/lib/pages/home/themes.ex b/lib/pages/home/themes.ex index d476168..d500067 100644 --- a/lib/pages/home/themes.ex +++ b/lib/pages/home/themes.ex @@ -7,24 +7,30 @@ defmodule Soonex.HomePage.Themes do def themes(assigns) do ~H""" - <.block id="themes" labelledby="soonex-themes-heading" eyebrow="Rooms" layout={:open}> + <.block id="looks" labelledby="soonex-looks-heading" eyebrow="Looks" layout={:open}> <:title> - Four rooms. One building. + Four looks. One launch. <:lede> - Paper stays near-white. Type, radius, and brand change the weather. Template Options is the - live review — not the headline. + Review neo, uno, duo, and leo with your client on this device. Paper stays calm — type, + corners, and brand change the room.

      Soonex

      - The active room sets this display. Switch neo, uno, duo, or leo in the corner and this line - moves with it. + This display follows the look you pick in Template Options. Switch rooms live — no second + build for the review.

      -
        -
      1. +
          +
        1. {room.id}

          -

          {room.line}

          +

          {room.line}

          +

          {room.use}

        @@ -33,10 +39,26 @@ defmodule Soonex.HomePage.Themes do defp rooms do [ - %{id: "neo", line: "Product studio — geometric display, open corners, cobalt."}, - %{id: "uno", line: "Calm ops — tight tracking, small radius, teal."}, - %{id: "duo", line: "Editorial atelier — serif display, soft corners, rust."}, - %{id: "leo", line: "Signal poster — strong display, square corners, orange."} + %{ + id: "neo", + line: "Product studio — geometric display, open corners, cobalt.", + use: "Default for product launches and investor-facing pages." + }, + %{ + id: "uno", + line: "Calm ops — tight tracking, small radius, teal.", + use: "Ops and tooling launches that need quiet confidence." + }, + %{ + id: "duo", + line: "Editorial atelier — serif display, soft corners, rust.", + use: "Studios and cultural brands that want editorial warmth." + }, + %{ + id: "leo", + line: "Signal poster — strong display, square corners, orange.", + use: "Campaign drops and poster-led announcements." + } ] end end diff --git a/lib/pages/home/waitlist.ex b/lib/pages/home/waitlist.ex index 080a008..982707c 100644 --- a/lib/pages/home/waitlist.ex +++ b/lib/pages/home/waitlist.ex @@ -24,13 +24,13 @@ defmodule Soonex.HomePage.Waitlist do Join the September list <:lede> - Role, interests, and team size. We will write when the date hits. + Role, interests, and team size. We write when the countdown ends.
        <.native_input @@ -114,7 +114,7 @@ defmodule Soonex.HomePage.Waitlist do

        - Demo form — toast only. Wire your provider after you fork. + Demo page — toast only until you wire a provider.

        """ diff --git a/lib/pages/not_found_page.ex b/lib/pages/not_found_page.ex index e3e0aaa..70a0a4e 100644 --- a/lib/pages/not_found_page.ex +++ b/lib/pages/not_found_page.ex @@ -30,7 +30,7 @@ defmodule Soonex.NotFoundPage do Page not found

        - That URL is not on Soonex. Head home for the waitlist, or read the shipping log. + That URL is not on Soonex. Head home to join the waitlist, or read the shipping log.

        <.navigate to={Soonex.Public.path("/")} class="button ui-brand ui-solid ui-size-md"> From a87444e44b167dbb08718f12c47c3029976a8f29 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 30 Aug 2026 03:55:34 +0000 Subject: [PATCH 52/65] Restage Soonex as a surface instrument desk. Strip all photography; hard-rewrite neo/uno/duo/leo palettes; rebuild home as brand-slab hero + control racks with custom data-part chrome so Soonex reads as a Corex template, not a Tailwind waitlist clone. Co-authored-by: Karim Semmoud --- _posts/2026-03-12-fork-day.md | 8 +- _posts/2026-04-08-four-rooms.md | 4 +- _posts/2026-05-08-clone-theme-ship.md | 6 +- _posts/2026-06-01-ci-before-pages.md | 4 - _posts/2026-07-14-header-timer.md | 2 - _posts/2026-08-02-accessibility-on-device.md | 4 +- assets/css/chrome.css | 345 ++++++++++++++----- assets/css/hosts.css | 31 +- config/config.exs | 279 +++++++-------- lib/layouts/media.ex | 2 +- lib/layouts/post_layout.ex | 17 - lib/layouts/root/footer.ex | 2 +- lib/layouts/root_layout.ex | 4 +- lib/layouts/section.ex | 1 + lib/pages/home/faq.ex | 6 +- lib/pages/home/hero.ex | 110 ++---- lib/pages/home/log.ex | 67 ++-- lib/pages/home/notes.ex | 56 +-- lib/pages/home/product.ex | 94 +++-- lib/pages/home/themes.ex | 34 +- lib/pages/home/waitlist.ex | 30 +- 21 files changed, 570 insertions(+), 536 deletions(-) diff --git a/_posts/2026-03-12-fork-day.md b/_posts/2026-03-12-fork-day.md index 1e7050f..5bba66b 100644 --- a/_posts/2026-03-12-fork-day.md +++ b/_posts/2026-03-12-fork-day.md @@ -3,9 +3,7 @@ layout: Soonex.PostLayout title: Fork day date: 2026-03-12 12:00:00 +0000 permalink: /blog/fork-day/ -description: Day one on the September list — waitlist on the page, countdown aimed at launch, stills ready to swap. -image: /images/photos/hero.jpg -image_alt: Sunlit studio with long work tables and hanging task lamps +description: Day one on the September list — waitlist on the page, countdown aimed at launch, lockup ready to rename. tags: - Studio - Notes @@ -14,7 +12,7 @@ sitemap: changefreq: monthly --- -Today we put **Soonex** on a calendar. The waitlist sits on the hero still. The countdown aims at **1 September**. Lockup and photography are yours to replace after you fork. +Today we put **Soonex** on a calendar. The waitlist sits on the hero slab. The countdown aims at **1 September**. Lockup and copy are yours to replace after you fork. ## What shipped @@ -23,7 +21,7 @@ Today we put **Soonex** on a calendar. The waitlist sits on the hero still. The - Header timer that appears after the hero 1. Fork the repo -2. Swap the lockup and stills +2. Swap the lockup and date 3. Point the countdown at your date > Reserve a seat. Launch when the date hits. diff --git a/_posts/2026-04-08-four-rooms.md b/_posts/2026-04-08-four-rooms.md index 908c48c..b278986 100644 --- a/_posts/2026-04-08-four-rooms.md +++ b/_posts/2026-04-08-four-rooms.md @@ -4,8 +4,6 @@ title: Four looks for client review date: 2026-04-08 09:00:00 +0000 permalink: /blog/four-rooms/ description: neo, uno, duo, and leo on one launch — type, corners, and brand change the room; paper stays calm. -image: /images/photos/studio.jpg -image_alt: A calm studio interior with a low sofa, plants, and a large window tags: - Design - Studio @@ -33,7 +31,7 @@ neo is the product studio. uno is calm ops. duo is the editorial atelier. leo is - [x] Four looks live on the landing - [x] Specimen updates with the active room -- [ ] Your photography in the frames +- [ ] Your brand lockup and date > [!TIP] > Pick a look with your client before you rewrite the stills. The waitlist and countdown stay the same. diff --git a/_posts/2026-05-08-clone-theme-ship.md b/_posts/2026-05-08-clone-theme-ship.md index 67fcc6b..20960e3 100644 --- a/_posts/2026-05-08-clone-theme-ship.md +++ b/_posts/2026-05-08-clone-theme-ship.md @@ -4,8 +4,6 @@ title: Clone, look, and ship date: 2026-05-08 12:00:00 +0000 permalink: /blog/clone-theme-ship/ description: Clone Soonex, pick a look for review, keep the waitlist and log, and aim the countdown at your date. -image: /images/covers/overlay.jpg -image_alt: Soft abstract color field in warm paper tones tags: - Launch - Template @@ -50,7 +48,7 @@ Markdown link: [Soonex on GitHub](https://github.com/corex-ui/soonex). Bare URL: - [x] Waitlist toast on submit - [ ] Real mailbox backend -- [ ] Your launch photography +- [ ] Your brand lockup and date --- @@ -84,8 +82,6 @@ Markdown link: [Soonex on GitHub](https://github.com/corex-ui/soonex). Bare URL: Default Open Graph asset: -![Soonex Open Graph](/images/og.svg) - --- ## GitHub-style alerts diff --git a/_posts/2026-06-01-ci-before-pages.md b/_posts/2026-06-01-ci-before-pages.md index c46e20e..62c874d 100644 --- a/_posts/2026-06-01-ci-before-pages.md +++ b/_posts/2026-06-01-ci-before-pages.md @@ -4,8 +4,6 @@ title: CI before Pages date: 2026-06-01 12:00:00 +0000 permalink: /blog/ci-before-pages/ description: Keep CI green before Pages deploys. Axe on the home document is the gate; the countdown stays aimed at 1 September. -image: /images/covers/shipping.jpg -image_alt: A laptop on a wooden desk with a code editor open tags: - Launch - Notes @@ -34,8 +32,6 @@ Stop `mix soonex.server` if port 4999 is bound. Then `MIX_ENV=test mix test`. | Build | `mix tableau.build` | Static `_site/` | | Serve | `mix soonex.server` | Local preview | -![Soonex Open Graph](/images/og.svg) - > [!TIP] > Keep CI green before Pages deploys so broken pushes never reach the public CDN. diff --git a/_posts/2026-07-14-header-timer.md b/_posts/2026-07-14-header-timer.md index 88b2800..ffc5b9b 100644 --- a/_posts/2026-07-14-header-timer.md +++ b/_posts/2026-07-14-header-timer.md @@ -4,8 +4,6 @@ title: The header timer and 1 September date: 2026-07-14 15:00:00 +0000 permalink: /blog/header-timer/ description: The countdown lives in the header, stays on one row, and aims at 1 September until you change the date. -image: /images/photos/orbit.jpg -image_alt: Earth from orbit against a dark sky tags: - Launch - Notes diff --git a/_posts/2026-08-02-accessibility-on-device.md b/_posts/2026-08-02-accessibility-on-device.md index 02c34f2..1509fca 100644 --- a/_posts/2026-08-02-accessibility-on-device.md +++ b/_posts/2026-08-02-accessibility-on-device.md @@ -4,8 +4,6 @@ title: Accessibility on this device date: 2026-08-02 11:00:00 +0000 permalink: /blog/accessibility-on-device/ description: Zoom, contrast, motion, cursor, focus, and link underline — preferences stay on this device in local storage. -image: /images/covers/overlay.jpg -image_alt: Soft abstract color field in warm paper tones tags: - Notes - Launch @@ -33,4 +31,4 @@ Scroll reveal lives in host CSS. Set Motion to Reduced in Accessibility and thos Use **bold**, *italic*, and `inline code` in the same paragraph when you document the dialog. -![Soonex Open Graph](/images/og.svg) + diff --git a/assets/css/chrome.css b/assets/css/chrome.css index 8160013..22df7e4 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -1,4 +1,4 @@ -/* Tiny layout helpers that utilities cannot express. */ +/* Instrument-desk chrome. Tokens only — no Corex recipe forks. */ html { scroll-behavior: smooth; @@ -38,24 +38,6 @@ html[data-motion="reduce"] { } } -.soonex-card-motion { - transition: - border-color var(--duration-fast) var(--soonex-ease), - box-shadow var(--duration-fast) var(--soonex-ease); -} - -@media (hover: hover) { - .soonex-card-motion:hover { - border-color: var(--color-brand); - box-shadow: var(--shadow-sm); - } -} - -html[data-motion="reduce"] .soonex-card-motion, -html[data-motion="reduce"] .soonex-card-motion:hover { - transition: none; -} - [data-site-header] { transition: border-color var(--duration-normal) var(--soonex-ease); } @@ -72,9 +54,7 @@ html[data-motion="reduce"] .soonex-card-motion:hover { } } -/* Header timer stays in flow (invisible until reveal) so the bar never jumps. - Corex digits flip in a 2em window (`top: calc(var(--value) * -2em)`). Do not - shrink that window — it clips the strip. Compact the type instead. */ +/* Header timer — compact type, keep 2em digit window. */ [data-site-header] .timer.ui-size-sm { --timer-digit: var(--text-sm); --timer-digit-leading: 2; @@ -119,40 +99,115 @@ html[data-motion="reduce"] .soonex-card-motion:hover { border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); } -.soonex-hero-wash { - background: linear-gradient( - 180deg, - color-mix(in oklab, var(--color-root) 18%, transparent) 0%, - var(--color-root) 86% - ); +/* ——— Hero brand slab ——— */ + +.soonex-hero-slab { + background-color: var(--color-brand); + color: var(--color-brand-contrast); +} + +.soonex-slab-kicker { + font-family: var(--font-sans); + font-size: var(--text-sm); + font-weight: var(--font-weight-semibold); + letter-spacing: var(--tracking-widest); + text-transform: uppercase; + color: color-mix(in oklab, var(--color-brand-contrast) 78%, transparent); +} + +.soonex-slab-display { + font-family: var(--font-display); + font-size: var(--text-6xl); + font-weight: var(--font-weight-bold); + letter-spacing: var(--tracking-tighter); + line-height: 0.92; + color: var(--color-brand-contrast); +} + +@media (width >= 40rem) { + .soonex-slab-display { + font-size: var(--text-7xl); + } } @media (width >= 64rem) { - .soonex-hero-wash { - background: linear-gradient( - 105deg, - var(--color-root) 0%, - color-mix(in oklab, var(--color-root) 86%, transparent) 42%, - color-mix(in oklab, var(--color-accent) 24%, transparent) 100% - ); + .soonex-slab-display { + font-size: var(--text-8xl); } } -.soonex-waitlist-object { - border: 1px solid var(--color-border); +.soonex-slab-lede { + font-size: var(--text-lg); + line-height: var(--leading-relaxed); + color: color-mix(in oklab, var(--color-brand-contrast) 82%, transparent); +} + +.soonex-slab-cta { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: var(--spacing-size); + padding-inline: calc(var(--spacing-space) * 1.25); + border-radius: var(--soonex-ctl-radius, var(--radius-md)); background-color: var(--color-root); - border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); - padding: calc(var(--spacing-space) * 1.15); - box-shadow: var(--shadow-sm); + color: var(--color-ink); + font-size: var(--text-base); + font-weight: var(--font-weight-semibold); + text-decoration: none; +} + +.soonex-slab-cta:hover { + background-color: var(--color-surface); +} + +.soonex-slab-link { + display: inline-flex; + align-items: center; + gap: 0.35rem; + color: var(--color-brand-contrast); + font-weight: var(--font-weight-semibold); + text-decoration: none; + border-block-end: 1px solid color-mix(in oklab, var(--color-brand-contrast) 45%, transparent); +} + +.soonex-slab-link:hover { + border-block-end-color: var(--color-brand-contrast); +} + +[data-theme="leo"] .soonex-slab-display, +[data-theme="leo"] .soonex-slab-kicker { + text-transform: uppercase; +} + +[data-theme="uno"] .soonex-slab-display { + letter-spacing: -0.05em; +} + +[data-theme="duo"] .soonex-slab-lede { + font-family: var(--font-serif); + font-style: italic; +} + +/* ——— Waitlist instrument rack ——— */ + +.soonex-waitlist-rack { + border: 1px solid var(--color-border); + background-color: var(--color-surface); + color: var(--color-ink); + border-radius: var(--soonex-ctl-radius, var(--radius-xl)); + padding: calc(var(--spacing-space) * 1.25); } @media (width >= 40rem) { - .soonex-waitlist-object { - padding: calc(var(--spacing-space) * 1.45); + .soonex-waitlist-rack { + padding: calc(var(--spacing-space) * 1.5); } } -/* Hero email + CTA read as one joined control on sm+. */ +.soonex-waitlist-rack--canonical { + border-width: 2px; +} + .soonex-waitlist-join { gap: var(--spacing-space-sm); } @@ -175,49 +230,106 @@ html[data-motion="reduce"] .soonex-card-motion:hover { } } +/* ——— Product tabs as segmented instrument ——— */ + .soonex-tab-panel { - min-height: 18rem; + min-height: 20rem; +} + +.tabs.soonex-product-tabs [data-scope="tabs"][data-part="list"] { + gap: 0; + border: 1px solid var(--color-border); + border-radius: var(--soonex-ctl-radius, var(--radius-md)); + background-color: var(--color-ui); + padding: 0.2rem; + overflow: hidden; +} + +.tabs.soonex-product-tabs [data-scope="tabs"][data-part="trigger"] { + font-weight: var(--font-weight-semibold); + border-radius: var(--soonex-ctl-radius, var(--radius-sm)); } .tabs.soonex-product-tabs [data-scope="tabs"][data-part="content"] { - padding: calc(var(--spacing-space) * 1.25); + margin-block-start: var(--spacing-space); + padding: calc(var(--spacing-space) * 1.35); background-color: var(--color-surface); border: 1px solid var(--color-border); - border-block-start: 0; + border-radius: var(--soonex-ctl-radius, var(--radius-xl)); + min-height: 20rem; } .soonex-tab-workspace { - max-width: 48rem; + max-width: 52rem; } .soonex-tab-workspace .soonex-room-specimen { font-size: var(--text-5xl); } -.soonex-check-list { +.soonex-instrument-list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; - gap: var(--spacing-space); + gap: calc(var(--spacing-space) * 1.1); } -.soonex-check-list > li { +.soonex-instrument-list > li { display: grid; grid-template-columns: auto 1fr; gap: var(--spacing-space); align-items: start; + padding-block: calc(var(--spacing-space) * 0.65); + border-block-end: 1px solid var(--color-border); } -.soonex-check-mark { - display: block; - width: 0.85rem; - height: 0.85rem; - margin-top: 0.35rem; - border-radius: var(--soonex-ctl-radius, var(--radius-sm)); - background-color: var(--color-brand); - box-shadow: inset 0 0 0 2px color-mix(in oklab, var(--color-brand) 35%, transparent); +.soonex-instrument-list > li:last-child { + border-block-end: 0; +} + +.soonex-instrument-index { + font-family: var(--font-mono); + font-size: var(--text-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-brand-text); + line-height: 1.6; +} + +.soonex-look-rail { + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + border-block: 1px solid var(--color-border); +} + +.soonex-look-rail > li { + display: grid; + grid-template-columns: 4rem 1fr; + gap: var(--spacing-space); + align-items: baseline; + padding-block: calc(var(--spacing-space) * 0.85); + border-block-end: 1px solid var(--color-border); +} + +.soonex-look-rail > li:last-child { + border-block-end: 0; +} + +.soonex-look-id { + font-family: var(--font-mono); + font-size: var(--text-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-brand-text); +} + +.soonex-look-line { + color: var(--color-ink-muted); + font-size: var(--text-sm); + line-height: var(--leading-relaxed); } .soonex-feature-bullets { @@ -248,9 +360,12 @@ html[data-motion="reduce"] .soonex-card-motion:hover { background-color: var(--color-brand); } -.soonex-mini-form, -.soonex-sample-entry { +.soonex-rack-specimen, +.soonex-ledger-row { + border: 1px solid var(--color-border); + background-color: var(--color-ui); border-radius: var(--soonex-ctl-radius, var(--radius-xl)); + padding: calc(var(--spacing-space) * 1.15); } .soonex-field-ghost { @@ -260,11 +375,27 @@ html[data-motion="reduce"] .soonex-card-motion:hover { padding-inline: var(--spacing-space); border: 1px solid var(--color-border); border-radius: var(--soonex-ctl-radius, var(--radius-md)); - background-color: var(--color-ui); + background-color: var(--color-surface); color: var(--color-ink-muted); font-size: var(--text-sm); } +.soonex-clone-strip { + display: flex; + flex-direction: column; + gap: var(--spacing-space); + border-block-start: 1px solid var(--color-border); + padding-block-start: calc(var(--spacing-space) * 1.35); +} + +@media (width >= 64rem) { + .soonex-clone-strip { + flex-direction: row; + align-items: end; + gap: calc(var(--spacing-space) * 1.5); + } +} + .soonex-pricing { display: flex; flex-direction: column; @@ -273,7 +404,7 @@ html[data-motion="reduce"] .soonex-card-motion:hover { } .soonex-pricing-tier { - padding-block: calc(var(--spacing-space) * 1.35); + padding-block: calc(var(--spacing-space) * 1.4); border-block-end: 1px solid var(--color-border); } @@ -281,38 +412,88 @@ html[data-motion="reduce"] .soonex-card-motion:hover { border-block-end: 0; } -.soonex-photo { - display: block; - width: 100%; - height: 100%; - object-fit: cover; +/* ——— Looks instrument grid ——— */ + +.soonex-looks-stage { + border-block-start: 1px solid var(--color-border); + padding-block-start: calc(var(--spacing-space) * 1.25); } -.soonex-avatars { - display: flex; - align-items: center; +.soonex-instrument-grid { + margin: 0; + padding: 0; + list-style: none; + display: grid; + grid-template-columns: 1fr; + gap: var(--spacing-space); } -.soonex-avatars .avatar + .avatar { - margin-inline-start: calc(var(--spacing-space-sm) * -1); +@media (width >= 40rem) { + .soonex-instrument-grid { + grid-template-columns: 1fr 1fr; + } } -.soonex-avatars .avatar [data-scope="avatar"][data-part="root"] { - border: 2px solid var(--color-root); +.soonex-instrument-slab { + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--soonex-ctl-radius, var(--radius-xl)); + padding: calc(var(--spacing-space) * 1.25); } -.soonex-toolchain { +.soonex-instrument-slab .soonex-room-name { + color: var(--color-ink); +} + +/* ——— Notes + ledger ——— */ + +.soonex-note-rail { + margin: 0; + padding: 0; + list-style: none; display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--spacing-space); + flex-direction: column; + gap: calc(var(--spacing-space) * 1.5); + border-block-start: 1px solid var(--color-border); + padding-block-start: calc(var(--spacing-space) * 1.25); +} + +.soonex-ledger { margin: 0; padding: 0; list-style: none; + display: flex; + flex-direction: column; +} + +.soonex-ledger-entry { + padding-block: calc(var(--spacing-space) * 1.35); + border-block-end: 1px solid var(--color-border); } -.soonex-toolchain img { - display: block; - height: var(--spacing-size); - width: auto; +.soonex-ledger-entry:first-child { + border-block-start: 1px solid var(--color-border); +} + +/* ——— FAQ accordion instrument ——— */ + +.accordion.soonex-faq-instrument [data-scope="accordion"][data-part="item-trigger"] { + font-weight: var(--font-weight-semibold); + padding-block: calc(var(--spacing-space) * 0.95); +} + +.accordion.soonex-faq-instrument [data-scope="accordion"][data-part="item"] { + border-color: var(--color-border); +} + +/* ——— Control family extras ——— */ + +.select [data-scope="select"][data-part="trigger"], +.tags-input [data-scope="tags-input"][data-part="control"], +.number-input [data-scope="number-input"][data-part="control"] { + border-radius: var(--soonex-ctl-radius, var(--radius-md)); +} + +.clipboard [data-scope="clipboard"][data-part="control"] { + border-radius: var(--soonex-ctl-radius, var(--radius-md)); } diff --git a/assets/css/hosts.css b/assets/css/hosts.css index c0d701a..6a9c27f 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -7,37 +7,12 @@ } body.layout { - background-image: - url("../images/grain.svg"), - radial-gradient( - ellipse 80% 50% at 110% -10%, - color-mix(in oklab, var(--color-brand) 10%, transparent), - transparent 55% - ), - radial-gradient( - ellipse 45% 40% at -8% 110%, - color-mix(in oklab, var(--color-accent) 7%, transparent), - transparent 50% - ); - background-repeat: repeat, no-repeat, no-repeat; - background-attachment: scroll, fixed, fixed; - background-blend-mode: multiply, normal, normal; + background-color: var(--color-root); + background-image: none; } [data-mode="dark"] body.layout { - background-image: - url("../images/grain.svg"), - radial-gradient( - ellipse 80% 50% at 110% -10%, - color-mix(in oklab, var(--color-brand) 16%, transparent), - transparent 55% - ), - radial-gradient( - ellipse 45% 40% at -8% 110%, - color-mix(in oklab, var(--color-accent) 10%, transparent), - transparent 50% - ); - background-blend-mode: overlay, normal, normal; + background-image: none; } html[data-motion="reduce"] body.layout { diff --git a/config/config.exs b/config/config.exs index 89ea6c7..c5883c9 100644 --- a/config/config.exs +++ b/config/config.exs @@ -76,7 +76,7 @@ config :tableau, Tableau.RSSExtension, enabled: true, title: "Soonex", description: - "Soonex ships 1 September — waitlist, shipping log, countdown, and four looks for client review." + "Soonex ships 1 September — instrument desk with waitlist, shipping log, countdown, and four looks." config :elixir, :time_zone_database, Tz.TimeZoneDatabase @@ -134,9 +134,9 @@ config :corex_design, themes: %{ neo: %{ seeds: %{ - neutral: "#F3F5F8", - accent: "#0E1218", - brand: "#1D4ED8", + neutral: "#EEF1F6", + accent: "#070A10", + brand: "#155EEF", alert: "#B42318", info: "#0E7490", success: "#15803D" @@ -144,43 +144,43 @@ config :corex_design, colors: %{ light: with_on.(%{ - root: l.(0.99, :neutral), - surface: l.(0.985, :neutral), - ui: fill.(0.96, :neutral), - accent: fill.(0.22, :accent), - brand: fill.(0.42, :brand), - ink: contrast.(:accent, :root, 12), - "ink-muted": contrast.(:accent, :root, 6.0), - link: contrast.(:brand, :root, 5.5), - border: contrast.(:neutral, :ui, 1.2), - focus: contrast.(:brand, :ui, 2.3), - shadow: contrast.(:accent, :root, 1.1) + root: l.(0.985, :neutral), + surface: l.(0.96, :neutral), + ui: fill.(0.92, :neutral), + accent: fill.(0.16, :accent), + brand: fill.(0.48, :brand), + ink: contrast.(:accent, :root, 13), + "ink-muted": contrast.(:accent, :root, 6.4), + link: contrast.(:brand, :root, 5.8), + border: contrast.(:neutral, :ui, 1.35), + focus: contrast.(:brand, :ui, 2.5), + shadow: contrast.(:accent, :root, 1.15) }), 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) + root: l.(0.045, :accent), + surface: l.(0.085, :accent), + ui: fill.(0.14, :accent), + accent: fill.(0.55, :accent), + brand: fill.(0.5, :brand), + ink: contrast.(:neutral, :root, 14), + "ink-muted": contrast.(:neutral, :root, 7.0), + link: contrast.(:brand, :root, 6.5), + border: contrast.(:neutral, :ui, 1.4), + focus: contrast.(:brand, :ui, 2.55), + shadow: contrast.(:accent, :root, 1.2) }) }, dimensions: %{ radius: %{ - xs: 0.35, - sm: 0.6, - md: 0.9, - lg: 1.2, - xl: 1.55, - "2xl": 1.95, - "3xl": 2.5, - "4xl": 3.2, + xs: 0.2, + sm: 0.4, + md: 0.7, + lg: 1.0, + xl: 1.35, + "2xl": 1.7, + "3xl": 2.2, + "4xl": 2.8, full: 9999 }, font: %{ @@ -221,19 +221,20 @@ config :corex_design, }, "small.eyebrow" => %{ font_weight: {:weight, :semibold}, - font_size: {:text, :base}, + font_size: {:text, :sm}, + letter_spacing: {:tracking, :wide}, color: "var(--color-brand-text)" }, - "blockquote" => %{font_family: {:font, :serif}, font_style: :italic}, + "blockquote" => %{font_family: {:font, :display}, font_weight: {:weight, :semibold}}, "code" => %{font_family: {:font, :mono}}, "kbd" => %{font_family: {:font, :mono}} } }, uno: %{ seeds: %{ - neutral: "#F4F8F6", - accent: "#04221C", - brand: "#0F766E", + neutral: "#E8F0ED", + accent: "#021A16", + brand: "#0D9488", alert: "#B91C1C", info: "#0369A1", success: "#166534" @@ -241,43 +242,43 @@ config :corex_design, colors: %{ light: with_on.(%{ - root: l.(0.99, :neutral), - surface: l.(0.985, :neutral), - ui: fill.(0.96, :neutral), - accent: fill.(0.28, :accent), - brand: fill.(0.34, :brand), - ink: contrast.(:accent, :root, 10), - "ink-muted": contrast.(:accent, :root, 5.5), - link: contrast.(:brand, :root, 5.8), - border: contrast.(:neutral, :ui, 1.18), - focus: contrast.(:brand, :ui, 2.2), - shadow: contrast.(:accent, :root, 1.06) + root: l.(0.97, :neutral), + surface: l.(0.94, :neutral), + ui: fill.(0.9, :neutral), + accent: fill.(0.22, :accent), + brand: fill.(0.4, :brand), + ink: contrast.(:accent, :root, 11), + "ink-muted": contrast.(:accent, :root, 5.8), + link: contrast.(:brand, :root, 6.0), + border: contrast.(:neutral, :ui, 1.28), + focus: contrast.(:brand, :ui, 2.35), + shadow: contrast.(:accent, :root, 1.08) }), 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) + root: l.(0.05, :accent), + surface: l.(0.095, :accent), + ui: fill.(0.155, :accent), + accent: fill.(0.55, :accent), + brand: fill.(0.55, :brand), + ink: contrast.(:neutral, :root, 13), + "ink-muted": contrast.(:neutral, :root, 6.5), + link: contrast.(:brand, :root, 7.2), + border: contrast.(:neutral, :ui, 1.35), + focus: contrast.(:brand, :ui, 2.4), + shadow: contrast.(:accent, :root, 1.16) }) }, dimensions: %{ radius: %{ - xs: 0.04, - sm: 0.08, - md: 0.14, - lg: 0.22, - xl: 0.32, - "2xl": 0.44, - "3xl": 0.58, - "4xl": 0.74, + xs: 0, + sm: 0.02, + md: 0.05, + lg: 0.1, + xl: 0.16, + "2xl": 0.24, + "3xl": 0.32, + "4xl": 0.4, full: 9999 }, font: %{ @@ -319,8 +320,9 @@ config :corex_design, }, "small.eyebrow" => %{ font_weight: {:weight, :medium}, - font_size: {:text, :sm}, - letter_spacing: {:tracking, :wide}, + font_size: {:text, :xs}, + letter_spacing: {:tracking, :widest}, + text_transform: "uppercase", color: "var(--color-brand-text)" }, "blockquote" => %{font_style: :italic}, @@ -330,8 +332,8 @@ config :corex_design, }, duo: %{ seeds: %{ - neutral: "#F8F6F3", - accent: "#3A1C0F", + neutral: "#F3EDE6", + accent: "#2A140C", brand: "#C2410C", alert: "#9F1239", info: "#3D5278", @@ -340,43 +342,43 @@ config :corex_design, colors: %{ light: with_on.(%{ - root: l.(0.99, :neutral), - surface: l.(0.985, :neutral), - ui: fill.(0.96, :neutral), - accent: fill.(0.32, :accent), - brand: fill.(0.44, :brand), - ink: contrast.(:accent, :root, 9.5), - "ink-muted": contrast.(:accent, :root, 5.2), - link: contrast.(:brand, :root, 5.6), - border: contrast.(:neutral, :ui, 1.16), - focus: contrast.(:brand, :ui, 2.15), - shadow: contrast.(:accent, :root, 1.28) + root: l.(0.98, :neutral), + surface: l.(0.95, :neutral), + ui: fill.(0.91, :neutral), + accent: fill.(0.28, :accent), + brand: fill.(0.46, :brand), + ink: contrast.(:accent, :root, 10), + "ink-muted": contrast.(:accent, :root, 5.4), + link: contrast.(:brand, :root, 5.8), + border: contrast.(:neutral, :ui, 1.22), + focus: contrast.(:brand, :ui, 2.2), + shadow: contrast.(:accent, :root, 1.3) }), 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) + root: l.(0.06, :accent), + surface: l.(0.11, :accent), + ui: fill.(0.18, :accent), + accent: fill.(0.52, :accent), + brand: fill.(0.58, :brand), + ink: contrast.(:neutral, :root, 12.5), + "ink-muted": contrast.(:neutral, :root, 6.4), + link: contrast.(:brand, :root, 7.2), + border: contrast.(:neutral, :ui, 1.25), + focus: contrast.(:brand, :ui, 2.35), + shadow: contrast.(:accent, :root, 1.35) }) }, dimensions: %{ radius: %{ - xs: 0.9, - sm: 1.4, - md: 2.0, - lg: 2.8, - xl: 3.6, - "2xl": 4.6, - "3xl": 6.0, - "4xl": 7.5, + xs: 1.0, + sm: 1.6, + md: 2.4, + lg: 3.2, + xl: 4.2, + "2xl": 5.4, + "3xl": 7.0, + "4xl": 9.0, full: 9999 }, font: %{ @@ -415,6 +417,7 @@ config :corex_design, "small.eyebrow" => %{ font_family: {:font, :sans}, font_weight: {:weight, :semibold}, + letter_spacing: {:tracking, :wide}, color: "var(--color-brand-text)" }, "blockquote" => %{font_family: {:font, :serif}, font_style: :italic}, @@ -424,9 +427,9 @@ config :corex_design, }, leo: %{ seeds: %{ - neutral: "#F6F5F2", - accent: "#111111", - brand: "#EA580C", + neutral: "#F0EEE9", + accent: "#0A0A0A", + brand: "#F97316", alert: "#C41E1E", info: "#0369A1", success: "#166534" @@ -434,43 +437,43 @@ config :corex_design, colors: %{ light: with_on.(%{ - root: l.(0.99, :neutral), - surface: l.(0.985, :neutral), - ui: fill.(0.96, :neutral), - accent: fill.(0.14, :accent), - brand: fill.(0.38, :brand), - ink: contrast.(:accent, :root, 12), - "ink-muted": contrast.(:accent, :root, 5.8), - link: contrast.(:brand, :root, 5.8), - border: contrast.(:accent, :ui, 1.22), - focus: contrast.(:brand, :ui, 2.5), - shadow: contrast.(:accent, :root, 1.04) + root: l.(0.985, :neutral), + surface: l.(0.955, :neutral), + ui: fill.(0.91, :neutral), + accent: fill.(0.1, :accent), + brand: fill.(0.45, :brand), + ink: contrast.(:accent, :root, 13), + "ink-muted": contrast.(:accent, :root, 6.0), + link: contrast.(:brand, :root, 6.0), + border: contrast.(:accent, :ui, 1.4), + focus: contrast.(:brand, :ui, 2.6), + shadow: contrast.(:accent, :root, 1.05) }), 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) + root: l.(0.03, :accent), + surface: l.(0.07, :accent), + ui: fill.(0.13, :accent), + accent: fill.(0.5, :accent), + brand: fill.(0.52, :brand), + ink: contrast.(:neutral, :root, 14.5), + "ink-muted": contrast.(:neutral, :root, 7.0), + link: contrast.(:brand, :root, 7.4), + border: contrast.(:neutral, :ui, 1.5), + focus: contrast.(:brand, :ui, 2.7), + shadow: contrast.(:accent, :root, 1.08) }) }, 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, + sm: 0, + md: 0, + lg: 0, + xl: 0, + "2xl": 0, + "3xl": 0, + "4xl": 0, full: 9999 }, font: %{ @@ -497,7 +500,7 @@ config :corex_design, text_transform: "uppercase", sm: %{font_size: {:text, :"5xl"}} }, - "h3" => %{font_weight: {:weight, :medium}}, + "h3" => %{font_weight: {:weight, :medium}, text_transform: "uppercase"}, "h4" => %{ font_weight: {:weight, :medium}, letter_spacing: {:tracking, :widest}, @@ -512,12 +515,12 @@ config :corex_design, }, "small.eyebrow" => %{ font_weight: {:weight, :medium}, - font_size: {:text, :sm}, + font_size: {:text, :xs}, letter_spacing: {:tracking, :widest}, text_transform: "uppercase", color: "var(--color-brand-text)" }, - "blockquote" => %{font_style: :italic}, + "blockquote" => %{font_weight: {:weight, :semibold}}, "code" => %{font_family: {:font, :mono}}, "kbd" => %{font_family: {:font, :mono}} } diff --git a/lib/layouts/media.ex b/lib/layouts/media.ex index 3541b0a..27e12aa 100644 --- a/lib/layouts/media.ex +++ b/lib/layouts/media.ex @@ -26,6 +26,6 @@ defmodule Soonex.Layouts.Media do end def credits do - "Photographs under the Unsplash License. Tool marks from Simple Icons (CC0)." + "Soonex · Corex template" end end diff --git a/lib/layouts/post_layout.ex b/lib/layouts/post_layout.ex index 33406e2..f41e4b1 100644 --- a/lib/layouts/post_layout.ex +++ b/lib/layouts/post_layout.ex @@ -5,8 +5,6 @@ defmodule Soonex.PostLayout do use Phoenix.Component use Corex - import Soonex.Layouts.Media, only: [photo: 1] - alias Soonex.Layouts.Shell def template(assigns) do @@ -33,7 +31,6 @@ 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"""
        @@ -63,12 +60,6 @@ defmodule Soonex.PostLayout do
        -
        -
        - <.photo src={@post_cover.src} alt={@post_cover.alt} width={1400} height={700} /> -
        -
        -
        {{:safe, render(@inner_content)}}
        @@ -76,12 +67,4 @@ defmodule Soonex.PostLayout do
    """ end - - defp cover(page) do - src = page[:image] - - if is_binary(src) and src != "" do - %{src: src, alt: page[:image_alt] || page[:title] || "Log cover"} - end - end end diff --git a/lib/layouts/root/footer.ex b/lib/layouts/root/footer.ex index 3fc96f9..becccf5 100644 --- a/lib/layouts/root/footer.ex +++ b/lib/layouts/root/footer.ex @@ -26,7 +26,7 @@ defmodule Soonex.Layouts.Root.Footer do
    <.lockup />

    - Waitlist, shipping log, countdown, and four looks. Ship 1 September. + Instrument desk for launch day — waitlist, ledger, countdown, four looks. Ship 1 September.

    diff --git a/lib/layouts/root_layout.ex b/lib/layouts/root_layout.ex index f3d7dc2..69e7794 100644 --- a/lib/layouts/root_layout.ex +++ b/lib/layouts/root_layout.ex @@ -221,7 +221,7 @@ defmodule Soonex.RootLayout do defp meta_description(page, site_name) do cond do page[:page_kind] == :home -> - "Soonex ships 1 September — waitlist, shipping log, countdown, and four looks for client review." + "Soonex ships 1 September — instrument desk with waitlist, shipping log, countdown, and four looks." page[:page_kind] == :blog_index -> "Shipping notes from #{site_name}: waitlist, looks, countdown, and the road to 1 September." @@ -239,7 +239,7 @@ defmodule Soonex.RootLayout do page[:description] true -> - "Soonex ships 1 September — waitlist, shipping log, countdown, and four looks for client review." + "Soonex ships 1 September — instrument desk with waitlist, shipping log, countdown, and four looks." end end diff --git a/lib/layouts/section.ex b/lib/layouts/section.ex index 4109384..787eea0 100644 --- a/lib/layouts/section.ex +++ b/lib/layouts/section.ex @@ -55,6 +55,7 @@ defmodule Soonex.Layouts.Section do defp tone_class(:root), do: "bg-root" defp tone_class(:surface), do: "border-y border-border bg-surface" + defp tone_class(:ui), do: "border-y border-border bg-ui" defp tone_class(:clear), do: "" defp stage_class(:bleed), do: "#{Shell.stage()} relative z-[1]" diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index cad57ab..4ac4109 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -22,7 +22,7 @@ defmodule Soonex.HomePage.Faq do <.accordion id="soonex-faq" - class="accordion ui-accent ui-size-md mx-auto w-full max-w-none" + class="accordion ui-accent ui-size-md soonex-faq-instrument mx-auto w-full max-w-none" multiple={false} collapsible={true} value="what" @@ -48,13 +48,13 @@ defmodule Soonex.HomePage.Faq do value: "what", label: "What is Soonex?", content: - "A launch workspace shipping 1 September — waitlist, public shipping log, countdown, and four looks for client review." + "A launch desk shipping 1 September — waitlist, public shipping log, countdown, and four looks for client review." }, %{ value: "ships", label: "What do I get on launch day?", content: - "The landing, waitlist fields, shipping log, header countdown, and four looks. Swap the stills, lockup, and date for your brand." + "The landing, waitlist fields, shipping log, header countdown, and four looks. Swap the lockup and date for your brand." }, %{ value: "looks", diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index 19d716a..eb58767 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -4,64 +4,53 @@ defmodule Soonex.HomePage.Hero do use Phoenix.Component use Corex - import Soonex.Layouts.Media, only: [photo: 1] - alias Soonex.Layouts.Shell def hero(assigns) do ~H""" -
    -
    - <.photo - src="/images/photos/hero.jpg" - alt="Sunlit studio with long work tables and hanging task lamps" - width={1600} - height={1200} - class="absolute inset-0 size-full" - loading="eager" - /> -
    -
    - -
    -
    -
    -

    +
    +
    +
    +
    +

    Launch · 1 September

    +

    Soonex - - ships 1 September. -

    - -

    - Reserve a seat for launch day. Waitlist, shipping log, countdown, and four looks — - ready when the date hits. +

    + Waitlist. Shipping log. Countdown. Four looks. One instrument desk for studios that + ship on a date.

    - -
    - <.navigate to="#waitlist" class="button ui-brand ui-solid ui-size-md"> +
    + <.navigate to="#waitlist" class="soonex-slab-cta"> Join waitlist - <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-md"> + <.navigate to={Soonex.Public.path("/blog")} class="soonex-slab-link"> Read the log <.heroicon name="hero-arrow-up-right" />
    -
    -
    -

    Reserve a September seat

    -

    - Tell us who you are. We write when the countdown ends. -

    +
    +
    +
    + Waitlist +

    Reserve a seat

    +

    + Email + role. We write before the countdown ends. +

    +
    -
    +
    <.native_input type="email" name="waitlist[email]" @@ -75,7 +64,7 @@ defmodule Soonex.HomePage.Hero do
    @@ -94,22 +83,10 @@ defmodule Soonex.HomePage.Hero do -
    -
    - <.avatar - :for={person <- people()} - id={person.id} - src={Soonex.Public.path(person.src)} - alt={person.alt} - class="avatar ui-size-sm" - > - <:fallback>{person.initials} - -
    -

    - 240 studios already on the list for 1 September. -

    -
    +

    + 240 studios + already on the list for 1 September. +

    @@ -119,27 +96,4 @@ defmodule Soonex.HomePage.Hero do """ end - - defp people do - [ - %{ - id: "soonex-hero-avatar-amira", - src: "/images/people/amira.jpg", - alt: "Amira N.", - initials: "AN" - }, - %{ - id: "soonex-hero-avatar-jonas", - src: "/images/people/jonas.jpg", - alt: "Jonas K.", - initials: "JK" - }, - %{ - id: "soonex-hero-avatar-leah", - src: "/images/people/leah.jpg", - alt: "Leah M.", - initials: "LM" - } - ] - end end diff --git a/lib/pages/home/log.ex b/lib/pages/home/log.ex index c346852..b95567d 100644 --- a/lib/pages/home/log.ex +++ b/lib/pages/home/log.ex @@ -4,7 +4,6 @@ defmodule Soonex.HomePage.Log do use Phoenix.Component use Corex - import Soonex.Layouts.Media, only: [photo: 1] import Soonex.Layouts.Section, only: [block: 1] alias Soonex.Layouts.Shell @@ -21,9 +20,15 @@ defmodule Soonex.HomePage.Log do assigns = assign(assigns, :entries, posts) ~H""" - <.block id="log" labelledby="soonex-log-heading" eyebrow="Shipping log" layout={:open}> + <.block + id="log" + labelledby="soonex-log-heading" + eyebrow="Shipping log" + tone={:surface} + layout={:open} + > <:title> - What we shipped on the road to September + Ledger on the road to September <:lede> Public notes on the waitlist, the four looks, and the countdown. The full index lives on the @@ -35,53 +40,29 @@ defmodule Soonex.HomePage.Log do

    No posts yet.

    -
      -
    1. -
      - <.photo - src={cover(post).src} - alt={cover(post).alt} - width={800} - height={500} - /> -
      -
      - {date_label(post)} -

      - <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> - {post[:title] || "Untitled"} - -

      -

      - {post[:description]} -

      - <.navigate - to={Soonex.Public.path(post.permalink)} - class="button ui-ghost ui-brand ui-size-sm mt-4 w-fit" - > - Read more <.heroicon name="hero-arrow-up-right" /> +
        +
      1. + {date_label(post)} +

        + <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> + {post[:title] || "Untitled"} -

      +

    +

    + {post[:description]} +

    + <.navigate + to={Soonex.Public.path(post.permalink)} + class="button ui-ghost ui-brand ui-size-sm mt-5 w-fit" + > + Read more <.heroicon name="hero-arrow-up-right" /> + """ end - defp cover(post) do - src = post[:image] - - if is_binary(src) and src != "" do - %{src: src, alt: post[:image_alt] || post[:title] || "Log cover"} - end - end - defp date_label(%{date: %DateTime{} = date}), do: Calendar.strftime(date, "%d %B %Y") defp date_label(_), do: nil end diff --git a/lib/pages/home/notes.ex b/lib/pages/home/notes.ex index 4e32505..248c52e 100644 --- a/lib/pages/home/notes.ex +++ b/lib/pages/home/notes.ex @@ -2,7 +2,6 @@ defmodule Soonex.HomePage.Notes do @moduledoc false use Phoenix.Component - use Corex import Soonex.Layouts.Section, only: [block: 1] @@ -11,48 +10,34 @@ defmodule Soonex.HomePage.Notes do assigns = assign(assigns, :supporting, supporting()) ~H""" - <.block id="notes" labelledby="soonex-notes-heading" eyebrow="Studios" layout={:editorial}> + <.block + id="notes" + labelledby="soonex-notes-heading" + eyebrow="Studios" + tone={:surface} + layout={:editorial} + > <:title> Already on the September list <:lede> Founders, engineers, and designers putting Soonex on a calendar — not a backlog. -
    +

    “{@featured.quote}”

    -
    - <.avatar - id={@featured.id} - src={Soonex.Public.path(@featured.src)} - alt={@featured.name} - class="avatar ui-size-md" - > - <:fallback>{@featured.initials} - -
    - {@featured.name} - {@featured.role} -
    +
    + {@featured.name} + {@featured.role}
    -
      +
      • “{voice.quote}”

        -
        - <.avatar - id={voice.id} - src={Soonex.Public.path(voice.src)} - alt={voice.name} - class="avatar ui-size-sm" - > - <:fallback>{voice.initials} - -
        - {voice.name} - {voice.role} -
        +
        + {voice.name} + {voice.role}
      • @@ -63,9 +48,6 @@ defmodule Soonex.HomePage.Notes do defp featured do %{ - id: "soonex-voice-amira", - src: "/images/people/amira.jpg", - initials: "AN", quote: "We had a date before we had a deck. Soonex put the waitlist and the countdown on the page the same afternoon.", name: "Amira N.", @@ -76,18 +58,12 @@ defmodule Soonex.HomePage.Notes do defp supporting do [ %{ - id: "soonex-voice-jonas", - src: "/images/people/jonas.jpg", - initials: "JK", quote: - "One clone path, controls included, no second design system. We pointed the date and shipped the stills.", + "One clone path, controls included, no second design system. We pointed the date and shipped.", name: "Jonas K.", role: "Staff engineer, Relay" }, %{ - id: "soonex-voice-leah", - src: "/images/people/leah.jpg", - initials: "LM", quote: "Four looks for the client review without rebuilding. They picked duo; we kept shipping.", name: "Leah M.", diff --git a/lib/pages/home/product.ex b/lib/pages/home/product.ex index a508796..ef0be57 100644 --- a/lib/pages/home/product.ex +++ b/lib/pages/home/product.ex @@ -14,11 +14,11 @@ defmodule Soonex.HomePage.Product do ~H""" <.block id="product" labelledby="soonex-product-heading" eyebrow="Product" layout={:open}> <:title> - Everything you need to launch + The desk that ships on a date <:lede> - A waitlist that converts, a public shipping log, a countdown aimed at your date, and four - looks for client review — one product, ready for 1 September. + Four instruments in one page — waitlist, log, countdown, looks. Open a panel. Wire what + you need. Launch when 1 September hits. <.tabs @@ -31,40 +31,39 @@ defmodule Soonex.HomePage.Product do <:content value="overview" class="soonex-tab-panel">

        - Soonex is a launch workspace for studios shipping on a hard date. Put the waitlist on - the page, keep a public log, and review four looks with your client before go-live. + Soonex is a launch desk for studios with a hard date. Put the waitlist on the page, + keep a public log, and switch looks with your client before go-live.

        -
          +
          1. - +
            -

            Waitlist on day one

            +

            Waitlist rack

            - Email, role, interests, and team size — with a launch toast on submit. + Email, role, interests, team size — toast on submit.

          2. - +
            -

            Public shipping log

            +

            Shipping ledger

            - Markdown entries with covers and tags. The index paginates in the browser. + Markdown entries with tags. The index paginates in the browser.

          3. - +
            -

            Countdown that stays honest

            +

            Honest countdown

            - Aimed at 1 September until you change the - date. Digits stay on one row. + Aimed at 1 September until you change it.

          4. -
        -
        + +
        <.navigate to="#waitlist" class="button ui-brand ui-solid ui-size-md"> Join waitlist @@ -75,12 +74,12 @@ defmodule Soonex.HomePage.Product do <:trigger value="waitlist">Waitlist <:content value="waitlist" class="soonex-tab-panel">
        -
        +
        -

        Capture who is coming

        +

        Fields you keep

        - The same fields live on the hero still and at the end of the page. Collect email, - role, interests, and team size — then show a clear success path. + Same rack on the hero slab and at the foot of the page. Collect who is coming — + then show a clear success path.

        • Email with validation
        • @@ -90,10 +89,10 @@ defmodule Soonex.HomePage.Product do
        • Launch notes switch
        -
        - Preview +
        + Rack

        Join the list

        -
        +
        you@studio.dev
        Founder
        Add an interest…
        @@ -113,10 +112,10 @@ defmodule Soonex.HomePage.Product do <:content value="log" class="soonex-tab-panel">

        - Keep a public trail of what shipped. Posts live as Markdown with covers and tags — - rebuild and the index updates. + A public ledger of what shipped. Posts are Markdown with tags — rebuild and the index + updates.

        -
        +
        02 August 2026

        Accessibility on this device @@ -145,26 +144,23 @@ defmodule Soonex.HomePage.Product do

        Soonex

        - Four looks for the same launch. Switch rooms in Template Options and review with your + Four instruments for the same launch. Switch in Template Options — review with your client without a second build.

        -
          -
        • -

          {room.name}

          -

          {room.line}

          +
            +
          • + {room.name} + {room.line}
          - <.navigate to="#looks" class="link ui-brand mt-6 inline-flex"> + <.navigate to="#looks" class="link ui-brand mt-8 inline-flex"> See all four looks <.heroicon name="hero-arrow-down" />
        -
        +
        Get Soonex

        @@ -188,13 +184,13 @@ defmodule Soonex.HomePage.Product do

        -
        +
        Plans

        - Pick a lane. Ship the same core. + Same core. Three lanes.

        - Every plan is the same Soonex launch. The columns describe how far you take it before 1 September. + Every plan is Soonex. The columns describe how far you take it before 1 September.

        @@ -221,10 +217,10 @@ defmodule Soonex.HomePage.Product do defp looks do [ - %{name: "neo", line: "Product studio — geometric display, open corners, cobalt."}, - %{name: "uno", line: "Calm ops — tight tracking, small radius, teal."}, - %{name: "duo", line: "Editorial atelier — serif display, soft corners, rust."}, - %{name: "leo", line: "Signal poster — strong display, square corners, orange."} + %{name: "neo", line: "Cold paper · electric cobalt · geometric display"}, + %{name: "uno", line: "Teal field · razor corners · compressed type"}, + %{name: "duo", line: "Warm surface · rust brand · editorial serif"}, + %{name: "leo", line: "Poster ink · signal orange · uppercase"} ] end @@ -244,19 +240,19 @@ defmodule Soonex.HomePage.Product do }, %{ name: "Studio", - headline: "Four looks, real stills, client review", - body: "Treat Template Options as the review. Photography and the shipping log stay.", + headline: "Four looks, client review", + body: "Treat Template Options as the review. The shipping log stays.", cta: "Join waitlist", cta_class: "button ui-brand ui-solid ui-size-sm w-fit", items: [ "neo, uno, duo, and leo on this device", - "Covers, tags, and client pagination", + "Tags and client pagination", "Header timer, toast, and clone strip" ] }, %{ name: "Agency", - headline: "Accessible handoff your team can repeat", + headline: "Accessible handoff you can repeat", body: "Ship with accessibility on and a clone path the whole studio can run.", cta: "Join waitlist", cta_class: "button ui-ghost ui-size-sm w-fit", diff --git a/lib/pages/home/themes.ex b/lib/pages/home/themes.ex index d500067..068ec57 100644 --- a/lib/pages/home/themes.ex +++ b/lib/pages/home/themes.ex @@ -9,28 +9,30 @@ defmodule Soonex.HomePage.Themes do ~H""" <.block id="looks" labelledby="soonex-looks-heading" eyebrow="Looks" layout={:open}> <:title> - Four looks. One launch. + Four instruments. One launch. <:lede> - Review neo, uno, duo, and leo with your client on this device. Paper stays calm — type, - corners, and brand change the room. + neo, uno, duo, leo — same page, different type, corners, and brand. Switch live in Template + Options. Surface stays the medium. -

        Soonex

        -

        - This display follows the look you pick in Template Options. Switch rooms live — no second - build for the review. -

        -
          +
          +

          Soonex

          +

          + This specimen follows the look you pick. No second build for the review. +

          +
          + +
          1. {room.id}

            {room.line}

            -

            {room.use}

            +

            {room.use}

          @@ -41,22 +43,22 @@ defmodule Soonex.HomePage.Themes do [ %{ id: "neo", - line: "Product studio — geometric display, open corners, cobalt.", + line: "Cold paper · electric cobalt · geometric display.", use: "Default for product launches and investor-facing pages." }, %{ id: "uno", - line: "Calm ops — tight tracking, small radius, teal.", + line: "Teal field · razor corners · compressed type.", use: "Ops and tooling launches that need quiet confidence." }, %{ id: "duo", - line: "Editorial atelier — serif display, soft corners, rust.", - use: "Studios and cultural brands that want editorial warmth." + line: "Warm surface · rust brand · editorial serif.", + use: "Studios and cultural brands that want editorial weight." }, %{ id: "leo", - line: "Signal poster — strong display, square corners, orange.", + line: "Poster ink · signal orange · uppercase.", use: "Campaign drops and poster-led announcements." } ] diff --git a/lib/pages/home/waitlist.ex b/lib/pages/home/waitlist.ex index 982707c..3c82ad6 100644 --- a/lib/pages/home/waitlist.ex +++ b/lib/pages/home/waitlist.ex @@ -6,8 +6,6 @@ defmodule Soonex.HomePage.Waitlist do import Soonex.Layouts.Section, only: [block: 1] - alias Soonex.Layouts.Shell - def waitlist(assigns) do assigns = assign(assigns, :role_items, role_items()) @@ -16,8 +14,8 @@ defmodule Soonex.HomePage.Waitlist do id="waitlist" labelledby="soonex-waitlist-heading" eyebrow="Waitlist" - tone={:surface} - align={:center} + tone={:ui} + align={:start} layout={:open} > <:title> @@ -26,13 +24,13 @@ defmodule Soonex.HomePage.Waitlist do <:lede> Role, interests, and team size. We write when the countdown ends. -
          -
          +
          + <.native_input type="email" name="waitlist[email]" @@ -111,11 +109,11 @@ defmodule Soonex.HomePage.Waitlist do -
          - -

          - Demo page — toast only until you wire a provider. -

          + +

          + Demo page — toast only until you wire a provider. +

          +
          """ end From 06d830ad31295fbbce98e6f97632aff1987ace7a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 31 Aug 2026 03:35:10 +0000 Subject: [PATCH 53/65] Restage Soonex as a typographic launch edition. Kill the brand-fill hero; rebuild on root paper with a bottom waitlist strip; collapse home to five beats; fold studios voice into the ledger; harden desk chrome so brand stays signal, never field. Co-authored-by: Karim Semmoud --- assets/css/chrome.css | 299 ++++++++++++++++++++----------------- config/config.exs | 8 +- lib/layouts/root/footer.ex | 2 +- lib/layouts/root_layout.ex | 4 +- lib/pages/home/faq.ex | 10 +- lib/pages/home/hero.ex | 138 +++++++++-------- lib/pages/home/log.ex | 68 +++++---- lib/pages/home/notes.ex | 74 --------- lib/pages/home/product.ex | 68 ++++----- lib/pages/home/themes.ex | 23 ++- lib/pages/home/waitlist.ex | 4 +- lib/pages/home_page.ex | 2 - 12 files changed, 326 insertions(+), 374 deletions(-) delete mode 100644 lib/pages/home/notes.ex diff --git a/assets/css/chrome.css b/assets/css/chrome.css index 22df7e4..dd12497 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -1,4 +1,4 @@ -/* Instrument-desk chrome. Tokens only — no Corex recipe forks. */ +/* Launch Edition chrome. Tokens only — brand as signal, never field. */ html { scroll-behavior: smooth; @@ -23,6 +23,16 @@ html[data-motion="reduce"] { } } + @keyframes soonex-rule-draw { + from { + transform: scaleX(0); + } + + to { + transform: scaleX(1); + } + } + .soonex-reveal { animation-name: soonex-reveal; animation-duration: var(--duration-slow); @@ -32,7 +42,16 @@ html[data-motion="reduce"] { animation-range: entry 0% entry 40%; } - html[data-motion="reduce"] .soonex-reveal { + .soonex-edition-meta .soonex-meta-rule { + transform-origin: left center; + animation-name: soonex-rule-draw; + animation-duration: var(--duration-slow); + animation-timing-function: var(--soonex-ease); + animation-fill-mode: both; + } + + html[data-motion="reduce"] .soonex-reveal, + html[data-motion="reduce"] .soonex-edition-meta .soonex-meta-rule { animation: none; transform: none; } @@ -54,7 +73,6 @@ html[data-motion="reduce"] { } } -/* Header timer — compact type, keep 2em digit window. */ [data-site-header] .timer.ui-size-sm { --timer-digit: var(--text-sm); --timer-digit-leading: 2; @@ -99,121 +117,103 @@ html[data-motion="reduce"] { border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); } -/* ——— Hero brand slab ——— */ - -.soonex-hero-slab { - background-color: var(--color-brand); - color: var(--color-brand-contrast); -} - -.soonex-slab-kicker { - font-family: var(--font-sans); - font-size: var(--text-sm); - font-weight: var(--font-weight-semibold); - letter-spacing: var(--tracking-widest); +.soonex-mono-meta { + font-family: var(--font-mono); + font-size: var(--text-xs); + font-weight: var(--font-weight-medium); + letter-spacing: var(--tracking-wide); text-transform: uppercase; - color: color-mix(in oklab, var(--color-brand-contrast) 78%, transparent); -} - -.soonex-slab-display { - font-family: var(--font-display); - font-size: var(--text-6xl); - font-weight: var(--font-weight-bold); - letter-spacing: var(--tracking-tighter); - line-height: 0.92; - color: var(--color-brand-contrast); -} - -@media (width >= 40rem) { - .soonex-slab-display { - font-size: var(--text-7xl); - } + color: var(--color-ink-muted); } -@media (width >= 64rem) { - .soonex-slab-display { - font-size: var(--text-8xl); - } -} +/* ——— Edition hero (paper, not brand fill) ——— */ -.soonex-slab-lede { - font-size: var(--text-lg); - line-height: var(--leading-relaxed); - color: color-mix(in oklab, var(--color-brand-contrast) 82%, transparent); +.soonex-edition-hero { + background-color: var(--color-root); + color: var(--color-ink); } -.soonex-slab-cta { - display: inline-flex; +.soonex-edition-meta { + display: flex; align-items: center; - justify-content: center; - min-height: var(--spacing-size); - padding-inline: calc(var(--spacing-space) * 1.25); - border-radius: var(--soonex-ctl-radius, var(--radius-md)); - background-color: var(--color-root); - color: var(--color-ink); - font-size: var(--text-base); - font-weight: var(--font-weight-semibold); - text-decoration: none; + gap: var(--spacing-space); + max-width: 28rem; } -.soonex-slab-cta:hover { - background-color: var(--color-surface); +.soonex-meta-rule { + display: block; + flex: 1 1 auto; + height: 1px; + background-color: var(--color-brand); + min-width: 2rem; } -.soonex-slab-link { - display: inline-flex; - align-items: center; - gap: 0.35rem; - color: var(--color-brand-contrast); - font-weight: var(--font-weight-semibold); - text-decoration: none; - border-block-end: 1px solid color-mix(in oklab, var(--color-brand-contrast) 45%, transparent); +.soonex-edition-display { + font-family: var(--font-display); + font-size: clamp(3.5rem, 14vw, 9.5rem); + font-weight: var(--font-weight-bold); + letter-spacing: var(--tracking-tighter); + line-height: 0.88; + color: var(--color-ink); + margin: 0; } -.soonex-slab-link:hover { - border-block-end-color: var(--color-brand-contrast); +.soonex-edition-lede { + font-size: var(--text-lg); + line-height: var(--leading-relaxed); + color: var(--color-ink-muted); } -[data-theme="leo"] .soonex-slab-display, -[data-theme="leo"] .soonex-slab-kicker { +[data-theme="leo"] .soonex-edition-display { text-transform: uppercase; } -[data-theme="uno"] .soonex-slab-display { - letter-spacing: -0.05em; +[data-theme="uno"] .soonex-edition-display { + letter-spacing: -0.06em; } -[data-theme="duo"] .soonex-slab-lede { +[data-theme="duo"] .soonex-edition-lede { font-family: var(--font-serif); font-style: italic; } -/* ——— Waitlist instrument rack ——— */ +/* ——— Waitlist strip (bottom instrument) ——— */ -.soonex-waitlist-rack { - border: 1px solid var(--color-border); - background-color: var(--color-surface); - color: var(--color-ink); - border-radius: var(--soonex-ctl-radius, var(--radius-xl)); - padding: calc(var(--spacing-space) * 1.25); +.soonex-waitlist-strip { + padding-block: calc(var(--spacing-space) * 1.15); } -@media (width >= 40rem) { - .soonex-waitlist-rack { - padding: calc(var(--spacing-space) * 1.5); - } +.soonex-strip-form { + display: flex; + flex-direction: column; + gap: var(--spacing-space); } -.soonex-waitlist-rack--canonical { - border-width: 2px; +@media (width >= 64rem) { + .soonex-strip-form { + flex-direction: row; + align-items: end; + gap: calc(var(--spacing-space) * 1.5); + } + + .soonex-strip-label { + flex: 0 0 12rem; + } + + .soonex-strip-roles { + flex: 0 0 auto; + } } .soonex-waitlist-join { + display: flex; + flex-direction: column; gap: var(--spacing-space-sm); } @media (width >= 40rem) { .soonex-waitlist-join { + flex-direction: row; align-items: stretch; gap: 0; } @@ -230,37 +230,48 @@ html[data-motion="reduce"] { } } -/* ——— Product tabs as segmented instrument ——— */ +.soonex-closing-rack { + border-block-start: 1px solid var(--color-border); + padding-block-start: calc(var(--spacing-space) * 1.35); +} + +/* ——— Product desk tabs ——— */ .soonex-tab-panel { - min-height: 20rem; + min-height: 18rem; } .tabs.soonex-product-tabs [data-scope="tabs"][data-part="list"] { gap: 0; - border: 1px solid var(--color-border); - border-radius: var(--soonex-ctl-radius, var(--radius-md)); - background-color: var(--color-ui); - padding: 0.2rem; - overflow: hidden; + border-block-end: 1px solid var(--color-border); + background: transparent; + padding: 0; + border-radius: 0; + width: 100%; } .tabs.soonex-product-tabs [data-scope="tabs"][data-part="trigger"] { font-weight: var(--font-weight-semibold); - border-radius: var(--soonex-ctl-radius, var(--radius-sm)); + border-radius: 0; + border-block-end: 2px solid transparent; + margin-block-end: -1px; +} + +.tabs.soonex-product-tabs [data-scope="tabs"][data-part="trigger"][data-selected] { + border-block-end-color: var(--color-brand); } .tabs.soonex-product-tabs [data-scope="tabs"][data-part="content"] { - margin-block-start: var(--spacing-space); - padding: calc(var(--spacing-space) * 1.35); - background-color: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--soonex-ctl-radius, var(--radius-xl)); - min-height: 20rem; + margin-block-start: calc(var(--spacing-space) * 1.25); + padding: 0; + background: transparent; + border: 0; + border-radius: 0; + min-height: 16rem; } .soonex-tab-workspace { - max-width: 52rem; + max-width: 48rem; } .soonex-tab-workspace .soonex-room-specimen { @@ -273,20 +284,19 @@ html[data-motion="reduce"] { list-style: none; display: flex; flex-direction: column; - gap: calc(var(--spacing-space) * 1.1); } .soonex-instrument-list > li { display: grid; - grid-template-columns: auto 1fr; + grid-template-columns: 3rem 1fr; gap: var(--spacing-space); align-items: start; - padding-block: calc(var(--spacing-space) * 0.65); + padding-block: calc(var(--spacing-space) * 0.85); border-block-end: 1px solid var(--color-border); } -.soonex-instrument-list > li:last-child { - border-block-end: 0; +.soonex-instrument-list > li:first-child { + border-block-start: 1px solid var(--color-border); } .soonex-instrument-index { @@ -360,24 +370,9 @@ html[data-motion="reduce"] { background-color: var(--color-brand); } -.soonex-rack-specimen, .soonex-ledger-row { - border: 1px solid var(--color-border); - background-color: var(--color-ui); - border-radius: var(--soonex-ctl-radius, var(--radius-xl)); - padding: calc(var(--spacing-space) * 1.15); -} - -.soonex-field-ghost { - display: flex; - align-items: center; - min-height: var(--spacing-size); - padding-inline: var(--spacing-space); - border: 1px solid var(--color-border); - border-radius: var(--soonex-ctl-radius, var(--radius-md)); - background-color: var(--color-surface); - color: var(--color-ink-muted); - font-size: var(--text-sm); + border-block: 1px solid var(--color-border); + padding-block: calc(var(--spacing-space) * 1.15); } .soonex-clone-strip { @@ -412,50 +407,60 @@ html[data-motion="reduce"] { border-block-end: 0; } -/* ——— Looks instrument grid ——— */ +/* ——— Looks specimen chart ——— */ .soonex-looks-stage { border-block-start: 1px solid var(--color-border); padding-block-start: calc(var(--spacing-space) * 1.25); } -.soonex-instrument-grid { +.soonex-specimen-chart { margin: 0; padding: 0; list-style: none; display: grid; grid-template-columns: 1fr; - gap: var(--spacing-space); + border: 1px solid var(--color-border); } @media (width >= 40rem) { - .soonex-instrument-grid { + .soonex-specimen-chart { grid-template-columns: 1fr 1fr; } } -.soonex-instrument-slab { - background-color: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--soonex-ctl-radius, var(--radius-xl)); +.soonex-specimen-cell { + background-color: var(--color-root); padding: calc(var(--spacing-space) * 1.25); + border-block-end: 1px solid var(--color-border); } -.soonex-instrument-slab .soonex-room-name { +@media (width >= 40rem) { + .soonex-specimen-cell:nth-child(odd) { + border-inline-end: 1px solid var(--color-border); + } + + .soonex-specimen-cell:nth-last-child(-n + 2) { + border-block-end: 0; + } +} + +@media (width < 40rem) { + .soonex-specimen-cell:last-child { + border-block-end: 0; + } +} + +.soonex-specimen-cell .soonex-room-name { color: var(--color-ink); } -/* ——— Notes + ledger ——— */ +/* ——— Ledger + voice ——— */ -.soonex-note-rail { - margin: 0; - padding: 0; - list-style: none; - display: flex; - flex-direction: column; - gap: calc(var(--spacing-space) * 1.5); +.soonex-voice-rule { border-block-start: 1px solid var(--color-border); padding-block-start: calc(var(--spacing-space) * 1.25); + max-width: 40rem; } .soonex-ledger { @@ -464,6 +469,7 @@ html[data-motion="reduce"] { list-style: none; display: flex; flex-direction: column; + border-block-start: 1px solid var(--color-border); } .soonex-ledger-entry { @@ -471,11 +477,20 @@ html[data-motion="reduce"] { border-block-end: 1px solid var(--color-border); } -.soonex-ledger-entry:first-child { - border-block-start: 1px solid var(--color-border); +.soonex-ledger-grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--spacing-space); +} + +@media (width >= 40rem) { + .soonex-ledger-grid { + grid-template-columns: 9rem 1fr; + gap: calc(var(--spacing-space) * 1.5); + } } -/* ——— FAQ accordion instrument ——— */ +/* ——— FAQ ——— */ .accordion.soonex-faq-instrument [data-scope="accordion"][data-part="item-trigger"] { font-weight: var(--font-weight-semibold); @@ -486,7 +501,7 @@ html[data-motion="reduce"] { border-color: var(--color-border); } -/* ——— Control family extras ——— */ +/* ——— Control family ——— */ .select [data-scope="select"][data-part="trigger"], .tags-input [data-scope="tags-input"][data-part="control"], @@ -497,3 +512,9 @@ html[data-motion="reduce"] { .clipboard [data-scope="clipboard"][data-part="control"] { border-radius: var(--soonex-ctl-radius, var(--radius-md)); } + +.radio-group.soonex-strip-roles [data-scope="radio-group"][data-part="root"] { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-space); +} diff --git a/config/config.exs b/config/config.exs index c5883c9..6e7af76 100644 --- a/config/config.exs +++ b/config/config.exs @@ -76,7 +76,7 @@ config :tableau, Tableau.RSSExtension, enabled: true, title: "Soonex", description: - "Soonex ships 1 September — instrument desk with waitlist, shipping log, countdown, and four looks." + "Soonex ships 1 September — typographic launch desk with waitlist, shipping log, countdown, and four looks." config :elixir, :time_zone_database, Tz.TimeZoneDatabase @@ -144,9 +144,9 @@ config :corex_design, colors: %{ light: with_on.(%{ - root: l.(0.985, :neutral), - surface: l.(0.96, :neutral), - ui: fill.(0.92, :neutral), + root: l.(0.99, :neutral), + surface: l.(0.955, :neutral), + ui: fill.(0.9, :neutral), accent: fill.(0.16, :accent), brand: fill.(0.48, :brand), ink: contrast.(:accent, :root, 13), diff --git a/lib/layouts/root/footer.ex b/lib/layouts/root/footer.ex index becccf5..0a44e09 100644 --- a/lib/layouts/root/footer.ex +++ b/lib/layouts/root/footer.ex @@ -26,7 +26,7 @@ defmodule Soonex.Layouts.Root.Footer do
          <.lockup />

          - Instrument desk for launch day — waitlist, ledger, countdown, four looks. Ship 1 September. + Launch edition — waitlist, ledger, countdown, four looks. Ship 1 September.

          diff --git a/lib/layouts/root_layout.ex b/lib/layouts/root_layout.ex index 69e7794..e62c6e9 100644 --- a/lib/layouts/root_layout.ex +++ b/lib/layouts/root_layout.ex @@ -221,7 +221,7 @@ defmodule Soonex.RootLayout do defp meta_description(page, site_name) do cond do page[:page_kind] == :home -> - "Soonex ships 1 September — instrument desk with waitlist, shipping log, countdown, and four looks." + "Soonex ships 1 September — typographic launch desk with waitlist, shipping log, countdown, and four looks." page[:page_kind] == :blog_index -> "Shipping notes from #{site_name}: waitlist, looks, countdown, and the road to 1 September." @@ -239,7 +239,7 @@ defmodule Soonex.RootLayout do page[:description] true -> - "Soonex ships 1 September — instrument desk with waitlist, shipping log, countdown, and four looks." + "Soonex ships 1 September — typographic launch desk with waitlist, shipping log, countdown, and four looks." end end diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index 4ac4109..953ecd9 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -8,12 +8,18 @@ defmodule Soonex.HomePage.Faq do def faq(assigns) do ~H""" - <.block id="questions" labelledby="soonex-faq-heading" eyebrow="Questions" layout={:sticky}> + <.block + id="questions" + labelledby="soonex-faq-heading" + eyebrow="Questions" + tone={:surface} + layout={:sticky} + > <:title> Before you join <:lede> - Straight answers about the launch, the waitlist, and what you get on day one. + Launch, waitlist, looks — and one honest answer about the fork. <:actions> <.navigate to="#waitlist" class="link ui-brand"> diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index eb58767..1df44d7 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -9,86 +9,82 @@ defmodule Soonex.HomePage.Hero do def hero(assigns) do ~H"""
          -
          -
          -
          -

          Launch · 1 September

          -

          - Soonex -

          -

          - Waitlist. Shipping log. Countdown. Four looks. One instrument desk for studios that - ship on a date. -

          -
          - <.navigate to="#waitlist" class="soonex-slab-cta"> - Join waitlist - - <.navigate to={Soonex.Public.path("/blog")} class="soonex-slab-link"> - Read the log <.heroicon name="hero-arrow-up-right" /> - -
          -
          +
          +
          + Edition + + 1 September 2026 +
          -
          -
          -
          - Waitlist -

          Reserve a seat

          -

          - Email + role. We write before the countdown ends. -

          -
          +

          + Soonex +

          -
          -
          - <.native_input - type="email" - name="waitlist[email]" - id="soonex-hero-waitlist-email" - required - autocomplete="email" - placeholder="you@studio.dev" - class="native-input ui-size-md ui-width-full min-w-0 flex-1" - > - <:label class="sr-only">Email - +
          +

          + Waitlist, shipping log, countdown, four looks — a launch desk for studios that ship on a + date. +

          + <.navigate + to={Soonex.Public.path("/blog")} + class="link ui-brand ui-size-sm w-fit lg:col-span-5 lg:justify-self-end" + > + Read the log <.heroicon name="hero-arrow-up-right" /> + +
          +
          - -
          +
          +
          + +
          + Waitlist +

          + 240 studios · write before launch +

          +
          - <.radio_group - id="soonex-hero-role" - name="waitlist[role]" - class="radio-group ui-brand ui-width-full" - value="founder" - items={[ - %{value: "founder", label: "Founder"}, - %{value: "engineer", label: "Engineer"}, - %{value: "designer", label: "Designer"} - ]} - > - <:label>I am a - - +
          + <.native_input + type="email" + name="waitlist[email]" + id="soonex-hero-waitlist-email" + required + autocomplete="email" + placeholder="you@studio.dev" + class="native-input ui-size-md ui-width-full min-w-0 flex-1" + > + <:label class="sr-only">Email + -

          - 240 studios - already on the list for 1 September. -

          +
          -
          + + <.radio_group + id="soonex-hero-role" + name="waitlist[role]" + class="radio-group ui-brand soonex-strip-roles" + value="founder" + items={[ + %{value: "founder", label: "Founder"}, + %{value: "engineer", label: "Engineer"}, + %{value: "designer", label: "Designer"} + ]} + > + <:label>I am a + +
          diff --git a/lib/pages/home/log.ex b/lib/pages/home/log.ex index b95567d..ee2d932 100644 --- a/lib/pages/home/log.ex +++ b/lib/pages/home/log.ex @@ -18,51 +18,67 @@ defmodule Soonex.HomePage.Log do |> Enum.take(3) assigns = assign(assigns, :entries, posts) + assigns = assign(assigns, :voice, voice()) ~H""" - <.block - id="log" - labelledby="soonex-log-heading" - eyebrow="Shipping log" - tone={:surface} - layout={:open} - > + <.block id="log" labelledby="soonex-log-heading" eyebrow="Shipping log" layout={:open}> <:title> Ledger on the road to September <:lede> - Public notes on the waitlist, the four looks, and the countdown. The full index lives on the - log. + Public notes on the waitlist, the four looks, and the countdown. <:actions> <.navigate to={Soonex.Public.path("/blog")} class="link ui-brand"> All entries <.heroicon name="hero-arrow-up-right" /> -

          No posts yet.

          -
            + +
            +

            “{@voice.quote}”

            +
            + {@voice.name} + {@voice.role} +
            +
            + +

            No posts yet.

            +
            1. - {date_label(post)} -

              - <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> - {post[:title] || "Untitled"} - -

              -

              - {post[:description]} -

              - <.navigate - to={Soonex.Public.path(post.permalink)} - class="button ui-ghost ui-brand ui-size-sm mt-5 w-fit" - > - Read more <.heroicon name="hero-arrow-up-right" /> - +
              + {date_label(post)} +
              +

              + <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> + {post[:title] || "Untitled"} + +

              +

              + {post[:description]} +

              + <.navigate + to={Soonex.Public.path(post.permalink)} + class="button ui-ghost ui-brand ui-size-sm mt-5 w-fit" + > + Read more <.heroicon name="hero-arrow-up-right" /> + +
              +
            """ end + defp voice do + %{ + quote: + "We had a date before we had a deck. Soonex put the waitlist and the countdown on the page the same afternoon.", + name: "Amira N.", + role: "Founder, Northline Studio" + } + end + defp date_label(%{date: %DateTime{} = date}), do: Calendar.strftime(date, "%d %B %Y") defp date_label(_), do: nil end diff --git a/lib/pages/home/notes.ex b/lib/pages/home/notes.ex deleted file mode 100644 index 248c52e..0000000 --- a/lib/pages/home/notes.ex +++ /dev/null @@ -1,74 +0,0 @@ -defmodule Soonex.HomePage.Notes do - @moduledoc false - - use Phoenix.Component - - import Soonex.Layouts.Section, only: [block: 1] - - def notes(assigns) do - assigns = assign(assigns, :featured, featured()) - assigns = assign(assigns, :supporting, supporting()) - - ~H""" - <.block - id="notes" - labelledby="soonex-notes-heading" - eyebrow="Studios" - tone={:surface} - layout={:editorial} - > - <:title> - Already on the September list - - <:lede> - Founders, engineers, and designers putting Soonex on a calendar — not a backlog. - -
            -

            “{@featured.quote}”

            -
            - {@featured.name} - {@featured.role} -
            -
            - -
              -
            • -
              -

              “{voice.quote}”

              -
              - {voice.name} - {voice.role} -
              -
              -
            • -
            - - """ - end - - defp featured do - %{ - quote: - "We had a date before we had a deck. Soonex put the waitlist and the countdown on the page the same afternoon.", - name: "Amira N.", - role: "Founder, Northline Studio" - } - end - - defp supporting do - [ - %{ - quote: - "One clone path, controls included, no second design system. We pointed the date and shipped.", - name: "Jonas K.", - role: "Staff engineer, Relay" - }, - %{ - quote: - "Four looks for the client review without rebuilding. They picked duo; we kept shipping.", - name: "Leah M.", - role: "Product designer, Fieldwork" - } - ] - end -end diff --git a/lib/pages/home/product.ex b/lib/pages/home/product.ex index ef0be57..a42ee20 100644 --- a/lib/pages/home/product.ex +++ b/lib/pages/home/product.ex @@ -17,8 +17,8 @@ defmodule Soonex.HomePage.Product do The desk that ships on a date <:lede> - Four instruments in one page — waitlist, log, countdown, looks. Open a panel. Wire what - you need. Launch when 1 September hits. + Four instruments. Open a panel. Launch when 1 September + hits. <.tabs @@ -31,14 +31,14 @@ defmodule Soonex.HomePage.Product do <:content value="overview" class="soonex-tab-panel">

            - Soonex is a launch desk for studios with a hard date. Put the waitlist on the page, - keep a public log, and switch looks with your client before go-live. + Soonex is a launch desk for studios with a hard date. Waitlist on the page, public log, + countdown aimed at launch, four looks for client review.

            1. -

              Waitlist rack

              +

              Waitlist strip

              Email, role, interests, team size — toast on submit.

              @@ -49,7 +49,7 @@ defmodule Soonex.HomePage.Product do

              Shipping ledger

              - Markdown entries with tags. The index paginates in the browser. + Markdown entries with tags. Index paginates in the browser.

            2. @@ -74,37 +74,21 @@ defmodule Soonex.HomePage.Product do <:trigger value="waitlist">Waitlist <:content value="waitlist" class="soonex-tab-panel">
              -
              -
              -

              Fields you keep

              -

              - Same rack on the hero slab and at the foot of the page. Collect who is coming — - then show a clear success path. -

              -
                -
              • Email with validation
              • -
              • Role select or radios
              • -
              • Interests as tags
              • -
              • Team size stepper
              • -
              • Launch notes switch
              • -
              -
              -
              - Rack -

              Join the list

              -
              -
              you@studio.dev
              -
              Founder
              -
              Add an interest…
              -
              - Join waitlist -
              -
              -

              - Wire your provider after launch. The toast already ships. -

              -
              -
              +

              Fields on the strip

              +

              + Same instruments on the edition strip and the closing rack. Collect who is coming — + then show a clear success path. +

              +
                +
              • Email with validation
              • +
              • Role select or radios
              • +
              • Interests as tags
              • +
              • Team size stepper
              • +
              • Launch notes switch
              • +
              +

              + Wire your provider after launch. The toast already ships. +

              @@ -116,13 +100,12 @@ defmodule Soonex.HomePage.Product do updates.

              - 02 August 2026 + 02 August 2026

              Accessibility on this device

              - Zoom, contrast, motion, cursor, focus, and link underline — stored in local storage - so preferences follow the visitor. + Zoom, contrast, motion, cursor, focus, and underline — stored on this device.

              <.navigate to={Soonex.Public.path("/blog/accessibility-on-device/")} @@ -144,8 +127,7 @@ defmodule Soonex.HomePage.Product do

              Soonex

              - Four instruments for the same launch. Switch in Template Options — review with your - client without a second build. + Four editions for the same launch. Switch in Template Options — no second build.

              • @@ -190,7 +172,7 @@ defmodule Soonex.HomePage.Product do Same core. Three lanes.

        - Every plan is Soonex. The columns describe how far you take it before 1 September. + Every plan is Soonex. How far you take it before 1 September.

        diff --git a/lib/pages/home/themes.ex b/lib/pages/home/themes.ex index 068ec57..fb57820 100644 --- a/lib/pages/home/themes.ex +++ b/lib/pages/home/themes.ex @@ -7,30 +7,37 @@ defmodule Soonex.HomePage.Themes do def themes(assigns) do ~H""" - <.block id="looks" labelledby="soonex-looks-heading" eyebrow="Looks" layout={:open}> + <.block + id="looks" + labelledby="soonex-looks-heading" + eyebrow="Looks" + tone={:surface} + layout={:open} + > <:title> - Four instruments. One launch. + Four editions. One launch. <:lede> - neo, uno, duo, leo — same page, different type, corners, and brand. Switch live in Template - Options. Surface stays the medium. + neo, uno, duo, leo — type, corners, brand. Paper stays the medium. Switch live in Template + Options. -
        +

        Soonex

        This specimen follows the look you pick. No second build for the review.

        -
          +
          1. -

            {room.id}

            + {room.id} +

            {room.id}

            {room.line}

            {room.use}

          2. diff --git a/lib/pages/home/waitlist.ex b/lib/pages/home/waitlist.ex index 3c82ad6..91c27fa 100644 --- a/lib/pages/home/waitlist.ex +++ b/lib/pages/home/waitlist.ex @@ -14,7 +14,7 @@ defmodule Soonex.HomePage.Waitlist do id="waitlist" labelledby="soonex-waitlist-heading" eyebrow="Waitlist" - tone={:ui} + tone={:surface} align={:start} layout={:open} > @@ -24,7 +24,7 @@ defmodule Soonex.HomePage.Waitlist do <:lede> Role, interests, and team size. We write when the countdown ends. -
            +
            <.hero /> <.product /> - <.notes /> <.themes /> <.log posts={@log_posts} /> <.faq /> From 2e7afb58700a8a1eddab9a5c7385a0df08318959 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 31 Aug 2026 06:22:23 +0000 Subject: [PATCH 54/65] Restage Soonex as a Framer-style product launch. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring photography back with a soft root wash, floating waitlist object, marquee, feature bento, sticky product desk, looks tiles, voice carousel, and closing waitlist CTA — Corex controls as the product, brand as accent. Co-authored-by: Karim Semmoud --- _posts/2026-03-12-fork-day.md | 4 +- _posts/2026-04-08-four-rooms.md | 2 + _posts/2026-05-08-clone-theme-ship.md | 2 + _posts/2026-06-01-ci-before-pages.md | 2 + _posts/2026-07-14-header-timer.md | 2 + _posts/2026-08-02-accessibility-on-device.md | 2 + assets/css/chrome.css | 430 +++++++------------ assets/css/hosts.css | 16 + config/config.exs | 12 +- lib/layouts/media.ex | 2 +- lib/layouts/post_layout.ex | 17 + lib/layouts/root/footer.ex | 2 +- lib/layouts/root/nav.ex | 2 +- lib/layouts/root_layout.ex | 4 +- lib/pages/home/bento.ex | 75 ++++ lib/pages/home/faq.ex | 18 +- lib/pages/home/hero.ex | 185 +++++--- lib/pages/home/log.ex | 78 ++-- lib/pages/home/marquee_band.ex | 44 ++ lib/pages/home/notes.ex | 93 ++++ lib/pages/home/product.ex | 160 ++----- lib/pages/home/themes.ex | 49 +-- lib/pages/home/waitlist.ex | 28 +- lib/pages/home_page.ex | 6 + 24 files changed, 680 insertions(+), 555 deletions(-) create mode 100644 lib/pages/home/bento.ex create mode 100644 lib/pages/home/marquee_band.ex create mode 100644 lib/pages/home/notes.ex diff --git a/_posts/2026-03-12-fork-day.md b/_posts/2026-03-12-fork-day.md index 5bba66b..f841179 100644 --- a/_posts/2026-03-12-fork-day.md +++ b/_posts/2026-03-12-fork-day.md @@ -4,6 +4,8 @@ title: Fork day date: 2026-03-12 12:00:00 +0000 permalink: /blog/fork-day/ description: Day one on the September list — waitlist on the page, countdown aimed at launch, lockup ready to rename. +image: /images/photos/hero.jpg +image_alt: Sunlit studio with long work tables and hanging task lamps tags: - Studio - Notes @@ -12,7 +14,7 @@ sitemap: changefreq: monthly --- -Today we put **Soonex** on a calendar. The waitlist sits on the hero slab. The countdown aims at **1 September**. Lockup and copy are yours to replace after you fork. +Today we put **Soonex** on a calendar. The waitlist sits on the hero still. The countdown aims at **1 September**. Lockup and copy are yours to replace after you fork. ## What shipped diff --git a/_posts/2026-04-08-four-rooms.md b/_posts/2026-04-08-four-rooms.md index b278986..9e04944 100644 --- a/_posts/2026-04-08-four-rooms.md +++ b/_posts/2026-04-08-four-rooms.md @@ -4,6 +4,8 @@ title: Four looks for client review date: 2026-04-08 09:00:00 +0000 permalink: /blog/four-rooms/ description: neo, uno, duo, and leo on one launch — type, corners, and brand change the room; paper stays calm. +image: /images/photos/studio.jpg +image_alt: A calm studio interior with a low sofa, plants, and a large window tags: - Design - Studio diff --git a/_posts/2026-05-08-clone-theme-ship.md b/_posts/2026-05-08-clone-theme-ship.md index 20960e3..9f36c6c 100644 --- a/_posts/2026-05-08-clone-theme-ship.md +++ b/_posts/2026-05-08-clone-theme-ship.md @@ -4,6 +4,8 @@ title: Clone, look, and ship date: 2026-05-08 12:00:00 +0000 permalink: /blog/clone-theme-ship/ description: Clone Soonex, pick a look for review, keep the waitlist and log, and aim the countdown at your date. +image: /images/covers/overlay.jpg +image_alt: Soft abstract color field in warm paper tones tags: - Launch - Template diff --git a/_posts/2026-06-01-ci-before-pages.md b/_posts/2026-06-01-ci-before-pages.md index 62c874d..cd3d211 100644 --- a/_posts/2026-06-01-ci-before-pages.md +++ b/_posts/2026-06-01-ci-before-pages.md @@ -4,6 +4,8 @@ title: CI before Pages date: 2026-06-01 12:00:00 +0000 permalink: /blog/ci-before-pages/ description: Keep CI green before Pages deploys. Axe on the home document is the gate; the countdown stays aimed at 1 September. +image: /images/covers/shipping.jpg +image_alt: A laptop on a wooden desk with a code editor open tags: - Launch - Notes diff --git a/_posts/2026-07-14-header-timer.md b/_posts/2026-07-14-header-timer.md index ffc5b9b..88b2800 100644 --- a/_posts/2026-07-14-header-timer.md +++ b/_posts/2026-07-14-header-timer.md @@ -4,6 +4,8 @@ title: The header timer and 1 September date: 2026-07-14 15:00:00 +0000 permalink: /blog/header-timer/ description: The countdown lives in the header, stays on one row, and aims at 1 September until you change the date. +image: /images/photos/orbit.jpg +image_alt: Earth from orbit against a dark sky tags: - Launch - Notes diff --git a/_posts/2026-08-02-accessibility-on-device.md b/_posts/2026-08-02-accessibility-on-device.md index 1509fca..435548f 100644 --- a/_posts/2026-08-02-accessibility-on-device.md +++ b/_posts/2026-08-02-accessibility-on-device.md @@ -4,6 +4,8 @@ title: Accessibility on this device date: 2026-08-02 11:00:00 +0000 permalink: /blog/accessibility-on-device/ description: Zoom, contrast, motion, cursor, focus, and link underline — preferences stay on this device in local storage. +image: /images/covers/overlay.jpg +image_alt: Soft abstract color field in warm paper tones tags: - Notes - Launch diff --git a/assets/css/chrome.css b/assets/css/chrome.css index dd12497..da04ba0 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -1,4 +1,4 @@ -/* Launch Edition chrome. Tokens only — brand as signal, never field. */ +/* Framer-style launch chrome. Tokens only — brand as accent, never hero fill. */ html { scroll-behavior: smooth; @@ -23,42 +23,56 @@ html[data-motion="reduce"] { } } - @keyframes soonex-rule-draw { - from { - transform: scaleX(0); - } - - to { - transform: scaleX(1); - } - } - .soonex-reveal { animation-name: soonex-reveal; animation-duration: var(--duration-slow); animation-timing-function: var(--soonex-ease); animation-fill-mode: none; animation-timeline: view(); - animation-range: entry 0% entry 40%; + animation-range: entry 0% entry 35%; } - .soonex-edition-meta .soonex-meta-rule { - transform-origin: left center; - animation-name: soonex-rule-draw; - animation-duration: var(--duration-slow); - animation-timing-function: var(--soonex-ease); - animation-fill-mode: both; - } - - html[data-motion="reduce"] .soonex-reveal, - html[data-motion="reduce"] .soonex-edition-meta .soonex-meta-rule { + html[data-motion="reduce"] .soonex-reveal { animation: none; transform: none; } } +.soonex-card-motion { + transition: + border-color var(--duration-fast) var(--soonex-ease), + box-shadow var(--duration-fast) var(--soonex-ease), + transform var(--duration-fast) var(--soonex-ease); +} + +@media (hover: hover) { + .soonex-card-motion:hover { + border-color: var(--color-brand); + box-shadow: var(--shadow-sm); + } +} + +html[data-motion="reduce"] .soonex-card-motion, +html[data-motion="reduce"] .soonex-card-motion:hover { + transition: none; + transform: none; +} + +/* Floating / frosted nav */ +.soonex-framer-nav { + background-color: color-mix(in oklab, var(--color-root) 82%, transparent); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +[data-mode="dark"] .soonex-framer-nav { + background-color: color-mix(in oklab, var(--color-root) 78%, transparent); +} + [data-site-header] { - transition: border-color var(--duration-normal) var(--soonex-ease); + transition: + border-color var(--duration-normal) var(--soonex-ease), + background-color var(--duration-normal) var(--soonex-ease); } [data-site-header] [data-header-bar] { @@ -117,103 +131,67 @@ html[data-motion="reduce"] { border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); } -.soonex-mono-meta { - font-family: var(--font-mono); - font-size: var(--text-xs); - font-weight: var(--font-weight-medium); - letter-spacing: var(--tracking-wide); - text-transform: uppercase; - color: var(--color-ink-muted); -} - -/* ——— Edition hero (paper, not brand fill) ——— */ - -.soonex-edition-hero { - background-color: var(--color-root); - color: var(--color-ink); +.soonex-photo { + display: block; + width: 100%; + height: 100%; + object-fit: cover; } -.soonex-edition-meta { +.soonex-avatars { display: flex; align-items: center; - gap: var(--spacing-space); - max-width: 28rem; -} - -.soonex-meta-rule { - display: block; - flex: 1 1 auto; - height: 1px; - background-color: var(--color-brand); - min-width: 2rem; -} - -.soonex-edition-display { - font-family: var(--font-display); - font-size: clamp(3.5rem, 14vw, 9.5rem); - font-weight: var(--font-weight-bold); - letter-spacing: var(--tracking-tighter); - line-height: 0.88; - color: var(--color-ink); - margin: 0; -} - -.soonex-edition-lede { - font-size: var(--text-lg); - line-height: var(--leading-relaxed); - color: var(--color-ink-muted); -} - -[data-theme="leo"] .soonex-edition-display { - text-transform: uppercase; } -[data-theme="uno"] .soonex-edition-display { - letter-spacing: -0.06em; +.soonex-avatars .avatar + .avatar { + margin-inline-start: calc(var(--spacing-space-sm) * -1); } -[data-theme="duo"] .soonex-edition-lede { - font-family: var(--font-serif); - font-style: italic; +.soonex-avatars .avatar [data-scope="avatar"][data-part="root"] { + border: 2px solid var(--color-root); } -/* ——— Waitlist strip (bottom instrument) ——— */ - -.soonex-waitlist-strip { - padding-block: calc(var(--spacing-space) * 1.15); -} - -.soonex-strip-form { - display: flex; - flex-direction: column; - gap: var(--spacing-space); +/* Hero media + soft root wash (not brand fill) */ +.soonex-hero-wash { + background: linear-gradient( + 180deg, + color-mix(in oklab, var(--color-root) 12%, transparent) 0%, + color-mix(in oklab, var(--color-root) 55%, transparent) 42%, + var(--color-root) 100% + ); } @media (width >= 64rem) { - .soonex-strip-form { - flex-direction: row; - align-items: end; - gap: calc(var(--spacing-space) * 1.5); + .soonex-hero-wash { + background: linear-gradient( + 105deg, + var(--color-root) 0%, + color-mix(in oklab, var(--color-root) 78%, transparent) 38%, + color-mix(in oklab, var(--color-root) 28%, transparent) 100% + ); } +} - .soonex-strip-label { - flex: 0 0 12rem; - } +.soonex-waitlist-object { + border: 1px solid var(--color-border); + background-color: var(--color-root); + border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); + padding: calc(var(--spacing-space) * 1.2); + box-shadow: var(--shadow-sm); +} - .soonex-strip-roles { - flex: 0 0 auto; +@media (width >= 40rem) { + .soonex-waitlist-object { + padding: calc(var(--spacing-space) * 1.45); } } .soonex-waitlist-join { - display: flex; - flex-direction: column; gap: var(--spacing-space-sm); } @media (width >= 40rem) { .soonex-waitlist-join { - flex-direction: row; align-items: stretch; gap: 0; } @@ -230,116 +208,100 @@ html[data-motion="reduce"] { } } -.soonex-closing-rack { - border-block-start: 1px solid var(--color-border); - padding-block-start: calc(var(--spacing-space) * 1.35); -} - -/* ——— Product desk tabs ——— */ - -.soonex-tab-panel { - min-height: 18rem; -} - -.tabs.soonex-product-tabs [data-scope="tabs"][data-part="list"] { - gap: 0; - border-block-end: 1px solid var(--color-border); - background: transparent; - padding: 0; - border-radius: 0; - width: 100%; +/* Marquee */ +.soonex-marquee-band { + overflow: hidden; } -.tabs.soonex-product-tabs [data-scope="tabs"][data-part="trigger"] { +.soonex-marquee-chip { + display: inline-flex; + align-items: center; + gap: 0.55rem; + font-size: var(--text-sm); font-weight: var(--font-weight-semibold); - border-radius: 0; - border-block-end: 2px solid transparent; - margin-block-end: -1px; + color: var(--color-ink); + white-space: nowrap; } -.tabs.soonex-product-tabs [data-scope="tabs"][data-part="trigger"][data-selected] { - border-block-end-color: var(--color-brand); +.soonex-marquee-dot { + width: 0.45rem; + height: 0.45rem; + border-radius: 9999px; + background-color: var(--color-brand); + flex-shrink: 0; } -.tabs.soonex-product-tabs [data-scope="tabs"][data-part="content"] { - margin-block-start: calc(var(--spacing-space) * 1.25); +/* Bento */ +.soonex-bento { + margin: 0; padding: 0; - background: transparent; - border: 0; - border-radius: 0; - min-height: 16rem; + list-style: none; + display: grid; + grid-template-columns: 1fr; + gap: var(--spacing-space); } -.soonex-tab-workspace { - max-width: 48rem; -} +@media (width >= 40rem) { + .soonex-bento { + grid-template-columns: 1fr 1fr; + } -.soonex-tab-workspace .soonex-room-specimen { - font-size: var(--text-5xl); + .soonex-bento-wide { + grid-column: 1 / -1; + } } -.soonex-instrument-list { - margin: 0; - padding: 0; - list-style: none; - display: flex; - flex-direction: column; -} +@media (width >= 64rem) { + .soonex-bento { + grid-template-columns: 1.2fr 1fr; + gap: calc(var(--spacing-space) * 1.1); + } -.soonex-instrument-list > li { - display: grid; - grid-template-columns: 3rem 1fr; - gap: var(--spacing-space); - align-items: start; - padding-block: calc(var(--spacing-space) * 0.85); - border-block-end: 1px solid var(--color-border); -} + .soonex-bento-wide { + grid-column: auto; + } -.soonex-instrument-list > li:first-child { - border-block-start: 1px solid var(--color-border); + .soonex-bento-cell:first-child { + grid-row: span 2; + } } -.soonex-instrument-index { - font-family: var(--font-mono); - font-size: var(--text-sm); - font-weight: var(--font-weight-semibold); - color: var(--color-brand-text); - line-height: 1.6; +.soonex-bento-cell { + border: 1px solid var(--color-border); + background-color: var(--color-surface); + border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); + padding: calc(var(--spacing-space) * 1.25); } -.soonex-look-rail { - margin: 0; - padding: 0; - list-style: none; - display: flex; - flex-direction: column; - border-block: 1px solid var(--color-border); +/* Product tabs */ +.tabs.soonex-product-tabs [data-scope="tabs"][data-part="list"] { + gap: 0.25rem; + border: 1px solid var(--color-border); + border-radius: var(--soonex-ctl-radius, var(--radius-xl)); + background-color: var(--color-ui); + padding: 0.25rem; } -.soonex-look-rail > li { - display: grid; - grid-template-columns: 4rem 1fr; - gap: var(--spacing-space); - align-items: baseline; - padding-block: calc(var(--spacing-space) * 0.85); - border-block-end: 1px solid var(--color-border); +.tabs.soonex-product-tabs [data-scope="tabs"][data-part="trigger"] { + font-weight: var(--font-weight-semibold); + border-radius: var(--soonex-ctl-radius, var(--radius-lg)); } -.soonex-look-rail > li:last-child { - border-block-end: 0; +.tabs.soonex-product-tabs [data-scope="tabs"][data-part="content"] { + margin-block-start: calc(var(--spacing-space) * 1.1); + padding: calc(var(--spacing-space) * 1.25); + background-color: var(--color-root); + border: 1px solid var(--color-border); + border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); + min-height: 14rem; } -.soonex-look-id { - font-family: var(--font-mono); - font-size: var(--text-sm); - font-weight: var(--font-weight-semibold); - color: var(--color-brand-text); +.soonex-tab-workspace { + max-width: 40rem; } -.soonex-look-line { - color: var(--color-ink-muted); - font-size: var(--text-sm); - line-height: var(--leading-relaxed); +.soonex-tab-workspace .soonex-room-specimen { + font-size: var(--text-5xl); } .soonex-feature-bullets { @@ -366,21 +328,16 @@ html[data-motion="reduce"] { top: 0.55em; width: 0.4rem; height: 0.4rem; - border-radius: var(--soonex-ctl-radius, 9999px); + border-radius: 9999px; background-color: var(--color-brand); } -.soonex-ledger-row { - border-block: 1px solid var(--color-border); - padding-block: calc(var(--spacing-space) * 1.15); -} - .soonex-clone-strip { display: flex; flex-direction: column; gap: var(--spacing-space); border-block-start: 1px solid var(--color-border); - padding-block-start: calc(var(--spacing-space) * 1.35); + padding-block-start: calc(var(--spacing-space) * 1.25); } @media (width >= 64rem) { @@ -394,12 +351,11 @@ html[data-motion="reduce"] { .soonex-pricing { display: flex; flex-direction: column; - gap: 0; border-block: 1px solid var(--color-border); } .soonex-pricing-tier { - padding-block: calc(var(--spacing-space) * 1.4); + padding-block: calc(var(--spacing-space) * 1.25); border-block-end: 1px solid var(--color-border); } @@ -407,114 +363,50 @@ html[data-motion="reduce"] { border-block-end: 0; } -/* ——— Looks specimen chart ——— */ - -.soonex-looks-stage { - border-block-start: 1px solid var(--color-border); - padding-block-start: calc(var(--spacing-space) * 1.25); -} - -.soonex-specimen-chart { +/* Looks bento */ +.soonex-looks-bento { margin: 0; padding: 0; list-style: none; display: grid; grid-template-columns: 1fr; - border: 1px solid var(--color-border); + gap: var(--spacing-space); } @media (width >= 40rem) { - .soonex-specimen-chart { + .soonex-looks-bento { grid-template-columns: 1fr 1fr; } } -.soonex-specimen-cell { - background-color: var(--color-root); +.soonex-look-tile { + border: 1px solid var(--color-border); + background-color: var(--color-surface); + border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); padding: calc(var(--spacing-space) * 1.25); - border-block-end: 1px solid var(--color-border); -} - -@media (width >= 40rem) { - .soonex-specimen-cell:nth-child(odd) { - border-inline-end: 1px solid var(--color-border); - } - - .soonex-specimen-cell:nth-last-child(-n + 2) { - border-block-end: 0; - } -} - -@media (width < 40rem) { - .soonex-specimen-cell:last-child { - border-block-end: 0; - } -} - -.soonex-specimen-cell .soonex-room-name { - color: var(--color-ink); -} - -/* ——— Ledger + voice ——— */ - -.soonex-voice-rule { - border-block-start: 1px solid var(--color-border); - padding-block-start: calc(var(--spacing-space) * 1.25); - max-width: 40rem; -} - -.soonex-ledger { - margin: 0; - padding: 0; - list-style: none; - display: flex; - flex-direction: column; - border-block-start: 1px solid var(--color-border); } -.soonex-ledger-entry { - padding-block: calc(var(--spacing-space) * 1.35); - border-block-end: 1px solid var(--color-border); +/* Voices carousel */ +.soonex-voices-carousel [data-scope="carousel"][data-part="item"] { + align-items: stretch; } -.soonex-ledger-grid { - display: grid; - grid-template-columns: 1fr; - gap: var(--spacing-space); -} - -@media (width >= 40rem) { - .soonex-ledger-grid { - grid-template-columns: 9rem 1fr; - gap: calc(var(--spacing-space) * 1.5); - } +.soonex-voice-slide { + border: 1px solid var(--color-border); + background-color: var(--color-root); + border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); + padding: calc(var(--spacing-space) * 1.5); + height: 100%; } -/* ——— FAQ ——— */ - .accordion.soonex-faq-instrument [data-scope="accordion"][data-part="item-trigger"] { font-weight: var(--font-weight-semibold); padding-block: calc(var(--spacing-space) * 0.95); } -.accordion.soonex-faq-instrument [data-scope="accordion"][data-part="item"] { - border-color: var(--color-border); -} - -/* ——— Control family ——— */ - .select [data-scope="select"][data-part="trigger"], .tags-input [data-scope="tags-input"][data-part="control"], -.number-input [data-scope="number-input"][data-part="control"] { - border-radius: var(--soonex-ctl-radius, var(--radius-md)); -} - +.number-input [data-scope="number-input"][data-part="control"], .clipboard [data-scope="clipboard"][data-part="control"] { border-radius: var(--soonex-ctl-radius, var(--radius-md)); } - -.radio-group.soonex-strip-roles [data-scope="radio-group"][data-part="root"] { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-space); -} diff --git a/assets/css/hosts.css b/assets/css/hosts.css index 6a9c27f..3ba4a21 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -4,6 +4,9 @@ --soonex-ease: cubic-bezier(0.22, 1, 0.36, 1); --soonex-header-offset: calc(var(--spacing-size) + 1.5rem); --soonex-ctl-radius: var(--radius-lg); + --soonex-ctl-border: var(--color-border); + --soonex-ctl-surface: var(--color-ui); + --soonex-ctl-shadow: var(--shadow-sm); } body.layout { @@ -194,6 +197,19 @@ pre.code.code--wide { --ctl-text: var(--text-base); } +.native-input [data-scope="native-input"][data-part="input"], +.select [data-scope="select"][data-part="trigger"], +.tags-input [data-scope="tags-input"][data-part="control"], +.number-input [data-scope="number-input"][data-part="control"] { + background-color: var(--soonex-ctl-surface, var(--color-ui)); + border-color: var(--soonex-ctl-border, var(--color-border)); +} + +.button.ui-solid:not(.ui-ghost), +.button.ui-brand.ui-solid { + box-shadow: var(--soonex-ctl-shadow, var(--shadow-sm)); +} + .select [data-scope="select"][data-part="trigger"], .menu [data-scope="menu"][data-part="trigger"] { --ctl-radius: inherit; diff --git a/config/config.exs b/config/config.exs index 6e7af76..218be92 100644 --- a/config/config.exs +++ b/config/config.exs @@ -76,7 +76,7 @@ config :tableau, Tableau.RSSExtension, enabled: true, title: "Soonex", description: - "Soonex ships 1 September — typographic launch desk with waitlist, shipping log, countdown, and four looks." + "Soonex ships 1 September — waitlist, shipping log, countdown, and four looks for client review." config :elixir, :time_zone_database, Tz.TimeZoneDatabase @@ -117,11 +117,11 @@ with_on = fn colors -> "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) + "accent-text" => contrast.(:accent, :root, 4.6), + "brand-text" => contrast.(:brand, :root, 4.6), + "alert-text" => contrast.(:alert, :root, 4.6), + "info-text" => contrast.(:info, :root, 4.6), + "success-text" => contrast.(:success, :root, 4.6) }) end diff --git a/lib/layouts/media.ex b/lib/layouts/media.ex index 27e12aa..3541b0a 100644 --- a/lib/layouts/media.ex +++ b/lib/layouts/media.ex @@ -26,6 +26,6 @@ defmodule Soonex.Layouts.Media do end def credits do - "Soonex · Corex template" + "Photographs under the Unsplash License. Tool marks from Simple Icons (CC0)." end end diff --git a/lib/layouts/post_layout.ex b/lib/layouts/post_layout.ex index f41e4b1..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 @@ -31,6 +33,7 @@ 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"""
            @@ -60,6 +63,12 @@ defmodule Soonex.PostLayout do
            +
            +
            + <.photo src={@post_cover.src} alt={@post_cover.alt} width={1400} height={700} /> +
            +
            +
            {{:safe, render(@inner_content)}}
            @@ -67,4 +76,12 @@ defmodule Soonex.PostLayout do
        """ 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/footer.ex b/lib/layouts/root/footer.ex index 0a44e09..3fc96f9 100644 --- a/lib/layouts/root/footer.ex +++ b/lib/layouts/root/footer.ex @@ -26,7 +26,7 @@ defmodule Soonex.Layouts.Root.Footer do
        <.lockup />

        - Launch edition — waitlist, ledger, countdown, four looks. Ship 1 September. + Waitlist, shipping log, countdown, and four looks. Ship 1 September.

        diff --git a/lib/layouts/root/nav.ex b/lib/layouts/root/nav.ex index 5b91d2d..98f33ff 100644 --- a/lib/layouts/root/nav.ex +++ b/lib/layouts/root/nav.ex @@ -17,7 +17,7 @@ defmodule Soonex.Layouts.Root.Nav do ~H"""
        - "Soonex ships 1 September — typographic launch desk with waitlist, shipping log, countdown, and four looks." + "Soonex ships 1 September — waitlist, shipping log, countdown, and four looks for client review." page[:page_kind] == :blog_index -> "Shipping notes from #{site_name}: waitlist, looks, countdown, and the road to 1 September." @@ -239,7 +239,7 @@ defmodule Soonex.RootLayout do page[:description] true -> - "Soonex ships 1 September — typographic launch desk with waitlist, shipping log, countdown, and four looks." + "Soonex ships 1 September — waitlist, shipping log, countdown, and four looks for client review." end end diff --git a/lib/pages/home/bento.ex b/lib/pages/home/bento.ex new file mode 100644 index 0000000..9924ede --- /dev/null +++ b/lib/pages/home/bento.ex @@ -0,0 +1,75 @@ +defmodule Soonex.HomePage.Bento do + @moduledoc false + + use Phoenix.Component + use Corex + + import Soonex.Layouts.Section, only: [block: 1] + + alias Soonex.Layouts.Shell + + def bento(assigns) do + ~H""" + <.block id="features" labelledby="soonex-bento-heading" eyebrow="Features" layout={:open}> + <:title> + Everything you need to launch + + <:lede> + Four instruments composed like a Framer product page — waitlist, log, countdown, looks. + + +
          +
        • + {cell.kicker} +

          {cell.title}

          +

          {cell.body}

          + <.navigate :if={cell.to} to={cell.to} class="link ui-brand mt-6 inline-flex"> + {cell.cta} <.heroicon name={cell.icon} /> + +
        • +
        + + """ + end + + defp cells do + [ + %{ + kicker: "01", + title: "Waitlist on day one", + body: "Email, role, interests, team size — with a launch toast on submit.", + to: "#waitlist", + cta: "Join the list", + icon: "hero-arrow-down", + span: "soonex-bento-wide" + }, + %{ + kicker: "02", + title: "Public shipping log", + body: "Markdown entries with covers and tags. The index paginates in the browser.", + to: Soonex.Public.path("/blog"), + cta: "Read the log", + icon: "hero-arrow-up-right", + span: "" + }, + %{ + kicker: "03", + title: "Honest countdown", + body: "Aimed at 1 September until you change the date. Digits stay on one row.", + to: "#product", + cta: "See product", + icon: "hero-arrow-down", + span: "" + }, + %{ + kicker: "04", + title: "Four looks", + body: "neo, uno, duo, leo — switch live for client review without a second build.", + to: "#looks", + cta: "Browse looks", + icon: "hero-arrow-down", + span: "soonex-bento-wide" + } + ] + end +end diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index 953ecd9..c3fcd87 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -8,18 +8,12 @@ defmodule Soonex.HomePage.Faq do def faq(assigns) do ~H""" - <.block - id="questions" - labelledby="soonex-faq-heading" - eyebrow="Questions" - tone={:surface} - layout={:sticky} - > + <.block id="questions" labelledby="soonex-faq-heading" eyebrow="Questions" layout={:sticky}> <:title> Before you join <:lede> - Launch, waitlist, looks — and one honest answer about the fork. + Straight answers about the launch, the waitlist, and what you get on day one. <:actions> <.navigate to="#waitlist" class="link ui-brand"> @@ -54,19 +48,19 @@ defmodule Soonex.HomePage.Faq do value: "what", label: "What is Soonex?", content: - "A launch desk shipping 1 September — waitlist, public shipping log, countdown, and four looks for client review." + "A launch workspace shipping 1 September — waitlist, public shipping log, countdown, and four looks for client review." }, %{ value: "ships", label: "What do I get on launch day?", content: - "The landing, waitlist fields, shipping log, header countdown, and four looks. Swap the lockup and date for your brand." + "The landing, waitlist fields, shipping log, header countdown, and four looks. Swap the stills, lockup, and date for your brand." }, %{ value: "looks", label: "What are the four looks?", content: - "neo, uno, duo, and leo. Same page, different type, corners, and brand. Switch them live in Template Options for a client review." + "neo, uno, duo, and leo. Same page, different type, corners, and brand. Switch them live in Template Options." }, %{ value: "form", @@ -78,7 +72,7 @@ defmodule Soonex.HomePage.Faq do value: "a11y", label: "How does accessibility work?", content: - "Open Accessibility for zoom, contrast, motion, cursor, focus, and underline. Choices stay on this device in local storage." + "Open Accessibility for zoom, contrast, motion, cursor, focus, and underline. Choices stay on this device." }, %{ value: "open", diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index 1df44d7..1cb8f42 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -4,87 +4,117 @@ defmodule Soonex.HomePage.Hero do use Phoenix.Component use Corex + import Soonex.Layouts.Media, only: [photo: 1] + alias Soonex.Layouts.Shell def hero(assigns) do ~H"""
        -
        -
        - Edition - - 1 September 2026 -
        - -

        - Soonex -

        - -
        -

        - Waitlist, shipping log, countdown, four looks — a launch desk for studios that ship on a - date. -

        - <.navigate - to={Soonex.Public.path("/blog")} - class="link ui-brand ui-size-sm w-fit lg:col-span-5 lg:justify-self-end" - > - Read the log <.heroicon name="hero-arrow-up-right" /> - -
        +
        + <.photo + src="/images/photos/hero.jpg" + alt="Sunlit studio with long work tables and hanging task lamps" + width={1600} + height={1200} + class="absolute inset-0 size-full" + loading="eager" + /> +
        -
        -
        - -
        - Waitlist -

        - 240 studios · write before launch -

        +
        +
        +
        + Launch · 1 September +

        + Soonex + + ships 1 September. + +

        +

        + Waitlist, shipping log, countdown, and four looks — the launch desk for studios that + ship on a date. +

        +
        + <.navigate to="#waitlist" class="button ui-brand ui-solid ui-size-md"> + Join waitlist + + <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-md"> + Read the log <.heroicon name="hero-arrow-up-right" /> +
        +
        -
        - <.native_input - type="email" - name="waitlist[email]" - id="soonex-hero-waitlist-email" - required - autocomplete="email" - placeholder="you@studio.dev" - class="native-input ui-size-md ui-width-full min-w-0 flex-1" +
        +
        +

        Reserve a September seat

        +

        + Tell us who you are. We write when the countdown ends. +

        + + - <:label class="sr-only">Email - +
        + <.native_input + type="email" + name="waitlist[email]" + id="soonex-hero-waitlist-email" + required + autocomplete="email" + placeholder="you@studio.dev" + class="native-input ui-size-md ui-width-full min-w-0 flex-1" + > + <:label class="sr-only">Email + - -
        + +
        + + <.radio_group + id="soonex-hero-role" + name="waitlist[role]" + class="radio-group ui-brand ui-width-full" + value="founder" + items={[ + %{value: "founder", label: "Founder"}, + %{value: "engineer", label: "Engineer"}, + %{value: "designer", label: "Designer"} + ]} + > + <:label>I am a + + - <.radio_group - id="soonex-hero-role" - name="waitlist[role]" - class="radio-group ui-brand soonex-strip-roles" - value="founder" - items={[ - %{value: "founder", label: "Founder"}, - %{value: "engineer", label: "Engineer"}, - %{value: "designer", label: "Designer"} - ]} - > - <:label>I am a - - +
        +
        + <.avatar + :for={person <- people()} + id={person.id} + src={Soonex.Public.path(person.src)} + alt={person.alt} + class="avatar ui-size-sm" + > + <:fallback>{person.initials} + +
        +

        + 240 studios already on the list for 1 September. +

        +
        +
        +
        @@ -92,4 +122,27 @@ defmodule Soonex.HomePage.Hero do """ end + + defp people do + [ + %{ + id: "soonex-hero-avatar-amira", + src: "/images/people/amira.jpg", + alt: "Amira N.", + initials: "AN" + }, + %{ + id: "soonex-hero-avatar-jonas", + src: "/images/people/jonas.jpg", + alt: "Jonas K.", + initials: "JK" + }, + %{ + id: "soonex-hero-avatar-leah", + src: "/images/people/leah.jpg", + alt: "Leah M.", + initials: "LM" + } + ] + end end diff --git a/lib/pages/home/log.ex b/lib/pages/home/log.ex index ee2d932..13d4b86 100644 --- a/lib/pages/home/log.ex +++ b/lib/pages/home/log.ex @@ -4,6 +4,7 @@ defmodule Soonex.HomePage.Log do use Phoenix.Component use Corex + import Soonex.Layouts.Media, only: [photo: 1] import Soonex.Layouts.Section, only: [block: 1] alias Soonex.Layouts.Shell @@ -18,12 +19,11 @@ defmodule Soonex.HomePage.Log do |> Enum.take(3) assigns = assign(assigns, :entries, posts) - assigns = assign(assigns, :voice, voice()) ~H""" <.block id="log" labelledby="soonex-log-heading" eyebrow="Shipping log" layout={:open}> <:title> - Ledger on the road to September + What we shipped on the road to September <:lede> Public notes on the waitlist, the four looks, and the countdown. @@ -33,36 +33,39 @@ defmodule Soonex.HomePage.Log do All entries <.heroicon name="hero-arrow-up-right" /> - -
        -

        “{@voice.quote}”

        -
        - {@voice.name} - {@voice.role} -
        -
        - -

        No posts yet.

        -
          -
        1. -
          - {date_label(post)} -
          -

          - <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> - {post[:title] || "Untitled"} - -

          -

          - {post[:description]} -

          - <.navigate - to={Soonex.Public.path(post.permalink)} - class="button ui-ghost ui-brand ui-size-sm mt-5 w-fit" - > - Read more <.heroicon name="hero-arrow-up-right" /> +

          No posts yet.

          +
            +
          1. +
            + <.photo + src={cover(post).src} + alt={cover(post).alt} + width={800} + height={500} + /> +
            +
            + {date_label(post)} +

            + <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> + {post[:title] || "Untitled"} -

            + +

            + {post[:description]} +

            + <.navigate + to={Soonex.Public.path(post.permalink)} + class="button ui-ghost ui-brand ui-size-sm mt-4 w-fit" + > + Read more <.heroicon name="hero-arrow-up-right" /> +
        @@ -70,13 +73,12 @@ defmodule Soonex.HomePage.Log do """ end - defp voice do - %{ - quote: - "We had a date before we had a deck. Soonex put the waitlist and the countdown on the page the same afternoon.", - name: "Amira N.", - role: "Founder, Northline Studio" - } + defp cover(post) do + src = post[:image] + + if is_binary(src) and src != "" do + %{src: src, alt: post[:image_alt] || post[:title] || "Log cover"} + end end defp date_label(%{date: %DateTime{} = date}), do: Calendar.strftime(date, "%d %B %Y") diff --git a/lib/pages/home/marquee_band.ex b/lib/pages/home/marquee_band.ex new file mode 100644 index 0000000..a9703d8 --- /dev/null +++ b/lib/pages/home/marquee_band.ex @@ -0,0 +1,44 @@ +defmodule Soonex.HomePage.MarqueeBand do + @moduledoc false + + use Phoenix.Component + use Corex + + def marquee_band(assigns) do + ~H""" +
        + <.marquee + id="soonex-proof-marquee" + class="marquee ui-brand soonex-proof-marquee" + items={items()} + duration={28} + spacing="3rem" + pause_on_interaction + aria_label="Soonex launch signals" + > + <:item :let={item}> + + + {item.label} + + + +
        + """ + end + + defp items do + [ + %{label: "Waitlist that converts"}, + %{label: "Public shipping log"}, + %{label: "Countdown to 1 September"}, + %{label: "neo · uno · duo · leo"}, + %{label: "240 studios on the list"}, + %{label: "Accessible by default"}, + %{label: "Clone · mix setup · ship"} + ] + end +end diff --git a/lib/pages/home/notes.ex b/lib/pages/home/notes.ex new file mode 100644 index 0000000..fbd3730 --- /dev/null +++ b/lib/pages/home/notes.ex @@ -0,0 +1,93 @@ +defmodule Soonex.HomePage.Notes do + @moduledoc false + + use Phoenix.Component + use Corex + + import Soonex.Layouts.Section, only: [block: 1] + + def notes(assigns) do + ~H""" + <.block + id="notes" + labelledby="soonex-notes-heading" + eyebrow="Studios" + tone={:surface} + layout={:open} + > + <:title> + Already on the September list + + <:lede> + Founders, engineers, and designers putting Soonex on a calendar. + + + <.carousel + id="soonex-voices" + class="carousel ui-brand ui-width-full soonex-voices-carousel" + items={voices()} + slides_per_page={1} + spacing="1.5rem" + > + <:item :let={voice}> +
        +

        “{voice.quote}”

        +
        + <.avatar + id={voice.id} + src={Soonex.Public.path(voice.src)} + alt={voice.name} + class="avatar ui-size-md" + > + <:fallback>{voice.initials} + +
        + {voice.name} + {voice.role} +
        +
        +
        + + <:prev_trigger> + <.heroicon name="hero-arrow-left" /> + + <:next_trigger> + <.heroicon name="hero-arrow-right" /> + + + + """ + end + + defp voices do + [ + %{ + id: "soonex-voice-amira", + src: "/images/people/amira.jpg", + initials: "AN", + quote: + "We had a date before we had a deck. Soonex put the waitlist and the countdown on the page the same afternoon.", + name: "Amira N.", + role: "Founder, Northline Studio" + }, + %{ + id: "soonex-voice-jonas", + src: "/images/people/jonas.jpg", + initials: "JK", + quote: + "One clone path, controls included, no second design system. We pointed the date and shipped the stills.", + name: "Jonas K.", + role: "Staff engineer, Relay" + }, + %{ + id: "soonex-voice-leah", + src: "/images/people/leah.jpg", + initials: "LM", + quote: + "Four looks for the client review without rebuilding. They picked duo; we kept shipping.", + name: "Leah M.", + role: "Product designer, Fieldwork" + } + ] + end +end diff --git a/lib/pages/home/product.ex b/lib/pages/home/product.ex index a42ee20..f6d6d85 100644 --- a/lib/pages/home/product.ex +++ b/lib/pages/home/product.ex @@ -12,14 +12,24 @@ defmodule Soonex.HomePage.Product do assigns = assign(assigns, :snippet, snippet()) ~H""" - <.block id="product" labelledby="soonex-product-heading" eyebrow="Product" layout={:open}> + <.block + id="product" + labelledby="soonex-product-heading" + eyebrow="Product" + tone={:surface} + layout={:sticky} + > <:title> - The desk that ships on a date + Open a panel. Ship the desk. <:lede> - Four instruments. Open a panel. Launch when 1 September - hits. + Waitlist, log, countdown, looks — one product UI for studios launching 1 September. + <:actions> + <.navigate to="#waitlist" class="button ui-brand ui-solid ui-size-md"> + Join waitlist + + <.tabs id="soonex-product-tabs" @@ -31,64 +41,33 @@ defmodule Soonex.HomePage.Product do <:content value="overview" class="soonex-tab-panel">

        - Soonex is a launch desk for studios with a hard date. Waitlist on the page, public log, - countdown aimed at launch, four looks for client review. + Soonex is a launch workspace for studios with a hard date. Put the waitlist on the + page, keep a public log, and review four looks with your client before go-live.

        -
          -
        1. - -
          -

          Waitlist strip

          -

          - Email, role, interests, team size — toast on submit. -

          -
          -
        2. -
        3. - -
          -

          Shipping ledger

          -

          - Markdown entries with tags. Index paginates in the browser. -

          -
          -
        4. -
        5. - -
          -

          Honest countdown

          -

          - Aimed at 1 September until you change it. -

          -
          -
        6. -
        -
        - <.navigate to="#waitlist" class="button ui-brand ui-solid ui-size-md"> - Join waitlist - -
        +
          +
        • Waitlist rack with toast on submit
        • +
        • Shipping log with covers and tags
        • +
        • Countdown aimed at 1 September
        • +
        • Four looks switched on this device
        • +
        <:trigger value="waitlist">Waitlist <:content value="waitlist" class="soonex-tab-panel">
        -

        Fields on the strip

        -

        - Same instruments on the edition strip and the closing rack. Collect who is coming — - then show a clear success path. +

        Capture who is coming

        +

        + Same fields on the hero still and at the end of the page. Wire your provider after + launch — the toast already ships.

        -
          +
          • Email with validation
          • Role select or radios
          • Interests as tags
          • Team size stepper
          • Launch notes switch
          -

          - Wire your provider after launch. The toast already ships. -

        @@ -96,26 +75,10 @@ defmodule Soonex.HomePage.Product do <:content value="log" class="soonex-tab-panel">

        - A public ledger of what shipped. Posts are Markdown with tags — rebuild and the index - updates. + Keep a public trail of what shipped. Posts live as Markdown with covers and tags.

        -
        - 02 August 2026 -

        - Accessibility on this device -

        -

        - Zoom, contrast, motion, cursor, focus, and underline — stored on this device. -

        - <.navigate - to={Soonex.Public.path("/blog/accessibility-on-device/")} - class="button ui-ghost ui-brand ui-size-sm mt-6 w-fit" - > - Read entry <.heroicon name="hero-arrow-up-right" /> - -
        -
        - <.navigate to={Soonex.Public.path("/blog")} class="link ui-brand"> +
        + <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-brand ui-size-sm"> All shipping notes <.heroicon name="hero-arrow-up-right" />
        @@ -127,27 +90,19 @@ defmodule Soonex.HomePage.Product do

        Soonex

        - Four editions for the same launch. Switch in Template Options — no second build. + Four looks for the same launch. Switch rooms in Template Options — no second build.

        -
          -
        • - {room.name} - {room.line} -
        • -
        - <.navigate to="#looks" class="link ui-brand mt-8 inline-flex"> + <.navigate to="#looks" class="link ui-brand mt-6 inline-flex"> See all four looks <.heroicon name="hero-arrow-down" />
        -
        +
        Get Soonex -

        - Clone once. mix setup, then mix soonex.server. -

        +

        Clone once. mix setup, then mix soonex.server.

        <.clipboard id="soonex-theme-snippet" @@ -166,16 +121,12 @@ defmodule Soonex.HomePage.Product do
        -
        +
        Plans

        Same core. Three lanes.

        -

        - Every plan is Soonex. How far you take it before 1 September. -

        - -
        +
        @@ -197,57 +148,34 @@ defmodule Soonex.HomePage.Product do """ end - defp looks do - [ - %{name: "neo", line: "Cold paper · electric cobalt · geometric display"}, - %{name: "uno", line: "Teal field · razor corners · compressed type"}, - %{name: "duo", line: "Warm surface · rust brand · editorial serif"}, - %{name: "leo", line: "Poster ink · signal orange · uppercase"} - ] - end - defp plans do [ %{ name: "Solo", headline: "Fork and put a date on the page", - body: "One look, the waitlist, and the log. Enough for an independent launch.", + body: "One look, the waitlist, and the log.", cta: "Join waitlist", cta_class: "button ui-ghost ui-size-sm w-fit", - items: [ - "Waitlist landing and shipping log", - "Keep neo, or switch once for your brand", - "Countdown aimed at 1 September" - ] + items: ["Waitlist + shipping log", "Countdown to 1 September"] }, %{ name: "Studio", headline: "Four looks, client review", - body: "Treat Template Options as the review. The shipping log stays.", + body: "Treat Template Options as the review.", cta: "Join waitlist", cta_class: "button ui-brand ui-solid ui-size-sm w-fit", - items: [ - "neo, uno, duo, and leo on this device", - "Tags and client pagination", - "Header timer, toast, and clone strip" - ] + items: ["neo, uno, duo, leo", "Header timer + toast"] }, %{ name: "Agency", - headline: "Accessible handoff you can repeat", - body: "Ship with accessibility on and a clone path the whole studio can run.", + headline: "Accessible handoff", + body: "Ship with accessibility on and a clone path the studio can run.", cta: "Join waitlist", cta_class: "button ui-ghost ui-size-sm w-fit", - items: [ - "Zoom, contrast, motion, focus, underline", - "Same fork for every client engagement", - "Clone, mix setup, mix soonex.server" - ] + items: ["Zoom, contrast, motion", "Clone · mix setup · ship"] } ] end - defp snippet do - "git clone https://github.com/corex-ui/soonex.git" - end + defp snippet, do: "git clone https://github.com/corex-ui/soonex.git" end diff --git a/lib/pages/home/themes.ex b/lib/pages/home/themes.ex index fb57820..bd4e1da 100644 --- a/lib/pages/home/themes.ex +++ b/lib/pages/home/themes.ex @@ -7,39 +7,30 @@ defmodule Soonex.HomePage.Themes do def themes(assigns) do ~H""" - <.block - id="looks" - labelledby="soonex-looks-heading" - eyebrow="Looks" - tone={:surface} - layout={:open} - > + <.block id="looks" labelledby="soonex-looks-heading" eyebrow="Looks" layout={:open}> <:title> - Four editions. One launch. + Four looks. One launch. <:lede> - neo, uno, duo, leo — type, corners, brand. Paper stays the medium. Switch live in Template - Options. + Switch neo, uno, duo, and leo live in Template Options. Paper stays calm — type, corners, + and brand change the room. -
        -

        Soonex

        -

        - This specimen follows the look you pick. No second build for the review. -

        -
        +

        Soonex

        +

        + This specimen follows the look you pick. No second build for the review. +

        -
          +
          1. - {room.id} -

            {room.id}

            +

            {room.id}

            {room.line}

            -

            {room.use}

            +

            {room.use}

          @@ -50,23 +41,23 @@ defmodule Soonex.HomePage.Themes do [ %{ id: "neo", - line: "Cold paper · electric cobalt · geometric display.", - use: "Default for product launches and investor-facing pages." + line: "Product studio — geometric display, open corners, cobalt.", + use: "Default for product launches." }, %{ id: "uno", - line: "Teal field · razor corners · compressed type.", - use: "Ops and tooling launches that need quiet confidence." + line: "Calm ops — tight tracking, small radius, teal.", + use: "Ops and tooling launches." }, %{ id: "duo", - line: "Warm surface · rust brand · editorial serif.", - use: "Studios and cultural brands that want editorial weight." + line: "Editorial atelier — serif display, soft corners, rust.", + use: "Studios that want editorial warmth." }, %{ id: "leo", - line: "Poster ink · signal orange · uppercase.", - use: "Campaign drops and poster-led announcements." + line: "Signal poster — strong display, square corners, orange.", + use: "Campaign drops and posters." } ] end diff --git a/lib/pages/home/waitlist.ex b/lib/pages/home/waitlist.ex index 91c27fa..72da15f 100644 --- a/lib/pages/home/waitlist.ex +++ b/lib/pages/home/waitlist.ex @@ -6,6 +6,8 @@ defmodule Soonex.HomePage.Waitlist do import Soonex.Layouts.Section, only: [block: 1] + alias Soonex.Layouts.Shell + def waitlist(assigns) do assigns = assign(assigns, :role_items, role_items()) @@ -15,7 +17,7 @@ defmodule Soonex.HomePage.Waitlist do labelledby="soonex-waitlist-heading" eyebrow="Waitlist" tone={:surface} - align={:start} + align={:center} layout={:open} > <:title> @@ -24,13 +26,13 @@ defmodule Soonex.HomePage.Waitlist do <:lede> Role, interests, and team size. We write when the countdown ends. -
          -
          + +
          <.native_input type="email" name="waitlist[email]" @@ -109,11 +111,11 @@ defmodule Soonex.HomePage.Waitlist do - -

          - Demo page — toast only until you wire a provider. -

          -
          +
          + +

          + Demo page — toast only until you wire a provider. +

          """ end diff --git a/lib/pages/home_page.ex b/lib/pages/home_page.ex index 9d8caa4..054b1ad 100644 --- a/lib/pages/home_page.ex +++ b/lib/pages/home_page.ex @@ -5,8 +5,11 @@ defmodule Soonex.HomePage do use Corex import Soonex.HomePage.Hero, only: [hero: 1] + import Soonex.HomePage.MarqueeBand, only: [marquee_band: 1] + import Soonex.HomePage.Bento, only: [bento: 1] import Soonex.HomePage.Product, only: [product: 1] import Soonex.HomePage.Themes, only: [themes: 1] + import Soonex.HomePage.Notes, only: [notes: 1] import Soonex.HomePage.Log, only: [log: 1] import Soonex.HomePage.Faq, only: [faq: 1] import Soonex.HomePage.Waitlist, only: [waitlist: 1] @@ -17,8 +20,11 @@ defmodule Soonex.HomePage do ~H"""
          <.hero /> + <.marquee_band /> + <.bento /> <.product /> <.themes /> + <.notes /> <.log posts={@log_posts} /> <.faq /> <.waitlist /> From c5a363b78f0fa77cf96abf6858e4d1284c6fdcef Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 31 Aug 2026 06:49:43 +0000 Subject: [PATCH 55/65] Restore cover thumbs on the log index cards. Blog entries show photo covers again beside dense rows so the shipping log matches the Framer launch tease on the home page. Co-authored-by: Karim Semmoud --- lib/layouts/articles.ex | 47 ++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/lib/layouts/articles.ex b/lib/layouts/articles.ex index 81722c7..26fd7f2 100644 --- a/lib/layouts/articles.ex +++ b/lib/layouts/articles.ex @@ -4,6 +4,8 @@ defmodule Soonex.Layouts.Articles do use Phoenix.Component use Corex + import Soonex.Layouts.Media, only: [photo: 1] + alias Soonex.Layouts.Shell attr(:posts, :list, required: true) @@ -20,19 +22,32 @@ defmodule Soonex.Layouts.Articles do
        1. - {date_label(post)} -

          - <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> - {post[:title] || "Untitled"} - -

          -

          - {post[:description]} -

          -
          - {tag} +
          + <.photo + src={cover(post).src} + alt={cover(post).alt} + width={800} + height={500} + /> +
          +
          + {date_label(post)} +

          + <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> + {post[:title] || "Untitled"} + +

          +

          + {post[:description]} +

          +
          + {tag} +
        @@ -71,6 +86,14 @@ defmodule Soonex.Layouts.Articles do defp date_label(%{date: %DateTime{} = date}), do: Calendar.strftime(date, "%d %B %Y") defp date_label(_), do: nil + defp cover(post) do + src = post[:image] + + if is_binary(src) and src != "" do + %{src: src, alt: post[:image_alt] || post[:title] || "Log cover"} + end + end + defp post_tags(post) do post |> Map.get(:tags, []) From 5ab368d1aeae5d1ed18b75c95bc22771cc91d4fb Mon Sep 17 00:00:00 2001 From: Karim Semmoud Date: Wed, 9 Sep 2026 10:09:37 +0700 Subject: [PATCH 56/65] Rebuild Soonex as a shippable launch template with four skins. Replace the Latin demo bands with English marketing sections, journal content, cookie consent, and polished chrome so founders can fork a finished waitlist page instead of another component gallery. Co-authored-by: Cursor --- README.md | 14 +- _posts/2026-03-12-duis-aute.md | 67 ++++++ _posts/2026-04-08-excepteur.md | 80 +++++++ _posts/2026-05-08-docs.md | 192 +++++++++++++++ _posts/2026-06-01-shipping.md | 92 ++++++++ _posts/2026-07-14-nemo-enim.md | 78 ++++++ _posts/2026-08-02-temporibus.md | 75 ++++++ assets/css/chrome.css | 375 +++++------------------------ assets/css/hosts.css | 359 ++++------------------------ assets/css/layout.css | 166 +++++++++++++ assets/css/site.css | 5 + assets/css/skins/duo.css | 166 +++++++++++++ assets/css/skins/leo.css | 181 ++++++++++++++ assets/css/skins/neo.css | 116 +++++++++ assets/css/skins/uno.css | 149 ++++++++++++ assets/js/cookie-consent.js | 169 +++++++++++++ assets/js/pager.js | 39 +++ assets/js/site.js | 11 +- assets/js/skins.js | 57 +++++ assets/js/skins/duo.js | 9 + assets/js/skins/leo.js | 9 + assets/js/skins/neo.js | 9 + assets/js/skins/uno.js | 9 + config/config.exs | 406 +++++++++++++------------------- extra/images/logo.svg | 2 +- lib/layouts/articles.ex | 58 +++-- lib/layouts/brand.ex | 10 +- lib/layouts/root/demo.ex | 10 + lib/layouts/root/footer.ex | 30 ++- lib/layouts/root/nav.ex | 45 ++-- lib/layouts/root_layout.ex | 67 +++--- lib/layouts/section.ex | 39 ++- lib/layouts/shell.ex | 68 +++--- lib/layouts/tag_layout.ex | 8 +- lib/pages/blog_index_page.ex | 14 +- lib/pages/home/faq.ex | 124 ++++++++-- lib/pages/home/features.ex | 62 +++++ lib/pages/home/hero.ex | 180 ++++++-------- lib/pages/home/journal.ex | 93 ++++++++ lib/pages/home/launch_band.ex | 77 ++++++ lib/pages/home/logos.ex | 55 +++++ lib/pages/home/pricing.ex | 99 ++++++++ lib/pages/home/product.ex | 209 ++++++---------- lib/pages/home/stats.ex | 48 ++++ lib/pages/home/steps.ex | 80 +++++++ lib/pages/home/voices.ex | 77 ++++++ lib/pages/home/waitlist.ex | 28 ++- lib/pages/home_page.ex | 38 +-- lib/pages/not_found_page.ex | 10 +- lib/pages/privacy_page.ex | 144 +++++++++++ lib/pages/tags_index_page.ex | 30 +-- lib/soonex/accessibility.ex | 4 +- lib/soonex/cookie_consent.ex | 240 +++++++++++++++++++ lib/soonex/launch.ex | 15 ++ mix.exs | 5 +- mix.lock | 30 +-- 56 files changed, 3435 insertions(+), 1397 deletions(-) create mode 100644 _posts/2026-03-12-duis-aute.md create mode 100644 _posts/2026-04-08-excepteur.md create mode 100644 _posts/2026-05-08-docs.md create mode 100644 _posts/2026-06-01-shipping.md create mode 100644 _posts/2026-07-14-nemo-enim.md create mode 100644 _posts/2026-08-02-temporibus.md create mode 100644 assets/css/layout.css create mode 100644 assets/css/skins/duo.css create mode 100644 assets/css/skins/leo.css create mode 100644 assets/css/skins/neo.css create mode 100644 assets/css/skins/uno.css create mode 100644 assets/js/cookie-consent.js create mode 100644 assets/js/pager.js create mode 100644 assets/js/skins.js create mode 100644 assets/js/skins/duo.js create mode 100644 assets/js/skins/leo.js create mode 100644 assets/js/skins/neo.js create mode 100644 assets/js/skins/uno.js create mode 100644 lib/pages/home/features.ex create mode 100644 lib/pages/home/journal.ex create mode 100644 lib/pages/home/launch_band.ex create mode 100644 lib/pages/home/logos.ex create mode 100644 lib/pages/home/pricing.ex create mode 100644 lib/pages/home/stats.ex create mode 100644 lib/pages/home/steps.ex create mode 100644 lib/pages/home/voices.ex create mode 100644 lib/pages/privacy_page.ex create mode 100644 lib/soonex/cookie_consent.ex create mode 100644 lib/soonex/launch.ex diff --git a/README.md b/README.md index b9d53f8..abe5c72 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ English-only **Tableau** static site: Corex components, theme/mode toggles, and ```shell cd soonex mix setup -mix soonex.server +mix server ``` - Dev site: `http://localhost:4999` (home at `/`). @@ -29,7 +29,7 @@ With `MIX_ENV=dev`, Corex MCP listens at `http://localhost:4004/corex/mcp` (Tabl ss -ltnp 'sport = :4999' # Linux lsof -nP -iTCP:4999 -sTCP:LISTEN # macOS kill -mix soonex.server # port check, then tableau.server +mix server # alias: port check, then tableau.server ``` Rebuild assets: `mix assets.build`. @@ -37,12 +37,14 @@ Rebuild assets: `mix assets.build`. ## Customize (where to edit) - **Brand / SEO:** lockup in [`lib/layouts/brand.ex`](lib/layouts/brand.ex) and [`extra/images/logo.svg`](extra/images/logo.svg); titles in [`lib/layouts/root_layout.ex`](lib/layouts/root_layout.ex). +- **Launch date:** [`lib/soonex/launch.ex`](lib/soonex/launch.ex). Hero badge and header countdown both read it. - **Themes:** overlay **all allowed keys** per theme in [`config/config.exs`](config/config.exs) (`seeds`, `colors.light` / `colors.dark`, `dimensions.radius`, `dimensions.font`, `typography`) plus top-level `scales:`. Contrast stays calculated; `*_scale` / duration / opacity keys are rejected. Then `mix corex.design.build`. [`lib/soonex/theme.ex`](lib/soonex/theme.ex) must list the same ids as `data-theme`. +- **Skins:** each theme has isolated CSS in [`assets/css/skins/`](assets/css/skins/) and optional JS in [`assets/js/skins/`](assets/js/skins/). Imports live in [`assets/css/site.css`](assets/css/site.css) and [`assets/js/skins.js`](assets/js/skins.js). To drop a look, delete the skin files, remove the imports, and remove the id from `Soonex.Theme` and `config :corex_design, :themes`. Shared marketing rhythm stays in [`assets/css/layout.css`](assets/css/layout.css). [`assets/css/hosts.css`](assets/css/hosts.css) is Corex host polish only. No token opacity (`bg-surface/90`). - **Accessibility:** Corex `--a11y` dialog in the demo FAB ([`lib/soonex/accessibility.ex`](lib/soonex/accessibility.ex)). Preferences live in `localStorage` (`phx:a11y`); `corex_design` must stay a **runtime** dep. - **Fonts:** self-hosted woff2 in [`extra/fonts/`](extra/fonts/), faces in [`assets/css/fonts.css`](assets/css/fonts.css). Stacks follow Corex 0.2 (neo = Outfit + Manrope; uno/duo/leo keep their Corex families). -- **Chrome:** sticky header in [`lib/layouts/root/nav.ex`](lib/layouts/root/nav.ex); condensed + countdown `hidden`/`inert` in [`assets/js/landing-scroll-chrome.js`](assets/js/landing-scroll-chrome.js). [`assets/css/hosts.css`](assets/css/hosts.css) is after `corex.css`. No token opacity (`bg-surface/90`). -- **Content:** home sections in [`lib/pages/home/`](lib/pages/home/), composed by [`lib/pages/home_page.ex`](lib/pages/home_page.ex). Band skeletons live in [`lib/layouts/section.ex`](lib/layouts/section.ex) (`:open`, `:editorial`, `:sticky`). FAQ is the sticky split. Anchors: `#product`, `#notes`, `#themes`, `#log`, `#questions`, `#waitlist`. -- **Log:** index at [`/blog`](lib/pages/blog_index_page.ex) (`layout_heading`, list, pagination); posts under [`_posts/`](_posts/) with `Soonex.PostLayout`; tags at [`/tags`](lib/pages/tags_index_page.ex). +- **Chrome:** sticky header in [`lib/layouts/root/nav.ex`](lib/layouts/root/nav.ex); condensed + countdown `hidden`/`inert` in [`assets/js/landing-scroll-chrome.js`](assets/js/landing-scroll-chrome.js). +- **Content:** home sections in [`lib/pages/home/`](lib/pages/home/), composed by [`lib/pages/home_page.ex`](lib/pages/home_page.ex). FAQ uses the sticky split (`layout={:sticky}` in [`lib/layouts/section.ex`](lib/layouts/section.ex)). +- **Blog:** index at [`/blog`](lib/pages/blog_index_page.ex) (`layout_heading`, cards, pagination); 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. MDX-style Tableau extras (tags, `include_dir`, sitemap) are summarized in **Tableau data, tags, and static extras** in the longer notes below. @@ -59,7 +61,7 @@ MDX-style Tableau extras (tags, `include_dir`, sitemap) are summarized in **Tabl - `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), a11y head script; header condensed + countdown in [`assets/js/landing-scroll-chrome.js`](assets/js/landing-scroll-chrome.js) (`hidden` + `inert` until past the hero sentinel). +- Client UI: [`assets/js/theme.js`](assets/js/theme.js), [`assets/js/mode.js`](assets/js/mode.js), a11y head script; header condensed + countdown in [`assets/js/landing-scroll-chrome.js`](assets/js/landing-scroll-chrome.js) (`hidden` + `inert` until past the hero sentinel). Optional theme skins in [`assets/js/skins.js`](assets/js/skins.js). ## Production and hosting diff --git a/_posts/2026-03-12-duis-aute.md b/_posts/2026-03-12-duis-aute.md new file mode 100644 index 0000000..095f561 --- /dev/null +++ b/_posts/2026-03-12-duis-aute.md @@ -0,0 +1,67 @@ +--- +layout: Soonex.PostLayout +title: First cut of the waitlist +date: 2026-03-12 12:00:00 +0000 +permalink: /blog/duis-aute-irure/ +description: Hero, named form fields, and a toast path so a founder can see success before attaching a list. +image: /images/photos/hero.jpg +image_alt: Sunlit studio with long work tables and hanging task lamps +tags: + - Studio + - Notes +sitemap: + priority: 0.6 + changefreq: monthly +--- + +The first thing a launch template has to do is look like a product, not a component gallery. We rebuilt the waitlist band around that constraint: one conversion path above the fold, a fuller form at the bottom, and a success state you can demo without standing up Mailchimp on day one. + +## Why the waitlist comes first + +Most “coming soon” pages treat email capture as an afterthought—a single input lost under a hero illustration. Soonex inverts that. The hero carries a compact email field so visitors who are ready can convert immediately. The waitlist section below adds context: role, interests, team size, and a short note field. Together they read like a product signup, not a generic newsletter block. + +That split matters for founders who need to show investors or early customers a credible funnel before backend work lands. You can walk someone through the page, submit the form, and show the toast without apologising for placeholder behaviour. + +## What shipped in this cut + +The forms use **named fields** throughout. That is deliberate: when you wire a provider, you should not rename inputs or restructure markup. + +| Field | Purpose | +| --- | --- | +| `waitlist[email]` | Primary identifier | +| `waitlist[role]` | Founder, engineer, designer, etc. | +| `waitlist[interests]` | Multi-select tags for segmentation | +| `waitlist[team_size]` | Company stage signal | +| `waitlist[notes]` | Free-text launch context | + +Submit handlers in this demo only trigger a Corex toast. No addresses are stored server-side. The copy in the toast is easy to replace when you connect ConvertKit, Buttondown, or your own endpoint. + +### Hero vs full band + +- **Hero form** — email plus a single Join button. Keeps the fold tight on mobile. +- **Waitlist band** — full field set inside a framed panel. Skins restyle the panel without touching field names. +- **Toast** — confirms success with title and description slots you control in HEEx. + +## Wiring a real list later + +When you are ready to collect addresses, keep the markup and point the form action at your provider or a small Phoenix/LiveView handler. The template already validates email client-side; add server-side validation and rate limiting in production. + +1. Choose a list provider or build an internal endpoint. +2. Replace the demo submit handler with a POST to that endpoint. +3. Update the privacy page to describe what you store and for how long. +4. Turn on analytics only after updating cookie consent if you measure conversions. + +> The waitlist is the conversion. Everything else on the page exists to make that click feel finished. + +> [!NOTE] +> This demo does not collect addresses. The same form structure is what you ship when your list is live. + +## Accessibility and motion + +Both forms use Corex native inputs and selects with visible labels or screen-reader-only labels where space is tight. Reduced motion does not remove the forms; it only disables ornamental scroll reveals. Test keyboard flow from the hero through the waitlist band before you swap photography. + +## What we did not add + +We intentionally skipped double opt-in UI, referral codes, and social login. Those belong in your product layer once you know your compliance requirements. The template gives you a professional shell; you add policy and persistence. + +If you fork Soonex today, replace the hero still, rewrite the toast copy, and you already have a waitlist story worth showing—not a lorem block waiting for design polish. diff --git a/_posts/2026-04-08-excepteur.md b/_posts/2026-04-08-excepteur.md new file mode 100644 index 0000000..010a3a6 --- /dev/null +++ b/_posts/2026-04-08-excepteur.md @@ -0,0 +1,80 @@ +--- +layout: Soonex.PostLayout +title: Four skins, one markup tree +date: 2026-04-08 09:00:00 +0000 +permalink: /blog/excepteur-sint-occaecat/ +description: Neo, uno, duo, and leo stop sharing one layout. Each theme has a CSS file you can delete. +image: /images/photos/studio.jpg +image_alt: A calm studio interior with a low sofa, plants, and a large window +tags: + - Design + - Studio +sitemap: + priority: 0.6 + changefreq: monthly +--- + +Recoloring a generic landing page is not a theme. Soonex keeps one HEEx tree and lets each Corex theme restyle chrome, density, and ornament from an isolated skin file. Switching from neo to leo should feel like opening a different template—not toggling a palette. + +## The split of responsibilities + +**Corex config** owns design tokens: color seeds, radius, typography stacks, contrast targets, and light/dark overlays. Run `mix corex.design.build` after you change `config/config.exs`. + +**Skin CSS** owns composition: nav shape, hero layout, FAQ treatment, pricing cards, and section rhythm. Each skin lives in `assets/css/skins/{theme}.css` and loads only when that theme is active via `data-theme` on the document root. + +That separation means you can tune brand colors globally while letting neo stay quiet and leo stay poster-loud without conditional spaghetti in HEEx. + +## What each skin is for + +| Theme | Reference feel | What changes | +| --- | --- | --- | +| neo | Salient | Centered sticky nav, soft hero grid, underline accent on headlines | +| uno | Shellular | Pill-shaped header bar, rounded cards, italic accent type | +| duo | Studio | Editorial hero with photo panel, serif accents in FAQ answers | +| leo | Keynote | Zero-radius frames, uppercase chrome, stacked hero, rule-based sections | + +None of these skins duplicate page modules. The home page, journal, and privacy route render the same structure; only CSS selectors under `[data-theme="…"]` differ. + +## Optional skin JavaScript + +Each theme can ship a tiny loader under `assets/js/skins/{theme}.js`. Loaders attach scroll-driven classes or hero parallax where motion is allowed. When `prefers-reduced-motion` is set—or when the accessibility panel disables motion—the loaders no-op. You can delete a skin’s JS file if you want a CSS-only look. + +### Checklist for a new skin + +- [x] Isolated CSS under `assets/css/skins` +- [x] Import in `assets/css/site.css` +- [x] Optional JS registered in `assets/js/skins.js` +- [ ] Your brand stills in the hero frame +- [ ] Copy pass so voice matches the visual tone + +> [!TIP] +> Delete `assets/css/skins/leo.css` and drop `:leo` from config to remove that look entirely. No HEEx edits required. + +## Tokens only in host CSS + +Skins may use `color-mix` against tokens but must not introduce opacity modifiers like `bg-surface/90`. That keeps contrast predictable when users crank accessibility settings. If a section needs separation, mix surface against root or add a border token—not alpha hacks on utility classes. + +## Launch date stays centralised + +Every skin reads the same launch target from one module: + +```elixir +defmodule Soonex.Launch do + def year_label, do: "1 December 2026" +end +``` + +Hero badges, stats bands, and the header countdown all call into `Soonex.Launch`. When you change the date for your product, edit one file—not four skin stylesheets. + +## Choosing a default + +Pick the skin that matches how you want founders to *feel* about your product on first load: + +- **neo** — credible SaaS, minimal ornament +- **uno** — friendly dev tool, rounded and approachable +- **duo** — design-led, photography-forward +- **leo** — bold announcement, conference keynote energy + +Set the default theme in `config/config.exs` and document the choice in your README. Visitors can still switch themes from Template Options; the demo is meant to show range, not indecision. + +If a theme switch is invisible, the skin failed. Soonex exists so that switch is obvious within the first scroll. diff --git a/_posts/2026-05-08-docs.md b/_posts/2026-05-08-docs.md new file mode 100644 index 0000000..0584311 --- /dev/null +++ b/_posts/2026-05-08-docs.md @@ -0,0 +1,192 @@ +--- +layout: Soonex.PostLayout +title: Using the Soonex template +date: 2026-05-08 12:00:00 +0000 +permalink: /blog/using-the-soonex-template/ +description: Clone, mix setup, mix server. Themes live in config. Skins live in CSS you can delete. +image: /images/covers/overlay.jpg +image_alt: Soft abstract color field in warm paper tones +tags: + - Themes + - Template +sitemap: + priority: 0.9 + changefreq: monthly +--- + +Soonex is a Tableau + Corex launch template. The waitlist, journal, countdown chrome, cookie consent, and four skins ship wired. Your first hour should go to photography, copy, and the launch date—not rebuilding sections from scratch. + +## Quick start + +```bash +git clone https://github.com/corex-ui/soonex.git +cd soonex +mix deps.get +mix setup +mix server +``` + +Open `http://localhost:4999` after the alias finishes. The dev server checks that port 4999 is free before starting Tableau preview. Do not use `mix soonex.server`; the alias in `mix.exs` is `mix server`. + +Production output lands in `_site/`. Run `mix assets.build` then `mix tableau.build` before deploy. + +## Customize brand and SEO + +Swap the lockup in `lib/layouts/brand.ex` and replace `extra/images/logo.svg`. Page titles and meta descriptions live in `lib/layouts/root_layout.ex` and per-page front matter. Set `SOONEX_PUBLIC_URL` in config when your origin is real so canonical URLs and Open Graph tags resolve correctly. + +Use **bold**, *italic*, ***combined***, `inline code`, and ~~strikethrough~~ in journal posts—the Markdown pipeline supports GitHub-style extensions. + +### Links and autolink + +Markdown link: [Hexdocs Corex](https://hexdocs.pm/corex). Bare URL with autolink: https://hexdocs.pm/mdex/MDEx.html. + +--- + +## Themes and skins + +Overlay seeds, light/dark colors, radius, font, and typography in `config/config.exs`, then run: + +```bash +mix corex.design.build +``` + +Visual composition lives in `assets/css/skins/{theme}.css`. Delete a skin file and its `@import` in `assets/css/site.css` to drop that look. + +### Workflow + +1. Edit `config :corex_design` for tokens +2. Run `mix corex.design.build` +3. Adjust skin CSS for layout differences +4. Switch themes from Template Options to verify contrast + +### Task list for a fork + +- [x] Self-hosted type stacks +- [x] Cookie consent banner and privacy page +- [ ] Your product stills in hero and journal covers +- [ ] A real waitlist endpoint +- [ ] Updated privacy copy for your jurisdiction + +--- + +## Section map + +| Home section | Module | Notes | +| --- | --- | --- | +| Hero | `Soonex.HomePage.Hero` | Badge, forms, hero photo | +| Logos | `Soonex.HomePage.Logos` | Tech marquee, original SVGs only | +| Features | `Soonex.HomePage.Features` | Bento collapsibles | +| Product | `Soonex.HomePage.Product` | Split copy and visual | +| Steps | `Soonex.HomePage.Steps` | Numbered how-it-works | +| Pricing | `Soonex.HomePage.Pricing` | Three plans, one featured | +| Launch | `Soonex.HomePage.LaunchBand` | Dated beats to public launch | +| Voices | `Soonex.HomePage.Voices` | Quote grid | +| FAQ | `Soonex.HomePage.Faq` | Accordion, custom trigger copy | +| Waitlist | `Soonex.HomePage.Waitlist` | Full form band | +| Journal | `Soonex.HomePage.Journal` | Latest three posts | + +Journal posts live in `_posts/` with `Soonex.PostLayout`. Tags index and blog index paginate through Corex pagination components. + +--- + +## Markdown features + +This post exercises what Tableau + MDEx render: + +> Fork the template. Replace the copy. Keep the tokens. + +> If a theme switch is invisible, the skin failed. + +### Images + +Default Open Graph asset: + +![Soonex Open Graph placeholder](/images/og.svg) + +Tech marks in the marquee use originals from `extra/images/tech/`: + +![Elixir](/images/tech/elixir.svg) + +![Phoenix](/images/tech/phoenixframework.svg) + +![Tailwind](/images/tech/tailwindcss.svg) + +![Hex](/images/tech/hex.svg) + +--- + +## GitHub-style alerts + +> [!NOTE] +> Contrast stays calculated. Do not lower `*-contrast` / `*-text` targets in the overlay. + +> [!TIP] +> `mix server` fails clearly if port 4999 is already taken. + +> [!IMPORTANT] +> Do not use token opacity such as `bg-surface/90`. Skins may `color-mix` a token against `transparent`. + +> [!WARNING] +> Host overlays cannot set `*_scale`, duration, or opacity. The validator rejects those keys. + +> [!CAUTION] +> Reduced motion disables scroll reveal, card lift, and skin JS. + +--- + +## Footnotes + +Launch date lives in `Soonex.Launch`[^alpha]. Header countdown appears after the hero[^beta]. + +[^alpha]: Change the target in one module. Hero badge and header timer both read it. +[^beta]: Header timer stays in flow (`invisible` + `inert`) until the hero sentinel is past. + +--- + +## Code samples + +Countdown helper: + +```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 +``` + +Build pipeline: + +```bash +mix deps.get +mix setup +mix assets.build +mix tableau.build +``` + +Skin loader entry: + +```javascript +export function initSkins() { + apply(document.documentElement.dataset.theme) +} +``` + +Inline: run `mix server` then open `http://localhost:4999`. + +--- + +## Horizontal rule and HTML + +Trusted raw HTML with `render: [unsafe: true]`: + +
        +Keyboard shortcuts in docs +

        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 new file mode 100644 index 0000000..1678a88 --- /dev/null +++ b/_posts/2026-06-01-shipping.md @@ -0,0 +1,92 @@ +--- +layout: Soonex.PostLayout +title: Shipping Tableau and Corex +date: 2026-06-01 12:00:00 +0000 +permalink: /blog/shipping-tableau-corex/ +description: Mix, Tailwind, and esbuild are already in the repo. CI has to stay green before Pages deploys. +image: /images/covers/shipping.jpg +image_alt: A laptop on a wooden desk with a code editor open +tags: + - Launch + - Notes +sitemap: + priority: 0.7 + changefreq: monthly +--- + +Shipping a static launch page should feel boring: green CI, deterministic build output, and a CDN that only updates when you mean it to. Soonex targets GitHub Pages with GitHub Actions as the source, but the same `_site/` artifact works on Netlify, Cloudflare Pages, or any static host. + +## Build output + +Everything public lives under `_site/` after a full build. HTML is generated by Tableau from Elixir page modules and Markdown posts. CSS comes from Tailwind plus Corex design tokens. JavaScript bundles through esbuild, including skin loaders and cookie consent. + +There is no runtime BEAM process in production—only files. That keeps hosting cost at zero and makes rollbacks a matter of redeploying a previous commit. + +## Local build sequence + +| Step | Command | Output | +| --- | --- | --- | +| Design tokens | `mix corex.design.build` | `assets/corex/` | +| CSS | `mix tailwind default` | `_site/css/site.css` | +| JS | `mix esbuild default` | `_site/js/` | +| Site | `mix tableau.build` | `_site/**/*.html` | + +The `mix assets.build` alias runs design, Tailwind, and esbuild together. Run it before `mix tableau.build` in CI so pages reference fresh assets. + +## CI checklist + +- [x] `mix assets.build` for CSS and JS +- [x] `mix tableau.build` for static HTML +- [x] `mix test` including accessibility audit +- [ ] Your `SOONEX_PUBLIC_URL` on the real origin +- [ ] Custom domain and HTTPS on the host + +GitHub Actions should publish Pages **only after** CI succeeds on a push to `main`. Set **Source** to GitHub Actions in the repository settings so a broken push never reaches the public CDN. + +> [!TIP] +> Keep CI green before Pages deploys. A failing test is cheaper than a broken launch page indexed by search engines. + +## Environment and URLs + +Set `SOONEX_PUBLIC_URL` when the origin is real. The demo default is `https://corex-ui.github.io/soonex`. Canonical links, sitemap entries, and Open Graph URLs all read that config value at build time. + +If you deploy to a subdirectory, keep the trailing path consistent in config and in your host’s base URL settings. Tableau permalinks are root-relative within `_site/`. + +## What we verify in CI + +```elixir +defmodule SoonexTest do + use ExUnit.Case + + test "accessibility audit passes on built HTML" do + assert File.dir?("_site") + # a11y_audit scans key routes + end +end +``` + +Formatting (`mix format --check-formatted`), compilation with `--warnings-as-errors`, Credo, and tests run on every push. Treat warnings as failures so drift does not accumulate. + +## Assets and cache + +Fingerprinting is not enabled by default—static filenames like `/css/site.css` are simple for a template. For high-traffic production forks, add cache-busting query strings or content hashes in your host config. + +Images ship from `extra/images/` into `_site/images/`. Optimise hero and cover photos before commit; the build does not compress JPEGs for you. + +## Rollback plan + +Because output is static, rollback is: + +1. Revert or fix the commit on `main` +2. Let CI rebuild and redeploy +3. Purge CDN cache if your host caches aggressively + +Keep launch-day changes small. Prefer copy and image swaps over structural edits unless you have rehearsed the build locally. + +Set `SOONEX_PUBLIC_URL` when the origin is real. The demo default is `https://corex-ui.github.io/soonex`[^ship]. + +[^ship]: Update this footnote and the privacy page when your production domain differs from the demo. + +![Soonex Open Graph](/images/og.svg) + +When `_site/` matches what you previewed locally, shipping is done. Everything after that is DNS and announcement—not pipeline archaeology. diff --git a/_posts/2026-07-14-nemo-enim.md b/_posts/2026-07-14-nemo-enim.md new file mode 100644 index 0000000..b1bfcf9 --- /dev/null +++ b/_posts/2026-07-14-nemo-enim.md @@ -0,0 +1,78 @@ +--- +layout: Soonex.PostLayout +title: Road to 1 December +date: 2026-07-14 15:00:00 +0000 +permalink: /blog/nemo-enim-ipsam/ +description: Four launch beats. Replace the dates in Soonex.Launch. The header countdown reads the same target. +image: /images/photos/orbit.jpg +image_alt: Earth from orbit against a dark sky +tags: + - Launch + - Calendar +sitemap: + priority: 0.6 + changefreq: monthly +--- + +Launch pages often drown visitors in a twelve-month roadmap nobody asked for. Soonex trims the story to **four beats**—each dated, each scannable—leading to a single public launch. The home band mirrors what a founder would actually say out loud: waitlist open, skins ready, journal live, ship date. + +## Why four beats + +A long calendar widget competes with the waitlist. Four cards fit above the fold on desktop and stack cleanly on mobile. Each beat can be featured with `data-featured="true"` styling so the current milestone reads louder without animation. + +The beats are content, not config. Edit copy in `Soonex.HomePage.LaunchBand` when your story changes. Only the **target date** is shared infrastructure. + +## One module for the countdown + +The public launch moment lives in `Soonex.Launch`: + +- `@target` — UTC datetime for 1 December 2026 in the demo +- `year_label/0` — human-readable label for hero badge and stats +- `countdown_ms/0` — milliseconds until launch for the header timer + +Hero badge and header timer both read these functions. Change the date once; do not scatter `DateTime.diff` across templates. + +> [!IMPORTANT] +> Change the date in one module. Skins, hero, header, and launch band stay in sync automatically. + +## Header timer behaviour + +The countdown in the nav uses Corex `<.timer>` with compact digits and **d h m s** unit labels. It stays invisible and inert until the visitor scrolls past the hero sentinel—then it fades in without shifting header height. That avoids the “sticky bar jumps when numbers appear” problem on long hero sections. + +Reduced motion keeps the timer visible but disables ornamental transitions. Digits still update every second. + +## What each beat represents in the demo + +| Beat | Demo meaning | +| --- | --- | +| Waitlist | Forms and toast path ready | +| Skins | Four themes shippable from CSS | +| Journal | Blog, tags, pagination wired | +| Launch | Public date shown in hero and timer | + +Replace these labels with your product milestones: private beta, design partners, pricing page, GA—whatever matches your go-to-market. + +## Stats band alignment + +The stats section under the hero echoes the same launch year so numbers and countdown never contradict. If you change `@target` to a different year, update stat copy in `Soonex.HomePage.Stats` in the same commit. + +The public launch beat is **1 December 2026** until you replace it[^orbit]. + +[^orbit]: `Soonex.Launch.year_label/0` is what the hero and stats band print. + +## Local preview + +```bash +mix server +``` + +Scroll the home page: hero badge shows the year label, header timer appears after the hero, launch band lists four beats with the final beat featured. Switch themes—the date does not move because it is not skin-local. + +## After you fork + +1. Set `@target` to your ship date. +2. Rewrite beat titles and descriptions. +3. Update journal posts so the narrative matches your timeline. +4. Announce when the waitlist opens for real—not when the template demo loads. + +A credible launch page tells time in one voice. Soonex keeps that voice in one module. diff --git a/_posts/2026-08-02-temporibus.md b/_posts/2026-08-02-temporibus.md new file mode 100644 index 0000000..736d186 --- /dev/null +++ b/_posts/2026-08-02-temporibus.md @@ -0,0 +1,75 @@ +--- +layout: Soonex.PostLayout +title: Voices after the skins +date: 2026-08-02 11:00:00 +0000 +permalink: /blog/temporibus-autem/ +description: Testimonials are a grid you can scan. Skins restyle the cards. The quotes stay placeholders you replace. +image: /images/covers/overlay.jpg +image_alt: Soft abstract color field in warm paper tones +tags: + - Voices + - Notes +sitemap: + priority: 0.6 + changefreq: monthly +--- + +Social proof on launch pages often arrives as an auto-advancing carousel—pretty in a keynote, fragile on mobile, and easy to ignore. Soonex uses a **three-column quote grid** on the home page instead. Visitors scan all three voices in one glance; skins restyle the cards without a second template. + +## Grid over carousel + +Carousels hide content behind interaction and motion. A grid respects reduced-motion preferences and works when JavaScript is slow or blocked. Duo can go editorial with serif pull quotes; leo can strip radius and lean on rules; neo keeps cards flat and quiet—all from the same HEEx in `Soonex.HomePage.Voices`. + +The journal link below the grid sends readers to longer-form posts. Home shows three voices; `/blog` paginates the rest. + +## What to replace + +The demo ships placeholder names, roles, and portraits under `extra/images/photos/`. Keep the structure; swap the substance. + +1. **Portraits** — use real customers, team members, or licensed stock at consistent aspect ratio. +2. **Quotes** — mention outcomes, not adjectives. “We shipped in a week” beats “Beautiful template.” +3. **Roles** — job title and company, or “Early access” if you lack logo permission yet. + +> If a theme switch is invisible, the template failed. The same applies to testimonials—if quotes could belong to any SaaS, rewrite them. + +### Content guidelines + +- One sentence per quote when possible; two if you need a concrete metric. +- Avoid superlatives without evidence. +- Match tone to the active skin: leo can be punchy; duo can be reflective. + +> [!WARNING] +> Do not add a second carousel for the journal. The home grid is three cards; `/blog` paginates the rest. + +## Skin-specific presentation + +| Skin | Card treatment | +| --- | --- | +| neo | Minimal border, small avatar | +| uno | Rounded surface cards, italic quote | +| duo | Serif quote body, wider gutters | +| leo | Square avatars, uppercase roles | + +Markup stays identical—only `[data-theme]` CSS changes spacing, type, and borders. + +## Accessibility + +Avatars include `alt` text derived from names in the demo. When you replace photos, write alt that identifies the speaker, not “headshot.” Quotes are plain paragraphs, not images of text, so they reflow at large type sizes from the accessibility panel. + +## Connecting to real customers + +When you have permission to publish testimonials: + +1. Collect quote, name, role, and optional logo in writing. +2. Update the `voice_items/0` list in `Soonex.HomePage.Voices`. +3. Add a journal post with the full case study and link from the grid if needed. + +Use **bold**, *italic*, and `inline code` in journal case studies when you cite product details—same Markdown pipeline as this post. + +## What we left out + +No star ratings, no Twitter embeds, no video testimonials. Those add third-party weight and consent complexity. Add them in your fork if your market expects them—and disclose them on the privacy page. + +![Soonex Open Graph placeholder](/images/og.svg) + +Replace the placeholders before you send traffic. Social proof only works when it sounds like your product—not like a theme demo. diff --git a/assets/css/chrome.css b/assets/css/chrome.css index da04ba0..abfa6df 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -1,4 +1,4 @@ -/* Framer-style launch chrome. Tokens only — brand as accent, never hero fill. */ +/* Tiny layout helpers that utilities cannot express. */ html { scroll-behavior: smooth; @@ -12,81 +12,17 @@ html[data-motion="reduce"] { transition: none; } -@supports (animation-timeline: view()) { - @keyframes soonex-reveal { - from { - transform: translateY(var(--spacing-space)); - } - - to { - transform: none; - } - } - - .soonex-reveal { - animation-name: soonex-reveal; - animation-duration: var(--duration-slow); - animation-timing-function: var(--soonex-ease); - animation-fill-mode: none; - animation-timeline: view(); - animation-range: entry 0% entry 35%; - } - - html[data-motion="reduce"] .soonex-reveal { - animation: none; - transform: none; - } -} - -.soonex-card-motion { - transition: - border-color var(--duration-fast) var(--soonex-ease), - box-shadow var(--duration-fast) var(--soonex-ease), - transform var(--duration-fast) var(--soonex-ease); -} - -@media (hover: hover) { - .soonex-card-motion:hover { - border-color: var(--color-brand); - box-shadow: var(--shadow-sm); - } -} - -html[data-motion="reduce"] .soonex-card-motion, -html[data-motion="reduce"] .soonex-card-motion:hover { - transition: none; - transform: none; -} - -/* Floating / frosted nav */ -.soonex-framer-nav { - background-color: color-mix(in oklab, var(--color-root) 82%, transparent); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); -} - -[data-mode="dark"] .soonex-framer-nav { - background-color: color-mix(in oklab, var(--color-root) 78%, transparent); -} - [data-site-header] { - transition: - border-color var(--duration-normal) var(--soonex-ease), - background-color var(--duration-normal) var(--soonex-ease); + transition: border-color var(--duration-normal) var(--soonex-ease); } [data-site-header] [data-header-bar] { min-height: var(--spacing-size); } -@media (width >= 64rem) { - .soonex-sticky-beat { - position: sticky; - top: var(--soonex-header-offset); - z-index: 1; - } -} - +/* Header timer stays in flow (invisible until reveal) so the bar never jumps. + Corex digits flip in a 2em window (`top: calc(var(--value) * -2em)`). Do not + shrink that window — it clips the strip. Compact the type instead. */ [data-site-header] .timer.ui-size-sm { --timer-digit: var(--text-sm); --timer-digit-leading: 2; @@ -101,15 +37,11 @@ html[data-motion="reduce"] .soonex-card-motion:hover { } [data-site-header] .timer [data-scope="timer"][data-part="item-label"] { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); + font-size: var(--text-xs); + line-height: 1; + font-variant-numeric: tabular-nums; + color: var(--color-ink-muted); white-space: nowrap; - border-width: 0; } @media (width < 40rem) { @@ -117,7 +49,7 @@ html[data-motion="reduce"] .soonex-card-motion:hover { .timer.ui-size-md, .timer.ui-size-lg { --timer-digit: var(--text-xl); - --timer-digit-leading: 2; + --timer-digit-leading: var(--text-xl--line-height); } .timer [data-scope="timer"][data-part="area"] { @@ -128,7 +60,7 @@ html[data-motion="reduce"] .soonex-card-motion:hover { } .soonex-frame { - border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); + border-radius: var(--radius-2xl); } .soonex-photo { @@ -143,270 +75,91 @@ html[data-motion="reduce"] .soonex-card-motion:hover { align-items: center; } -.soonex-avatars .avatar + .avatar { +.soonex-avatars li + li { margin-inline-start: calc(var(--spacing-space-sm) * -1); } -.soonex-avatars .avatar [data-scope="avatar"][data-part="root"] { +.soonex-avatar { + width: 2.25rem; + height: 2.25rem; + object-fit: cover; + border-radius: var(--radius-full); border: 2px solid var(--color-root); } -/* Hero media + soft root wash (not brand fill) */ -.soonex-hero-wash { - background: linear-gradient( - 180deg, - color-mix(in oklab, var(--color-root) 12%, transparent) 0%, - color-mix(in oklab, var(--color-root) 55%, transparent) 42%, - var(--color-root) 100% - ); -} - -@media (width >= 64rem) { - .soonex-hero-wash { - background: linear-gradient( - 105deg, - var(--color-root) 0%, - color-mix(in oklab, var(--color-root) 78%, transparent) 38%, - color-mix(in oklab, var(--color-root) 28%, transparent) 100% - ); - } -} - -.soonex-waitlist-object { - border: 1px solid var(--color-border); - background-color: var(--color-root); - border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); - padding: calc(var(--spacing-space) * 1.2); - box-shadow: var(--shadow-sm); -} - -@media (width >= 40rem) { - .soonex-waitlist-object { - padding: calc(var(--spacing-space) * 1.45); - } -} - -.soonex-waitlist-join { - gap: var(--spacing-space-sm); +.soonex-avatar-lg { + width: 3.5rem; + height: 3.5rem; } -@media (width >= 40rem) { - .soonex-waitlist-join { - align-items: stretch; - gap: 0; - } - - .soonex-waitlist-join .native-input [data-scope="native-input"][data-part="input"] { - border-end-end-radius: 0; - border-start-end-radius: 0; - } - - .soonex-waitlist-join > button { - border-start-start-radius: 0; - border-end-start-radius: 0; - margin-inline-start: -1px; - } -} - -/* Marquee */ -.soonex-marquee-band { - overflow: hidden; -} - -.soonex-marquee-chip { - display: inline-flex; +.marquee [data-scope="marquee"][data-part="content"] { align-items: center; - gap: 0.55rem; - font-size: var(--text-sm); - font-weight: var(--font-weight-semibold); - color: var(--color-ink); - white-space: nowrap; } -.soonex-marquee-dot { - width: 0.45rem; - height: 0.45rem; - border-radius: 9999px; - background-color: var(--color-brand); +.marquee [data-scope="marquee"][data-part="item"] { flex-shrink: 0; } -/* Bento */ -.soonex-bento { - margin: 0; - padding: 0; - list-style: none; - display: grid; - grid-template-columns: 1fr; - gap: var(--spacing-space); +.marquee [data-scope="marquee"][data-part="edge"] { + --edge-background: var(--color-root); } -@media (width >= 40rem) { - .soonex-bento { - grid-template-columns: 1fr 1fr; - } - - .soonex-bento-wide { - grid-column: 1 / -1; - } -} - -@media (width >= 64rem) { - .soonex-bento { - grid-template-columns: 1.2fr 1fr; - gap: calc(var(--spacing-space) * 1.1); - } - - .soonex-bento-wide { - grid-column: auto; - } - - .soonex-bento-cell:first-child { - grid-row: span 2; - } -} - -.soonex-bento-cell { - border: 1px solid var(--color-border); - background-color: var(--color-surface); - border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); - padding: calc(var(--spacing-space) * 1.25); +.marquee [data-scope="marquee"][data-part="edge"][data-side="start"], +.marquee [data-scope="marquee"][data-part="edge"][data-side="end"] { + width: 3rem; + max-width: 3rem; } -/* Product tabs */ -.tabs.soonex-product-tabs [data-scope="tabs"][data-part="list"] { - gap: 0.25rem; - border: 1px solid var(--color-border); - border-radius: var(--soonex-ctl-radius, var(--radius-xl)); - background-color: var(--color-ui); - padding: 0.25rem; +html[data-motion="reduce"] .marquee [data-scope="marquee"][data-part="content"] { + animation: none; } -.tabs.soonex-product-tabs [data-scope="tabs"][data-part="trigger"] { - font-weight: var(--font-weight-semibold); - border-radius: var(--soonex-ctl-radius, var(--radius-lg)); +.data-list.data-list--wide, +.tabs.tabs--wide { + max-width: none; } -.tabs.soonex-product-tabs [data-scope="tabs"][data-part="content"] { - margin-block-start: calc(var(--spacing-space) * 1.1); - padding: calc(var(--spacing-space) * 1.25); - background-color: var(--color-root); - border: 1px solid var(--color-border); - border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); - min-height: 14rem; -} - -.soonex-tab-workspace { - max-width: 40rem; -} - -.soonex-tab-workspace .soonex-room-specimen { - font-size: var(--text-5xl); -} - -.soonex-feature-bullets { - margin: 0; - padding: 0; - list-style: none; +.data-list.data-list--wide + [data-scope="data-list"][data-part="root"][data-orientation="horizontal"] + [data-scope="data-list"][data-part="item"] { display: flex; - flex-direction: column; - gap: calc(var(--spacing-space) * 0.55); -} - -.soonex-feature-bullets > li { - position: relative; - padding-inline-start: 1.1rem; - color: var(--color-ink-muted); - font-size: var(--text-sm); - line-height: var(--leading-relaxed); -} - -.soonex-feature-bullets > li::before { - content: ""; - position: absolute; - inset-inline-start: 0; - top: 0.55em; - width: 0.4rem; - height: 0.4rem; - border-radius: 9999px; - background-color: var(--color-brand); -} - -.soonex-clone-strip { - display: flex; - flex-direction: column; - gap: var(--spacing-space); - border-block-start: 1px solid var(--color-border); - padding-block-start: calc(var(--spacing-space) * 1.25); + width: 100%; + flex-direction: row; + align-items: center; } -@media (width >= 64rem) { - .soonex-clone-strip { - flex-direction: row; - align-items: end; - gap: calc(var(--spacing-space) * 1.5); - } +.data-list.data-list--wide + [data-scope="data-list"][data-part="root"][data-orientation="horizontal"] + [data-scope="data-list"][data-part="label"] { + min-width: 4.5rem; + max-width: 8rem; + width: auto; + flex: 0 0 auto; } -.soonex-pricing { +.data-list.data-list--wide + [data-scope="data-list"][data-part="root"][data-orientation="horizontal"] + [data-scope="data-list"][data-part="content"] { display: flex; - flex-direction: column; - border-block: 1px solid var(--color-border); -} - -.soonex-pricing-tier { - padding-block: calc(var(--spacing-space) * 1.25); - border-block-end: 1px solid var(--color-border); -} - -.soonex-pricing-tier:last-child { - border-block-end: 0; -} - -/* Looks bento */ -.soonex-looks-bento { - margin: 0; - padding: 0; - list-style: none; - display: grid; - grid-template-columns: 1fr; - gap: var(--spacing-space); -} - -@media (width >= 40rem) { - .soonex-looks-bento { - grid-template-columns: 1fr 1fr; - } -} - -.soonex-look-tile { - border: 1px solid var(--color-border); - background-color: var(--color-surface); - border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); - padding: calc(var(--spacing-space) * 1.25); -} - -/* Voices carousel */ -.soonex-voices-carousel [data-scope="carousel"][data-part="item"] { - align-items: stretch; + flex: 1 1 0%; + width: auto; + min-width: 0; + align-items: center; } -.soonex-voice-slide { - border: 1px solid var(--color-border); - background-color: var(--color-root); - border-radius: var(--soonex-ctl-radius, var(--radius-2xl)); - padding: calc(var(--spacing-space) * 1.5); - height: 100%; +.data-list.data-list--wide + [data-scope="data-list"][data-part="root"][data-orientation="horizontal"] + [data-scope="data-list"][data-part="content"] + .link { + max-width: none; + width: 100%; } -.accordion.soonex-faq-instrument [data-scope="accordion"][data-part="item-trigger"] { - font-weight: var(--font-weight-semibold); - padding-block: calc(var(--spacing-space) * 0.95); +.soonex-tags-list.data-list { + background-color: transparent; + border: 0; } -.select [data-scope="select"][data-part="trigger"], -.tags-input [data-scope="tags-input"][data-part="control"], -.number-input [data-scope="number-input"][data-part="control"], -.clipboard [data-scope="clipboard"][data-part="control"] { - border-radius: var(--soonex-ctl-radius, var(--radius-md)); +.soonex-tags-list.data-list [data-scope="data-list"][data-part="item"] { + background-color: transparent; } diff --git a/assets/css/hosts.css b/assets/css/hosts.css index 3ba4a21..0d9269a 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -3,23 +3,6 @@ [data-theme] { --soonex-ease: cubic-bezier(0.22, 1, 0.36, 1); --soonex-header-offset: calc(var(--spacing-size) + 1.5rem); - --soonex-ctl-radius: var(--radius-lg); - --soonex-ctl-border: var(--color-border); - --soonex-ctl-surface: var(--color-ui); - --soonex-ctl-shadow: var(--shadow-sm); -} - -body.layout { - background-color: var(--color-root); - background-image: none; -} - -[data-mode="dark"] body.layout { - background-image: none; -} - -html[data-motion="reduce"] body.layout { - background-attachment: scroll; } .button, @@ -38,11 +21,45 @@ html[data-motion="reduce"] body.layout { .marquee.ui-width-full, .carousel.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%; @@ -64,9 +81,9 @@ html[data-motion="reduce"] body.layout { } .carousel.ui-width-full - [data-scope="carousel"][data-part="root"][data-orientation="horizontal"] { - max-height: none; - aspect-ratio: auto; + [data-scope="carousel"][data-part="root"][data-orientation="vertical"] { + max-height: min(80svh, var(--container-md)); + padding-inline-end: var(--spacing-space); } .select.soonex-nav-select [data-scope="select"][data-part="trigger"] { @@ -109,6 +126,11 @@ html[data-motion="reduce"] body.layout { border-width: 0; } +.soonex-mark-wrap { + font-size: 2rem; + line-height: 1; +} + .menu.ui-width-fit [data-scope="menu"][data-part="root"] > [data-scope="menu"][data-part="trigger"] { width: auto; min-width: var(--ctl-size, var(--spacing-size)); @@ -122,303 +144,6 @@ html[data-motion="reduce"] body.layout { padding-block: var(--spacing-space); } -.collapsible.ui-width-full [data-scope="collapsible"][data-part="trigger"] { - max-width: none; - width: 100%; -} - -.tabs.ui-width-full { - max-width: none; -} - -.tabs.ui-width-full [data-scope="tabs"][data-part="root"] { - width: 100%; -} - -.tabs.ui-width-full [data-scope="tabs"][data-part="list"] { - display: flex; - width: 100%; - max-width: none; - flex-wrap: nowrap; - justify-content: stretch; - align-items: stretch; -} - -.tabs.ui-width-full [data-scope="tabs"][data-part="trigger"] { - display: flex; - flex: 1 1 0; - width: 0; - min-width: 0; - max-width: none; - justify-content: center; - text-align: center; -} - -.tabs.ui-width-full [data-scope="tabs"][data-part="content"][data-orientation="horizontal"] { - border-end-start-radius: var(--soonex-ctl-radius, var(--radius-md)); - border-end-end-radius: var(--soonex-ctl-radius, var(--radius-md)); -} - -.tabs.soonex-product-tabs [data-scope="tabs"][data-part="list"] { - gap: 0; -} - -.tabs.soonex-product-tabs [data-scope="tabs"][data-part="trigger"] { - font-weight: var(--font-weight-semibold); -} - -.carousel.ui-width-full [data-scope="carousel"][data-part="item"] { - align-items: stretch; -} - -.carousel.ui-width-full [data-scope="carousel"][data-part="item"] > * { - width: 100%; - height: 100%; -} - pre.code.code--wide { max-width: none; } - -/* - Soonex control language. There is no ui-input utility — triggers and field - hosts share --ctl-* so header CTA, waitlist submit, and inputs read as one family. -*/ -:is( - .button.ui-trigger--circle, - .button.ui-trigger--square, - .native-input, - .select, - .tags-input, - .number-input -) { - --ctl-space: var(--spacing-space); - --ctl-size: var(--spacing-size); - --ctl-text: var(--text-base); -} - -.native-input [data-scope="native-input"][data-part="input"], -.select [data-scope="select"][data-part="trigger"], -.tags-input [data-scope="tags-input"][data-part="control"], -.number-input [data-scope="number-input"][data-part="control"] { - background-color: var(--soonex-ctl-surface, var(--color-ui)); - border-color: var(--soonex-ctl-border, var(--color-border)); -} - -.button.ui-solid:not(.ui-ghost), -.button.ui-brand.ui-solid { - box-shadow: var(--soonex-ctl-shadow, var(--shadow-sm)); -} - -.select [data-scope="select"][data-part="trigger"], -.menu [data-scope="menu"][data-part="trigger"] { - --ctl-radius: inherit; - --ctl-size: var(--ctl-size, var(--spacing-size)); - --ctl-space: var(--ctl-space, var(--spacing-space)); - --ctl-text: var(--ctl-text, var(--text-base)); -} - -/* Dialog triggers are .button hosts. Keep ui-size-* (the Template Options - cluster is ui-size-sm). Only inherit radius so corners stay Soonex. */ -.dialog [data-scope="dialog"][data-part="trigger"] { - --ctl-radius: inherit; -} - -/* Corex pins floating-panel triggers to 0.8 * --spacing-size (md). Let - ui-size-sm on the demo cluster set one height with Accessibility. */ -.floating-panel [data-scope="floating-panel"][data-part="trigger"] { - min-height: var(--ctl-size, var(--spacing-size)); -} - -/* - Shared Soonex radius. :where() keeps specificity at 0 so ui-rounded-* - on a host still wins. Circles stay full. -*/ -[data-theme="neo"] { - --soonex-ctl-radius: var(--radius-xl); -} - -[data-theme="uno"] { - --soonex-ctl-radius: var(--radius-sm); -} - -[data-theme="duo"] { - --soonex-ctl-radius: var(--radius-3xl); -} - -[data-theme="leo"] { - --soonex-ctl-radius: var(--radius-none); -} - -:where( - .button, - .badge, - .native-input, - .avatar, - .menu, - .select, - .tabs, - .toast, - .pagination, - .clipboard, - .accordion, - .collapsible, - .carousel, - .date-picker, - .tags-input, - .number-input, - .radio-group, - .data-table, - .switch, - .checkbox, - .timer, - .tooltip, - .dialog, - .floating-panel -):not(.ui-trigger--circle) { - --ctl-radius: var(--soonex-ctl-radius); -} - -[data-theme="uno"] h1, -[data-theme="uno"] h2, -[data-theme="uno"] .soonex-quote, -[data-theme="uno"] .soonex-stat-value, -[data-theme="uno"] .soonex-room-specimen, -[data-theme="uno"] .soonex-room-name, -[data-theme="uno"] .soonex-card-title { - letter-spacing: -0.04em; -} - -[data-theme="duo"] blockquote, -[data-theme="duo"] .soonex-quote, -[data-theme="duo"] .soonex-tab-workspace > .lede, -[data-theme="duo"] .soonex-room .lede { - font-family: var(--font-serif); - font-style: italic; -} - -[data-theme="leo"] .soonex-room-specimen, -[data-theme="leo"] .soonex-room-name, -[data-theme="leo"] .soonex-card-title, -[data-theme="leo"] .soonex-lockup-word { - text-transform: uppercase; - letter-spacing: -0.02em; -} - -[data-theme="neo"] .soonex-room-specimen { - letter-spacing: -0.06em; -} - -.soonex-lockup-word { - font-family: var(--font-display); - font-size: var(--text-lg); - font-weight: var(--font-weight-semibold); - letter-spacing: var(--tracking-tight); - color: var(--color-ink); -} - -.soonex-card-title { - font-family: var(--font-display); - font-size: var(--text-xl); - font-weight: var(--font-weight-semibold); - letter-spacing: var(--tracking-tight); - line-height: var(--text-xl--line-height); - color: var(--color-ink); -} - -.soonex-quote { - font-family: var(--font-display); - font-size: var(--text-xl); - font-weight: var(--font-weight-semibold); - line-height: var(--leading-snug); - color: var(--color-ink); -} - -.soonex-stat-value { - font-family: var(--font-display); - font-size: var(--text-4xl); - font-weight: var(--font-weight-semibold); - letter-spacing: var(--tracking-tight); - line-height: 1; - color: var(--color-ink); -} - -.soonex-room-specimen { - font-family: var(--font-display); - font-size: var(--text-6xl); - font-weight: var(--font-weight-bold); - letter-spacing: var(--tracking-tighter); - line-height: 0.95; - color: var(--color-ink); -} - -.soonex-room-name { - font-family: var(--font-display); - font-size: var(--text-4xl); - font-weight: var(--font-weight-semibold); - letter-spacing: var(--tracking-tight); - line-height: 1; - color: var(--color-ink); -} - -.soonex-room { - color: var(--color-ink); -} - -small.eyebrow { - font-weight: var(--font-weight-semibold); - color: var(--color-brand-text); -} - -[data-theme="leo"] small.eyebrow { - font-size: var(--text-sm); - letter-spacing: var(--tracking-widest); - text-transform: uppercase; -} - -/* - Corex pins ui-trigger--square to radius-sm. Map square triggers (and the - same recipe on button/badge/toggle) onto Soonex's per-theme corner. -*/ -@layer utilities { - .tabs.ui-width-full { - max-width: none; - width: 100%; - } - - .tabs.ui-width-full [data-scope="tabs"][data-part="list"] { - display: flex; - width: 100%; - max-width: none; - flex-wrap: nowrap; - } - - .tabs.ui-width-full [data-scope="tabs"][data-part="trigger"] { - display: flex; - flex: 1 1 0; - width: 0; - min-width: 0; - max-width: none; - justify-content: center; - } - - .ui-trigger--square { - border-radius: var(--soonex-ctl-radius, var(--radius-md)); - --ctl-radius: var(--soonex-ctl-radius, var(--radius-md)); - } - - .button.ui-trigger--square, - .badge.ui-trigger--square { - border-radius: var(--soonex-ctl-radius, var(--radius-md)); - --ctl-radius: var(--soonex-ctl-radius, var(--radius-md)); - } - - .toggle.ui-trigger--square [data-scope="toggle"][data-part="root"] { - border-radius: var(--soonex-ctl-radius, var(--radius-md)); - --ctl-radius: var(--soonex-ctl-radius, var(--radius-md)); - } - - .ui-solid:not(.ui-trigger--square):not(.ui-trigger--circle) { - --ctl-radius: var(--soonex-ctl-radius, var(--radius-lg)); - } -} diff --git a/assets/css/layout.css b/assets/css/layout.css new file mode 100644 index 0000000..c4f6a5c --- /dev/null +++ b/assets/css/layout.css @@ -0,0 +1,166 @@ +/* Shared marketing rhythm. Tokens only. Skins override composition. */ + +.soonex-hero-grid { + display: grid; + align-items: center; + gap: 2.5rem; +} + +@media (width >= 64rem) { + .soonex-hero-grid { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 3rem; + } +} + +.soonex-hero-heading { + text-wrap: pretty; +} + +.soonex-accent { + color: var(--color-brand-text); +} + +.soonex-launch-badge { + margin: 0; +} + +.soonex-hero-form { + max-width: 28rem; +} + +.soonex-plan[data-featured="true"] { + background: var(--color-brand); + color: var(--color-brand-contrast); + border-color: var(--color-brand); +} + +.soonex-plan[data-featured="true"] .soonex-eyebrow, +.soonex-plan[data-featured="true"] .display, +.soonex-plan[data-featured="true"] p, +.soonex-plan[data-featured="true"] li { + color: inherit; +} + +.soonex-step-index, +.soonex-faq-index { + font-variant-numeric: tabular-nums; +} + +.soonex-tech-chip { + display: inline-flex; + flex-shrink: 0; + align-items: center; + gap: 0.75rem; + white-space: nowrap; +} + +.soonex-tech-chip img { + display: block; + width: 2rem; + height: 2rem; + object-fit: contain; + filter: none; + opacity: 1; +} + +.soonex-launch { + display: grid; + gap: 1rem; +} + +.soonex-launch-beat[data-featured="true"] { + border-color: var(--color-brand); +} + +.soonex-faq-answer .link, +.soonex-faq-answer .clipboard { + margin-top: 0.25rem; +} + +@media (width >= 40rem) { + .soonex-launch { + grid-template-columns: 1fr 1fr; + } +} + +.soonex-claim-icon { + display: flex; + width: 2.5rem; + height: 2.5rem; + align-items: center; + justify-content: center; + color: var(--color-brand-text); +} + +.soonex-faq-trigger { + display: flex; + width: 100%; + min-width: 0; + align-items: baseline; + gap: 0.75rem; + text-align: start; +} + +.soonex-faq [data-part="item-trigger"] { + padding-inline: calc(var(--spacing-space) + 0.75rem); +} + +.soonex-bento { + align-items: start; +} + +.soonex-bento-hero { + align-self: start; + width: 100%; +} + +.soonex-bento-cells .collapsible:nth-child(2) [data-scope="collapsible"][data-part="trigger"] { + padding-inline: calc(var(--spacing-space) + 0.75rem); +} + +@supports (animation-timeline: view()) { + @keyframes soonex-reveal { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } + + .soonex-reveal { + transform: none; + filter: none; + animation-name: soonex-reveal; + animation-duration: var(--duration-slow); + animation-timing-function: var(--soonex-ease); + animation-fill-mode: none; + animation-timeline: view(); + animation-range: entry 0% entry 40%; + } + + html[data-motion="reduce"] .soonex-reveal { + animation: none; + } +} + +.soonex-card-motion { + transition: + border-color var(--duration-fast) var(--soonex-ease), + box-shadow var(--duration-fast) var(--soonex-ease); +} + +@media (hover: hover) { + .soonex-card-motion:hover { + border-color: var(--color-brand); + box-shadow: var(--shadow-sm); + } +} + +html[data-motion="reduce"] .soonex-card-motion, +html[data-motion="reduce"] .soonex-card-motion:hover { + transition: none; + box-shadow: none; +} diff --git a/assets/css/site.css b/assets/css/site.css index e566d2e..f75a565 100644 --- a/assets/css/site.css +++ b/assets/css/site.css @@ -8,6 +8,11 @@ @source "../../lib"; @import "./chrome.css"; +@import "./layout.css"; +@import "./skins/neo.css"; +@import "./skins/uno.css"; +@import "./skins/duo.css"; +@import "./skins/leo.css"; @import "./prose.css"; @custom-variant dark (&:where([data-mode=dark], [data-mode=dark] *)); diff --git a/assets/css/skins/duo.css b/assets/css/skins/duo.css new file mode 100644 index 0000000..28da6b4 --- /dev/null +++ b/assets/css/skins/duo.css @@ -0,0 +1,166 @@ +/* duo — Studio / Spotlight. Editorial serif, photography as product, magazine density. */ + +[data-theme="duo"] :is( + .button, + .badge, + .native-input, + .avatar, + .menu, + .select, + .tabs, + .toast, + .pagination, + .clipboard, + .accordion, + .collapsible, + .carousel, + .date-picker, + .tags-input, + .number-input, + .radio-group, + .data-table, + .switch, + .checkbox, + .timer, + .tooltip, + .dialog, + .floating-panel +) { + --ctl-radius: var(--radius-2xl); +} + +[data-theme="duo"] .soonex-section { + padding-block: 5rem; +} + +@media (width >= 40rem) { + [data-theme="duo"] .soonex-section { + padding-block: 7rem; + } +} + +[data-theme="duo"] .soonex-section-heading { + font-family: var(--font-display); +} + +[data-theme="duo"] .soonex-hero { + isolation: isolate; + padding-block: 4rem; +} + +[data-theme="duo"] .soonex-hero-grid { + display: contents; +} + +[data-theme="duo"] [data-hero-visual] { + position: absolute; + inset: 0; + z-index: 0; +} + +[data-theme="duo"] .soonex-hero-frame { + position: absolute; + inset: 0; + min-height: 100%; + border: 0; + border-radius: 0; +} + +[data-theme="duo"] [data-hero-copy] { + position: relative; + z-index: 1; + max-width: 34rem; + margin-block: 4rem; + padding: 2rem; + border: 1px solid var(--color-border); + background: var(--color-surface); + border-radius: var(--radius-2xl); +} + +@media (width >= 40rem) { + [data-theme="duo"] [data-hero-copy] { + padding: 2.5rem; + } +} + +[data-theme="duo"] .soonex-hero-proof { + display: none; +} + +[data-theme="duo"] blockquote, +[data-theme="duo"] .soonex-quote { + font-family: var(--font-serif); + font-style: italic; + font-weight: 400; +} + +[data-theme="duo"] .soonex-claims { + gap: 4rem; +} + +[data-theme="duo"] .soonex-bento-caption { + background: var(--color-surface); +} + +[data-theme="duo"] .soonex-voices { + grid-template-columns: 1fr; +} + +@media (width >= 64rem) { + [data-theme="duo"] .soonex-voices { + grid-template-columns: 1.2fr 1fr 1fr; + } + + [data-theme="duo"] .soonex-voice:first-child .soonex-quote { + font-size: var(--text-2xl); + } +} + +[data-theme="duo"] .soonex-journal { + grid-template-columns: 1fr; +} + +@media (width >= 64rem) { + [data-theme="duo"] .soonex-journal { + grid-template-columns: 1.4fr 1fr 1fr; + } +} + +[data-theme="duo"] .soonex-waitlist-form { + max-width: 36rem; + padding: 3rem; +} + +[data-theme="duo"] .soonex-steps { + grid-template-columns: 1fr; + gap: 3rem; +} + +[data-theme="duo"] .soonex-step-index { + font-family: var(--font-serif); + font-style: italic; +} + +[data-theme="duo"] .soonex-faq [data-scope="accordion"][data-part="item-content"] { + font-family: var(--font-serif); + font-style: italic; +} + +[data-theme="duo"] .soonex-faq-index { + font-family: var(--font-serif); + font-style: italic; +} + +[data-theme="duo"] .soonex-launch { + grid-template-columns: 1fr; +} + +@media (width >= 64rem) { + [data-theme="duo"] .soonex-launch { + grid-template-columns: 1fr 1fr; + } + + [data-theme="duo"] .soonex-launch-beat[data-featured="true"] { + grid-column: 1 / -1; + } +} diff --git a/assets/css/skins/leo.css b/assets/css/skins/leo.css new file mode 100644 index 0000000..d3fcf8d --- /dev/null +++ b/assets/css/skins/leo.css @@ -0,0 +1,181 @@ +/* leo — Keynote / Swiss poster. Zero radius, grid overlay, uppercase labels, slab form. */ + +[data-theme="leo"] :is( + .button, + .badge, + .native-input, + .avatar, + .menu, + .select, + .tabs, + .toast, + .pagination, + .clipboard, + .accordion, + .collapsible, + .carousel, + .date-picker, + .tags-input, + .number-input, + .radio-group, + .data-table, + .switch, + .checkbox, + .timer, + .tooltip, + .dialog, + .floating-panel +) { + --ctl-radius: 0; +} + +[data-theme="leo"] .soonex-panel, +[data-theme="leo"] .soonex-frame, +[data-theme="leo"] .soonex-hero-frame { + border-radius: 0; +} + +[data-theme="leo"] :is(.button, .badge) { + letter-spacing: 0.08em; + text-transform: uppercase; +} + +[data-theme="leo"] .soonex-eyebrow, +[data-theme="leo"] .soonex-launch-badge, +[data-theme="leo"] .soonex-nav-links .link, +[data-theme="leo"] .soonex-step-index { + letter-spacing: 0.14em; + text-transform: uppercase; +} + +[data-theme="leo"] [data-site-header] { + background: var(--color-root); + border-bottom: 1px solid var(--color-border); +} + +[data-theme="leo"] .soonex-hero { + isolation: isolate; +} + +[data-theme="leo"] .soonex-hero .soonex-stage { + position: relative; + z-index: 1; +} + +[data-theme="leo"] .soonex-hero-grid { + grid-template-columns: 1fr; + justify-items: center; + gap: 3rem; +} + +[data-theme="leo"] .soonex-hero-heading { + font-size: clamp(3.25rem, 9vw, 7.5rem); + line-height: 0.95; + letter-spacing: -0.05em; + max-width: 18ch; +} + +[data-theme="leo"] .soonex-hero-form { + max-width: none; + padding: 1.25rem; + border: 1px solid var(--color-border); + background: var(--color-surface); +} + +[data-theme="leo"] .soonex-hero-copy { + justify-self: stretch; + width: 100%; +} + +[data-theme="leo"] .soonex-hero-visual { + width: 100%; + max-width: 36rem; + margin-inline: auto; +} + +[data-theme="leo"] .soonex-claims { + gap: 0; + border-top: 1px solid var(--color-border); +} + +[data-theme="leo"] .soonex-claim { + padding-block: 2rem; + border-bottom: 1px solid var(--color-border); +} + +@media (width >= 40rem) { + [data-theme="leo"] .soonex-claims { + grid-template-columns: 1fr; + } +} + +[data-theme="leo"] .soonex-bento { + gap: 0; + border: 1px solid var(--color-border); +} + +[data-theme="leo"] .soonex-bento > * { + border-radius: 0; + border: 0; + border-bottom: 1px solid var(--color-border); +} + +@media (width >= 64rem) { + [data-theme="leo"] .soonex-bento-hero { + border-inline-end: 1px solid var(--color-border); + } +} + +[data-theme="leo"] .soonex-plan[data-featured="true"] { + background: var(--color-accent); + color: var(--color-accent-contrast); +} + +[data-theme="leo"] .soonex-plan[data-featured="true"] .soonex-eyebrow, +[data-theme="leo"] .soonex-plan[data-featured="true"] .display, +[data-theme="leo"] .soonex-plan[data-featured="true"] p, +[data-theme="leo"] .soonex-plan[data-featured="true"] li { + color: inherit; +} + +[data-theme="leo"] .soonex-steps { + gap: 0; + border-top: 1px solid var(--color-border); +} + +[data-theme="leo"] .soonex-step { + padding-block: 2rem; + border-bottom: 1px solid var(--color-border); +} + +@media (width >= 64rem) { + [data-theme="leo"] .soonex-steps { + grid-template-columns: 1fr; + } +} + +[data-theme="leo"] .soonex-faq-index { + letter-spacing: 0.14em; + text-transform: uppercase; +} + +[data-theme="leo"] .soonex-faq [data-scope="accordion"][data-part="item"] { + border: 0; + border-radius: 0; +} + +[data-theme="leo"] .soonex-launch { + gap: 0; + border: 1px solid var(--color-border); +} + +[data-theme="leo"] .soonex-launch-beat { + border-radius: 0; + border: 0; + border-bottom: 1px solid var(--color-border); +} + +[data-theme="leo"] .soonex-waitlist-form { + max-width: 40rem; + border-radius: 0; +} diff --git a/assets/css/skins/neo.css b/assets/css/skins/neo.css new file mode 100644 index 0000000..e4a832a --- /dev/null +++ b/assets/css/skins/neo.css @@ -0,0 +1,116 @@ +/* neo — Salient / Radiant. Centered hero, brand underline, quiet sticky nav. */ + +[data-theme="neo"] :is( + .button, + .badge, + .native-input, + .avatar, + .menu, + .select, + .tabs, + .toast, + .pagination, + .clipboard, + .accordion, + .collapsible, + .carousel, + .date-picker, + .tags-input, + .number-input, + .radio-group, + .data-table, + .switch, + .checkbox, + .timer, + .tooltip, + .dialog, + .floating-panel +) { + --ctl-radius: var(--radius-xl); +} + +[data-theme="neo"] .soonex-hero-grid { + display: flex; + flex-direction: column; + justify-items: center; + text-align: center; +} + +[data-theme="neo"] .soonex-hero-visual { + display: none; +} + +[data-theme="neo"] [data-hero-copy] { + max-width: 44rem; +} + +[data-theme="neo"] .soonex-hero-lede, +[data-theme="neo"] .soonex-hero-form { + margin-inline: auto; +} + +[data-theme="neo"] .soonex-hero-actions, +[data-theme="neo"] .soonex-hero-proof { + justify-content: center; +} + +[data-theme="neo"] [data-hero-visual] { + width: min(100%, 48rem); +} + +[data-theme="neo"] .soonex-accent { + position: relative; + display: inline-block; + isolation: isolate; + color: var(--color-brand-text); +} + +[data-theme="neo"] .soonex-accent::after { + content: ""; + position: absolute; + inset-inline: 0; + bottom: -0.2em; + height: 0.14em; + z-index: -1; + border-radius: var(--radius-full); + background: var(--color-brand); +} + +[data-theme="neo"] [data-section="features"] .soonex-intro, +[data-theme="neo"] [data-section="steps"] .soonex-intro, +[data-theme="neo"] [data-section="pricing"] .soonex-intro, +[data-theme="neo"] [data-section="voices"] .soonex-intro, +[data-theme="neo"] [data-section="journal"] .soonex-intro { + margin-inline: auto; + text-align: center; +} + +[data-theme="neo"] [data-section="features"] .soonex-lede, +[data-theme="neo"] [data-section="steps"] .soonex-lede, +[data-theme="neo"] [data-section="pricing"] .soonex-lede, +[data-theme="neo"] [data-section="voices"] .soonex-lede, +[data-theme="neo"] [data-section="journal"] .soonex-lede { + margin-inline: auto; +} + +[data-theme="neo"] [data-section="features"] .soonex-section-actions, +[data-theme="neo"] [data-section="journal"] .soonex-section-actions { + justify-content: center; +} + +[data-theme="neo"] .soonex-bento-hero { + min-height: 22rem; +} + +[data-theme="neo"] .soonex-plan[data-featured="true"] { + box-shadow: var(--shadow-sm); +} + +[data-theme="neo"] .soonex-faq [data-scope="accordion"][data-part="item"] { + border: 0; + border-radius: 0; +} + +[data-theme="neo"] .soonex-launch-beat[data-featured="true"] { + box-shadow: var(--shadow-sm); +} diff --git a/assets/css/skins/uno.css b/assets/css/skins/uno.css new file mode 100644 index 0000000..6214d75 --- /dev/null +++ b/assets/css/skins/uno.css @@ -0,0 +1,149 @@ +/* uno — Shellular / Pocket. Pill chrome, italic accent, split hero, 2×2 product. */ + +[data-theme="uno"] :is( + .button, + .badge, + .native-input, + .avatar, + .menu, + .select, + .tabs, + .toast, + .pagination, + .clipboard, + .accordion, + .collapsible, + .carousel, + .date-picker, + .tags-input, + .number-input, + .radio-group, + .data-table, + .switch, + .checkbox, + .timer, + .tooltip, + .dialog, + .floating-panel +) { + --ctl-radius: var(--radius-full); +} + +[data-theme="uno"] .soonex-panel, +[data-theme="uno"] .soonex-frame, +[data-theme="uno"] .soonex-hero-frame { + border-radius: var(--radius-2xl); +} + +[data-theme="uno"] [data-site-header] { + background: var(--color-root); + border-color: transparent; + padding-block: 0.75rem; +} + +[data-theme="uno"] [data-site-header][data-condensed] { + border-color: transparent; +} + +[data-theme="uno"] [data-scroll-progress] { + display: none; +} + +[data-theme="uno"] [data-header-bar] { + border: 1px solid var(--color-border); + background: var(--color-surface); + border-radius: var(--radius-full); + padding-inline: 1rem; + box-shadow: var(--shadow-sm); +} + +[data-theme="uno"] .soonex-accent { + font-family: var(--font-serif); + font-style: italic; + font-weight: 500; + letter-spacing: -0.03em; +} + +[data-theme="uno"] .soonex-hero-heading { + letter-spacing: -0.04em; +} + +[data-theme="uno"] .soonex-hero-proof { + padding: 0.75rem 1rem; + border: 1px solid var(--color-border); + background: var(--color-surface); + border-radius: var(--radius-2xl); +} + +[data-theme="uno"] .soonex-claims { + gap: 1rem; +} + +@media (width >= 40rem) { + [data-theme="uno"] .soonex-claims { + grid-template-columns: 1fr 1fr; + } +} + +[data-theme="uno"] .soonex-claim { + padding: 1.5rem; + border: 1px solid var(--color-border); + background: var(--color-surface); + border-radius: var(--radius-2xl); +} + +[data-theme="uno"] .soonex-bento { + grid-template-columns: 1fr; +} + +@media (width >= 40rem) { + [data-theme="uno"] .soonex-bento { + grid-template-columns: 1fr 1fr; + } + + [data-theme="uno"] .soonex-bento-hero { + grid-column: 1 / -1; + } + + [data-theme="uno"] .soonex-bento-cell { + grid-column: auto; + } +} + +[data-theme="uno"] .soonex-steps { + gap: 1rem; +} + +[data-theme="uno"] .soonex-step { + padding: 1.5rem; + border: 1px solid var(--color-border); + background: var(--color-surface); + border-radius: var(--radius-2xl); +} + +[data-theme="uno"] .soonex-launch-beat, +[data-theme="uno"] .soonex-plan { + border-radius: var(--radius-2xl); +} + +[data-theme="uno"] .soonex-faq [data-scope="accordion"][data-part="item"] { + margin-block: 0.5rem; + border: 1px solid var(--color-border); + background: var(--color-surface); + border-radius: var(--radius-2xl); +} + +[data-theme="uno"] .soonex-faq [data-scope="accordion"][data-part="item-trigger"] { + padding-inline: 1.25rem; +} + +[data-theme="uno"] .soonex-bento-cells .collapsible { + border: 1px solid var(--color-border); + background: var(--color-surface); + border-radius: var(--radius-2xl); + padding: 0.5rem 1rem; +} + +[data-theme="uno"] .display { + letter-spacing: -0.03em; +} 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/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/site.js b/assets/js/site.js index 67ae42a..fd7899f 100644 --- a/assets/js/site.js +++ b/assets/js/site.js @@ -13,7 +13,9 @@ import { Menu } from "corex/menu" import { Clipboard } from "corex/clipboard" import { initLanding } from "./landing.js" import { initWaitlistForm } from "./waitlist.js" -import { initPagination } from "./pagination.js" +import { initSkins } from "./skins.js" +import { initPager } from "./pager.js" +import "./cookie-consent.js" const csrfToken = document .querySelector("meta[name='csrf-token']") @@ -52,12 +54,13 @@ const liveSocket = new LiveSocket("/live", Socket, { }, }) -initWaitlistForm() -initPagination() - liveSocket.disableDebug() liveSocket.connect() if (document.querySelector("[data-landing]")) { initLanding() } + +initWaitlistForm() +initSkins() +initPager() diff --git a/assets/js/skins.js b/assets/js/skins.js new file mode 100644 index 0000000..7bd0936 --- /dev/null +++ b/assets/js/skins.js @@ -0,0 +1,57 @@ +const skins = { + neo: () => import("./skins/neo.js"), + uno: () => import("./skins/uno.js"), + duo: () => import("./skins/duo.js"), + leo: () => import("./skins/leo.js"), +} + +let active = { id: null, teardown: null } + +function motionReduce() { + return document.documentElement.getAttribute("data-motion") === "reduce" +} + +async function apply(theme) { + if (motionReduce()) { + if (typeof active.teardown === "function") { + active.teardown() + } + active = { id: null, teardown: null } + return + } + + if (active.id === theme) { + return + } + + if (typeof active.teardown === "function") { + active.teardown() + } + + active = { id: theme, teardown: null } + const load = skins[theme] + if (!load) { + return + } + + const mod = await load() + if (active.id !== theme) { + return + } + + active.teardown = typeof mod.activate === "function" ? mod.activate() || null : null +} + +export function initSkins() { + const root = document.documentElement + apply(root.dataset.theme) + + const observer = new MutationObserver(() => { + apply(root.dataset.theme) + }) + + observer.observe(root, { + attributes: true, + attributeFilter: ["data-theme", "data-motion"], + }) +} diff --git a/assets/js/skins/duo.js b/assets/js/skins/duo.js new file mode 100644 index 0000000..60dfc22 --- /dev/null +++ b/assets/js/skins/duo.js @@ -0,0 +1,9 @@ +export function activate() { + const root = document.documentElement + root.setAttribute("data-skin-ready", "duo") + return () => { + if (root.getAttribute("data-skin-ready") === "duo") { + root.removeAttribute("data-skin-ready") + } + } +} diff --git a/assets/js/skins/leo.js b/assets/js/skins/leo.js new file mode 100644 index 0000000..9ecbb0e --- /dev/null +++ b/assets/js/skins/leo.js @@ -0,0 +1,9 @@ +export function activate() { + const root = document.documentElement + root.setAttribute("data-skin-ready", "leo") + return () => { + if (root.getAttribute("data-skin-ready") === "leo") { + root.removeAttribute("data-skin-ready") + } + } +} diff --git a/assets/js/skins/neo.js b/assets/js/skins/neo.js new file mode 100644 index 0000000..8bf13f5 --- /dev/null +++ b/assets/js/skins/neo.js @@ -0,0 +1,9 @@ +export function activate() { + const root = document.documentElement + root.setAttribute("data-skin-ready", "neo") + return () => { + if (root.getAttribute("data-skin-ready") === "neo") { + root.removeAttribute("data-skin-ready") + } + } +} diff --git a/assets/js/skins/uno.js b/assets/js/skins/uno.js new file mode 100644 index 0000000..18a9b7f --- /dev/null +++ b/assets/js/skins/uno.js @@ -0,0 +1,9 @@ +export function activate() { + const root = document.documentElement + root.setAttribute("data-skin-ready", "uno") + return () => { + if (root.getAttribute("data-skin-ready") === "uno") { + root.removeAttribute("data-skin-ready") + } + } +} diff --git a/config/config.exs b/config/config.exs index 218be92..421deba 100644 --- a/config/config.exs +++ b/config/config.exs @@ -76,7 +76,7 @@ config :tableau, Tableau.RSSExtension, enabled: true, title: "Soonex", description: - "Soonex ships 1 September — waitlist, shipping log, countdown, and four looks for client review." + "Tableau + Corex launch template: waitlist, journal, countdown, and four skins you customize in config." config :elixir, :time_zone_database, Tz.TimeZoneDatabase @@ -117,11 +117,11 @@ with_on = fn colors -> "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, :root, 4.6), - "brand-text" => contrast.(:brand, :root, 4.6), - "alert-text" => contrast.(:alert, :root, 4.6), - "info-text" => contrast.(:info, :root, 4.6), - "success-text" => contrast.(:success, :root, 4.6) + "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 @@ -134,9 +134,9 @@ config :corex_design, themes: %{ neo: %{ seeds: %{ - neutral: "#EEF1F6", - accent: "#070A10", - brand: "#155EEF", + neutral: "#EEEAE4", + accent: "#161618", + brand: "#1D4ED8", alert: "#B42318", info: "#0E7490", success: "#15803D" @@ -145,42 +145,42 @@ config :corex_design, light: with_on.(%{ root: l.(0.99, :neutral), - surface: l.(0.955, :neutral), - ui: fill.(0.9, :neutral), - accent: fill.(0.16, :accent), - brand: fill.(0.48, :brand), - ink: contrast.(:accent, :root, 13), - "ink-muted": contrast.(:accent, :root, 6.4), - link: contrast.(:brand, :root, 5.8), - border: contrast.(:neutral, :ui, 1.35), - focus: contrast.(:brand, :ui, 2.5), - shadow: contrast.(:accent, :root, 1.15) + 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.045, :accent), - surface: l.(0.085, :accent), - ui: fill.(0.14, :accent), - accent: fill.(0.55, :accent), - brand: fill.(0.5, :brand), - ink: contrast.(:neutral, :root, 14), - "ink-muted": contrast.(:neutral, :root, 7.0), - link: contrast.(:brand, :root, 6.5), - border: contrast.(:neutral, :ui, 1.4), - focus: contrast.(:brand, :ui, 2.55), - shadow: contrast.(:accent, :root, 1.2) + 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.2, - sm: 0.4, - md: 0.7, + xs: 0.28, + sm: 0.48, + md: 0.72, lg: 1.0, - xl: 1.35, - "2xl": 1.7, - "3xl": 2.2, - "4xl": 2.8, + xl: 1.25, + "2xl": 1.5, + "3xl": 1.9, + "4xl": 2.4, full: 9999 }, font: %{ @@ -195,46 +195,31 @@ config :corex_design, "h1" => %{ font_family: {:font, :display}, font_weight: {:weight, :bold}, - letter_spacing: {:tracking, :tighter}, - font_size: {:text, :"5xl"}, - sm: %{font_size: {:text, :"6xl"}}, - lg: %{font_size: {:text, :"8xl"}} + letter_spacing: {:tracking, :tighter} }, "h2" => %{ font_family: {:font, :display}, font_weight: {:weight, :bold}, - letter_spacing: {:tracking, :tight}, - font_size: {:text, :"4xl"}, - sm: %{font_size: {:text, :"5xl"}} - }, - "h3" => %{ - font_family: {:font, :display}, - font_weight: {:weight, :semibold}, - font_size: {:text, :xl} + letter_spacing: {:tracking, :tight} }, + "h3" => %{font_family: {:font, :display}, font_weight: {:weight, :semibold}}, "h4" => %{font_family: {:font, :sans}, font_weight: {:weight, :semibold}}, "p" => %{line_height: {:leading, :relaxed}}, - "p.lede" => %{ - font_size: {:text, :lg}, - line_height: {:leading, :relaxed}, - color: "var(--color-ink-muted)" - }, - "small.eyebrow" => %{ - font_weight: {:weight, :semibold}, - font_size: {:text, :sm}, - letter_spacing: {:tracking, :wide}, - color: "var(--color-brand-text)" + "p.display" => %{ + font_family: {:font, :display}, + font_weight: {:weight, :bold}, + letter_spacing: {:tracking, :tighter} }, - "blockquote" => %{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}} } }, uno: %{ seeds: %{ - neutral: "#E8F0ED", - accent: "#021A16", - brand: "#0D9488", + neutral: "#F6F6F6", + accent: "#071614", + brand: "#0F766E", alert: "#B91C1C", info: "#0369A1", success: "#166534" @@ -242,43 +227,43 @@ config :corex_design, colors: %{ light: with_on.(%{ - root: l.(0.97, :neutral), - surface: l.(0.94, :neutral), - ui: fill.(0.9, :neutral), - accent: fill.(0.22, :accent), - brand: fill.(0.4, :brand), - ink: contrast.(:accent, :root, 11), - "ink-muted": contrast.(:accent, :root, 5.8), - link: contrast.(:brand, :root, 6.0), - border: contrast.(:neutral, :ui, 1.28), - focus: contrast.(:brand, :ui, 2.35), - shadow: contrast.(:accent, :root, 1.08) + 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.05, :accent), - surface: l.(0.095, :accent), - ui: fill.(0.155, :accent), - accent: fill.(0.55, :accent), - brand: fill.(0.55, :brand), - ink: contrast.(:neutral, :root, 13), - "ink-muted": contrast.(:neutral, :root, 6.5), - link: contrast.(:brand, :root, 7.2), - border: contrast.(:neutral, :ui, 1.35), - focus: contrast.(:brand, :ui, 2.4), - shadow: contrast.(:accent, :root, 1.16) + 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, - sm: 0.02, - md: 0.05, - lg: 0.1, - xl: 0.16, - "2xl": 0.24, - "3xl": 0.32, - "4xl": 0.4, + 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: %{ @@ -286,54 +271,29 @@ config :corex_design, display: ["Sora", "ui-sans-serif", "system-ui", "sans-serif"], mono: ["JetBrains Mono", "ui-monospace", "monospace"], code: ["JetBrains Mono", "ui-monospace", "monospace"], - serif: ["ui-serif", "Georgia", "serif"] + serif: ["Playfair Display", "Georgia", "serif"] } }, typography: %{ "h1" => %{ font_family: {:font, :display}, font_weight: {:weight, :semibold}, - letter_spacing: {:tracking, :tighter}, - font_size: {:text, :"4xl"}, - sm: %{font_size: {:text, :"5xl"}}, - lg: %{font_size: {:text, :"6xl"}} - }, - "h2" => %{ - font_family: {:font, :display}, - font_weight: {:weight, :semibold}, - letter_spacing: {:tracking, :tighter}, - font_size: {:text, :"3xl"}, - sm: %{font_size: {:text, :"4xl"}} - }, - "h3" => %{ - font_family: {:font, :display}, - font_weight: {:weight, :medium}, letter_spacing: {:tracking, :tight} }, - "h4" => %{font_weight: {:weight, :medium}, 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.lede" => %{ - font_size: {:text, :base}, - line_height: {:leading, :snug}, - letter_spacing: {:tracking, :tight}, - color: "var(--color-ink-muted)" - }, - "small.eyebrow" => %{ - font_weight: {:weight, :medium}, - font_size: {:text, :xs}, - letter_spacing: {:tracking, :widest}, - text_transform: "uppercase", - color: "var(--color-brand-text)" - }, - "blockquote" => %{font_style: :italic}, + "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: "#F3EDE6", - accent: "#2A140C", + neutral: "#F7EDE4", + accent: "#3A2618", brand: "#C2410C", alert: "#9F1239", info: "#3D5278", @@ -341,44 +301,47 @@ config :corex_design, }, colors: %{ light: - with_on.(%{ - root: l.(0.98, :neutral), - surface: l.(0.95, :neutral), - ui: fill.(0.91, :neutral), - accent: fill.(0.28, :accent), - brand: fill.(0.46, :brand), - ink: contrast.(:accent, :root, 10), - "ink-muted": contrast.(:accent, :root, 5.4), - link: contrast.(:brand, :root, 5.8), - border: contrast.(:neutral, :ui, 1.22), - focus: contrast.(:brand, :ui, 2.2), - shadow: contrast.(:accent, :root, 1.3) - }), + 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.06, :accent), - surface: l.(0.11, :accent), - ui: fill.(0.18, :accent), - accent: fill.(0.52, :accent), - brand: fill.(0.58, :brand), - ink: contrast.(:neutral, :root, 12.5), - "ink-muted": contrast.(:neutral, :root, 6.4), - link: contrast.(:brand, :root, 7.2), - border: contrast.(:neutral, :ui, 1.25), - focus: contrast.(:brand, :ui, 2.35), - shadow: contrast.(:accent, :root, 1.35) + 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: 1.0, - sm: 1.6, - md: 2.4, - lg: 3.2, - xl: 4.2, - "2xl": 5.4, - "3xl": 7.0, - "4xl": 9.0, + 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: %{ @@ -393,32 +356,16 @@ config :corex_design, "h1" => %{ font_family: {:font, :display}, font_weight: {:weight, :bold}, - letter_spacing: {:tracking, :tight}, - font_size: {:text, :"5xl"}, - sm: %{font_size: {:text, :"6xl"}}, - lg: %{font_size: {:text, :"8xl"}} - }, - "h2" => %{ - font_family: {:font, :display}, - font_weight: {:weight, :semibold}, - font_size: {:text, :"4xl"}, - sm: %{font_size: {:text, :"5xl"}} + 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.lede" => %{ - font_family: {:font, :serif}, - font_style: :italic, - font_size: {:text, :xl}, - line_height: {:leading, :relaxed}, - color: "var(--color-ink-muted)" - }, - "small.eyebrow" => %{ - font_family: {:font, :sans}, - font_weight: {:weight, :semibold}, - letter_spacing: {:tracking, :wide}, - color: "var(--color-brand-text)" + "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}}, @@ -427,9 +374,9 @@ config :corex_design, }, leo: %{ seeds: %{ - neutral: "#F0EEE9", - accent: "#0A0A0A", - brand: "#F97316", + neutral: "#E6DCCE", + accent: "#0C0C0C", + brand: "#E2410A", alert: "#C41E1E", info: "#0369A1", success: "#166534" @@ -438,42 +385,42 @@ config :corex_design, light: with_on.(%{ root: l.(0.985, :neutral), - surface: l.(0.955, :neutral), - ui: fill.(0.91, :neutral), - accent: fill.(0.1, :accent), - brand: fill.(0.45, :brand), - ink: contrast.(:accent, :root, 13), - "ink-muted": contrast.(:accent, :root, 6.0), - link: contrast.(:brand, :root, 6.0), - border: contrast.(:accent, :ui, 1.4), - focus: contrast.(:brand, :ui, 2.6), - shadow: contrast.(:accent, :root, 1.05) + 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.03, :accent), - surface: l.(0.07, :accent), - ui: fill.(0.13, :accent), - accent: fill.(0.5, :accent), - brand: fill.(0.52, :brand), - ink: contrast.(:neutral, :root, 14.5), - "ink-muted": contrast.(:neutral, :root, 7.0), - link: contrast.(:brand, :root, 7.4), - border: contrast.(:neutral, :ui, 1.5), - focus: contrast.(:brand, :ui, 2.7), - shadow: contrast.(:accent, :root, 1.08) + 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, - md: 0, - lg: 0, - xl: 0, - "2xl": 0, - "3xl": 0, - "4xl": 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: %{ @@ -485,42 +432,17 @@ config :corex_design, } }, typography: %{ - "h1" => %{ - font_weight: {:weight, :bold}, - letter_spacing: {:tracking, :tighter}, - font_size: {:text, :"5xl"}, - text_transform: "uppercase", - sm: %{font_size: {:text, :"6xl"}}, - lg: %{font_size: {:text, :"8xl"}} - }, - "h2" => %{ - font_weight: {:weight, :bold}, - letter_spacing: {:tracking, :tight}, - font_size: {:text, :"4xl"}, - text_transform: "uppercase", - sm: %{font_size: {:text, :"5xl"}} - }, - "h3" => %{font_weight: {:weight, :medium}, text_transform: "uppercase"}, + "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}, - text_transform: "uppercase" + font_size: {:text, :sm} }, "p" => %{line_height: {:leading, :snug}}, - "p.lede" => %{ - font_size: {:text, :lg}, - line_height: {:leading, :snug}, - color: "var(--color-ink-muted)" - }, - "small.eyebrow" => %{ - font_weight: {:weight, :medium}, - font_size: {:text, :xs}, - letter_spacing: {:tracking, :widest}, - text_transform: "uppercase", - color: "var(--color-brand-text)" - }, - "blockquote" => %{font_weight: {:weight, :semibold}}, + "p.display" => %{font_weight: {:weight, :semibold}, letter_spacing: {:tracking, :tight}}, + "blockquote" => %{font_style: :italic}, "code" => %{font_family: {:font, :mono}}, "kbd" => %{font_family: {:font, :mono}} } diff --git a/extra/images/logo.svg b/extra/images/logo.svg index b67fcce..82deb3e 100644 --- a/extra/images/logo.svg +++ b/extra/images/logo.svg @@ -1,7 +1,7 @@ diff --git a/lib/layouts/articles.ex b/lib/layouts/articles.ex index 26fd7f2..205e862 100644 --- a/lib/layouts/articles.ex +++ b/lib/layouts/articles.ex @@ -10,47 +10,45 @@ defmodule Soonex.Layouts.Articles do 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}

        -
          +

          {@empty}

          +
        +
        - - +
        +
        """ end @@ -60,14 +58,14 @@ defmodule Soonex.Layouts.Articles do def pager(assigns) do ~H""" -
        @page_size} class="mt-12 flex justify-center"> +
        <.pagination id={@id} class="pagination ui-brand ui-size-sm" count={@count} page_size={@page_size} type={:button} - on_page_change_client="pagination-page-changed" + on_page_change_client="soonex:page-change" > <:prev_trigger> <.heroicon name="hero-chevron-left" /> @@ -83,17 +81,17 @@ defmodule Soonex.Layouts.Articles do """ end - defp date_label(%{date: %DateTime{} = date}), do: Calendar.strftime(date, "%d %B %Y") - defp date_label(_), do: nil - defp cover(post) do src = post[:image] if is_binary(src) and src != "" do - %{src: src, alt: post[:image_alt] || post[:title] || "Log cover"} + %{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, []) diff --git a/lib/layouts/brand.ex b/lib/layouts/brand.ex index 04fe220..c611f18 100644 --- a/lib/layouts/brand.ex +++ b/lib/layouts/brand.ex @@ -16,13 +16,15 @@ defmodule Soonex.Layouts.Brand do type="href" class={["link ui-nav ui-size-md flex items-center gap-2 no-underline", @class]} > - <.mark class="size-8 shrink-0" /> - Soonex + + <.mark class="soonex-mark" /> + + Soonex """ end - attr(:class, :any, default: "size-8") + attr(:class, :any, default: "soonex-mark") def mark(assigns) do ~H""" @@ -35,7 +37,7 @@ defmodule Soonex.Layouts.Brand do > diff --git a/lib/layouts/root/demo.ex b/lib/layouts/root/demo.ex index b03a27e..f8feaea 100644 --- a/lib/layouts/root/demo.ex +++ b/lib/layouts/root/demo.ex @@ -49,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 diff --git a/lib/layouts/root/footer.ex b/lib/layouts/root/footer.ex index 3fc96f9..a1bf586 100644 --- a/lib/layouts/root/footer.ex +++ b/lib/layouts/root/footer.ex @@ -4,6 +4,7 @@ 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 @@ -20,13 +21,13 @@ defmodule Soonex.Layouts.Root.Footer do |> assign(:hexdocs, @hexdocs) ~H""" -
        +
        <.lockup /> -

        - Waitlist, shipping log, countdown, and four looks. Ship 1 September. +

        + A Tableau + Corex launch template: waitlist, journal, and four skins you can tell apart.

        @@ -35,15 +36,15 @@ defmodule Soonex.Layouts.Root.Footer do
        • <.navigate - to={Soonex.Public.path("/") <> "#product"} + to={Soonex.Public.path("/") <> "#features"} class="link ui-nav ui-size-sm" > - Product + Features
        • - <.navigate to={Soonex.Public.path("/") <> "#looks"} class="link ui-nav ui-size-sm"> - Looks + <.navigate to={Soonex.Public.path("/") <> "#pricing"} class="link ui-nav ui-size-sm"> + Plans
        • @@ -65,11 +66,11 @@ defmodule Soonex.Layouts.Root.Footer do
        -

        Log

        +

        Journal

        • <.navigate to={Soonex.Public.path("/blog")} class="link ui-nav ui-size-sm"> - All entries + All posts
        • @@ -108,10 +109,19 @@ defmodule Soonex.Layouts.Root.Footer do © {Date.utc_today().year} {@copyright_holder}

          {Soonex.Layouts.Media.credits()}

          +
          + <.navigate + to={Soonex.Public.path("/privacy/")} + class="link ui-nav ui-size-sm text-ink-muted" + > + Privacy + + <.cookie_dialog /> +
        <.navigate to={Soonex.Public.path("/") <> "#waitlist"} - class="button ui-brand ui-solid ui-size-md w-fit" + class="button ui-brand ui-solid ui-size-sm w-fit" > Join waitlist diff --git a/lib/layouts/root/nav.ex b/lib/layouts/root/nav.ex index 98f33ff..498a43d 100644 --- a/lib/layouts/root/nav.ex +++ b/lib/layouts/root/nav.ex @@ -7,6 +7,7 @@ defmodule Soonex.Layouts.Root.Nav do import Soonex.Layouts.Brand, only: [lockup: 1] alias Soonex.Layouts.Shell + alias Soonex.Public attr(:countdown_start_ms, :integer, required: true) attr(:page_path, :string, default: "/") @@ -17,7 +18,7 @@ defmodule Soonex.Layouts.Root.Nav do ~H"""
        -
        <.navigate - to={Soonex.Public.path("/") <> "#waitlist"} - class="button ui-brand ui-solid ui-size-md" + to={Public.path("/") <> "#waitlist"} + class="button ui-brand ui-solid ui-size-sm" > Join waitlist @@ -112,7 +113,7 @@ defmodule Soonex.Layouts.Root.Nav do ] end - defp nav_current?(page_path, %{id: :log}) do + defp nav_current?(page_path, %{id: :journal}) do String.starts_with?(page_path, "/blog") or String.starts_with?(page_path, "/tags") end @@ -120,10 +121,10 @@ defmodule Soonex.Layouts.Root.Nav do defp desktop_links do [ - %{id: :product, label: "Product", to: Soonex.Public.path("/") <> "#product"}, - %{id: :looks, label: "Looks", to: Soonex.Public.path("/") <> "#looks"}, - %{id: :log, label: "Log", to: Soonex.Public.path("/blog")}, - %{id: :questions, label: "Questions", to: Soonex.Public.path("/") <> "#questions"} + %{id: :product, label: "Product", to: Public.path("/") <> "#features"}, + %{id: :launch, label: "Launch", to: Public.path("/") <> "#launch"}, + %{id: :journal, label: "Journal", to: Public.path("/blog")}, + %{id: :questions, label: "Questions", to: Public.path("/") <> "#questions"} ] end @@ -131,32 +132,32 @@ defmodule Soonex.Layouts.Root.Nav do Corex.List.new([ %{ label: "Product", - value: "product", - to: Soonex.Public.path("/") <> "#product", + value: "features", + to: Public.path("/") <> "#features", redirect: :href }, %{ - label: "Looks", - value: "looks", - to: Soonex.Public.path("/") <> "#looks", + label: "Launch", + value: "launch", + to: Public.path("/") <> "#launch", redirect: :href }, %{ - label: "Log", + label: "Journal", value: "blog", - to: Soonex.Public.path("/blog"), + to: Public.path("/blog"), redirect: :href }, %{ label: "Questions", value: "questions", - to: Soonex.Public.path("/") <> "#questions", + to: Public.path("/") <> "#questions", redirect: :href }, %{ label: "Join waitlist", value: "waitlist", - to: Soonex.Public.path("/") <> "#waitlist", + to: Public.path("/") <> "#waitlist", redirect: :href } ]) diff --git a/lib/layouts/root_layout.ex b/lib/layouts/root_layout.ex index f3d7dc2..8ff5ffa 100644 --- a/lib/layouts/root_layout.ex +++ b/lib/layouts/root_layout.ex @@ -7,6 +7,7 @@ defmodule Soonex.RootLayout do use Phoenix.Component use Corex + import Soonex.CookieConsent, only: [cookie_consent: 1] import Soonex.Layouts.Root.Demo, only: [demo_site_controls: 1] import Soonex.Layouts.Root.Footer, only: [site_footer: 1] import Soonex.Layouts.Root.Nav, only: [site_nav: 1] @@ -18,11 +19,7 @@ defmodule Soonex.RootLayout do site_name = "Soonex" copyright_holder = "Soonex" - countdown_start_ms = - max( - DateTime.diff(~U[2026-09-01 00:00:00Z], DateTime.utc_now(), :millisecond), - 0 - ) + countdown_start_ms = Soonex.Launch.countdown_ms() tableau_config = case Tableau.Config.get() do @@ -78,6 +75,7 @@ defmodule Soonex.RootLayout do {Soonex.Theme.head_script()} {Soonex.Mode.head_script()} {Soonex.Accessibility.head_script()} + {Soonex.CookieConsent.head_script()} @@ -170,6 +168,7 @@ defmodule Soonex.RootLayout do <.site_footer copyright_holder={@copyright_holder} /> + <.cookie_consent privacy_path={Soonex.Public.path("/privacy/")} /> <.toast_group id="layout-toast" class="toast" phx-update="ignore" flash={@flash}> <:loading> @@ -198,49 +197,47 @@ defmodule Soonex.RootLayout do md_page?(page) and present_string?(page[:title]) -> page[:title] - page[:page_kind] == :home -> - "#{site_name} · Launch 1 September" - - page[:page_kind] == :blog_index -> - "Log · #{site_name}" - - page[:page_kind] == :not_found -> - "Page not found · #{site_name}" - - page[:page_kind] == :tags_index -> - "Tags · #{site_name}" - tag_page?(page) -> "#{page[:tag]} · Tags · #{site_name}" true -> - site_name + kind_title(page[:page_kind], site_name) end end - defp meta_description(page, site_name) do - cond do - page[:page_kind] == :home -> - "Soonex ships 1 September — waitlist, shipping log, countdown, and four looks for client review." + defp kind_title(:home, site_name), do: "#{site_name} · Launch #{Soonex.Launch.year_label()}" + defp kind_title(:blog_index, site_name), do: "Journal · #{site_name}" + defp kind_title(:not_found, site_name), do: "Page not found · #{site_name}" + defp kind_title(:privacy, site_name), do: "Privacy · #{site_name}" + defp kind_title(:tags_index, site_name), do: "Tags · #{site_name}" + defp kind_title(_kind, site_name), do: site_name - page[:page_kind] == :blog_index -> - "Shipping notes from #{site_name}: waitlist, looks, countdown, and the road to 1 September." + defp meta_description(page, site_name) do + if tag_page?(page) do + "Journal posts tagged #{page[:tag]} on #{site_name}." + else + kind_description(page[:page_kind], site_name) || + page_description(page) || + "Tableau + Corex launch template: waitlist, journal, and four theme skins." + end + end - page[:page_kind] == :not_found -> - "That page is not on #{site_name}. Head home or read the shipping log." + defp kind_description(:home, _site_name), + do: + "Tableau + Corex launch template: waitlist, journal, countdown, and four skins you customize in config." - page[:page_kind] == :tags_index -> - "Browse the #{site_name} shipping log by topic — launch, looks, studio, and notes." + defp kind_description(:blog_index, site_name), do: "Shipping notes from #{site_name}." + defp kind_description(:not_found, site_name), do: "That page is not on #{site_name}." - tag_page?(page) -> - "Log entries tagged #{page[:tag]} on #{site_name}." + defp kind_description(:privacy, _site_name), + do: + "Necessary preferences stay on this device. Analytics and marketing stay off unless you allow them." - present_string?(page[:description]) -> - page[:description] + defp kind_description(:tags_index, site_name), do: "Browse journal tags on #{site_name}." + defp kind_description(_kind, _site_name), do: nil - true -> - "Soonex ships 1 September — waitlist, shipping log, countdown, and four looks for client review." - end + defp page_description(page) do + if present_string?(page[:description]), do: page[:description] end defp page_path_from_page(page) when is_map(page) do diff --git a/lib/layouts/section.ex b/lib/layouts/section.ex index 787eea0..213a999 100644 --- a/lib/layouts/section.ex +++ b/lib/layouts/section.ex @@ -8,10 +8,12 @@ defmodule Soonex.Layouts.Section do attr(:id, :string, required: true) attr(:labelledby, :string, required: true) attr(:eyebrow, :string, required: true) + attr(:section, :string, default: nil) attr(:tone, :atom, default: :root) attr(:align, :atom, default: :start) - attr(:layout, :atom, default: :open, values: [:bleed, :open, :editorial, :sticky, :stack]) + attr(:layout, :atom, default: :stack, values: [:stack, :sticky]) attr(:compact, :boolean, default: false) + attr(:reveal, :boolean, default: true) slot(:title, required: true) slot(:lede) @@ -22,12 +24,13 @@ defmodule Soonex.Layouts.Section do ~H"""
        - {@eyebrow} +

        {@eyebrow}

        {render_slot(@title)}

        @@ -36,7 +39,7 @@ defmodule Soonex.Layouts.Section do

        {render_slot(@actions)}
        @@ -49,36 +52,30 @@ defmodule Soonex.Layouts.Section do """ end - defp section_class(:bleed, _compact), do: "#{Shell.section_hero()} soonex-reveal" - defp section_class(_layout, true), do: "#{Shell.section_compact()} soonex-reveal" - defp section_class(_layout, false), do: "#{Shell.section()} soonex-reveal" + defp section_class(true, true), do: "#{Shell.section_compact()} soonex-reveal" + defp section_class(false, true), do: "#{Shell.section()} soonex-reveal" + defp section_class(true, false), do: Shell.section_compact() + defp section_class(false, false), do: Shell.section() defp tone_class(:root), do: "bg-root" defp tone_class(:surface), do: "border-y border-border bg-surface" - defp tone_class(:ui), do: "border-y border-border bg-ui" - defp tone_class(:clear), do: "" - defp stage_class(:bleed), do: "#{Shell.stage()} relative z-[1]" defp stage_class(:sticky), do: "#{Shell.stage()} #{Shell.sticky_grid()}" - defp stage_class(:editorial), do: "#{Shell.stage()} #{Shell.editorial_grid()}" - defp stage_class(_layout), do: Shell.stage() + defp stage_class(:stack), do: Shell.stage() defp intro_class(:sticky, _align), do: Shell.sticky_intro() - defp intro_class(:editorial, _align), do: Shell.editorial_intro() - defp intro_class(_layout, :center), do: Shell.intro_center() - defp intro_class(_layout, :start), do: Shell.intro() + defp intro_class(:stack, :center), do: Shell.intro_center() + defp intro_class(:stack, :start), do: Shell.intro() + defp lede_align(:sticky, _align), do: "" defp lede_align(:stack, :center), do: "mx-auto" - defp lede_align(_layout, _align), do: "" + defp lede_align(:stack, :start), do: "" + defp actions_align(:sticky, _align), do: "" defp actions_align(:stack, :center), do: "justify-center" - defp actions_align(_layout, :center), do: "justify-center" - defp actions_align(_layout, _align), do: "" + defp actions_align(:stack, :start), do: "" defp body_class(:sticky, _compact), do: Shell.sticky_body() - defp body_class(:editorial, _compact), do: Shell.editorial_body() - defp body_class(:open, _compact), do: Shell.body_tight() - defp body_class(:bleed, _compact), do: Shell.body_tight() defp body_class(:stack, true), do: Shell.body_tight() defp body_class(:stack, false), do: Shell.body() end diff --git a/lib/layouts/shell.ex b/lib/layouts/shell.ex index 6d4ac2c..ed2a865 100644 --- a/lib/layouts/shell.ex +++ b/lib/layouts/shell.ex @@ -1,69 +1,55 @@ defmodule Soonex.Layouts.Shell do @moduledoc false - # Named skeletons — not the Tailwind-UI eyebrow → h2 → card-grid band. - # :bleed full-bleed media (hero). Stage is overlay only. - # :open heading cluster, then type — no panels. - # :editorial heading + supporting column. - # :sticky FAQ split. - # :stack last-resort marketing band. + # Shared marketing band (Salient-width). Skins override density via + # .soonex-section / .soonex-hero / .soonex-stage on [data-theme]. - def section, do: "scroll-mt-24 w-full py-24 sm:py-32" + def section, do: "soonex-section scroll-mt-24 w-full py-24 sm:py-32" - def section_compact, do: "scroll-mt-24 w-full py-16 sm:py-20" + def section_compact, + do: "soonex-section soonex-section--compact scroll-mt-24 w-full py-16 sm:py-20" def section_hero, do: - "relative isolate flex min-h-svh w-full flex-col justify-end overflow-x-clip lg:justify-center" + "soonex-hero relative flex min-h-svh w-full flex-col justify-center overflow-x-clip py-16 sm:py-24" - def stage, do: "mx-auto w-full max-w-[90rem] px-6 lg:px-8" + def stage, do: "soonex-stage mx-auto w-full max-w-7xl px-6 lg:px-8" - def intro, do: "mx-auto max-w-3xl lg:mx-0" + def intro, do: "soonex-intro mx-auto max-w-2xl lg:mx-0" - def intro_center, do: "mx-auto max-w-2xl lg:text-center" + def intro_center, do: "soonex-intro soonex-intro--center mx-auto max-w-2xl lg:text-center" - def body, do: "mx-auto mt-12 w-full sm:mt-16" + def body, do: "soonex-body mx-auto mt-16 w-full sm:mt-20 lg:mt-24" - def body_tight, do: "mx-auto mt-8 w-full sm:mt-10" + def body_tight, do: "soonex-body soonex-body--tight mx-auto mt-10 w-full sm:mt-12" def sticky_grid, - do: "grid grid-cols-1 items-start gap-16 lg:grid-cols-12 lg:gap-12" + do: "soonex-sticky-grid grid grid-cols-1 items-start gap-16 lg:grid-cols-12 lg:gap-12" def sticky_intro, - do: "mx-auto max-w-2xl lg:sticky lg:top-28 lg:col-span-5 lg:mx-0" + do: "soonex-sticky-intro mx-auto max-w-2xl lg:sticky lg:top-28 lg:col-span-5 lg:mx-0" - def sticky_body, do: "w-full min-w-0 lg:col-span-7" + def sticky_body, do: "soonex-sticky-body w-full min-w-0 lg:col-span-7" - def editorial_grid, - do: "grid grid-cols-1 items-start gap-12 lg:grid-cols-12 lg:gap-16" + def eyebrow, do: "soonex-eyebrow m-0 text-base/7 font-semibold text-brand-text" - def editorial_intro, do: "mx-auto max-w-2xl lg:col-span-5 lg:mx-0" - - def editorial_body, do: "w-full min-w-0 lg:col-span-7" - - def eyebrow, do: "eyebrow m-0" - - def section_heading, do: "mt-2 text-pretty" - - def page_heading, do: "mt-2 text-pretty" + def section_heading, + do: + "soonex-section-heading display mt-2 text-pretty text-4xl font-semibold tracking-tight text-ink sm:text-5xl" - def lede, do: "lede mt-6 max-w-2xl text-pretty" + def page_heading, + do: + "soonex-page-heading display mt-2 text-pretty text-4xl font-semibold tracking-tight text-ink sm:text-5xl" - def card_title, do: "soonex-card-title mt-2" + def lede, do: "soonex-lede mt-6 max-w-2xl text-pretty text-lg/8 text-ink-muted" def panel, - do: "soonex-frame relative overflow-hidden border border-border bg-surface" - - def frame, do: "soonex-frame relative overflow-hidden" - - def feature_list, do: "m-0 mt-6 flex list-none flex-col gap-3 p-0" - - def chip_row, do: "flex flex-wrap items-center gap-2" + do: "soonex-panel soonex-frame relative overflow-hidden border border-border bg-surface" - def lanes, do: "m-0 flex list-none flex-col gap-8 p-0" + def panel_open, + do: "soonex-panel soonex-frame relative overflow-visible border border-border bg-surface" - def log_list, do: "m-0 flex list-none flex-col divide-y divide-border p-0" + def frame, do: "soonex-frame relative overflow-hidden border border-border bg-surface" - def feature_bullets, - do: "soonex-feature-bullets m-0 flex list-none flex-col gap-2 p-0" + def data_list, do: "data-list data-list--wide ui-accent ui-size-md w-full max-w-none" end diff --git a/lib/layouts/tag_layout.ex b/lib/layouts/tag_layout.ex index 4a2526e..d3e3ad3 100644 --- a/lib/layouts/tag_layout.ex +++ b/lib/layouts/tag_layout.ex @@ -26,20 +26,20 @@ defmodule Soonex.TagLayout do <.layout_heading class="layout-heading" subtitle_tag="p"> <:title>{@tag_label} <:subtitle> - {@tag_count} {if @tag_count == 1, do: "shipping-log entry", else: "shipping-log entries"} + {@tag_count} {if @tag_count == 1, do: "post", else: "posts"} tagged in the journal <:actions> <.navigate to={Soonex.Public.path("/tags")} class="button ui-ghost ui-size-sm"> <.heroicon name="hero-arrow-left" /> All tags <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-sm"> - Log + Journal -
        - <.cards posts={@tag_posts} /> +
        + <.cards posts={@tag_posts} pager_id="soonex-tag-pagination" page_size={3} /> <.pager id="soonex-tag-pagination" count={@tag_count} page_size={3} />
        diff --git a/lib/pages/blog_index_page.ex b/lib/pages/blog_index_page.ex index 09f7c08..9ff4787 100644 --- a/lib/pages/blog_index_page.ex +++ b/lib/pages/blog_index_page.ex @@ -4,7 +4,7 @@ defmodule Soonex.BlogIndexPage do use Tableau.Page, layout: Soonex.RootLayout, permalink: "/blog", - title: "Log", + title: "Journal", page_kind: :blog_index, sitemap: %{priority: 0.7, changefreq: "weekly"} @@ -30,10 +30,12 @@ defmodule Soonex.BlogIndexPage do
        <.layout_heading class="layout-heading" subtitle_tag="p"> - <:title>Log + <:title>Journal <:subtitle> - Public notes on the road to 1 September — {@blog_count} - {if @blog_count == 1, do: "entry", else: "entries"}. + Shipping notes for the waitlist, skins, and launch date. {@blog_count} {if @blog_count == + 1, + do: "post", + else: "posts"}. <:actions> <.navigate to={Soonex.Public.path("/")} class="button ui-ghost ui-size-sm"> @@ -45,8 +47,8 @@ defmodule Soonex.BlogIndexPage do -
        - <.cards posts={@sorted_posts} /> +
        + <.cards posts={@sorted_posts} pager_id="soonex-blog-pagination" page_size={3} /> <.pager id="soonex-blog-pagination" count={@blog_count} page_size={3} />
        diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index c3fcd87..6e0edf6 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -8,12 +8,19 @@ defmodule Soonex.HomePage.Faq do def faq(assigns) do ~H""" - <.block id="questions" labelledby="soonex-faq-heading" eyebrow="Questions" layout={:sticky}> + <.block + id="questions" + section="faq" + labelledby="soonex-faq-heading" + eyebrow="Questions" + layout={:sticky} + > <:title> - Before you join + Before you fork. Then it's yours. <:lede> - Straight answers about the launch, the waitlist, and what you get on day one. + The waitlist, skins, and journal are already in the tree. Photography and copy are the + first things you replace. <:actions> <.navigate to="#waitlist" class="link ui-brand"> @@ -22,17 +29,62 @@ defmodule Soonex.HomePage.Faq do <.accordion id="soonex-faq" - class="accordion ui-accent ui-size-md soonex-faq-instrument mx-auto w-full max-w-none" + class="accordion ui-accent ui-size-md ui-width-full soonex-faq" multiple={false} - collapsible={true} - value="what" + collapsible + value="ships" items={faq_items()} > <:trigger :let={item}> - {item.label} + + + {faq_index(item.value)} + + {item.label} + <:content :let={item}> -

        {item.content}

        +
        +

        {item.content}

        + <.navigate :if={item.value == "ships"} to="#features" class="link ui-brand w-fit"> + See the product <.heroicon name="hero-arrow-down" /> + + <.navigate :if={item.value == "waitlist"} to="#waitlist" class="link ui-brand w-fit"> + Open the waitlist <.heroicon name="hero-arrow-down" /> + +
          +
        • neo
        • +
        • uno
        • +
        • duo
        • +
        • leo
        • +
        + <.navigate + :if={item.value == "journal"} + to={Soonex.Public.path("/blog")} + class="link ui-brand w-fit" + > + Read the journal <.heroicon name="hero-arrow-up-right" /> + + <.clipboard + :if={item.value == "toolchain"} + id="soonex-faq-setup" + class="clipboard ui-brand ui-size-sm ui-width-fit self-start" + value="mix setup" + > + <:label class="sr-only">mix setup + <:copy> + <.heroicon name="hero-clipboard" /> + Copy + + <:copied> + <.heroicon name="hero-check" /> + Copied + + +
        <:indicator> <.heroicon name="hero-chevron-right" /> @@ -42,43 +94,65 @@ defmodule Soonex.HomePage.Faq do """ end + defp faq_index("ships"), do: "01" + defp faq_index("waitlist"), do: "02" + defp faq_index("themes"), do: "03" + defp faq_index("journal"), do: "04" + defp faq_index("motion"), do: "05" + defp faq_index("a11y"), do: "06" + defp faq_index("toolchain"), do: "07" + defp faq_index("countdown"), do: "08" + defp faq_index(_value), do: "00" + defp faq_items do Corex.Content.new([ %{ - value: "what", - label: "What is Soonex?", + value: "ships", + label: "What ships when I fork this template?", content: - "A launch workspace shipping 1 September — waitlist, public shipping log, countdown, and four looks for client review." + "A waitlist landing page, a journal, countdown chrome, and four theme skins. Photography, copy, and the launch date are placeholders you replace." }, %{ - value: "ships", - label: "What do I get on launch day?", + value: "waitlist", + label: "Does the waitlist collect real addresses?", content: - "The landing, waitlist fields, shipping log, header countdown, and four looks. Swap the stills, lockup, and date for your brand." + "Not in this demo. Submit still shows a toast so you can see the success path without storing mailboxes. Wire the same form to your list later — interests, role, team size, and launch notes are already named fields." }, %{ - value: "looks", - label: "What are the four looks?", + value: "themes", + label: "How do the four themes look so different?", content: - "neo, uno, duo, and leo. Same page, different type, corners, and brand. Switch them live in Template Options." + "Corex config owns seeds, radius, and type. Isolated CSS in assets/css/skins restyles nav chrome, hero composition, and ornament. Delete a skin file and its import to drop that look." }, %{ - value: "form", - label: "Will you email me after I join?", + value: "journal", + label: "Where does the journal live, and can I add posts?", content: - "Yes — when you wire a provider. On this demo page, submit shows the launch toast so you can see the success path." + "Posts are Markdown under the journal route. The home grid is a short reading list; the index paginates the rest. Add a file, rebuild, and the cover, date, and title show up." + }, + %{ + value: "motion", + label: "How does scroll motion behave with reduced motion?", + content: + "Scroll reveal, card lift, and skin ornament follow scroll and hover. Set Motion to Reduced in Accessibility and those animations stop. The condensed header and progress bar stay as chrome." }, %{ value: "a11y", - label: "How does accessibility work?", + label: "What do the accessibility controls change?", + content: + "The person-in-circle control at the corner opens zoom, contrast, motion, cursor, focus, and link underline for this device. Choices persist in local storage and write data attributes on the document." + }, + %{ + value: "toolchain", + label: "Do I need npm or a JavaScript bundler to start?", content: - "Open Accessibility for zoom, contrast, motion, cursor, focus, and underline. Choices stay on this device." + "No. Mix, Tailwind, and esbuild already in the project are enough. There is no package.json. Copy the setup command and you are in." }, %{ - value: "open", - label: "Is this open source?", + value: "countdown", + label: "How is the countdown wired to a launch date?", content: - "Yes. Clone the repo, run mix setup and mix soonex.server. There is no package.json — Mix, Tailwind, and esbuild are already in the project." + "Soonex.Launch holds the target. The hero shows a launch badge; the header timer appears after the hero. Change the target in that module when you have a real date." } ]) end diff --git a/lib/pages/home/features.ex b/lib/pages/home/features.ex new file mode 100644 index 0000000..6fe2bb5 --- /dev/null +++ b/lib/pages/home/features.ex @@ -0,0 +1,62 @@ +defmodule Soonex.HomePage.Features do + @moduledoc false + + use Phoenix.Component + use Corex + + import Soonex.Layouts.Section, only: [block: 1] + + def features(assigns) do + ~H""" + <.block + id="features" + section="features" + labelledby="soonex-features-heading" + eyebrow="Product" + tone={:root} + > + <:title> + Everything a launch site needs. Nothing you have to invent. + + <:lede> + Themes, waitlist, journal, and accessibility are already wired. You change copy, seeds, and + the launch date. + +
          +
        • + +

          + {feature.title} +

          +

          {feature.body}

          +
        • +
        + + """ + end + + defp claims do + [ + %{ + icon: "hero-swatch", + title: "Corex tokens, not a palette dump", + body: + "Light and dark both ship. Brand, accent, and ink stay readable because the design pipeline owns contrast, not a handful of hex codes in CSS." + }, + %{ + icon: "hero-rectangle-group", + title: "Four skins that actually differ", + body: + "Neo, uno, duo, and leo change chrome, density, and ornament — not just hue. Switch them from Template Options on this page." + }, + %{ + icon: "hero-command-line", + title: "Mix in, waitlist out", + body: + "mix setup then mix server. The waitlist toast is wired so you can see the success path before you attach a real list." + } + ] + end +end diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index 1cb8f42..64415f7 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -11,108 +11,91 @@ defmodule Soonex.HomePage.Hero do def hero(assigns) do ~H"""
        -
        - <.photo - src="/images/photos/hero.jpg" - alt="Sunlit studio with long work tables and hanging task lamps" - width={1600} - height={1200} - class="absolute inset-0 size-full" - loading="eager" - /> -
        -
        +
        +
        +
        +

        + Launching {Soonex.Launch.year_label()} +

        -
        -
        -
        - Launch · 1 September -

        - Soonex - - ships 1 September. - +

        + A launch page you can actually ship.

        -

        - Waitlist, shipping log, countdown, and four looks — the launch desk for studios that - ship on a date. + +

        + Soonex is a Tableau + Corex template: waitlist, journal, countdown chrome, and four + skins that do not look like recolors. Fork it, replace the copy, and go live.

        -
        - <.navigate to="#waitlist" class="button ui-brand ui-solid ui-size-md"> + +
        + <.navigate to="#waitlist" class="button ui-brand ui-solid ui-size-lg"> Join waitlist - <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-md"> - Read the log <.heroicon name="hero-arrow-up-right" /> + <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-lg"> + Read the journal <.heroicon name="hero-arrow-up-right" />
        -
        - -
        -
        -

        Reserve a September seat

        -

        - Tell us who you are. We write when the countdown ends. -

        -
        + <.native_input + type="email" + name="waitlist[email]" + id="soonex-hero-waitlist-email" + required + autocomplete="email" + placeholder="you@studio.dev" + class="native-input ui-size-md ui-width-full min-w-0 flex-1" > -
        - <.native_input - type="email" - name="waitlist[email]" - id="soonex-hero-waitlist-email" - required - autocomplete="email" - placeholder="you@studio.dev" - class="native-input ui-size-md ui-width-full min-w-0 flex-1" - > - <:label class="sr-only">Email - + <:label class="sr-only">Email + + + - -
        - - <.radio_group - id="soonex-hero-role" - name="waitlist[role]" - class="radio-group ui-brand ui-width-full" - value="founder" - items={[ - %{value: "founder", label: "Founder"}, - %{value: "engineer", label: "Engineer"}, - %{value: "designer", label: "Designer"} - ]} - > - <:label>I am a - - - -
        -
        - <.avatar - :for={person <- people()} - id={person.id} - src={Soonex.Public.path(person.src)} +
        +
          +
        • + <.photo + src={person.src} alt={person.alt} - class="avatar ui-size-sm" - > - <:fallback>{person.initials} - -
        -

        - 240 studios already on the list for 1 September. -

        -
        + width={72} + height={72} + class="soonex-avatar" + loading="eager" + /> + +
      +

      + Built for founders who want a finished waitlist, not another component demo. +

      +
    +
    + +
    +
    + <.photo + src="/images/photos/hero.jpg" + alt="Sunlit studio with long work tables and hanging task lamps" + width={1600} + height={1200} + class="absolute inset-0 size-full" + loading="eager" + />
    @@ -125,24 +108,9 @@ defmodule Soonex.HomePage.Hero do defp people do [ - %{ - id: "soonex-hero-avatar-amira", - src: "/images/people/amira.jpg", - alt: "Amira N.", - initials: "AN" - }, - %{ - id: "soonex-hero-avatar-jonas", - src: "/images/people/jonas.jpg", - alt: "Jonas K.", - initials: "JK" - }, - %{ - id: "soonex-hero-avatar-leah", - src: "/images/people/leah.jpg", - alt: "Leah M.", - initials: "LM" - } + %{src: "/images/people/amira.jpg", alt: "Amira N."}, + %{src: "/images/people/jonas.jpg", alt: "Jonas K."}, + %{src: "/images/people/leah.jpg", alt: "Leah M."} ] end end diff --git a/lib/pages/home/journal.ex b/lib/pages/home/journal.ex new file mode 100644 index 0000000..5a02f75 --- /dev/null +++ b/lib/pages/home/journal.ex @@ -0,0 +1,93 @@ +defmodule Soonex.HomePage.Journal do + @moduledoc false + + use Phoenix.Component + use Corex + + import Soonex.Layouts.Media, only: [photo: 1] + import Soonex.Layouts.Section, only: [block: 1] + + alias Soonex.Layouts.Shell + + attr(:posts, :list, default: []) + + def journal(assigns) do + posts = + assigns.posts + |> List.wrap() + |> Enum.sort_by(& &1[:date], {:desc, DateTime}) + |> Enum.take(3) + + assigns = assign(assigns, :highlight_posts, posts) + + ~H""" + <.block + id="journal" + section="journal" + labelledby="soonex-journal-heading" + eyebrow="Journal" + tone={:root} + > + <:title> + Shipping notes, not a lorem feed. + + <:lede> + Posts are Markdown under _posts. The home grid is a short reading list; the index paginates + the rest. + + <:actions> + <.navigate to={Soonex.Public.path("/blog")} class="link ui-brand"> + All posts <.heroicon name="hero-arrow-up-right" /> + + +
    +

    No posts yet.

    +
    +
    +
    +
    + <.photo + src={cover(post).src} + alt={cover(post).alt} + width={1400} + height={900} + /> +
    +
    +

    {date_label(post)}

    +

    + {post[:title] || "Untitled"} +

    +

    + {post[:description]} +

    + <.navigate + to={Soonex.Public.path(post.permalink)} + class="button ui-ghost ui-brand ui-size-sm mt-6 w-fit" + > + Read more <.heroicon name="hero-arrow-up-right" /> + +
    +
    +
    + + """ + 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 +end diff --git a/lib/pages/home/launch_band.ex b/lib/pages/home/launch_band.ex new file mode 100644 index 0000000..b9d68c7 --- /dev/null +++ b/lib/pages/home/launch_band.ex @@ -0,0 +1,77 @@ +defmodule Soonex.HomePage.LaunchBand do + @moduledoc false + + use Phoenix.Component + use Corex + + import Soonex.Layouts.Section, only: [block: 1] + + alias Soonex.Layouts.Shell + + def launch_band(assigns) do + ~H""" + <.block + id="launch" + section="launch" + labelledby="soonex-launch-heading" + eyebrow="Launch" + tone={:surface} + > + <:title> + Road to {Soonex.Launch.year_label()} + + <:lede> + Four beats. Replace the dates in Soonex.Launch. The header countdown reads the same target. + +
      +
    1. +

      + {beat.moment} +

      +

      + {beat.title} +

      +

      {beat.body}

      +
    2. +
    + + """ + end + + defp beats do + [ + %{ + moment: "12 March 2026", + title: "First cut of the waitlist", + featured: false, + body: + "Hero, form fields, and the toast path landed so a founder can see the success state before attaching a list." + }, + %{ + moment: "1 June 2026", + title: "Four skins, one markup tree", + featured: false, + body: + "Neo, uno, duo, and leo stopped sharing one layout. Each theme now has a CSS file you can delete." + }, + %{ + moment: "2 August 2026", + title: "Journal and tags", + featured: false, + body: + "Markdown posts, covers, and RSS joined the landing page so the launch log is part of the template." + }, + %{ + moment: Soonex.Launch.year_label(), + title: "Public launch", + featured: true, + body: + "Replace this date in Soonex.Launch. The header countdown appears after the hero and counts down to the same instant." + } + ] + end +end diff --git a/lib/pages/home/logos.ex b/lib/pages/home/logos.ex new file mode 100644 index 0000000..916e2ce --- /dev/null +++ b/lib/pages/home/logos.ex @@ -0,0 +1,55 @@ +defmodule Soonex.HomePage.Logos do + @moduledoc false + + use Phoenix.Component + use Corex + + def logos(assigns) do + assigns = assign(assigns, :tech, tech_items()) + + ~H""" +
    +

    + The same stack you already ship with +

    + <.marquee + id="soonex-tech-marquee" + class="marquee ui-width-full" + duration={20} + spacing="2rem" + pause_on_interaction + items={@tech} + > + <:item :let={item}> + + + {item.name} + + + +
    + """ + end + + defp tech_items do + [ + %{name: "Elixir", src: "/images/tech/elixir.svg"}, + %{name: "Phoenix", src: "/images/tech/phoenixframework.svg"}, + %{name: "Tailwind", src: "/images/tech/tailwindcss.svg"}, + %{name: "HTML5", src: "/images/tech/html5.svg"}, + %{name: "JavaScript", src: "/images/tech/javascript.svg"}, + %{name: "TypeScript", src: "/images/tech/typescript.svg"}, + %{name: "Hex", src: "/images/tech/hex.svg"} + ] + end +end diff --git a/lib/pages/home/pricing.ex b/lib/pages/home/pricing.ex new file mode 100644 index 0000000..ec2bf3e --- /dev/null +++ b/lib/pages/home/pricing.ex @@ -0,0 +1,99 @@ +defmodule Soonex.HomePage.Pricing do + @moduledoc false + + use Phoenix.Component + use Corex + + import Soonex.Layouts.Section, only: [block: 1] + + alias Soonex.Layouts.Shell + + def pricing(assigns) do + ~H""" + <.block + id="pricing" + section="pricing" + labelledby="soonex-pricing-heading" + eyebrow="Plans" + tone={:root} + > + <:title> + One template. Three ways to ship it. + + <:lede> + These tiers are placeholders you rename. The middle card is the one you keep when you fork. + +
    +
    +

    {plan.name}

    +

    {plan.headline}

    +

    + {plan.price} +

    +

    {plan.body}

    +
      +
    • + <.heroicon name="hero-check" class="mt-0.5 size-4 shrink-0" /> + {item} +
    • +
    + <.navigate + to="#waitlist" + class={ + if plan.featured, + do: "button ui-size-md mt-8 w-full bg-root text-ink", + else: "button ui-brand ui-solid ui-size-md mt-8 w-full" + } + > + {plan.cta} + +
    +
    + + """ + end + + defp plan_class(%{featured: true}) do + "soonex-frame relative overflow-hidden border" + end + + defp plan_class(_plan), do: Shell.panel() + + defp plans do + [ + %{ + name: "Launch", + headline: "Fork and go live", + price: "Open", + featured: false, + cta: "Join waitlist", + body: "The waitlist landing page, countdown chrome, and a single theme you rebrand.", + items: ["Waitlist form + toast", "Journal + RSS", "One Corex theme"] + }, + %{ + name: "Studio", + headline: "All four skins", + price: "Open", + featured: true, + cta: "Start with Studio", + body: + "Switch neo, uno, duo, and leo from config. Keep the ones you like; delete the rest.", + items: ["Four skins + dark mode", "Accessibility dialog", "Launch timeline"] + }, + %{ + name: "Agency", + headline: "Ship it for a client", + price: "Open", + featured: false, + cta: "Join waitlist", + body: + "Rename the OTP app, swap the lockup, and hand over a site that already passes axe.", + items: ["mix project.rename", "Self-hosted fonts", "GitHub Pages workflow"] + } + ] + end +end diff --git a/lib/pages/home/product.ex b/lib/pages/home/product.ex index f6d6d85..85c7c32 100644 --- a/lib/pages/home/product.ex +++ b/lib/pages/home/product.ex @@ -4,178 +4,103 @@ defmodule Soonex.HomePage.Product do use Phoenix.Component use Corex + import Soonex.Layouts.Media, only: [photo: 1] import Soonex.Layouts.Section, only: [block: 1] alias Soonex.Layouts.Shell def product(assigns) do - assigns = assign(assigns, :snippet, snippet()) - ~H""" <.block id="product" + section="product" labelledby="soonex-product-heading" - eyebrow="Product" + eyebrow="Inside" tone={:surface} - layout={:sticky} > <:title> - Open a panel. Ship the desk. + The page is the product. Replace the stills. <:lede> - Waitlist, log, countdown, looks — one product UI for studios launching 1 September. + Photography, lockup, and launch date are placeholders. The bands, tokens, and waitlist + fields stay. - <:actions> - <.navigate to="#waitlist" class="button ui-brand ui-solid ui-size-md"> - Join waitlist - - - - <.tabs - id="soonex-product-tabs" - class="tabs ui-brand ui-size-md ui-width-full soonex-product-tabs" - indicator - value="overview" - > - <:trigger value="overview">Overview - <:content value="overview" class="soonex-tab-panel"> -
    -

    - Soonex is a launch workspace for studios with a hard date. Put the waitlist on the - page, keep a public log, and review four looks with your client before go-live. -

    -
      -
    • Waitlist rack with toast on submit
    • -
    • Shipping log with covers and tags
    • -
    • Countdown aimed at 1 September
    • -
    • Four looks switched on this device
    • -
    -
    - - - <:trigger value="waitlist">Waitlist - <:content value="waitlist" class="soonex-tab-panel"> -
    -

    Capture who is coming

    -

    - Same fields on the hero still and at the end of the page. Wire your provider after - launch — the toast already ships. -

    -
      -
    • Email with validation
    • -
    • Role select or radios
    • -
    • Interests as tags
    • -
    • Team size stepper
    • -
    • Launch notes switch
    • -
    -
    - - - <:trigger value="log">Log - <:content value="log" class="soonex-tab-panel"> -
    -

    - Keep a public trail of what shipped. Posts live as Markdown with covers and tags. -

    -
    - <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-brand ui-size-sm"> - All shipping notes <.heroicon name="hero-arrow-up-right" /> - -
    -
    - - - <:trigger value="looks">Looks - <:content value="looks" class="soonex-tab-panel"> -
    -

    Soonex

    -

    - Four looks for the same launch. Switch rooms in Template Options — no second build. -

    - <.navigate to="#looks" class="link ui-brand mt-6 inline-flex"> - See all four looks <.heroicon name="hero-arrow-down" /> - +
    +
    + <.photo + src="/images/photos/studio.jpg" + alt="A calm studio interior with a low sofa, plants, and a large window" + width={1400} + height={1600} + class="absolute inset-0 size-full" + /> +
    +

    Studio

    +

    + Drop in your product stills. Keep the grid. +

    - - - -
    -
    - Get Soonex -

    Clone once. mix setup, then mix soonex.server.

    -
    - <.clipboard - id="soonex-theme-snippet" - class="clipboard ui-brand ui-solid ui-width-full ui-size-md min-w-0 flex-1" - value={@snippet} - > - <:label>Clone - <:copy> - <.heroicon name="hero-clipboard" /> - Copy - - <:copied> - <.heroicon name="hero-check" /> - Copied - - -
    +
    -
    - Plans -

    - Same core. Three lanes. -

    -
    -
    -
    -
    - {plan.name} -

    {plan.headline}

    -

    {plan.body}

    -
    - <.navigate to="#waitlist" class={"#{plan.cta_class} shrink-0"}> - {plan.cta} - -
    -
      -
    • {item}
    • -
    -
    +
    + <.collapsible + :for={cell <- cells()} + id={cell.id} + class="collapsible ui-brand ui-width-full soonex-bento-cell" + open={cell.open} + > + <:trigger> + + {cell.kicker} + + {cell.title} + + + <.heroicon name="hero-chevron-down" /> + + <:content> +

    {cell.body}

    + +
    """ end - defp plans do + defp cells do [ %{ - name: "Solo", - headline: "Fork and put a date on the page", - body: "One look, the waitlist, and the log.", - cta: "Join waitlist", - cta_class: "button ui-ghost ui-size-sm w-fit", - items: ["Waitlist + shipping log", "Countdown to 1 September"] + id: "soonex-inside-waitlist", + open: true, + kicker: "Waitlist", + title: "Named fields, ready to wire", + body: + "Email, role, interests, team size, and launch notes are already in the form. Submit shows a toast in this demo." }, %{ - name: "Studio", - headline: "Four looks, client review", - body: "Treat Template Options as the review.", - cta: "Join waitlist", - cta_class: "button ui-brand ui-solid ui-size-sm w-fit", - items: ["neo, uno, duo, leo", "Header timer + toast"] + id: "soonex-inside-journal", + open: false, + kicker: "Journal", + title: "Markdown posts, tags, RSS", + body: "Add a file under _posts, rebuild, and the cover, date, and title show up." }, %{ - name: "Agency", - headline: "Accessible handoff", - body: "Ship with accessibility on and a clone path the studio can run.", - cta: "Join waitlist", - cta_class: "button ui-ghost ui-size-sm w-fit", - items: ["Zoom, contrast, motion", "Clone · mix setup · ship"] + id: "soonex-inside-skins", + open: false, + kicker: "Skins", + title: "Custom CSS you can delete", + body: + "Each theme lives in assets/css/skins. Host CSS does not copy Corex recipes or set overlay opacity." + }, + %{ + id: "soonex-inside-access", + open: false, + kicker: "Access", + title: "Preferences stay on device", + body: + "Zoom, contrast, motion, cursor, focus, and link underline persist in local storage. Reset returns the defaults." } ] end - - defp snippet, do: "git clone https://github.com/corex-ui/soonex.git" end diff --git a/lib/pages/home/stats.ex b/lib/pages/home/stats.ex new file mode 100644 index 0000000..f291857 --- /dev/null +++ b/lib/pages/home/stats.ex @@ -0,0 +1,48 @@ +defmodule Soonex.HomePage.Stats do + @moduledoc false + + use Phoenix.Component + + import Soonex.Layouts.Section, only: [block: 1] + + alias Soonex.Layouts.Shell + + def stats(assigns) do + ~H""" + <.block + id="stats" + section="stats" + labelledby="soonex-stats-heading" + eyebrow="Numbers" + tone={:surface} + align={:center} + compact + > + <:title> + Small surface. Finished chrome. + + <:lede> + Counts you can stand behind when you fork the template. Replace them when you have real ones. + +
    +
    +
    {caption}
    +
    {stat}
    +
    +
    + + """ + end + + defp stat_items do + [ + {"4", "Theme skins"}, + {"5", "Waitlist fields"}, + {Soonex.Launch.label(), "Launch day"}, + {"0", "npm packages"} + ] + end +end diff --git a/lib/pages/home/steps.ex b/lib/pages/home/steps.ex new file mode 100644 index 0000000..597f185 --- /dev/null +++ b/lib/pages/home/steps.ex @@ -0,0 +1,80 @@ +defmodule Soonex.HomePage.Steps do + @moduledoc false + + use Phoenix.Component + use Corex + + import Soonex.Layouts.Section, only: [block: 1] + + def steps(assigns) do + ~H""" + <.block + id="steps" + section="steps" + labelledby="soonex-steps-heading" + eyebrow="How it works" + tone={:root} + > + <:title> + Up and running in three commands. + + <:lede> + No npm project. Mix, Tailwind, and esbuild are already in the repo. Clone, set up, and serve. + +
      +
    1. +

      + {step.index} +

      +

      {step.title}

      +

      {step.body}

      + <.clipboard + id={step.clip_id} + class="clipboard ui-brand ui-solid ui-width-fit ui-size-sm mt-6 self-start" + value={step.command} + > + <:label class="sr-only">{step.title} + <:copy> + <.heroicon name="hero-clipboard" /> + Copy + + <:copied> + <.heroicon name="hero-check" /> + Copied + + +
    2. +
    + + """ + end + + defp step_items do + [ + %{ + index: "01", + clip_id: "soonex-step-clone", + title: "Clone the template", + body: + "Grab the repo. Photography, copy, and the launch date are the first things you replace.", + command: "git clone https://github.com/corex-ui/soonex.git" + }, + %{ + index: "02", + clip_id: "soonex-step-setup", + title: "mix setup", + body: + "Fetches Hex deps and builds Corex design CSS from config. Fonts are already self-hosted.", + command: "mix setup" + }, + %{ + index: "03", + clip_id: "soonex-step-server", + title: "mix server", + body: + "Serves the site on localhost:4999. Template Options in the corner switches theme and mode.", + command: "mix server" + } + ] + end +end diff --git a/lib/pages/home/voices.ex b/lib/pages/home/voices.ex new file mode 100644 index 0000000..308be14 --- /dev/null +++ b/lib/pages/home/voices.ex @@ -0,0 +1,77 @@ +defmodule Soonex.HomePage.Voices do + @moduledoc false + + use Phoenix.Component + + import Soonex.Layouts.Media, only: [photo: 1] + import Soonex.Layouts.Section, only: [block: 1] + + alias Soonex.Layouts.Shell + + def voices(assigns) do + ~H""" + <.block + id="voices" + section="voices" + labelledby="soonex-voices-heading" + eyebrow="Voices" + tone={:root} + > + <:title> + Real workflows, not a component tour. + + <:lede> + These quotes are placeholders. Keep the grid. Skins restyle the cards; the markup stays. + +
      +
    • +
      +

      + “{voice.quote}” +

      +
      + <.photo + src={voice.src} + alt={voice.name} + width={72} + height={72} + class="soonex-avatar soonex-avatar-lg" + /> +
      + {voice.name} + {voice.role} +
      +
      +
      +
    • +
    + + """ + end + + defp voice_items do + [ + %{ + src: "/images/people/amira.jpg", + quote: + "We forked Soonex on a Thursday and had a waitlist on a custom domain before the weekend. The skins were the part we did not have to argue about.", + name: "Amira N.", + role: "Founder, studio launch" + }, + %{ + src: "/images/people/jonas.jpg", + quote: + "I changed the launch date in one module, ran mix corex.design.build after a seed tweak, and the contrast still held. That is the whole job.", + name: "Jonas K.", + role: "Staff engineer" + }, + %{ + src: "/images/people/leah.jpg", + quote: + "Leo does not look like a recolor of neo. That is the bar. If a theme switch is invisible, the template failed.", + name: "Leah M.", + role: "Product designer" + } + ] + end +end diff --git a/lib/pages/home/waitlist.ex b/lib/pages/home/waitlist.ex index 72da15f..f42829a 100644 --- a/lib/pages/home/waitlist.ex +++ b/lib/pages/home/waitlist.ex @@ -14,23 +14,25 @@ defmodule Soonex.HomePage.Waitlist do ~H""" <.block id="waitlist" + section="waitlist" labelledby="soonex-waitlist-heading" eyebrow="Waitlist" tone={:surface} align={:center} - layout={:open} + reveal={false} > <:title> - Join the September list + Be on the list when {Soonex.Launch.label()} lands. <:lede> - Role, interests, and team size. We write when the countdown ends. + This demo does not store addresses. Submit still shows the success toast so you can see the + path before you wire a real list.
    <.native_input @@ -50,13 +52,18 @@ defmodule Soonex.HomePage.Waitlist do name="waitlist[role]" class="select ui-brand ui-size-md ui-width-full" value={["founder"]} + items={@role_items} positioning={ %Corex.Positioning{ + strategy: "absolute", placement: "bottom-start", - same_width: true + same_width: true, + gutter: 8, + slide: false, + fit_viewport: false, + flip: false } } - items={@role_items} > <:label>Role <:trigger> @@ -67,7 +74,7 @@ defmodule Soonex.HomePage.Waitlist do <.tags_input id="soonex-waitlist-interests" name="waitlist[interests]" - class="tags-input ui-brand ui-size-md ui-width-full" + class="tags-input ui-brand ui-width-full" value={[]} translation={ %Corex.TagsInput.Translation{ @@ -84,7 +91,7 @@ defmodule Soonex.HomePage.Waitlist do <.number_input id="soonex-waitlist-team" name="waitlist[team_size]" - class="number-input ui-brand ui-size-md ui-width-full" + class="number-input ui-brand ui-width-full" min={1.0} max={200.0} step={1.0} @@ -113,9 +120,6 @@ defmodule Soonex.HomePage.Waitlist do
    -

    - Demo page — toast only until you wire a provider. -

    """ end diff --git a/lib/pages/home_page.ex b/lib/pages/home_page.ex index 054b1ad..48591a9 100644 --- a/lib/pages/home_page.ex +++ b/lib/pages/home_page.ex @@ -5,38 +5,38 @@ defmodule Soonex.HomePage do use Corex import Soonex.HomePage.Hero, only: [hero: 1] - import Soonex.HomePage.MarqueeBand, only: [marquee_band: 1] - import Soonex.HomePage.Bento, only: [bento: 1] + import Soonex.HomePage.Logos, only: [logos: 1] + import Soonex.HomePage.Features, only: [features: 1] import Soonex.HomePage.Product, only: [product: 1] - import Soonex.HomePage.Themes, only: [themes: 1] - import Soonex.HomePage.Notes, only: [notes: 1] - import Soonex.HomePage.Log, only: [log: 1] + import Soonex.HomePage.Steps, only: [steps: 1] + import Soonex.HomePage.Pricing, only: [pricing: 1] + import Soonex.HomePage.LaunchBand, only: [launch_band: 1] + import Soonex.HomePage.Voices, only: [voices: 1] + import Soonex.HomePage.Stats, only: [stats: 1] + import Soonex.HomePage.Journal, only: [journal: 1] import Soonex.HomePage.Faq, only: [faq: 1] import Soonex.HomePage.Waitlist, only: [waitlist: 1] def template(assigns) do - assigns = Map.put(assigns, :log_posts, posts(assigns)) + assigns = + assigns + |> Map.put(:posts, assigns |> Map.get(:posts, []) |> List.wrap()) ~H"""
    <.hero /> - <.marquee_band /> - <.bento /> + <.logos /> + <.features /> <.product /> - <.themes /> - <.notes /> - <.log posts={@log_posts} /> + <.steps /> + <.pricing /> + <.launch_band /> + <.voices /> + <.stats /> + <.journal posts={@posts} /> <.faq /> <.waitlist />
    """ end - - defp posts(assigns) do - cond do - is_list(assigns[:posts]) -> assigns.posts - is_map(assigns[:page]) and is_list(assigns.page[:posts]) -> assigns.page.posts - true -> [] - end - end end diff --git a/lib/pages/not_found_page.ex b/lib/pages/not_found_page.ex index 70a0a4e..0b4e013 100644 --- a/lib/pages/not_found_page.ex +++ b/lib/pages/not_found_page.ex @@ -25,19 +25,19 @@ defmodule Soonex.NotFoundPage do
    <.lockup /> - 404 -

    +

    404

    +

    Page not found

    -

    - That URL is not on Soonex. Head home to join the waitlist, or read the shipping log. +

    + That URL is not on this site. Head home or read the journal.

    <.navigate to={Soonex.Public.path("/")} class="button ui-brand ui-solid ui-size-md"> Home <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-md"> - Log + Journal
    diff --git a/lib/pages/privacy_page.ex b/lib/pages/privacy_page.ex new file mode 100644 index 0000000..24babe9 --- /dev/null +++ b/lib/pages/privacy_page.ex @@ -0,0 +1,144 @@ +defmodule Soonex.PrivacyPage do + @moduledoc false + + use Tableau.Page, + layout: Soonex.RootLayout, + permalink: "/privacy/", + title: "Privacy", + description: + "How Soonex handles data on this demo site: local preferences, cookie consent, and what we do not collect.", + page_kind: :privacy, + sitemap: %{priority: 0.3, changefreq: "yearly"} + + use Phoenix.Component + use Corex + + alias Soonex.Layouts.Shell + + def template(assigns) do + ~H""" +
    +
    + <.layout_heading class="layout-heading" subtitle_tag="p"> + <:title>Privacy + <:subtitle> + This page describes how the Soonex demo handles information on your device. It is not legal advice. + + <:actions> + <.navigate to={Soonex.Public.path("/")} class="button ui-ghost ui-size-sm"> + <.heroicon name="hero-arrow-left" /> Home + + + + +
    +

    + Last updated: 1 September 2026 +

    + +

    Who this applies to

    +

    + Soonex is a static launch-page template published by Corex for demonstration and forking. + When you browse the hosted demo or run it locally, the site behaves as described below. + If you fork the template and deploy it under your own domain, you are responsible for updating + this page and your consent banner to match your product and jurisdiction. +

    + +

    What we do not collect

    +

    + The waitlist forms on the home page are wired for UX only. Submitting an email shows a success + toast but does not send your address to a server, database, or third-party list provider. + There is no account system, no payment flow, and no server-side session in this template. +

    + +

    Information stored on your device

    +

    + To make the demo usable across visits, Soonex stores a small set of preferences in your browser + using localStorage. These values stay on your device and are not transmitted to us. +

    +
      +
    • + Theme and color mode + — which Corex theme (neo, uno, duo, leo) and light or dark mode you selected. +
    • +
    • + Accessibility settings + — contrast, motion, and related choices from the accessibility panel. +
    • +
    • + Cookie consent + — whether you accepted, rejected, or customised analytics and marketing categories. +
    • +
    +

    + You can clear these at any time through your browser settings or by removing site data for this origin. + The cookie banner can be reopened from the footer to change your choices. +

    + +

    Cookie categories

    +

    + The consent banner groups cookies into three categories. Only necessary storage is active by default. +

    +
      +
    • + Necessary + — required for theme, accessibility, and remembering your consent decision. + These cannot be disabled without breaking the demo experience. +
    • +
    • + Analytics + — would cover measurement tags such as privacy-friendly analytics. + This template does not load analytics scripts even if you allow the category. +
    • +
    • + Marketing — would cover remarketing or social pixels. + This template does not load marketing tags even if you allow the category. +
    • +
    + +

    Third parties

    +

    + The static build may reference self-hosted fonts and images. It does not embed third-party trackers, + social widgets, or hosted video players. If you add those integrations when forking the template, + disclose them here and wire them through the consent module in lib/soonex/cookie_consent.ex. +

    + +

    Retention

    +

    + Local preference data persists until you clear it or until the browser removes it under its own storage policies. + Because nothing is sent to a backend in the demo, we do not hold copies of your choices on our servers. +

    + +

    Your rights

    +

    + Depending on where you live, you may have rights to access, correct, or delete personal data. + On this demo, the only data involved is what your browser stores locally; you control it directly. + For a production deployment you operate, provide a contact path and process requests under your own policy. +

    + +

    Children

    +

    + The Soonex demo is a developer-facing marketing template. It is not directed at children and does not + knowingly collect information from anyone under 16. +

    + +

    Changes

    +

    + We may update this page when the template changes—for example, if a fork adds real list collection or analytics. + The “Last updated” date at the top will change when we do. +

    + +

    Contact

    +

    + Questions about this demo policy can be sent to info@netoum.com. + For production sites you deploy, replace this address with your own. +

    +
    +
    +
    + """ + end +end diff --git a/lib/pages/tags_index_page.ex b/lib/pages/tags_index_page.ex index 7d107f1..323e651 100644 --- a/lib/pages/tags_index_page.ex +++ b/lib/pages/tags_index_page.ex @@ -37,36 +37,38 @@ defmodule Soonex.TagsIndexPage do
    <.layout_heading class="layout-heading" subtitle_tag="p"> <:title>Tags - <:subtitle>Browse the shipping log by theme, launch, studio, and notes. + <:subtitle>Browse the journal by topic <:actions> <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-sm"> - <.heroicon name="hero-arrow-left" /> Log + <.heroicon name="hero-arrow-left" /> Journal -
    +

    No tags yet.

    -
      - -
    + +
    <.pager id="soonex-tags-pagination" count={length(@tag_cards)} page_size={6} />
    diff --git a/lib/soonex/accessibility.ex b/lib/soonex/accessibility.ex index be2416e..6686db7 100644 --- a/lib/soonex/accessibility.ex +++ b/lib/soonex/accessibility.ex @@ -110,7 +110,7 @@ defmodule Soonex.Accessibility do def accessibility_dialog_id, do: "a11y-dialog" - attr(:trigger_class, :string, default: "button ui-size-sm") + attr(:trigger_class, :string, default: "button ui-brand ui-solid ui-size-sm ui-trigger--circle") def accessibility_panel(assigns) do assigns = assign(assigns, :axes, Accessibility.axes()) @@ -126,7 +126,7 @@ defmodule Soonex.Accessibility do final_focus={"dialog:#{accessibility_dialog_id()}:trigger"} > <:trigger class={@trigger_class} aria_label="Accessibility"> - <.heroicon name="hero-user-circle" /> Accessibility + <.heroicon name="hero-user" /> <:title>Accessibility <:description> diff --git a/lib/soonex/cookie_consent.ex b/lib/soonex/cookie_consent.ex new file mode 100644 index 0000000..9ed3889 --- /dev/null +++ b/lib/soonex/cookie_consent.ex @@ -0,0 +1,240 @@ +defmodule Soonex.CookieConsent do + @moduledoc false + + use Phoenix.Component + use Corex + + @dialog_id "cookie-consent-dialog" + @banner_id "cookie-consent-banner" + @storage_key "soonex:cookie-consent" + + def dialog_id, do: @dialog_id + def banner_id, do: @banner_id + def storage_key, do: @storage_key + + def head_script do + Phoenix.HTML.raw(""" + + """) + end + + attr(:privacy_path, :string, required: true) + + def cookie_consent(assigns) do + assigns = assign(assigns, :banner_id, @banner_id) + + ~H""" + + """ + end + + def cookie_dialog(assigns) do + assigns = assign(assigns, :dialog_id, @dialog_id) + + ~H""" + <.dialog + id={@dialog_id} + class="dialog cookie-consent-dialog ui-size-md ui-rounded-lg ui-width-fit ui-max-height-dvh [&_[data-part=positioner]]:p-space-md [&_[data-part=description]]:text-ink-muted" + modal + prevent_scroll + animation="instant" + > + <:trigger class="link ui-nav ui-size-sm text-ink-muted"> + Cookies + + <:title> + + <.cookie_icon class="size-[1.25em] shrink-0" /> + Cookie preferences + + + <:description> + Necessary preferences always stay on. Toggle the rest below. + + <:close_trigger> + <.heroicon name="hero-x-mark" /> + + <:content class="flex min-h-0 max-h-dvh w-full flex-col overflow-hidden ui-width-full sm:ui-width-xl"> +
    +
      +
    • +
      +

      Necessary

      +

      + Theme, mode, and accessibility settings required for the site to work. +

      +
      + <.switch id="cookie-consent-necessary" class="switch ui-size-sm" checked disabled> + <:label class="sr-only">Necessary cookies + +
    • +
    • +
      +

      Analytics

      +

      + Help us understand how the site is used. Off by default on this marketing site today. +

      +
      + <.switch id="cookie-consent-analytics" class="switch ui-size-sm" name="analytics"> + <:label class="sr-only">Analytics cookies + +
    • +
    • +
      +

      Marketing

      +

      + Optional marketing tags. Off by default; nothing loads unless you allow them. +

      +
      + <.switch id="cookie-consent-marketing" class="switch ui-size-sm" name="marketing"> + <:label class="sr-only">Marketing cookies + +
    • +
    +
    + +
    + <.action + type="button" + class="button ui-ghost ui-size-sm ui-rounded-md ui-width-fit" + data-cookie-consent="reset" + > + Essential only + + <.action + type="button" + class="button ui-brand ui-solid ui-size-sm ui-rounded-md ui-width-fit text-brand-contrast" + data-cookie-consent="save" + > + Save preferences + +
    + + + """ + end + + attr(:class, :string, default: nil) + + def cookie_icon(assigns) do + ~H""" + + """ + end +end diff --git a/lib/soonex/launch.ex b/lib/soonex/launch.ex new file mode 100644 index 0000000..30b7e30 --- /dev/null +++ b/lib/soonex/launch.ex @@ -0,0 +1,15 @@ +defmodule Soonex.Launch do + @moduledoc false + + @target ~U[2026-12-01 00:00:00Z] + + def target, do: @target + + def label, do: "1 December" + + def year_label, do: "1 December 2026" + + def countdown_ms do + max(DateTime.diff(@target, DateTime.utc_now(), :millisecond), 0) + end +end diff --git a/mix.exs b/mix.exs index a104385..2db27ad 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,6 @@ defmodule Soonex.MixProject do version: "0.2.1", elixir: "~> 1.17", start_permanent: Mix.env() == :prod, - compilers: Mix.compilers() ++ [:corex_design], elixirc_paths: elixirc_paths(Mix.env()), aliases: aliases(), deps: deps(), @@ -58,7 +57,7 @@ defmodule Soonex.MixProject do {:rustler_precompiled, "~> 0.9", override: true}, {:makeup_syntect, "~> 0.1.4"}, {:wallaby, "~> 0.30", only: :test, runtime: false}, - {:a11y_audit, "~> 0.3.1", only: :test, runtime: false}, + {:a11y_audit, "~> 0.5.0", only: :test, runtime: false}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:ex_slop, "~> 0.1", only: [:dev, :test], runtime: false}, {:usage_rules, "~> 1.1", only: :dev} @@ -93,7 +92,7 @@ defmodule Soonex.MixProject do "tableau.build" ], test: ["pre.test", "test"], - server: ["soonex.server"], + server: ["soonex.port_check", "tableau.server"], "assets.build": [ "corex.design.build", "tailwind default", diff --git a/mix.lock b/mix.lock index fd79476..d6072be 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,12 @@ %{ - "a11y_audit": {:hex, :a11y_audit, "0.3.4", "fdf1951c9e80fcb6cdf28c82cfcc3a7a8d373353ff2e31b9879ff98a5f58e86a", [:mix], [{:hound, "~> 1.1", [hex: :hound, repo: "hexpm", optional: true]}, {:wallaby, "~> 0.30", [hex: :wallaby, repo: "hexpm", optional: true]}], "hexpm", "9d7380bf1c424140118dccc1e8bcf2f9531300cdde47753e2fc44be38847a58d"}, + "a11y_audit": {:hex, :a11y_audit, "0.5.0", "482212a46edce463d3e127c75ffa3fbd8ecbeb786623ec05c4cfc83f5961927c", [:mix], [{:hound, "~> 1.1", [hex: :hound, repo: "hexpm", optional: true]}, {:wallaby, "~> 0.30", [hex: :wallaby, repo: "hexpm", optional: true]}], "hexpm", "ea579da335cd2a155a37e3996c99c4cdf123679a636572b0c25a311b2bdb21a0"}, "bandit": {:hex, :bandit, "1.12.5", "af205a8e550f304caae09a97d29fd3c79a7f337526ea7cd772d2ff11d2f7c800", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "c5684ca062fa407cac115aec3256383f3e2ec9fdced7904d59cf5a7bb7ed6181"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "certifi": {:hex, :certifi, "2.17.0", "835748414307e15e05b17d0e518190228ce648b08d569a5cc93a85a40f3e5c9b", [:rebar3], [], "hexpm", "8122798a17f0293c80daada25d0f81c7f4d708c73fef782c7c9b1950e26e4d21"}, "color": {:hex, :color, "0.13.0", "068110e5397ac5d3c9f97658282e0f4ab9a32468be6d7a2a91a8804e67b228d7", [:mix], [{:bandit, "~> 1.5", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "de127946869931d418bac2d82dc29feae1a8f5f729f135922fbccf0059a58ab2"}, - "corex": {:hex, :corex, "0.2.1", "44ec61b8b183ae5395075f513fc0a38e7a7a3b7101037fbaef42fcc118270a5e", [:mix], [{:gettext, "~> 1.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.8.1", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "b5ef5ef9a9bfd1a905140cfb06caf3f3032f8ee4c59becf17823f3ca210f3de5"}, - "corex_design": {:hex, :corex_design, "0.2.1", "b20a0d01c3deea5163b057668b1e1e31f810963c36acd907b6102e322c9c074b", [:mix], [{:color, "~> 0.11", [hex: :color, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}], "hexpm", "b02a9ba8e26cf1b33bcff3cb1166b34173531fe81d5a66fbb7cbe526fa0383ec"}, - "corex_mcp": {:hex, :corex_mcp, "0.2.1", "ed5fdcf29936b68afbc51aa1add65ec12d8952361983fa6fe50a2181938ac3e3", [:mix], [{:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "dd2a0de9da112281452a14a29c359c79f26df26b4d5b76c0c0e8cfb221a388f9"}, + "corex": {:hex, :corex, "0.2.2", "3ad0179d61e48bb8ef7d6ab709fe55d2e79989983f945e291460f8354384d5d5", [:mix], [{:gettext, "~> 1.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.8.1", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "6e3321ed33f115b1179ae096596f464dc25a636e49146697eea848b2b9d7f56e"}, + "corex_design": {:hex, :corex_design, "0.2.2", "d2318fe2ab55be0e0563c01acb4134e5db0edf440749a68f2eb1c2191cdacae5", [:mix], [{:color, "~> 0.11", [hex: :color, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}], "hexpm", "d63e11336305a1a67fe9835a63197108edb76acfdc5d1f5b6b920d3d97681b47"}, + "corex_mcp": {:hex, :corex_mcp, "0.2.2", "fe7220ae85dc2f00f212109d71523963c774f50dd6ae6e2fe59e56b4dea8a831", [:mix], [{:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d5806e4a1361a52d3ec8ca204ad9faf66928ce53ff952fdd1bfb4828930411ef"}, "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, "date_time_parser": {:hex, :date_time_parser, "1.3.0", "6ba16850b5ab83dd126576451023ab65349e29af2336ca5084aa1e37025b476e", [:mix], [{:kday, "~> 1.0", [hex: :kday, repo: "hexpm", optional: false]}], "hexpm", "93c8203a8ddc66b1f1531fc0e046329bf0b250c75ffa09567ef03d2c09218e8c"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, @@ -18,14 +18,14 @@ "floki": {:hex, :floki, "0.38.4", "10f98971e892aed2c2f1b3a0f928e488e3797e1c6dd3dfd98db40b14e9a78bcf", [:mix], [], "hexpm", "bdb34645eee8e79845c7edaca2d4099a52804ee4d4a3ecc683a69451f0244973"}, "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"}, "glob_ex": {:hex, :glob_ex, "0.1.12", "7b2d9369c20e2697efcfd185d13d6e84c94cd3bfd2730fbde613141c2e015c00", [:mix], [], "hexpm", "2e2fac83f113514434c7eaf267b4c38af2f91766f1cab2c5db7053b7fc1ee0bb"}, - "h2": {:hex, :h2, "0.10.4", "31ef580056e19977b6542eea2718aa19a45841205cd82cd48535dd7683caea4c", [:rebar3], [], "hexpm", "8350920ac7c259fb759a2830d3ce0dd248c48422a031d1760bebdd2020fa30a9"}, - "hackney": {:hex, :hackney, "4.6.0", "5e7e4d571b06a764ecfa89174b6ad482cf2eb221adf6b79c041965efee8f397a", [:rebar3], [{:certifi, "~> 2.17.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.10.4", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:quic, "~> 1.7.0", [hex: :quic, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.3", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "cd103f9817048ccbff9eafff3708293beefecc29ab4e481c766ac502de5118b9"}, + "h2": {:hex, :h2, "0.12.0", "f393539ee2728f8118fb2024b6d5f3e2c45e40ceb31b18b4e9bf5e50d028f80f", [:rebar3], [], "hexpm", "beaafc93c54cdc5d623247334d3970cdf4bc66b6b8b296b74ba1d7c7513c3dfc"}, + "hackney": {:hex, :hackney, "4.7.4", "8fe2ddaa3ca27de99d68e682d72b66d07d2331da680f77c8000580a0122c69e6", [:rebar3], [{:certifi, "~> 2.17.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.12.0", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.5", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.5", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "d07d7e1358353ab6cc75132f058c155287f3e013d43f709fbb79d79eeab98195"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "httpoison": {:hex, :httpoison, "3.0.0", "8566a933bb9175236d1ec335978445b67cd1f5b5d3ead6ca4b80be469d41f5d9", [:mix], [{:hackney, "~> 4.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "9130197b7658901c493d6fcfb842fb9676300fa8a6c8ed058c8889cf1a77f3c2"}, "idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"}, - "igniter": {:hex, :igniter, "0.8.3", "9de74d3885efae43b0b58dc6f7b816963c4bbd391e6b6fe6922ee21c4e384c76", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.5", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "afc5e3848d885e680da5c3b65e5e7717555a08cd12305190ff2be76427af39ff"}, + "igniter": {:hex, :igniter, "0.8.4", "f79f1bbdc2fb7b9ca030a22d12a585b060cbf5b94b9d3f23b1148578a9e05d11", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.5", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "a9b1cbec996ccb100b4f7d8130129b2dd3f18eb4224ac9a0e907e428ca90dbd7"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "kday": {:hex, :kday, "1.1.0", "64efac85279a12283eaaf3ad6f13001ca2dff943eda8c53288179775a8c057a0", [:mix], [{:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}], "hexpm", "69703055d63b8d5b260479266c78b0b3e66f7aecdd2022906cd9bf09892a266d"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, @@ -40,7 +40,7 @@ "mdex_native": {:hex, :mdex_native, "0.2.8", "20b7cbf330c1ca81b8da4132b8d01952cded11f6dfc2abe8fef25c13681b15e4", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "004a5565b6c96a06400901eb1e4e603585e00b23262d3f595c3f4aa38b83ef66"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, - "mint": {:hex, :mint, "1.9.3", "3337184d69179695c7a9f1714d92c11e629d36c8c037a21cf490131d3d150554", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5f7c9342480c069dbbc4eeac3490303c9e01870ff01a7f1d29b6107054fc1e74"}, + "mint": {:hex, :mint, "1.10.0", "85af3353bfc504f5bdfe494bd92b8490f87a306dc659ee1ad0af435107e898dc", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "8b16fb72aaa7531d206a1f05e4cc85509ba531ccec7a17a22736c9c95cbb24d1"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, @@ -48,34 +48,34 @@ "parse_trans": {:hex, :parse_trans, "3.4.2", "c352ddc1a0d5e54f9b1654d45f9c432eef76f9cea371c55ddff769ef688fdb74", [:rebar3], [], "hexpm", "4c25347de3b7c35732d32e69ab43d1ceee0beae3f3b3ade1b59cbd3dd224d9ca"}, "phoenix": {:hex, :phoenix, "1.8.13", "e33192826d9bed4022bdb3f5a7b36c04362049d9390fd1b581d5ad6261779268", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 2.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ad14e24d10e5a52d5f80429053bbe3a5d124311a2868fceb0a01a2e859c44539"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.2.10", "eb4958045f71d4962373e9ed5967b592375a9dafc73f52997f3996759998a39d", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bcf9d64846b770bc64b1a58dc40af406d80eb61b6e516f7aea4cc8cc15e0a1d9"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.2.11", "1c83fdfd007e8d554076baee00ee6f55bb9c87ad9439f33ee520e47ae4a983bb", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c4087267cbd4dc4ed7ac36512a97830af8e9c857ca7cb22557f8640fc5f9d306"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.3.0", "03916bfbc31a5121945b3cfffe5aec647a5c97fe1dc172a319b94428562359c9", [:mix], [], "hexpm", "eec7be6e9cf02e2551d389b558402d6c637cd3973796326e7ba4bb03c6b2e91d"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"}, "plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"}, - "quic": {:hex, :quic, "1.7.1", "1ac5331739e17928a797d5ff103629a9faa91167481c326d7043b323e3bdc3b1", [:rebar3], [], "hexpm", "9b85784f1d78f5c3adfee5f52c3448cac3b03f5f0403d5dc782231c3d3ad709d"}, - "req": {:hex, :req, "0.7.3", "b141f1b465dabc5fb8ce67bd2f15a85fc80f6c75719910699560e32ce49f62ef", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "73b303030dccc2b6d023ee5ada380825ab3a7cd3863aead493db09ec420ffdf2"}, + "quic": {:hex, :quic, "1.8.2", "c315176d2c4fad0725e52f2a8033b96d01c4fecfc7e6a7333615ff10041142a5", [:rebar3], [], "hexpm", "274d2f41ee9c00d8d6415248df9fb5637381fe69b5387771dc37e91038a65479"}, + "req": {:hex, :req, "0.7.4", "23e9ffec17de032a46a4b15ed65c09793893bf4a7c680f4bbf6227fce6bdf74d", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "4b192d63253e8dcc6221ef992ea9ebef7d3555166e8423aa5b553e86bc3c69a2"}, "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"}, "schematic": {:hex, :schematic, "0.5.2", "07b666b971b780bcfbae7a5032945886dc9fb7e31834d7438c6e572ef80c2866", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cef597a88260823a806c106071213bd85670b855007cd70e21284a716fe44348"}, "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, "sourceror": {:hex, :sourceror, "1.12.2", "85bfd48159f020c0cbfc72f289f11456fdc05dc43719b6f2589fb969faefa113", [:mix], [], "hexpm", "da37d3da09c5b890528802c7056a8f585a061973820d7656b6e3649c14f0e9cb"}, - "spitfire": {:hex, :spitfire, "0.4.0", "6d98c10cf585434b9439ba0c6dd3cc7aeff0e06ab73bfe5488f42e7c0f883d9b", [:mix], [], "hexpm", "7e5c6d1523c111b59f332f9dc49edc0377111d0c17167a29830f0e98233f5472"}, + "spitfire": {:hex, :spitfire, "0.4.1", "69e90335d00ca328295e1e1e77cac5d7575aa6d34274e3467ebfc654b8858be3", [:mix], [], "hexpm", "27d86f67681179682b15c6758d64ac2eb2b3637ed8340800c8b885c69754cdcd"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "tableau": {:hex, :tableau, "0.30.0", "12b507e6c517b4e87be469edf466fe542a36c6cda57b0f5607a12ae123a7f8e6", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: false]}, {:date_time_parser, "~> 1.2", [hex: :date_time_parser, repo: "hexpm", optional: false]}, {:html_entities, "~> 0.5.2", [hex: :html_entities, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16.0", [hex: :libgraph, repo: "hexpm", optional: false]}, {:mdex, "~> 0.11.1", [hex: :mdex, repo: "hexpm", optional: false]}, {:schematic, "~> 0.5.1", [hex: :schematic, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}, {:tz, "~> 0.28.1", [hex: :tz, repo: "hexpm", optional: false]}, {:web_dev_utils, "~> 0.3", [hex: :web_dev_utils, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "4534c5b74a7702bd64a4015d59bbd40cfc55da022d49afaeaca0ab76b84db42e"}, "tailwind": {:hex, :tailwind, "0.5.1", "35435b13158c90d37da11e1cfc808755fca1d7b6c5ab87b1b19c5de87e2f0a10", [:mix], [], "hexpm", "c4e26302a59fec72abc5610ecb6ad2116d9aa31f31aab2d4b8eb6e95d25a689c"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, - "tesla": {:hex, :tesla, "1.21.0", "de3dc7b0ddbbd72a2fdab02decc977c0fa910aa04cbfb92aaca5e93a839ad2a5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21 or >= 4.0.2 and < 5.0.0-0", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "1a0b8c9a7c3676beafd33db5e13f5e486af936c1a9a9f56b037e7861d916d333"}, + "tesla": {:hex, :tesla, "1.21.3", "377e74491164595d89ad4e24b738a448c743f830a491a6c638b9e46995bca23d", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21 or >= 4.0.2 and < 5.0.0-0", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.5.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "558aca0d763aaed4cba418b6078c290ffd55ddd1b03e58d6163cb7438851855e"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"}, "tz": {:hex, :tz, "0.28.2", "6c47f3d1a8ee5c33a1d8f0ba49e5a851b0a30c408a587d907aff6e71228b3b32", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.6", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "a6bf7355a33f0a7511602ab4566432ac0901d8abff81e94e455bca19708a0c87"}, - "usage_rules": {:hex, :usage_rules, "1.2.7", "aaacfc9eda3b33d37703ed2321f32ad89845282513168f8592a1826eeabf50a7", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "8601999d754974f361f1fd816f0747dd7ea39aa04b1bf6244d158a9616be204e"}, + "usage_rules": {:hex, :usage_rules, "1.2.8", "ace34d6e7a2a300a03446387ffd6a2219418fb6dbe7ee5d02131535fc9b3d8db", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "f49a69c7219a5b97bfb7e97d4470d790a94ac2cf049d2b1ceda6bd2b243e9cc1"}, "wallaby": {:hex, :wallaby, "0.31.0", "fb8a7d1f0bb31d94a951c1b642d37805190ae47bd9c4904c5fb0564bfbc91ca9", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.3.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "a3e7a5f13c1db49d32c17dafe8d4654fe556987efd12d422673b902455871c50"}, "web_dev_utils": {:hex, :web_dev_utils, "0.3.0", "463f3695dcdee9a6f18dbc783ee76f0ecdd1c9a489a6482aadcf166b3e5fee12", [:mix], [{:file_system, "~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "c239d4d24bfc61110a98b7d75bb7bbbb1752a5d8fcb5a2d369c5d4f64d1d7aec"}, "web_driver_client": {:hex, :web_driver_client, "0.3.0", "25c53ffdeb779ff933c7cc145b71229662421078c10dbc43b3afab846e65366c", [:mix], [{:hackney, "~> 1.6 or ~> 4.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "f89a43505b4fa5e1d5dc50818980a4d2703e390dca29fa0ad39911b0eb46b65a"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.6.0", "73db5ab8aaefd1a876a97ce3e6afc96562625de69ef17a4e04426e034849d0b8", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "50021a85bce8f203b086705d9e0c5415e2c7eb05d319111b0428fe71f9934617"}, - "webtransport": {:hex, :webtransport, "0.4.3", "df0c53da138cdc5f3390a31ef612caa3a041c23e18c1a0b6fbd4d72743277806", [:rebar3], [{:h2, "~> 0.10.4", [hex: :h2, repo: "hexpm", optional: false]}, {:quic, "~> 1.7.0", [hex: :quic, repo: "hexpm", optional: false]}], "hexpm", "3ae8c76696cdb56bee3caf1dd853a0a596339886468ca17c578a9bfd647dab9d"}, + "webtransport": {:hex, :webtransport, "0.4.5", "0e387202bbe707389fe81373ef8c56faa9d5aa321bb4800fa4765ee7c1399785", [:rebar3], [{:h2, "~> 0.12", [hex: :h2, repo: "hexpm", optional: false]}, {:quic, "~> 1.8.0", [hex: :quic, repo: "hexpm", optional: false]}], "hexpm", "bcb512239e48e551d5bd5c667312a9a7de4f29b89d84b1d33c5af44e1f3f730d"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"}, } From d95943f0e887ceb09da96d1e1deadf156ea2350d Mon Sep 17 00:00:00 2001 From: Karim Semmoud Date: Wed, 9 Sep 2026 10:11:01 +0700 Subject: [PATCH 57/65] Drop unused home modules left from the remote staging pass. Removes bento, log, notes, themes, and marquee_band so compile stays clean after the template rebuild merge. Co-authored-by: Cursor --- lib/pages/home/bento.ex | 75 --------------------------- lib/pages/home/log.ex | 86 ------------------------------- lib/pages/home/marquee_band.ex | 44 ---------------- lib/pages/home/notes.ex | 93 ---------------------------------- lib/pages/home/themes.ex | 64 ----------------------- 5 files changed, 362 deletions(-) delete mode 100644 lib/pages/home/bento.ex delete mode 100644 lib/pages/home/log.ex delete mode 100644 lib/pages/home/marquee_band.ex delete mode 100644 lib/pages/home/notes.ex delete mode 100644 lib/pages/home/themes.ex diff --git a/lib/pages/home/bento.ex b/lib/pages/home/bento.ex deleted file mode 100644 index 9924ede..0000000 --- a/lib/pages/home/bento.ex +++ /dev/null @@ -1,75 +0,0 @@ -defmodule Soonex.HomePage.Bento do - @moduledoc false - - use Phoenix.Component - use Corex - - import Soonex.Layouts.Section, only: [block: 1] - - alias Soonex.Layouts.Shell - - def bento(assigns) do - ~H""" - <.block id="features" labelledby="soonex-bento-heading" eyebrow="Features" layout={:open}> - <:title> - Everything you need to launch - - <:lede> - Four instruments composed like a Framer product page — waitlist, log, countdown, looks. - - -
      -
    • - {cell.kicker} -

      {cell.title}

      -

      {cell.body}

      - <.navigate :if={cell.to} to={cell.to} class="link ui-brand mt-6 inline-flex"> - {cell.cta} <.heroicon name={cell.icon} /> - -
    • -
    - - """ - end - - defp cells do - [ - %{ - kicker: "01", - title: "Waitlist on day one", - body: "Email, role, interests, team size — with a launch toast on submit.", - to: "#waitlist", - cta: "Join the list", - icon: "hero-arrow-down", - span: "soonex-bento-wide" - }, - %{ - kicker: "02", - title: "Public shipping log", - body: "Markdown entries with covers and tags. The index paginates in the browser.", - to: Soonex.Public.path("/blog"), - cta: "Read the log", - icon: "hero-arrow-up-right", - span: "" - }, - %{ - kicker: "03", - title: "Honest countdown", - body: "Aimed at 1 September until you change the date. Digits stay on one row.", - to: "#product", - cta: "See product", - icon: "hero-arrow-down", - span: "" - }, - %{ - kicker: "04", - title: "Four looks", - body: "neo, uno, duo, leo — switch live for client review without a second build.", - to: "#looks", - cta: "Browse looks", - icon: "hero-arrow-down", - span: "soonex-bento-wide" - } - ] - end -end diff --git a/lib/pages/home/log.ex b/lib/pages/home/log.ex deleted file mode 100644 index 13d4b86..0000000 --- a/lib/pages/home/log.ex +++ /dev/null @@ -1,86 +0,0 @@ -defmodule Soonex.HomePage.Log do - @moduledoc false - - use Phoenix.Component - use Corex - - import Soonex.Layouts.Media, only: [photo: 1] - import Soonex.Layouts.Section, only: [block: 1] - - alias Soonex.Layouts.Shell - - attr(:posts, :list, default: []) - - def log(assigns) do - posts = - assigns.posts - |> List.wrap() - |> Enum.sort_by(& &1[:date], {:desc, DateTime}) - |> Enum.take(3) - - assigns = assign(assigns, :entries, posts) - - ~H""" - <.block id="log" labelledby="soonex-log-heading" eyebrow="Shipping log" layout={:open}> - <:title> - What we shipped on the road to September - - <:lede> - Public notes on the waitlist, the four looks, and the countdown. - - <:actions> - <.navigate to={Soonex.Public.path("/blog")} class="link ui-brand"> - All entries <.heroicon name="hero-arrow-up-right" /> - - -

    No posts yet.

    -
      -
    1. -
      - <.photo - src={cover(post).src} - alt={cover(post).alt} - width={800} - height={500} - /> -
      -
      - {date_label(post)} -

      - <.navigate to={Soonex.Public.path(post.permalink)} class="link ui-nav"> - {post[:title] || "Untitled"} - -

      -

      - {post[:description]} -

      - <.navigate - to={Soonex.Public.path(post.permalink)} - class="button ui-ghost ui-brand ui-size-sm mt-4 w-fit" - > - Read more <.heroicon name="hero-arrow-up-right" /> - -
      -
    2. -
    - - """ - end - - defp cover(post) do - src = post[:image] - - if is_binary(src) and src != "" do - %{src: src, alt: post[:image_alt] || post[:title] || "Log cover"} - end - end - - defp date_label(%{date: %DateTime{} = date}), do: Calendar.strftime(date, "%d %B %Y") - defp date_label(_), do: nil -end diff --git a/lib/pages/home/marquee_band.ex b/lib/pages/home/marquee_band.ex deleted file mode 100644 index a9703d8..0000000 --- a/lib/pages/home/marquee_band.ex +++ /dev/null @@ -1,44 +0,0 @@ -defmodule Soonex.HomePage.MarqueeBand do - @moduledoc false - - use Phoenix.Component - use Corex - - def marquee_band(assigns) do - ~H""" -
    - <.marquee - id="soonex-proof-marquee" - class="marquee ui-brand soonex-proof-marquee" - items={items()} - duration={28} - spacing="3rem" - pause_on_interaction - aria_label="Soonex launch signals" - > - <:item :let={item}> - - - {item.label} - - - -
    - """ - end - - defp items do - [ - %{label: "Waitlist that converts"}, - %{label: "Public shipping log"}, - %{label: "Countdown to 1 September"}, - %{label: "neo · uno · duo · leo"}, - %{label: "240 studios on the list"}, - %{label: "Accessible by default"}, - %{label: "Clone · mix setup · ship"} - ] - end -end diff --git a/lib/pages/home/notes.ex b/lib/pages/home/notes.ex deleted file mode 100644 index fbd3730..0000000 --- a/lib/pages/home/notes.ex +++ /dev/null @@ -1,93 +0,0 @@ -defmodule Soonex.HomePage.Notes do - @moduledoc false - - use Phoenix.Component - use Corex - - import Soonex.Layouts.Section, only: [block: 1] - - def notes(assigns) do - ~H""" - <.block - id="notes" - labelledby="soonex-notes-heading" - eyebrow="Studios" - tone={:surface} - layout={:open} - > - <:title> - Already on the September list - - <:lede> - Founders, engineers, and designers putting Soonex on a calendar. - - - <.carousel - id="soonex-voices" - class="carousel ui-brand ui-width-full soonex-voices-carousel" - items={voices()} - slides_per_page={1} - spacing="1.5rem" - > - <:item :let={voice}> -
    -

    “{voice.quote}”

    -
    - <.avatar - id={voice.id} - src={Soonex.Public.path(voice.src)} - alt={voice.name} - class="avatar ui-size-md" - > - <:fallback>{voice.initials} - -
    - {voice.name} - {voice.role} -
    -
    -
    - - <:prev_trigger> - <.heroicon name="hero-arrow-left" /> - - <:next_trigger> - <.heroicon name="hero-arrow-right" /> - - - - """ - end - - defp voices do - [ - %{ - id: "soonex-voice-amira", - src: "/images/people/amira.jpg", - initials: "AN", - quote: - "We had a date before we had a deck. Soonex put the waitlist and the countdown on the page the same afternoon.", - name: "Amira N.", - role: "Founder, Northline Studio" - }, - %{ - id: "soonex-voice-jonas", - src: "/images/people/jonas.jpg", - initials: "JK", - quote: - "One clone path, controls included, no second design system. We pointed the date and shipped the stills.", - name: "Jonas K.", - role: "Staff engineer, Relay" - }, - %{ - id: "soonex-voice-leah", - src: "/images/people/leah.jpg", - initials: "LM", - quote: - "Four looks for the client review without rebuilding. They picked duo; we kept shipping.", - name: "Leah M.", - role: "Product designer, Fieldwork" - } - ] - end -end diff --git a/lib/pages/home/themes.ex b/lib/pages/home/themes.ex deleted file mode 100644 index bd4e1da..0000000 --- a/lib/pages/home/themes.ex +++ /dev/null @@ -1,64 +0,0 @@ -defmodule Soonex.HomePage.Themes do - @moduledoc false - - use Phoenix.Component - - import Soonex.Layouts.Section, only: [block: 1] - - def themes(assigns) do - ~H""" - <.block id="looks" labelledby="soonex-looks-heading" eyebrow="Looks" layout={:open}> - <:title> - Four looks. One launch. - - <:lede> - Switch neo, uno, duo, and leo live in Template Options. Paper stays calm — type, corners, - and brand change the room. - - -

    Soonex

    -

    - This specimen follows the look you pick. No second build for the review. -

    - -
      -
    1. -

      {room.id}

      -

      {room.line}

      -

      {room.use}

      -
    2. -
    - - """ - end - - defp rooms do - [ - %{ - id: "neo", - line: "Product studio — geometric display, open corners, cobalt.", - use: "Default for product launches." - }, - %{ - id: "uno", - line: "Calm ops — tight tracking, small radius, teal.", - use: "Ops and tooling launches." - }, - %{ - id: "duo", - line: "Editorial atelier — serif display, soft corners, rust.", - use: "Studios that want editorial warmth." - }, - %{ - id: "leo", - line: "Signal poster — strong display, square corners, orange.", - use: "Campaign drops and posters." - } - ] - end -end From 60baa449d21d9a5279ef2f849584c5dfbab3321b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 03:17:15 +0000 Subject: [PATCH 58/65] Harden for Corex 0.2.2 and mark Soonex 0.2.2 merge-ready - Pin Corex family deps to ~> 0.2 with corex_design runtime: false - Confirm :corex_design is absent from Mix compilers (0.2.2 requirement) - Bump template version to 0.2.2; document Framer restage + hardening in CHANGELOG - Restore AGENTS.md for Cloud Agent workflows (mix server, no npm package.json) - Update README prerequisites and accessibility notes Co-authored-by: Karim Semmoud --- AGENTS.md | 43 +++++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 8 ++++++++ README.md | 4 ++-- mix.exs | 8 ++++---- 4 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..233d6d6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,43 @@ +# AGENTS.md + +## Cursor Cloud specific instructions + +Soonex is an Elixir **Tableau** static-site generator (Corex UI components, Tailwind v4, esbuild). +There is one product: the static marketing/blog site. Standard commands live in `README.md` and +`mix.exs` aliases — prefer those; the notes below only cover non-obvious cloud gotchas. + +### Toolchain activation +- 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 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/`. +- After changing Corex config (`config :corex_design` in `config/config.exs`) run + `mix corex.design.build`. + +### Lint / test +- Lint: `mix credo`. +- Tests: `mix test` (the `test` alias first runs `pre.test`, which builds the static site into + `_site/`, then runs ExUnit). The only test is a **Wallaby** browser accessibility check + (`test/soonex/home_a11y_test.exs`) that drives headless Chrome via chromedriver against a Bandit + server on port 4999. Chrome and a matching `chromedriver` are already installed at + `/usr/local/bin`; the suite works without setting `WALLABY_*` env vars, but you can point at them + explicitly with `WALLABY_CHROME_BINARY` / `WALLABY_CHROMEDRIVER_PATH` if needed. +- If you upgrade Chrome, replace `/usr/local/bin/chromedriver` with the matching version from + Chrome for Testing, or Wallaby tests will fail to start a session. + +### 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 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 9309f21..7422d31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.2.2 + +- Framer-style product launch restage: photo hero with waitlist object, proof marquee, feature bento, product desk, pricing, launch timeline, voice carousel, journal tease, FAQ, and closing waitlist CTA. +- 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 restaged 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. diff --git a/README.md b/README.md index abe5c72..3fc6a45 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ English-only **Tableau** static site: Corex components, theme/mode toggles, and ## Prerequisites - Elixir ~> 1.17 -- Hex packages `corex`, `corex_design`, and `corex_mcp` (`~> 0.2.1`) +- Hex packages `corex`, `corex_design`, and `corex_mcp` (`~> 0.2`) ## Quick start @@ -40,7 +40,7 @@ Rebuild assets: `mix assets.build`. - **Launch date:** [`lib/soonex/launch.ex`](lib/soonex/launch.ex). Hero badge and header countdown both read it. - **Themes:** overlay **all allowed keys** per theme in [`config/config.exs`](config/config.exs) (`seeds`, `colors.light` / `colors.dark`, `dimensions.radius`, `dimensions.font`, `typography`) plus top-level `scales:`. Contrast stays calculated; `*_scale` / duration / opacity keys are rejected. Then `mix corex.design.build`. [`lib/soonex/theme.ex`](lib/soonex/theme.ex) must list the same ids as `data-theme`. - **Skins:** each theme has isolated CSS in [`assets/css/skins/`](assets/css/skins/) and optional JS in [`assets/js/skins/`](assets/js/skins/). Imports live in [`assets/css/site.css`](assets/css/site.css) and [`assets/js/skins.js`](assets/js/skins.js). To drop a look, delete the skin files, remove the imports, and remove the id from `Soonex.Theme` and `config :corex_design, :themes`. Shared marketing rhythm stays in [`assets/css/layout.css`](assets/css/layout.css). [`assets/css/hosts.css`](assets/css/hosts.css) is Corex host polish only. No token opacity (`bg-surface/90`). -- **Accessibility:** Corex `--a11y` dialog in the demo FAB ([`lib/soonex/accessibility.ex`](lib/soonex/accessibility.ex)). Preferences live in `localStorage` (`phx:a11y`); `corex_design` must stay a **runtime** dep. +- **Accessibility:** Corex `--a11y` dialog in the demo FAB ([`lib/soonex/accessibility.ex`](lib/soonex/accessibility.ex)). Preferences live in `localStorage` (`phx:a11y`); run `mix corex.design.build` after changing accessibility config. - **Fonts:** self-hosted woff2 in [`extra/fonts/`](extra/fonts/), faces in [`assets/css/fonts.css`](assets/css/fonts.css). Stacks follow Corex 0.2 (neo = Outfit + Manrope; uno/duo/leo keep their Corex families). - **Chrome:** sticky header in [`lib/layouts/root/nav.ex`](lib/layouts/root/nav.ex); condensed + countdown `hidden`/`inert` in [`assets/js/landing-scroll-chrome.js`](assets/js/landing-scroll-chrome.js). - **Content:** home sections in [`lib/pages/home/`](lib/pages/home/), composed by [`lib/pages/home_page.ex`](lib/pages/home_page.ex). FAQ uses the sticky split (`layout={:sticky}` in [`lib/layouts/section.ex`](lib/layouts/section.ex)). diff --git a/mix.exs b/mix.exs index 2db27ad..12ca8ac 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Soonex.MixProject do def project do [ app: :soonex, - version: "0.2.1", + version: "0.2.2", elixir: "~> 1.17", start_permanent: Mix.env() == :prod, elixirc_paths: elixirc_paths(Mix.env()), @@ -43,9 +43,9 @@ defmodule Soonex.MixProject do app: false, compile: false, depth: 1}, - {:corex, "~> 0.2.1"}, - {:corex_design, "~> 0.2.1"}, - {:corex_mcp, "~> 0.2.1", only: [:dev, :test]}, + {:corex, "~> 0.2"}, + {:corex_design, "~> 0.2", runtime: false}, + {:corex_mcp, "~> 0.2", only: [:dev, :test]}, {:color, "~> 0.11"}, {:floki, "~> 0.38"}, {:makeup, "~> 1.2"}, From 955a9b1d8614f4aedd9cea8140e548847345c674 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 03:21:09 +0000 Subject: [PATCH 59/65] Fail fast when inotify-tools is missing on Linux dev server Tableau live-reload websockets subscribe to :web_dev_utils_file_watcher, which never starts without inotifywait. Surface a clear install hint before mix server instead of repeated GenServer termination logs. Co-authored-by: Karim Semmoud --- lib/mix/tasks/soonex.server.ex | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/mix/tasks/soonex.server.ex b/lib/mix/tasks/soonex.server.ex index 1ce9390..d3192d3 100644 --- a/lib/mix/tasks/soonex.server.ex +++ b/lib/mix/tasks/soonex.server.ex @@ -11,6 +11,23 @@ defmodule Mix.Tasks.Soonex.Server do @impl Mix.Task def run(args) do Mix.Task.run("soonex.port_check", []) + warn_missing_inotify!() Mix.Task.run("tableau.server", args) end + + defp warn_missing_inotify! do + if :os.type() == {:unix, :linux} and is_nil(System.find_executable("inotifywait")) do + Mix.raise(""" + inotify-tools is required for Tableau live reload on Linux. + + Without `inotifywait`, the dev server starts but live-reload websockets crash with: + + GenServer.call(:web_dev_utils_file_watcher, :subscribe, 5000) + + Install it, then retry: + + sudo apt-get install -y inotify-tools + """) + end + end end From d5945bacd9483c639aadb4e3fddc8caeb8824ef6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 03:36:50 +0000 Subject: [PATCH 60/65] Redesign Soonex home around Corex Design-only premium SaaS layout Replace the Framer-style restage with a clean coming-soon template inspired by premium SaaS landing pages: centered hero, trust badges, features, how-it-works tabs, pricing, journal, FAQ, and waitlist sections with Corex copy and generous whitespace. Simplify customization to a single neo theme with light/dark modes. Remove per-theme skin CSS/JS, marquee, carousel, scroll chrome, and theme switcher. Update README, CHANGELOG, privacy copy, and palette config accordingly. Bump version to 0.3.0. Co-authored-by: Karim Semmoud --- CHANGELOG.md | 11 +- README.md | 13 +- assets/css/chrome.css | 156 +------------------ assets/css/fonts.css | 90 +---------- assets/css/hosts.css | 72 --------- assets/css/layout.css | 166 -------------------- assets/css/site.css | 5 - assets/css/skins/duo.css | 166 -------------------- assets/css/skins/leo.css | 181 ---------------------- assets/css/skins/neo.css | 116 -------------- assets/css/skins/uno.css | 149 ------------------ assets/js/landing-scroll-chrome.js | 83 ---------- assets/js/landing.js | 22 --- assets/js/site.js | 13 -- assets/js/skins.js | 57 ------- assets/js/skins/duo.js | 9 -- assets/js/skins/leo.js | 9 -- assets/js/skins/neo.js | 9 -- assets/js/skins/uno.js | 9 -- assets/js/theme.js | 33 ---- config/config.exs | 237 +---------------------------- lib/layouts/root/demo.ex | 76 +++------ lib/layouts/root/footer.ex | 6 +- lib/layouts/root/nav.ex | 104 +++---------- lib/layouts/root_layout.ex | 15 +- lib/layouts/section.ex | 11 +- lib/layouts/shell.ex | 49 +++--- lib/pages/blog_index_page.ex | 2 +- lib/pages/home/faq.ex | 96 +++--------- lib/pages/home/features.ex | 33 ++-- lib/pages/home/hero.ex | 135 +++++----------- lib/pages/home/how_it_works.ex | 74 +++++++++ lib/pages/home/journal.ex | 33 +--- lib/pages/home/launch_band.ex | 77 ---------- lib/pages/home/logos.ex | 55 ------- lib/pages/home/pricing.ex | 43 +++--- lib/pages/home/product.ex | 106 ------------- lib/pages/home/stats.ex | 48 ------ lib/pages/home/steps.ex | 80 ---------- lib/pages/home/trust.ex | 36 +++++ lib/pages/home/voices.ex | 77 ---------- lib/pages/home/waitlist.ex | 52 +------ lib/pages/home_page.ex | 16 +- lib/pages/privacy_page.ex | 2 +- lib/soonex/palette/config.ex | 186 +--------------------- lib/soonex/theme.ex | 34 +---- mix.exs | 2 +- 47 files changed, 349 insertions(+), 2705 deletions(-) delete mode 100644 assets/css/layout.css delete mode 100644 assets/css/skins/duo.css delete mode 100644 assets/css/skins/leo.css delete mode 100644 assets/css/skins/neo.css delete mode 100644 assets/css/skins/uno.css delete mode 100644 assets/js/landing-scroll-chrome.js delete mode 100644 assets/js/landing.js delete mode 100644 assets/js/skins.js delete mode 100644 assets/js/skins/duo.js delete mode 100644 assets/js/skins/leo.js delete mode 100644 assets/js/skins/neo.js delete mode 100644 assets/js/skins/uno.js delete mode 100644 assets/js/theme.js create mode 100644 lib/pages/home/how_it_works.ex delete mode 100644 lib/pages/home/launch_band.ex delete mode 100644 lib/pages/home/logos.ex delete mode 100644 lib/pages/home/product.ex delete mode 100644 lib/pages/home/stats.ex delete mode 100644 lib/pages/home/steps.ex create mode 100644 lib/pages/home/trust.ex delete mode 100644 lib/pages/home/voices.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index 7422d31..aa8ef2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,18 @@ # Changelog +## 0.3.0 + +- Full home redesign: premium SaaS layout (hero, trust badges, features, how-it-works tabs, pricing, journal, FAQ, waitlist) with Corex-focused copy and generous whitespace. +- Simplify customization to Corex Design only: single `neo` theme with light/dark modes; remove per-theme skin CSS/JS, Framer photography, frosted scroll chrome, marquee, and carousel. +- Drop `assets/css/layout.css`, `assets/css/skins/*`, `assets/js/skins/*`, `assets/js/landing*.js`, and `assets/js/theme.js`. +- Demo controls: light/dark mode toggle and accessibility panel only (no theme switcher). +- Update README, privacy copy, and blog index lede for the new direction. + ## 0.2.2 -- Framer-style product launch restage: photo hero with waitlist object, proof marquee, feature bento, product desk, pricing, launch timeline, voice carousel, journal tease, FAQ, and closing waitlist CTA. - 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 restaged home page. +- 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 diff --git a/README.md b/README.md index 3fc6a45..6801316 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Soonex -English-only **Tableau** static site: Corex components, theme/mode toggles, and the same contrast pipeline as the SaaS template. +English-only **Tableau** static site: Corex components, light/dark mode, and the same contrast pipeline as the SaaS template. **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). @@ -37,12 +37,11 @@ Rebuild assets: `mix assets.build`. ## Customize (where to edit) - **Brand / SEO:** lockup in [`lib/layouts/brand.ex`](lib/layouts/brand.ex) and [`extra/images/logo.svg`](extra/images/logo.svg); titles in [`lib/layouts/root_layout.ex`](lib/layouts/root_layout.ex). -- **Launch date:** [`lib/soonex/launch.ex`](lib/soonex/launch.ex). Hero badge and header countdown both read it. -- **Themes:** overlay **all allowed keys** per theme in [`config/config.exs`](config/config.exs) (`seeds`, `colors.light` / `colors.dark`, `dimensions.radius`, `dimensions.font`, `typography`) plus top-level `scales:`. Contrast stays calculated; `*_scale` / duration / opacity keys are rejected. Then `mix corex.design.build`. [`lib/soonex/theme.ex`](lib/soonex/theme.ex) must list the same ids as `data-theme`. -- **Skins:** each theme has isolated CSS in [`assets/css/skins/`](assets/css/skins/) and optional JS in [`assets/js/skins/`](assets/js/skins/). Imports live in [`assets/css/site.css`](assets/css/site.css) and [`assets/js/skins.js`](assets/js/skins.js). To drop a look, delete the skin files, remove the imports, and remove the id from `Soonex.Theme` and `config :corex_design, :themes`. Shared marketing rhythm stays in [`assets/css/layout.css`](assets/css/layout.css). [`assets/css/hosts.css`](assets/css/hosts.css) is Corex host polish only. No token opacity (`bg-surface/90`). +- **Launch date:** [`lib/soonex/launch.ex`](lib/soonex/launch.ex). Hero badge and countdown timer both read it. +- **Theme:** overlay allowed keys in [`config/config.exs`](config/config.exs) under `config :corex_design` (`seeds`, `colors.light` / `colors.dark`, `dimensions.radius`, `dimensions.font`, `typography`, top-level `scales:`). Contrast stays calculated; `*_scale` / duration / opacity keys are rejected. Then `mix corex.design.build`. Light/dark mode toggles via [`lib/soonex/mode.ex`](lib/soonex/mode.ex). - **Accessibility:** Corex `--a11y` dialog in the demo FAB ([`lib/soonex/accessibility.ex`](lib/soonex/accessibility.ex)). Preferences live in `localStorage` (`phx:a11y`); run `mix corex.design.build` after changing accessibility config. -- **Fonts:** self-hosted woff2 in [`extra/fonts/`](extra/fonts/), faces in [`assets/css/fonts.css`](assets/css/fonts.css). Stacks follow Corex 0.2 (neo = Outfit + Manrope; uno/duo/leo keep their Corex families). -- **Chrome:** sticky header in [`lib/layouts/root/nav.ex`](lib/layouts/root/nav.ex); condensed + countdown `hidden`/`inert` in [`assets/js/landing-scroll-chrome.js`](assets/js/landing-scroll-chrome.js). +- **Fonts:** self-hosted woff2 in [`extra/fonts/`](extra/fonts/), faces in [`assets/css/fonts.css`](assets/css/fonts.css). Default stack: Outfit (display) + Manrope (sans) + JetBrains Mono. +- **Chrome:** sticky header in [`lib/layouts/root/nav.ex`](lib/layouts/root/nav.ex); minimal host polish in [`assets/css/hosts.css`](assets/css/hosts.css) and [`assets/css/chrome.css`](assets/css/chrome.css). Prefer Corex `ui-*` modifiers over custom CSS. - **Content:** home sections in [`lib/pages/home/`](lib/pages/home/), composed by [`lib/pages/home_page.ex`](lib/pages/home_page.ex). FAQ uses the sticky split (`layout={:sticky}` in [`lib/layouts/section.ex`](lib/layouts/section.ex)). - **Blog:** index at [`/blog`](lib/pages/blog_index_page.ex) (`layout_heading`, cards, pagination); 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. @@ -61,7 +60,7 @@ MDX-style Tableau extras (tags, `include_dir`, sitemap) are summarized in **Tabl - `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), a11y head script; header condensed + countdown in [`assets/js/landing-scroll-chrome.js`](assets/js/landing-scroll-chrome.js) (`hidden` + `inert` until past the hero sentinel). Optional theme skins in [`assets/js/skins.js`](assets/js/skins.js). +- Client UI: [`assets/js/mode.js`](assets/js/mode.js) for light/dark mode, a11y head script, waitlist toast in [`assets/js/waitlist.js`](assets/js/waitlist.js). ## Production and hosting diff --git a/assets/css/chrome.css b/assets/css/chrome.css index abfa6df..edaec79 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -1,4 +1,4 @@ -/* Tiny layout helpers that utilities cannot express. */ +/* Minimal host helpers. Prefer Corex ui-* modifiers and design tokens in HEEx. */ html { scroll-behavior: smooth; @@ -8,158 +8,16 @@ html[data-motion="reduce"] { scroll-behavior: auto; } -[data-motion="reduce"] [data-site-header] { - transition: none; +.soonex-plan[data-featured="true"] { + background: var(--color-brand); + color: var(--color-brand-contrast); + border-color: var(--color-brand); } -[data-site-header] { - transition: border-color var(--duration-normal) var(--soonex-ease); +.soonex-plan[data-featured="true"] :is(.soonex-eyebrow, .display, p, li) { + color: inherit; } -[data-site-header] [data-header-bar] { - min-height: var(--spacing-size); -} - -/* Header timer stays in flow (invisible until reveal) so the bar never jumps. - Corex digits flip in a 2em window (`top: calc(var(--value) * -2em)`). Do not - shrink that window — it clips the strip. Compact the type instead. */ -[data-site-header] .timer.ui-size-sm { - --timer-digit: var(--text-sm); - --timer-digit-leading: 2; -} - -[data-site-header] .timer [data-scope="timer"][data-part="area"] { - align-items: center; -} - -[data-site-header] .timer [data-scope="timer"][data-part="separator"] { - align-self: center; -} - -[data-site-header] .timer [data-scope="timer"][data-part="item-label"] { - font-size: var(--text-xs); - line-height: 1; - font-variant-numeric: tabular-nums; - color: var(--color-ink-muted); - white-space: nowrap; -} - -@media (width < 40rem) { - .timer.ui-size-sm, - .timer.ui-size-md, - .timer.ui-size-lg { - --timer-digit: var(--text-xl); - --timer-digit-leading: var(--text-xl--line-height); - } - - .timer [data-scope="timer"][data-part="area"] { - flex-wrap: nowrap; - justify-content: space-between; - gap: var(--spacing-space-xs); - } -} - -.soonex-frame { - border-radius: var(--radius-2xl); -} - -.soonex-photo { - display: block; - width: 100%; - height: 100%; - object-fit: cover; -} - -.soonex-avatars { - display: flex; - align-items: center; -} - -.soonex-avatars li + li { - margin-inline-start: calc(var(--spacing-space-sm) * -1); -} - -.soonex-avatar { - width: 2.25rem; - height: 2.25rem; - object-fit: cover; - border-radius: var(--radius-full); - border: 2px solid var(--color-root); -} - -.soonex-avatar-lg { - width: 3.5rem; - height: 3.5rem; -} - -.marquee [data-scope="marquee"][data-part="content"] { - align-items: center; -} - -.marquee [data-scope="marquee"][data-part="item"] { - flex-shrink: 0; -} - -.marquee [data-scope="marquee"][data-part="edge"] { - --edge-background: var(--color-root); -} - -.marquee [data-scope="marquee"][data-part="edge"][data-side="start"], -.marquee [data-scope="marquee"][data-part="edge"][data-side="end"] { - width: 3rem; - max-width: 3rem; -} - -html[data-motion="reduce"] .marquee [data-scope="marquee"][data-part="content"] { - animation: none; -} - -.data-list.data-list--wide, .tabs.tabs--wide { max-width: none; } - -.data-list.data-list--wide - [data-scope="data-list"][data-part="root"][data-orientation="horizontal"] - [data-scope="data-list"][data-part="item"] { - display: flex; - width: 100%; - flex-direction: row; - align-items: center; -} - -.data-list.data-list--wide - [data-scope="data-list"][data-part="root"][data-orientation="horizontal"] - [data-scope="data-list"][data-part="label"] { - min-width: 4.5rem; - max-width: 8rem; - width: auto; - flex: 0 0 auto; -} - -.data-list.data-list--wide - [data-scope="data-list"][data-part="root"][data-orientation="horizontal"] - [data-scope="data-list"][data-part="content"] { - display: flex; - flex: 1 1 0%; - width: auto; - min-width: 0; - align-items: center; -} - -.data-list.data-list--wide - [data-scope="data-list"][data-part="root"][data-orientation="horizontal"] - [data-scope="data-list"][data-part="content"] - .link { - max-width: none; - width: 100%; -} - -.soonex-tags-list.data-list { - background-color: transparent; - border: 0; -} - -.soonex-tags-list.data-list [data-scope="data-list"][data-part="item"] { - background-color: transparent; -} diff --git a/assets/css/fonts.css b/assets/css/fonts.css index 286daa4..65c2d1a 100644 --- a/assets/css/fonts.css +++ b/assets/css/fonts.css @@ -1,101 +1,23 @@ -/* 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-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; + src: url("/fonts/manrope-latin-wght-normal.woff2") format("woff2"); } @font-face { - font-family: Sora; + font-family: "Outfit"; font-style: normal; + font-weight: 100 900; 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; + src: url("/fonts/outfit-latin-wght-normal.woff2") format("woff2"); } @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; + src: url("/fonts/jetbrains-mono-latin-wght-normal.woff2") format("woff2"); } diff --git a/assets/css/hosts.css b/assets/css/hosts.css index 0d9269a..aa79f41 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -2,7 +2,6 @@ [data-theme] { --soonex-ease: cubic-bezier(0.22, 1, 0.36, 1); - --soonex-header-offset: calc(var(--spacing-size) + 1.5rem); } .button, @@ -18,8 +17,6 @@ .date-picker.ui-width-full, .data-table.ui-width-full, .select.ui-width-full, -.marquee.ui-width-full, -.carousel.ui-width-full, .collapsible.ui-width-full, .accordion.ui-width-full, .switch.ui-width-full { @@ -48,13 +45,6 @@ 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; @@ -65,54 +55,6 @@ max-width: 100%; } -.carousel.ui-width-full { - max-height: none; - isolation: isolate; -} - -.carousel.ui-width-full [data-scope="carousel"][data-part="root"] { - aspect-ratio: auto; - overflow: hidden; - z-index: 0; -} - -.carousel.ui-width-full [data-scope="carousel"][data-part="control"] { - z-index: 1; -} - -.carousel.ui-width-full - [data-scope="carousel"][data-part="root"][data-orientation="vertical"] { - max-height: min(80svh, var(--container-md)); - padding-inline-end: var(--spacing-space); -} - -.select.soonex-nav-select [data-scope="select"][data-part="trigger"] { - width: auto; - min-width: var(--ctl-size, var(--spacing-size)); - margin-inline-end: 0; - aspect-ratio: 1 / 1; - padding-inline: 0; - justify-content: center; -} - -.select.soonex-nav-select [data-scope="select"][data-part="trigger"] > [data-part="item-text"] { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -.select.soonex-nav-select [data-scope="select"][data-part="content"] { - z-index: 60; - min-width: var(--container-3xs); - width: max-content; -} - .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; @@ -126,20 +68,6 @@ border-width: 0; } -.soonex-mark-wrap { - font-size: 2rem; - line-height: 1; -} - -.menu.ui-width-fit [data-scope="menu"][data-part="root"] > [data-scope="menu"][data-part="trigger"] { - width: auto; - min-width: var(--ctl-size, var(--spacing-size)); - margin-inline-end: 0; - aspect-ratio: 1 / 1; - padding-inline: 0; - justify-content: center; -} - .accordion [data-scope="accordion"][data-part="item-trigger"] { padding-block: var(--spacing-space); } diff --git a/assets/css/layout.css b/assets/css/layout.css deleted file mode 100644 index c4f6a5c..0000000 --- a/assets/css/layout.css +++ /dev/null @@ -1,166 +0,0 @@ -/* Shared marketing rhythm. Tokens only. Skins override composition. */ - -.soonex-hero-grid { - display: grid; - align-items: center; - gap: 2.5rem; -} - -@media (width >= 64rem) { - .soonex-hero-grid { - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 3rem; - } -} - -.soonex-hero-heading { - text-wrap: pretty; -} - -.soonex-accent { - color: var(--color-brand-text); -} - -.soonex-launch-badge { - margin: 0; -} - -.soonex-hero-form { - max-width: 28rem; -} - -.soonex-plan[data-featured="true"] { - background: var(--color-brand); - color: var(--color-brand-contrast); - border-color: var(--color-brand); -} - -.soonex-plan[data-featured="true"] .soonex-eyebrow, -.soonex-plan[data-featured="true"] .display, -.soonex-plan[data-featured="true"] p, -.soonex-plan[data-featured="true"] li { - color: inherit; -} - -.soonex-step-index, -.soonex-faq-index { - font-variant-numeric: tabular-nums; -} - -.soonex-tech-chip { - display: inline-flex; - flex-shrink: 0; - align-items: center; - gap: 0.75rem; - white-space: nowrap; -} - -.soonex-tech-chip img { - display: block; - width: 2rem; - height: 2rem; - object-fit: contain; - filter: none; - opacity: 1; -} - -.soonex-launch { - display: grid; - gap: 1rem; -} - -.soonex-launch-beat[data-featured="true"] { - border-color: var(--color-brand); -} - -.soonex-faq-answer .link, -.soonex-faq-answer .clipboard { - margin-top: 0.25rem; -} - -@media (width >= 40rem) { - .soonex-launch { - grid-template-columns: 1fr 1fr; - } -} - -.soonex-claim-icon { - display: flex; - width: 2.5rem; - height: 2.5rem; - align-items: center; - justify-content: center; - color: var(--color-brand-text); -} - -.soonex-faq-trigger { - display: flex; - width: 100%; - min-width: 0; - align-items: baseline; - gap: 0.75rem; - text-align: start; -} - -.soonex-faq [data-part="item-trigger"] { - padding-inline: calc(var(--spacing-space) + 0.75rem); -} - -.soonex-bento { - align-items: start; -} - -.soonex-bento-hero { - align-self: start; - width: 100%; -} - -.soonex-bento-cells .collapsible:nth-child(2) [data-scope="collapsible"][data-part="trigger"] { - padding-inline: calc(var(--spacing-space) + 0.75rem); -} - -@supports (animation-timeline: view()) { - @keyframes soonex-reveal { - from { - opacity: 0; - } - - to { - opacity: 1; - } - } - - .soonex-reveal { - transform: none; - filter: none; - animation-name: soonex-reveal; - animation-duration: var(--duration-slow); - animation-timing-function: var(--soonex-ease); - animation-fill-mode: none; - animation-timeline: view(); - animation-range: entry 0% entry 40%; - } - - html[data-motion="reduce"] .soonex-reveal { - animation: none; - } -} - -.soonex-card-motion { - transition: - border-color var(--duration-fast) var(--soonex-ease), - box-shadow var(--duration-fast) var(--soonex-ease); -} - -@media (hover: hover) { - .soonex-card-motion:hover { - border-color: var(--color-brand); - box-shadow: var(--shadow-sm); - } -} - -html[data-motion="reduce"] .soonex-card-motion, -html[data-motion="reduce"] .soonex-card-motion:hover { - transition: none; - box-shadow: none; -} diff --git a/assets/css/site.css b/assets/css/site.css index f75a565..e566d2e 100644 --- a/assets/css/site.css +++ b/assets/css/site.css @@ -8,11 +8,6 @@ @source "../../lib"; @import "./chrome.css"; -@import "./layout.css"; -@import "./skins/neo.css"; -@import "./skins/uno.css"; -@import "./skins/duo.css"; -@import "./skins/leo.css"; @import "./prose.css"; @custom-variant dark (&:where([data-mode=dark], [data-mode=dark] *)); diff --git a/assets/css/skins/duo.css b/assets/css/skins/duo.css deleted file mode 100644 index 28da6b4..0000000 --- a/assets/css/skins/duo.css +++ /dev/null @@ -1,166 +0,0 @@ -/* duo — Studio / Spotlight. Editorial serif, photography as product, magazine density. */ - -[data-theme="duo"] :is( - .button, - .badge, - .native-input, - .avatar, - .menu, - .select, - .tabs, - .toast, - .pagination, - .clipboard, - .accordion, - .collapsible, - .carousel, - .date-picker, - .tags-input, - .number-input, - .radio-group, - .data-table, - .switch, - .checkbox, - .timer, - .tooltip, - .dialog, - .floating-panel -) { - --ctl-radius: var(--radius-2xl); -} - -[data-theme="duo"] .soonex-section { - padding-block: 5rem; -} - -@media (width >= 40rem) { - [data-theme="duo"] .soonex-section { - padding-block: 7rem; - } -} - -[data-theme="duo"] .soonex-section-heading { - font-family: var(--font-display); -} - -[data-theme="duo"] .soonex-hero { - isolation: isolate; - padding-block: 4rem; -} - -[data-theme="duo"] .soonex-hero-grid { - display: contents; -} - -[data-theme="duo"] [data-hero-visual] { - position: absolute; - inset: 0; - z-index: 0; -} - -[data-theme="duo"] .soonex-hero-frame { - position: absolute; - inset: 0; - min-height: 100%; - border: 0; - border-radius: 0; -} - -[data-theme="duo"] [data-hero-copy] { - position: relative; - z-index: 1; - max-width: 34rem; - margin-block: 4rem; - padding: 2rem; - border: 1px solid var(--color-border); - background: var(--color-surface); - border-radius: var(--radius-2xl); -} - -@media (width >= 40rem) { - [data-theme="duo"] [data-hero-copy] { - padding: 2.5rem; - } -} - -[data-theme="duo"] .soonex-hero-proof { - display: none; -} - -[data-theme="duo"] blockquote, -[data-theme="duo"] .soonex-quote { - font-family: var(--font-serif); - font-style: italic; - font-weight: 400; -} - -[data-theme="duo"] .soonex-claims { - gap: 4rem; -} - -[data-theme="duo"] .soonex-bento-caption { - background: var(--color-surface); -} - -[data-theme="duo"] .soonex-voices { - grid-template-columns: 1fr; -} - -@media (width >= 64rem) { - [data-theme="duo"] .soonex-voices { - grid-template-columns: 1.2fr 1fr 1fr; - } - - [data-theme="duo"] .soonex-voice:first-child .soonex-quote { - font-size: var(--text-2xl); - } -} - -[data-theme="duo"] .soonex-journal { - grid-template-columns: 1fr; -} - -@media (width >= 64rem) { - [data-theme="duo"] .soonex-journal { - grid-template-columns: 1.4fr 1fr 1fr; - } -} - -[data-theme="duo"] .soonex-waitlist-form { - max-width: 36rem; - padding: 3rem; -} - -[data-theme="duo"] .soonex-steps { - grid-template-columns: 1fr; - gap: 3rem; -} - -[data-theme="duo"] .soonex-step-index { - font-family: var(--font-serif); - font-style: italic; -} - -[data-theme="duo"] .soonex-faq [data-scope="accordion"][data-part="item-content"] { - font-family: var(--font-serif); - font-style: italic; -} - -[data-theme="duo"] .soonex-faq-index { - font-family: var(--font-serif); - font-style: italic; -} - -[data-theme="duo"] .soonex-launch { - grid-template-columns: 1fr; -} - -@media (width >= 64rem) { - [data-theme="duo"] .soonex-launch { - grid-template-columns: 1fr 1fr; - } - - [data-theme="duo"] .soonex-launch-beat[data-featured="true"] { - grid-column: 1 / -1; - } -} diff --git a/assets/css/skins/leo.css b/assets/css/skins/leo.css deleted file mode 100644 index d3fcf8d..0000000 --- a/assets/css/skins/leo.css +++ /dev/null @@ -1,181 +0,0 @@ -/* leo — Keynote / Swiss poster. Zero radius, grid overlay, uppercase labels, slab form. */ - -[data-theme="leo"] :is( - .button, - .badge, - .native-input, - .avatar, - .menu, - .select, - .tabs, - .toast, - .pagination, - .clipboard, - .accordion, - .collapsible, - .carousel, - .date-picker, - .tags-input, - .number-input, - .radio-group, - .data-table, - .switch, - .checkbox, - .timer, - .tooltip, - .dialog, - .floating-panel -) { - --ctl-radius: 0; -} - -[data-theme="leo"] .soonex-panel, -[data-theme="leo"] .soonex-frame, -[data-theme="leo"] .soonex-hero-frame { - border-radius: 0; -} - -[data-theme="leo"] :is(.button, .badge) { - letter-spacing: 0.08em; - text-transform: uppercase; -} - -[data-theme="leo"] .soonex-eyebrow, -[data-theme="leo"] .soonex-launch-badge, -[data-theme="leo"] .soonex-nav-links .link, -[data-theme="leo"] .soonex-step-index { - letter-spacing: 0.14em; - text-transform: uppercase; -} - -[data-theme="leo"] [data-site-header] { - background: var(--color-root); - border-bottom: 1px solid var(--color-border); -} - -[data-theme="leo"] .soonex-hero { - isolation: isolate; -} - -[data-theme="leo"] .soonex-hero .soonex-stage { - position: relative; - z-index: 1; -} - -[data-theme="leo"] .soonex-hero-grid { - grid-template-columns: 1fr; - justify-items: center; - gap: 3rem; -} - -[data-theme="leo"] .soonex-hero-heading { - font-size: clamp(3.25rem, 9vw, 7.5rem); - line-height: 0.95; - letter-spacing: -0.05em; - max-width: 18ch; -} - -[data-theme="leo"] .soonex-hero-form { - max-width: none; - padding: 1.25rem; - border: 1px solid var(--color-border); - background: var(--color-surface); -} - -[data-theme="leo"] .soonex-hero-copy { - justify-self: stretch; - width: 100%; -} - -[data-theme="leo"] .soonex-hero-visual { - width: 100%; - max-width: 36rem; - margin-inline: auto; -} - -[data-theme="leo"] .soonex-claims { - gap: 0; - border-top: 1px solid var(--color-border); -} - -[data-theme="leo"] .soonex-claim { - padding-block: 2rem; - border-bottom: 1px solid var(--color-border); -} - -@media (width >= 40rem) { - [data-theme="leo"] .soonex-claims { - grid-template-columns: 1fr; - } -} - -[data-theme="leo"] .soonex-bento { - gap: 0; - border: 1px solid var(--color-border); -} - -[data-theme="leo"] .soonex-bento > * { - border-radius: 0; - border: 0; - border-bottom: 1px solid var(--color-border); -} - -@media (width >= 64rem) { - [data-theme="leo"] .soonex-bento-hero { - border-inline-end: 1px solid var(--color-border); - } -} - -[data-theme="leo"] .soonex-plan[data-featured="true"] { - background: var(--color-accent); - color: var(--color-accent-contrast); -} - -[data-theme="leo"] .soonex-plan[data-featured="true"] .soonex-eyebrow, -[data-theme="leo"] .soonex-plan[data-featured="true"] .display, -[data-theme="leo"] .soonex-plan[data-featured="true"] p, -[data-theme="leo"] .soonex-plan[data-featured="true"] li { - color: inherit; -} - -[data-theme="leo"] .soonex-steps { - gap: 0; - border-top: 1px solid var(--color-border); -} - -[data-theme="leo"] .soonex-step { - padding-block: 2rem; - border-bottom: 1px solid var(--color-border); -} - -@media (width >= 64rem) { - [data-theme="leo"] .soonex-steps { - grid-template-columns: 1fr; - } -} - -[data-theme="leo"] .soonex-faq-index { - letter-spacing: 0.14em; - text-transform: uppercase; -} - -[data-theme="leo"] .soonex-faq [data-scope="accordion"][data-part="item"] { - border: 0; - border-radius: 0; -} - -[data-theme="leo"] .soonex-launch { - gap: 0; - border: 1px solid var(--color-border); -} - -[data-theme="leo"] .soonex-launch-beat { - border-radius: 0; - border: 0; - border-bottom: 1px solid var(--color-border); -} - -[data-theme="leo"] .soonex-waitlist-form { - max-width: 40rem; - border-radius: 0; -} diff --git a/assets/css/skins/neo.css b/assets/css/skins/neo.css deleted file mode 100644 index e4a832a..0000000 --- a/assets/css/skins/neo.css +++ /dev/null @@ -1,116 +0,0 @@ -/* neo — Salient / Radiant. Centered hero, brand underline, quiet sticky nav. */ - -[data-theme="neo"] :is( - .button, - .badge, - .native-input, - .avatar, - .menu, - .select, - .tabs, - .toast, - .pagination, - .clipboard, - .accordion, - .collapsible, - .carousel, - .date-picker, - .tags-input, - .number-input, - .radio-group, - .data-table, - .switch, - .checkbox, - .timer, - .tooltip, - .dialog, - .floating-panel -) { - --ctl-radius: var(--radius-xl); -} - -[data-theme="neo"] .soonex-hero-grid { - display: flex; - flex-direction: column; - justify-items: center; - text-align: center; -} - -[data-theme="neo"] .soonex-hero-visual { - display: none; -} - -[data-theme="neo"] [data-hero-copy] { - max-width: 44rem; -} - -[data-theme="neo"] .soonex-hero-lede, -[data-theme="neo"] .soonex-hero-form { - margin-inline: auto; -} - -[data-theme="neo"] .soonex-hero-actions, -[data-theme="neo"] .soonex-hero-proof { - justify-content: center; -} - -[data-theme="neo"] [data-hero-visual] { - width: min(100%, 48rem); -} - -[data-theme="neo"] .soonex-accent { - position: relative; - display: inline-block; - isolation: isolate; - color: var(--color-brand-text); -} - -[data-theme="neo"] .soonex-accent::after { - content: ""; - position: absolute; - inset-inline: 0; - bottom: -0.2em; - height: 0.14em; - z-index: -1; - border-radius: var(--radius-full); - background: var(--color-brand); -} - -[data-theme="neo"] [data-section="features"] .soonex-intro, -[data-theme="neo"] [data-section="steps"] .soonex-intro, -[data-theme="neo"] [data-section="pricing"] .soonex-intro, -[data-theme="neo"] [data-section="voices"] .soonex-intro, -[data-theme="neo"] [data-section="journal"] .soonex-intro { - margin-inline: auto; - text-align: center; -} - -[data-theme="neo"] [data-section="features"] .soonex-lede, -[data-theme="neo"] [data-section="steps"] .soonex-lede, -[data-theme="neo"] [data-section="pricing"] .soonex-lede, -[data-theme="neo"] [data-section="voices"] .soonex-lede, -[data-theme="neo"] [data-section="journal"] .soonex-lede { - margin-inline: auto; -} - -[data-theme="neo"] [data-section="features"] .soonex-section-actions, -[data-theme="neo"] [data-section="journal"] .soonex-section-actions { - justify-content: center; -} - -[data-theme="neo"] .soonex-bento-hero { - min-height: 22rem; -} - -[data-theme="neo"] .soonex-plan[data-featured="true"] { - box-shadow: var(--shadow-sm); -} - -[data-theme="neo"] .soonex-faq [data-scope="accordion"][data-part="item"] { - border: 0; - border-radius: 0; -} - -[data-theme="neo"] .soonex-launch-beat[data-featured="true"] { - box-shadow: var(--shadow-sm); -} diff --git a/assets/css/skins/uno.css b/assets/css/skins/uno.css deleted file mode 100644 index 6214d75..0000000 --- a/assets/css/skins/uno.css +++ /dev/null @@ -1,149 +0,0 @@ -/* uno — Shellular / Pocket. Pill chrome, italic accent, split hero, 2×2 product. */ - -[data-theme="uno"] :is( - .button, - .badge, - .native-input, - .avatar, - .menu, - .select, - .tabs, - .toast, - .pagination, - .clipboard, - .accordion, - .collapsible, - .carousel, - .date-picker, - .tags-input, - .number-input, - .radio-group, - .data-table, - .switch, - .checkbox, - .timer, - .tooltip, - .dialog, - .floating-panel -) { - --ctl-radius: var(--radius-full); -} - -[data-theme="uno"] .soonex-panel, -[data-theme="uno"] .soonex-frame, -[data-theme="uno"] .soonex-hero-frame { - border-radius: var(--radius-2xl); -} - -[data-theme="uno"] [data-site-header] { - background: var(--color-root); - border-color: transparent; - padding-block: 0.75rem; -} - -[data-theme="uno"] [data-site-header][data-condensed] { - border-color: transparent; -} - -[data-theme="uno"] [data-scroll-progress] { - display: none; -} - -[data-theme="uno"] [data-header-bar] { - border: 1px solid var(--color-border); - background: var(--color-surface); - border-radius: var(--radius-full); - padding-inline: 1rem; - box-shadow: var(--shadow-sm); -} - -[data-theme="uno"] .soonex-accent { - font-family: var(--font-serif); - font-style: italic; - font-weight: 500; - letter-spacing: -0.03em; -} - -[data-theme="uno"] .soonex-hero-heading { - letter-spacing: -0.04em; -} - -[data-theme="uno"] .soonex-hero-proof { - padding: 0.75rem 1rem; - border: 1px solid var(--color-border); - background: var(--color-surface); - border-radius: var(--radius-2xl); -} - -[data-theme="uno"] .soonex-claims { - gap: 1rem; -} - -@media (width >= 40rem) { - [data-theme="uno"] .soonex-claims { - grid-template-columns: 1fr 1fr; - } -} - -[data-theme="uno"] .soonex-claim { - padding: 1.5rem; - border: 1px solid var(--color-border); - background: var(--color-surface); - border-radius: var(--radius-2xl); -} - -[data-theme="uno"] .soonex-bento { - grid-template-columns: 1fr; -} - -@media (width >= 40rem) { - [data-theme="uno"] .soonex-bento { - grid-template-columns: 1fr 1fr; - } - - [data-theme="uno"] .soonex-bento-hero { - grid-column: 1 / -1; - } - - [data-theme="uno"] .soonex-bento-cell { - grid-column: auto; - } -} - -[data-theme="uno"] .soonex-steps { - gap: 1rem; -} - -[data-theme="uno"] .soonex-step { - padding: 1.5rem; - border: 1px solid var(--color-border); - background: var(--color-surface); - border-radius: var(--radius-2xl); -} - -[data-theme="uno"] .soonex-launch-beat, -[data-theme="uno"] .soonex-plan { - border-radius: var(--radius-2xl); -} - -[data-theme="uno"] .soonex-faq [data-scope="accordion"][data-part="item"] { - margin-block: 0.5rem; - border: 1px solid var(--color-border); - background: var(--color-surface); - border-radius: var(--radius-2xl); -} - -[data-theme="uno"] .soonex-faq [data-scope="accordion"][data-part="item-trigger"] { - padding-inline: 1.25rem; -} - -[data-theme="uno"] .soonex-bento-cells .collapsible { - border: 1px solid var(--color-border); - background: var(--color-surface); - border-radius: var(--radius-2xl); - padding: 0.5rem 1rem; -} - -[data-theme="uno"] .display { - letter-spacing: -0.03em; -} diff --git a/assets/js/landing-scroll-chrome.js b/assets/js/landing-scroll-chrome.js deleted file mode 100644 index cb75bc0..0000000 --- a/assets/js/landing-scroll-chrome.js +++ /dev/null @@ -1,83 +0,0 @@ -const stickyRevealPx = 96 -const condensedPx = 8 - -export function scrollProgress01() { - const doc = document.documentElement - const maxScroll = Math.max(1, doc.scrollHeight - window.innerHeight) - return Math.min(1, Math.max(0, window.scrollY / maxScroll)) -} - -function motionReduce() { - return document.documentElement.getAttribute("data-motion") === "reduce" -} - -export function bindLandingScrollChrome(header, countdown, heroBoundary, progressFill) { - const headerEl = header instanceof HTMLElement ? header : null - const countdownEl = countdown instanceof HTMLElement ? countdown : null - const boundaryEl = heroBoundary instanceof HTMLElement ? heroBoundary : null - const progressOk = progressFill instanceof HTMLElement - - if (!headerEl && !countdownEl && !progressOk) { - return () => {} - } - - let alive = true - let rafId = null - let lastHeaderOffset = "" - - const pastReveal = () => { - if (boundaryEl) { - const r = boundaryEl.getBoundingClientRect() - return boundaryEl.hasAttribute("data-hero-sentinel") - ? r.top <= stickyRevealPx - : r.bottom <= stickyRevealPx - } - return true - } - - const showCountdown = (show) => { - if (!countdownEl) { - return - } - countdownEl.toggleAttribute("data-shown", show) - countdownEl.toggleAttribute("aria-hidden", !show) - countdownEl.toggleAttribute("inert", !show) - countdownEl.classList.toggle("invisible", !show) - } - - const tick = () => { - if (!alive) { - return - } - if (headerEl) { - headerEl.toggleAttribute("data-condensed", window.scrollY > condensedPx) - if (motionReduce()) { - headerEl.style.transition = "none" - } - const offset = `${headerEl.getBoundingClientRect().height}px` - if (offset !== lastHeaderOffset) { - lastHeaderOffset = offset - document.documentElement.style.setProperty("--soonex-header-offset", offset) - } - } - showCountdown(pastReveal()) - if (progressOk) { - const p = scrollProgress01() - progressFill.style.transform = `scaleX(${p})` - progressFill.style.transformOrigin = "left center" - if (motionReduce()) { - progressFill.style.transition = "none" - } - } - 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 cd902da..0000000 --- a/assets/js/landing.js +++ /dev/null @@ -1,22 +0,0 @@ -import { bindLandingScrollChrome } from "./landing-scroll-chrome.js" - -export function initLanding() { - const root = document.querySelector("[data-landing]") - if (!(root instanceof HTMLElement)) { - return () => {} - } - - const header = document.querySelector("[data-site-header]") - const countdown = document.querySelector("[data-header-countdown]") - 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 teardown = bindLandingScrollChrome(header, countdown, heroBoundary, progressFill) - - window.addEventListener("pagehide", teardown, { once: true }) - - return teardown -} diff --git a/assets/js/site.js b/assets/js/site.js index fd7899f..04d579d 100644 --- a/assets/js/site.js +++ b/assets/js/site.js @@ -1,4 +1,3 @@ -import "./theme.js" import "./mode.js" import { Socket } from "phoenix" import { LiveSocket } from "phoenix_live_view" @@ -11,9 +10,7 @@ import { Dialog } from "corex/dialog" import { ToggleGroup } from "corex/toggle-group" import { Menu } from "corex/menu" import { Clipboard } from "corex/clipboard" -import { initLanding } from "./landing.js" import { initWaitlistForm } from "./waitlist.js" -import { initSkins } from "./skins.js" import { initPager } from "./pager.js" import "./cookie-consent.js" @@ -21,8 +18,6 @@ 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, marketing widgets only when present. const liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken }, hooks: { @@ -37,12 +32,9 @@ const liveSocket = new LiveSocket("/live", Socket, { ...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"), Pagination: () => import("corex/pagination"), - Carousel: () => import("corex/carousel"), Tooltip: () => import("corex/tooltip"), Collapsible: () => import("corex/collapsible"), Switch: () => import("corex/switch"), @@ -57,10 +49,5 @@ const liveSocket = new LiveSocket("/live", Socket, { liveSocket.disableDebug() liveSocket.connect() -if (document.querySelector("[data-landing]")) { - initLanding() -} - initWaitlistForm() -initSkins() initPager() diff --git a/assets/js/skins.js b/assets/js/skins.js deleted file mode 100644 index 7bd0936..0000000 --- a/assets/js/skins.js +++ /dev/null @@ -1,57 +0,0 @@ -const skins = { - neo: () => import("./skins/neo.js"), - uno: () => import("./skins/uno.js"), - duo: () => import("./skins/duo.js"), - leo: () => import("./skins/leo.js"), -} - -let active = { id: null, teardown: null } - -function motionReduce() { - return document.documentElement.getAttribute("data-motion") === "reduce" -} - -async function apply(theme) { - if (motionReduce()) { - if (typeof active.teardown === "function") { - active.teardown() - } - active = { id: null, teardown: null } - return - } - - if (active.id === theme) { - return - } - - if (typeof active.teardown === "function") { - active.teardown() - } - - active = { id: theme, teardown: null } - const load = skins[theme] - if (!load) { - return - } - - const mod = await load() - if (active.id !== theme) { - return - } - - active.teardown = typeof mod.activate === "function" ? mod.activate() || null : null -} - -export function initSkins() { - const root = document.documentElement - apply(root.dataset.theme) - - const observer = new MutationObserver(() => { - apply(root.dataset.theme) - }) - - observer.observe(root, { - attributes: true, - attributeFilter: ["data-theme", "data-motion"], - }) -} diff --git a/assets/js/skins/duo.js b/assets/js/skins/duo.js deleted file mode 100644 index 60dfc22..0000000 --- a/assets/js/skins/duo.js +++ /dev/null @@ -1,9 +0,0 @@ -export function activate() { - const root = document.documentElement - root.setAttribute("data-skin-ready", "duo") - return () => { - if (root.getAttribute("data-skin-ready") === "duo") { - root.removeAttribute("data-skin-ready") - } - } -} diff --git a/assets/js/skins/leo.js b/assets/js/skins/leo.js deleted file mode 100644 index 9ecbb0e..0000000 --- a/assets/js/skins/leo.js +++ /dev/null @@ -1,9 +0,0 @@ -export function activate() { - const root = document.documentElement - root.setAttribute("data-skin-ready", "leo") - return () => { - if (root.getAttribute("data-skin-ready") === "leo") { - root.removeAttribute("data-skin-ready") - } - } -} diff --git a/assets/js/skins/neo.js b/assets/js/skins/neo.js deleted file mode 100644 index 8bf13f5..0000000 --- a/assets/js/skins/neo.js +++ /dev/null @@ -1,9 +0,0 @@ -export function activate() { - const root = document.documentElement - root.setAttribute("data-skin-ready", "neo") - return () => { - if (root.getAttribute("data-skin-ready") === "neo") { - root.removeAttribute("data-skin-ready") - } - } -} diff --git a/assets/js/skins/uno.js b/assets/js/skins/uno.js deleted file mode 100644 index 18a9b7f..0000000 --- a/assets/js/skins/uno.js +++ /dev/null @@ -1,9 +0,0 @@ -export function activate() { - const root = document.documentElement - root.setAttribute("data-skin-ready", "uno") - return () => { - if (root.getAttribute("data-skin-ready") === "uno") { - root.removeAttribute("data-skin-ready") - } - } -} diff --git a/assets/js/theme.js b/assets/js/theme.js deleted file mode 100644 index feae714..0000000 --- a/assets/js/theme.js +++ /dev/null @@ -1,33 +0,0 @@ -import { - bindStoredPreference, - documentRoot, - firstDetailValue, - parseList, -} from "./controls-shared.js" - -const themes = () => parseList("data-themes") - -const defaultTheme = () => - documentRoot().getAttribute("data-default-theme") || themes()[0] || "neo" - -const resolveTheme = (raw) => { - const allowed = themes() - return raw && allowed.includes(raw) ? raw : defaultTheme() -} - -bindStoredPreference({ - attr: "data-theme", - controlId: "theme-switcher", - resolve: resolveTheme, - syncControl: (theme) => { - const root = document.getElementById("theme-switcher") - if (!root || !theme) return - const current = root.getAttribute("data-value") || "" - if (current.split(",").map((s) => s.trim()).includes(theme)) return - root.dispatchEvent( - new CustomEvent("corex:select:set-value", { detail: { value: [theme] } }), - ) - }, - clientEvent: "corex:set-theme", - parseClientEvent: (event) => firstDetailValue(event) || defaultTheme(), -}) diff --git a/config/config.exs b/config/config.exs index 421deba..69ea711 100644 --- a/config/config.exs +++ b/config/config.exs @@ -76,7 +76,7 @@ config :tableau, Tableau.RSSExtension, enabled: true, title: "Soonex", description: - "Tableau + Corex launch template: waitlist, journal, countdown, and four skins you customize in config." + "Tableau + Corex coming-soon template: accessible Phoenix UI, waitlist, journal, and config-driven design tokens." config :elixir, :time_zone_database, Tz.TimeZoneDatabase @@ -214,238 +214,6 @@ config :corex_design, "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], @@ -476,13 +244,10 @@ config :corex_design, :checkbox, :"data-list", :timer, - :marquee, :accordion, - :avatar, :"floating-panel", :scrollbar, :pagination, - :carousel, :switch, :"data-table", :collapsible, diff --git a/lib/layouts/root/demo.ex b/lib/layouts/root/demo.ex index f8feaea..6609b5a 100644 --- a/lib/layouts/root/demo.ex +++ b/lib/layouts/root/demo.ex @@ -20,7 +20,7 @@ defmodule Soonex.Layouts.Root.Demo do id="site-controls" class="floating-panel" dir="ltr" - size={%{width: 250, height: 120}} + size={%{width: 220, height: 96}} positioning={ %Corex.Positioning{ placement: "bottom-end", @@ -31,71 +31,37 @@ defmodule Soonex.Layouts.Root.Demo do translation={%Corex.FloatingPanel.Translation{close: "Close"}} > <:trigger class="button ui-size-sm"> - <.heroicon name="hero-cog-6-tooth" /> Template Options + <.heroicon name="hero-cog-6-tooth" /> Options - <:title>Template Options + <:title>Display <:close_trigger> <.heroicon name="hero-x-mark" /> <:content> -
    -
    - <.select - id="theme-switcher" - class="select ui-size-sm w-full min-w-0" - dir="ltr" - items={Soonex.Theme.select_items()} - value={[]} - 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 - <:trigger> - <.heroicon name="hero-chevron-down" /> - - <:item_indicator> - <.heroicon name="hero-check" /> - - - - <.toggle - id="mode-switcher" - class="toggle ui-size-sm" - data-toggle-dual-label - pressed={@mode == "dark"} - dir="ltr" - on_pressed_change_client="corex:set-mode" - > - Color mode - - <.heroicon name="hero-moon" /> - - - <.heroicon name="hero-sun" /> - - -
    -
    + <.toggle + id="mode-switcher" + class="toggle ui-size-sm" + data-toggle-dual-label + pressed={@mode == "dark"} + dir="ltr" + on_pressed_change_client="corex:set-mode" + > + Color mode + + <.heroicon name="hero-moon" /> + + + <.heroicon name="hero-sun" /> + + <.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 a1bf586..3671553 100644 --- a/lib/layouts/root/footer.ex +++ b/lib/layouts/root/footer.ex @@ -27,7 +27,7 @@ defmodule Soonex.Layouts.Root.Footer do
    <.lockup />

    - A Tableau + Corex launch template: waitlist, journal, and four skins you can tell apart. + Tableau + Corex coming-soon template with accessible Phoenix UI, a waitlist, and a journal.

    @@ -49,10 +49,10 @@ defmodule Soonex.Layouts.Root.Footer do
  • <.navigate - to={Soonex.Public.path("/") <> "#questions"} + to={Soonex.Public.path("/") <> "#how-it-works"} class="link ui-nav ui-size-sm" > - Questions + How it works
  • diff --git a/lib/layouts/root/nav.ex b/lib/layouts/root/nav.ex index 498a43d..ec4afe1 100644 --- a/lib/layouts/root/nav.ex +++ b/lib/layouts/root/nav.ex @@ -9,35 +9,18 @@ defmodule Soonex.Layouts.Root.Nav do alias Soonex.Layouts.Shell alias Soonex.Public - attr(:countdown_start_ms, :integer, required: true) attr(:page_path, :string, default: "/") def site_nav(assigns) do assigns = assign(assigns, :nav_select_items, nav_select_items()) ~H""" -
    - - -
    +
    +
    <.select id="soonex-mobile-nav" - class="select ui-size-sm ui-width-fit soonex-nav-select lg:hidden" + class="select ui-size-sm ui-width-fit lg:hidden" redirect update_trigger={false} positioning={ @@ -49,7 +32,7 @@ defmodule Soonex.Layouts.Root.Nav do strategy: "fixed" } } - translation={%Corex.Select.Translation{placeholder: "Open menu"}} + translation={%Corex.Select.Translation{placeholder: "Menu"}} items={@nav_select_items} > <:trigger> @@ -59,7 +42,7 @@ defmodule Soonex.Layouts.Root.Nav do <.lockup />
    -
    """ @@ -108,8 +64,8 @@ defmodule Soonex.Layouts.Root.Nav do current? = nav_current?(page_path, item) [ - "link ui-nav ui-size-sm relative after:absolute after:inset-x-0 after:-bottom-1 after:h-px after:origin-left after:bg-brand after:transition-transform", - if(current?, do: "after:scale-x-100", else: "after:scale-x-0 hover:after:scale-x-100") + "link ui-nav ui-size-sm", + if(current?, do: "text-ink", else: "text-ink-muted hover:text-ink") ] end @@ -122,44 +78,26 @@ defmodule Soonex.Layouts.Root.Nav do defp desktop_links do [ %{id: :product, label: "Product", to: Public.path("/") <> "#features"}, - %{id: :launch, label: "Launch", to: Public.path("/") <> "#launch"}, + %{id: :how, label: "How it works", to: Public.path("/") <> "#how-it-works"}, + %{id: :pricing, label: "Plans", to: Public.path("/") <> "#pricing"}, %{id: :journal, label: "Journal", to: Public.path("/blog")}, - %{id: :questions, label: "Questions", to: Public.path("/") <> "#questions"} + %{id: :questions, label: "FAQ", to: Public.path("/") <> "#questions"} ] end defp nav_select_items do Corex.List.new([ + %{label: "Product", value: "features", to: Public.path("/") <> "#features", redirect: :href}, %{ - label: "Product", - value: "features", - to: Public.path("/") <> "#features", + label: "How it works", + value: "how", + to: Public.path("/") <> "#how-it-works", redirect: :href }, - %{ - label: "Launch", - value: "launch", - to: Public.path("/") <> "#launch", - redirect: :href - }, - %{ - label: "Journal", - value: "blog", - to: Public.path("/blog"), - redirect: :href - }, - %{ - label: "Questions", - value: "questions", - to: Public.path("/") <> "#questions", - redirect: :href - }, - %{ - label: "Join waitlist", - value: "waitlist", - to: Public.path("/") <> "#waitlist", - redirect: :href - } + %{label: "Plans", value: "pricing", to: Public.path("/") <> "#pricing", redirect: :href}, + %{label: "Journal", value: "blog", to: Public.path("/blog"), redirect: :href}, + %{label: "FAQ", value: "questions", to: Public.path("/") <> "#questions", redirect: :href}, + %{label: "Join waitlist", value: "waitlist", to: Public.path("/") <> "#waitlist", redirect: :href} ]) end end diff --git a/lib/layouts/root_layout.ex b/lib/layouts/root_layout.ex index 8ff5ffa..d01dfab 100644 --- a/lib/layouts/root_layout.ex +++ b/lib/layouts/root_layout.ex @@ -19,8 +19,6 @@ defmodule Soonex.RootLayout do site_name = "Soonex" copyright_holder = "Soonex" - countdown_start_ms = Soonex.Launch.countdown_ms() - tableau_config = case Tableau.Config.get() do {:ok, %Tableau.Config{} = c} -> c @@ -52,7 +50,6 @@ defmodule Soonex.RootLayout do |> Map.put(:default_theme, Soonex.Theme.default_theme()) |> Map.put(:theme, Soonex.Theme.current(assigns)) |> Map.put(:mode, Soonex.Mode.current(assigns)) - |> Map.put(:countdown_start_ms, countdown_start_ms) |> Map.put(:canonical_url, canonical_url) |> Map.put(:base_url, base_url) |> Map.put(:page_path, page_path) @@ -157,13 +154,9 @@ defmodule Soonex.RootLayout do <.navigate to="#main-content" class="link link--skip">Skip to content <.demo_site_controls mode={@mode} /> - <.site_nav countdown_start_ms={@countdown_start_ms} page_path={@page_path} /> + <.site_nav page_path={@page_path} /> -
    +
    {render(@inner_content)}
    @@ -218,13 +211,13 @@ defmodule Soonex.RootLayout do else kind_description(page[:page_kind], site_name) || page_description(page) || - "Tableau + Corex launch template: waitlist, journal, and four theme skins." + "Tableau + Corex coming-soon template with accessible Phoenix UI components." end end defp kind_description(:home, _site_name), do: - "Tableau + Corex launch template: waitlist, journal, countdown, and four skins you customize in config." + "Tableau + Corex coming-soon template: accessible Phoenix UI, waitlist, journal, and config-driven design tokens." defp kind_description(:blog_index, site_name), do: "Shipping notes from #{site_name}." defp kind_description(:not_found, site_name), do: "That page is not on #{site_name}." diff --git a/lib/layouts/section.ex b/lib/layouts/section.ex index 213a999..e01d111 100644 --- a/lib/layouts/section.ex +++ b/lib/layouts/section.ex @@ -13,7 +13,6 @@ defmodule Soonex.Layouts.Section do attr(:align, :atom, default: :start) attr(:layout, :atom, default: :stack, values: [:stack, :sticky]) attr(:compact, :boolean, default: false) - attr(:reveal, :boolean, default: true) slot(:title, required: true) slot(:lede) @@ -25,7 +24,7 @@ defmodule Soonex.Layouts.Section do
    @@ -39,7 +38,7 @@ defmodule Soonex.Layouts.Section do

    {render_slot(@actions)}
    @@ -52,10 +51,8 @@ defmodule Soonex.Layouts.Section do """ end - defp section_class(true, true), do: "#{Shell.section_compact()} soonex-reveal" - defp section_class(false, true), do: "#{Shell.section()} soonex-reveal" - defp section_class(true, false), do: Shell.section_compact() - defp section_class(false, false), do: Shell.section() + defp section_class(true), do: Shell.section_compact() + defp section_class(false), do: Shell.section() defp tone_class(:root), do: "bg-root" defp tone_class(:surface), do: "border-y border-border bg-surface" diff --git a/lib/layouts/shell.ex b/lib/layouts/shell.ex index ed2a865..f4c8730 100644 --- a/lib/layouts/shell.ex +++ b/lib/layouts/shell.ex @@ -1,55 +1,46 @@ defmodule Soonex.Layouts.Shell do @moduledoc false - # Shared marketing band (Salient-width). Skins override density via - # .soonex-section / .soonex-hero / .soonex-stage on [data-theme]. + def section, do: "scroll-mt-24 w-full py-24 sm:py-32" - def section, do: "soonex-section scroll-mt-24 w-full py-24 sm:py-32" - - def section_compact, - do: "soonex-section soonex-section--compact scroll-mt-24 w-full py-16 sm:py-20" + def section_compact, do: "scroll-mt-24 w-full py-16 sm:py-20" def section_hero, - do: - "soonex-hero relative flex min-h-svh w-full flex-col justify-center overflow-x-clip py-16 sm:py-24" + do: "relative flex min-h-[80svh] w-full flex-col justify-center py-20 sm:py-28" - def stage, do: "soonex-stage mx-auto w-full max-w-7xl px-6 lg:px-8" + def stage, do: "mx-auto w-full max-w-6xl px-6 lg:px-8" - def intro, do: "soonex-intro mx-auto max-w-2xl lg:mx-0" + def intro, do: "mx-auto max-w-2xl lg:mx-0" - def intro_center, do: "soonex-intro soonex-intro--center mx-auto max-w-2xl lg:text-center" + def intro_center, do: "mx-auto max-w-2xl text-center" - def body, do: "soonex-body mx-auto mt-16 w-full sm:mt-20 lg:mt-24" + def body, do: "mx-auto mt-16 w-full sm:mt-20 lg:mt-24" - def body_tight, do: "soonex-body soonex-body--tight mx-auto mt-10 w-full sm:mt-12" + def body_tight, do: "mx-auto mt-10 w-full sm:mt-12" - def sticky_grid, - do: "soonex-sticky-grid grid grid-cols-1 items-start gap-16 lg:grid-cols-12 lg:gap-12" + def sticky_grid, do: "grid grid-cols-1 items-start gap-16 lg:grid-cols-12 lg:gap-12" - def sticky_intro, - do: "soonex-sticky-intro mx-auto max-w-2xl lg:sticky lg:top-28 lg:col-span-5 lg:mx-0" + def sticky_intro, do: "mx-auto max-w-2xl lg:sticky lg:top-28 lg:col-span-5 lg:mx-0" - def sticky_body, do: "soonex-sticky-body w-full min-w-0 lg:col-span-7" + def sticky_body, do: "w-full min-w-0 lg:col-span-7" - def eyebrow, do: "soonex-eyebrow m-0 text-base/7 font-semibold text-brand-text" + def eyebrow, + do: "soonex-eyebrow m-0 text-sm/6 font-semibold tracking-wide text-brand-text uppercase" def section_heading, - do: - "soonex-section-heading display mt-2 text-pretty text-4xl font-semibold tracking-tight text-ink sm:text-5xl" + do: "display mt-3 text-pretty text-4xl font-semibold tracking-tight text-ink sm:text-5xl" def page_heading, - do: - "soonex-page-heading display mt-2 text-pretty text-4xl font-semibold tracking-tight text-ink sm:text-5xl" + do: "display mt-3 text-pretty text-4xl font-semibold tracking-tight text-ink sm:text-5xl" - def lede, do: "soonex-lede mt-6 max-w-2xl text-pretty text-lg/8 text-ink-muted" + def lede, do: "mt-5 max-w-2xl text-pretty text-lg/8 text-ink-muted" - def panel, - do: "soonex-panel soonex-frame relative overflow-hidden border border-border bg-surface" + def panel, do: "relative overflow-hidden rounded-2xl border border-border bg-surface p-6 sm:p-8" def panel_open, - do: "soonex-panel soonex-frame relative overflow-visible border border-border bg-surface" + do: "relative overflow-visible rounded-2xl border border-border bg-surface p-6 sm:p-8" - def frame, do: "soonex-frame relative overflow-hidden border border-border bg-surface" + def frame, do: "relative overflow-hidden rounded-2xl border border-border bg-surface" - def data_list, do: "data-list data-list--wide ui-accent ui-size-md w-full max-w-none" + def data_list, do: "data-list ui-accent ui-size-md w-full max-w-none" end diff --git a/lib/pages/blog_index_page.ex b/lib/pages/blog_index_page.ex index 9ff4787..374014f 100644 --- a/lib/pages/blog_index_page.ex +++ b/lib/pages/blog_index_page.ex @@ -32,7 +32,7 @@ defmodule Soonex.BlogIndexPage do <.layout_heading class="layout-heading" subtitle_tag="p"> <:title>Journal <:subtitle> - Shipping notes for the waitlist, skins, and launch date. {@blog_count} {if @blog_count == + Shipping notes for Corex, the waitlist, and launch prep. {@blog_count} {if @blog_count == 1, do: "post", else: "posts"}. diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index 6e0edf6..8717c74 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -14,13 +14,14 @@ defmodule Soonex.HomePage.Faq do labelledby="soonex-faq-heading" eyebrow="Questions" layout={:sticky} + tone={:surface} > <:title> - Before you fork. Then it's yours. + Before you fork Soonex. <:lede> - The waitlist, skins, and journal are already in the tree. Photography and copy are the - first things you replace. + Customization lives in config :corex_design and HEEx content modules. No parallel CSS + framework to maintain. <:actions> <.navigate to="#waitlist" class="link ui-brand"> @@ -29,45 +30,24 @@ defmodule Soonex.HomePage.Faq do <.accordion id="soonex-faq" - class="accordion ui-accent ui-size-md ui-width-full soonex-faq" + class="accordion ui-accent ui-size-md ui-width-full" multiple={false} collapsible - value="ships" + value="corex" items={faq_items()} > <:trigger :let={item}> - - - {faq_index(item.value)} - - {item.label} - + {item.label} <:content :let={item}> -
    +

    {item.content}

    - <.navigate :if={item.value == "ships"} to="#features" class="link ui-brand w-fit"> - See the product <.heroicon name="hero-arrow-down" /> + <.navigate :if={item.value == "customize"} to="#how-it-works" class="link ui-brand w-fit"> + See how it works <.heroicon name="hero-arrow-down" /> <.navigate :if={item.value == "waitlist"} to="#waitlist" class="link ui-brand w-fit"> Open the waitlist <.heroicon name="hero-arrow-down" /> -
      -
    • neo
    • -
    • uno
    • -
    • duo
    • -
    • leo
    • -
    - <.navigate - :if={item.value == "journal"} - to={Soonex.Public.path("/blog")} - class="link ui-brand w-fit" - > - Read the journal <.heroicon name="hero-arrow-up-right" /> - <.clipboard :if={item.value == "toolchain"} id="soonex-faq-setup" @@ -94,65 +74,37 @@ defmodule Soonex.HomePage.Faq do """ end - defp faq_index("ships"), do: "01" - defp faq_index("waitlist"), do: "02" - defp faq_index("themes"), do: "03" - defp faq_index("journal"), do: "04" - defp faq_index("motion"), do: "05" - defp faq_index("a11y"), do: "06" - defp faq_index("toolchain"), do: "07" - defp faq_index("countdown"), do: "08" - defp faq_index(_value), do: "00" - defp faq_items do Corex.Content.new([ %{ - value: "ships", - label: "What ships when I fork this template?", + value: "corex", + label: "What is Corex in this template?", content: - "A waitlist landing page, a journal, countdown chrome, and four theme skins. Photography, copy, and the launch date are placeholders you replace." + "Corex is an accessible Phoenix UI kit. Soonex uses its static-site integration: design tokens from config :corex_design, ui-* modifiers in HEEx, and client hooks for interactive widgets." }, %{ - value: "waitlist", - label: "Does the waitlist collect real addresses?", + value: "customize", + label: "How do I change colors and typography?", content: - "Not in this demo. Submit still shows a toast so you can see the success path without storing mailboxes. Wire the same form to your list later — interests, role, team size, and launch notes are already named fields." + "Edit seeds, dimensions, and typography under config :corex_design, then run mix corex.design.build. Light and dark modes regenerate from the same config." }, %{ - value: "themes", - label: "How do the four themes look so different?", - content: - "Corex config owns seeds, radius, and type. Isolated CSS in assets/css/skins restyles nav chrome, hero composition, and ornament. Delete a skin file and its import to drop that look." - }, - %{ - value: "journal", - label: "Where does the journal live, and can I add posts?", - content: - "Posts are Markdown under the journal route. The home grid is a short reading list; the index paginates the rest. Add a file, rebuild, and the cover, date, and title show up." - }, - %{ - value: "motion", - label: "How does scroll motion behave with reduced motion?", - content: - "Scroll reveal, card lift, and skin ornament follow scroll and hover. Set Motion to Reduced in Accessibility and those animations stop. The condensed header and progress bar stay as chrome." - }, - %{ - value: "a11y", - label: "What do the accessibility controls change?", + value: "waitlist", + label: "Does the waitlist store email addresses?", content: - "The person-in-circle control at the corner opens zoom, contrast, motion, cursor, focus, and link underline for this device. Choices persist in local storage and write data attributes on the document." + "Not in this demo. Submit shows a toast so you can verify the UX before wiring your list provider. Field names are already structured for a real backend." }, %{ value: "toolchain", - label: "Do I need npm or a JavaScript bundler to start?", + label: "Do I need npm?", content: - "No. Mix, Tailwind, and esbuild already in the project are enough. There is no package.json. Copy the setup command and you are in." + "No. Mix, Tailwind, and esbuild ship in the repo. There is no package.json — run mix setup and mix server to work locally." }, %{ - value: "countdown", - label: "How is the countdown wired to a launch date?", + value: "a11y", + label: "What do the accessibility controls do?", content: - "Soonex.Launch holds the target. The hero shows a launch badge; the header timer appears after the hero. Change the target in that module when you have a real date." + "The person icon opens Corex accessibility settings: text size, contrast, motion, cursor, focus, and link underline. Choices persist in localStorage on this device." } ]) end diff --git a/lib/pages/home/features.ex b/lib/pages/home/features.ex index 6fe2bb5..9539c1f 100644 --- a/lib/pages/home/features.ex +++ b/lib/pages/home/features.ex @@ -6,6 +6,8 @@ defmodule Soonex.HomePage.Features do import Soonex.Layouts.Section, only: [block: 1] + alias Soonex.Layouts.Shell + def features(assigns) do ~H""" <.block @@ -14,18 +16,19 @@ defmodule Soonex.HomePage.Features do labelledby="soonex-features-heading" eyebrow="Product" tone={:root} + align={:center} > <:title> - Everything a launch site needs. Nothing you have to invent. + Corex components with contrast you can trust. <:lede> - Themes, waitlist, journal, and accessibility are already wired. You change copy, seeds, and - the launch date. + Tokens, modes, and accessibility are solved in config — not in one-off CSS. Customize seeds, + radius, and typography, then ship the same markup in light and dark. -
      -
    • -
    - - """ end - - defp people do - [ - %{src: "/images/people/amira.jpg", alt: "Amira N."}, - %{src: "/images/people/jonas.jpg", alt: "Jonas K."}, - %{src: "/images/people/leah.jpg", alt: "Leah M."} - ] - end end diff --git a/lib/pages/home/how_it_works.ex b/lib/pages/home/how_it_works.ex new file mode 100644 index 0000000..f7f9457 --- /dev/null +++ b/lib/pages/home/how_it_works.ex @@ -0,0 +1,74 @@ +defmodule Soonex.HomePage.HowItWorks do + @moduledoc false + + use Phoenix.Component + use Corex + + import Soonex.Layouts.Section, only: [block: 1] + + def how_it_works(assigns) do + ~H""" + <.block + id="how-it-works" + section="how-it-works" + labelledby="soonex-how-heading" + eyebrow="How it works" + tone={:surface} + > + <:title> + From clone to launch in three commands. + + <:lede> + Mix handles dependencies, Corex design CSS, Tailwind, and esbuild. You edit config and + content — not a parallel design system. + + <.tabs + id="soonex-how-tabs" + class="tabs tabs--wide ui-brand ui-width-full" + value="setup" + items={tab_items()} + /> +
    + <.clipboard + id="soonex-how-copy" + class="clipboard ui-brand ui-solid ui-width-fit ui-size-sm" + value="mix setup" + > + <:label class="sr-only">mix setup + <:copy> + <.heroicon name="hero-clipboard" /> + Copy mix setup + + <:copied> + <.heroicon name="hero-check" /> + Copied + + +
    + + """ + end + + defp tab_items do + Corex.Content.new([ + %{ + value: "setup", + label: "Setup", + content: + "Run mix setup to fetch Hex deps and build Corex design assets from config :corex_design." + }, + %{ + value: "customize", + label: "Customize", + content: + "Edit seeds, radius, and typography in config/config.exs, then mix corex.design.build. Update HEEx in lib/pages/home for copy." + }, + %{ + value: "ship", + label: "Ship", + content: + "Run MIX_ENV=prod mix build to emit _site/. Deploy to GitHub Pages or any static host." + } + ]) + end +end diff --git a/lib/pages/home/journal.ex b/lib/pages/home/journal.ex index 5a02f75..e3d03f9 100644 --- a/lib/pages/home/journal.ex +++ b/lib/pages/home/journal.ex @@ -4,7 +4,6 @@ defmodule Soonex.HomePage.Journal do use Phoenix.Component use Corex - import Soonex.Layouts.Media, only: [photo: 1] import Soonex.Layouts.Section, only: [block: 1] alias Soonex.Layouts.Shell @@ -26,39 +25,27 @@ defmodule Soonex.HomePage.Journal do section="journal" labelledby="soonex-journal-heading" eyebrow="Journal" - tone={:root} + tone={:surface} > <:title> - Shipping notes, not a lorem feed. + Notes from building with Corex. <:lede> - Posts are Markdown under _posts. The home grid is a short reading list; the index paginates - the rest. + Markdown posts under _posts. The index paginates; the home page shows the latest three. <:actions> <.navigate to={Soonex.Public.path("/blog")} class="link ui-brand"> All posts <.heroicon name="hero-arrow-up-right" /> -
    +

    No posts yet.

    -
    -
    - <.photo - src={cover(post).src} - alt={cover(post).alt} - width={1400} - height={900} - /> -
    +

    {date_label(post)}

    @@ -80,14 +67,6 @@ defmodule Soonex.HomePage.Journal do """ 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 end diff --git a/lib/pages/home/launch_band.ex b/lib/pages/home/launch_band.ex deleted file mode 100644 index b9d68c7..0000000 --- a/lib/pages/home/launch_band.ex +++ /dev/null @@ -1,77 +0,0 @@ -defmodule Soonex.HomePage.LaunchBand do - @moduledoc false - - use Phoenix.Component - use Corex - - import Soonex.Layouts.Section, only: [block: 1] - - alias Soonex.Layouts.Shell - - def launch_band(assigns) do - ~H""" - <.block - id="launch" - section="launch" - labelledby="soonex-launch-heading" - eyebrow="Launch" - tone={:surface} - > - <:title> - Road to {Soonex.Launch.year_label()} - - <:lede> - Four beats. Replace the dates in Soonex.Launch. The header countdown reads the same target. - -
      -
    1. -

      - {beat.moment} -

      -

      - {beat.title} -

      -

      {beat.body}

      -
    2. -
    - - """ - end - - defp beats do - [ - %{ - moment: "12 March 2026", - title: "First cut of the waitlist", - featured: false, - body: - "Hero, form fields, and the toast path landed so a founder can see the success state before attaching a list." - }, - %{ - moment: "1 June 2026", - title: "Four skins, one markup tree", - featured: false, - body: - "Neo, uno, duo, and leo stopped sharing one layout. Each theme now has a CSS file you can delete." - }, - %{ - moment: "2 August 2026", - title: "Journal and tags", - featured: false, - body: - "Markdown posts, covers, and RSS joined the landing page so the launch log is part of the template." - }, - %{ - moment: Soonex.Launch.year_label(), - title: "Public launch", - featured: true, - body: - "Replace this date in Soonex.Launch. The header countdown appears after the hero and counts down to the same instant." - } - ] - end -end diff --git a/lib/pages/home/logos.ex b/lib/pages/home/logos.ex deleted file mode 100644 index 916e2ce..0000000 --- a/lib/pages/home/logos.ex +++ /dev/null @@ -1,55 +0,0 @@ -defmodule Soonex.HomePage.Logos do - @moduledoc false - - use Phoenix.Component - use Corex - - def logos(assigns) do - assigns = assign(assigns, :tech, tech_items()) - - ~H""" -
    -

    - The same stack you already ship with -

    - <.marquee - id="soonex-tech-marquee" - class="marquee ui-width-full" - duration={20} - spacing="2rem" - pause_on_interaction - items={@tech} - > - <:item :let={item}> - - - {item.name} - - - -
    - """ - end - - defp tech_items do - [ - %{name: "Elixir", src: "/images/tech/elixir.svg"}, - %{name: "Phoenix", src: "/images/tech/phoenixframework.svg"}, - %{name: "Tailwind", src: "/images/tech/tailwindcss.svg"}, - %{name: "HTML5", src: "/images/tech/html5.svg"}, - %{name: "JavaScript", src: "/images/tech/javascript.svg"}, - %{name: "TypeScript", src: "/images/tech/typescript.svg"}, - %{name: "Hex", src: "/images/tech/hex.svg"} - ] - end -end diff --git a/lib/pages/home/pricing.ex b/lib/pages/home/pricing.ex index ec2bf3e..4828cdd 100644 --- a/lib/pages/home/pricing.ex +++ b/lib/pages/home/pricing.ex @@ -16,26 +16,26 @@ defmodule Soonex.HomePage.Pricing do labelledby="soonex-pricing-heading" eyebrow="Plans" tone={:root} + align={:center} > <:title> - One template. Three ways to ship it. + Start free. Scale when you ship. <:lede> - These tiers are placeholders you rename. The middle card is the one you keep when you fork. + Placeholder tiers you can rename for your product. The middle option is the default fork + path for teams who want the full template. -
    +

    {plan.name}

    {plan.headline}

    -

    - {plan.price} -

    +

    {plan.price}

    {plan.body}

    -
      +
      • <.heroicon name="hero-check" class="mt-0.5 size-4 shrink-0" /> {item} @@ -57,42 +57,37 @@ defmodule Soonex.HomePage.Pricing do """ end - defp plan_class(%{featured: true}) do - "soonex-frame relative overflow-hidden border" - end - + defp plan_class(%{featured: true}), do: Shell.frame() defp plan_class(_plan), do: Shell.panel() defp plans do [ %{ name: "Launch", - headline: "Fork and go live", + headline: "Fork and customize", price: "Open", featured: false, cta: "Join waitlist", - body: "The waitlist landing page, countdown chrome, and a single theme you rebrand.", - items: ["Waitlist form + toast", "Journal + RSS", "One Corex theme"] + body: "Waitlist landing, journal, and light/dark modes with Corex tokens.", + items: ["Waitlist + toast demo", "Journal + tags", "GitHub Pages workflow"] }, %{ name: "Studio", - headline: "All four skins", + headline: "Full template", price: "Open", featured: true, cta: "Start with Studio", - body: - "Switch neo, uno, duo, and leo from config. Keep the ones you like; delete the rest.", - items: ["Four skins + dark mode", "Accessibility dialog", "Launch timeline"] + body: "Everything in Launch plus accessibility controls and richer form fields.", + items: ["Corex a11y dialog", "Tabs, accordion, selects", "mix project.rename task"] }, %{ - name: "Agency", - headline: "Ship it for a client", + name: "Team", + headline: "Ship for clients", price: "Open", featured: false, cta: "Join waitlist", - body: - "Rename the OTP app, swap the lockup, and hand over a site that already passes axe.", - items: ["mix project.rename", "Self-hosted fonts", "GitHub Pages workflow"] + body: "Hand off a static site that already passes axe and documents customization in README.", + items: ["Self-hosted fonts", "Config-driven theming", "Tableau + Corex docs linked"] } ] end diff --git a/lib/pages/home/product.ex b/lib/pages/home/product.ex deleted file mode 100644 index 85c7c32..0000000 --- a/lib/pages/home/product.ex +++ /dev/null @@ -1,106 +0,0 @@ -defmodule Soonex.HomePage.Product do - @moduledoc false - - use Phoenix.Component - use Corex - - import Soonex.Layouts.Media, only: [photo: 1] - import Soonex.Layouts.Section, only: [block: 1] - - alias Soonex.Layouts.Shell - - def product(assigns) do - ~H""" - <.block - id="product" - section="product" - labelledby="soonex-product-heading" - eyebrow="Inside" - tone={:surface} - > - <:title> - The page is the product. Replace the stills. - - <:lede> - Photography, lockup, and launch date are placeholders. The bands, tokens, and waitlist - fields stay. - -
        -
        - <.photo - src="/images/photos/studio.jpg" - alt="A calm studio interior with a low sofa, plants, and a large window" - width={1400} - height={1600} - class="absolute inset-0 size-full" - /> -
        -

        Studio

        -

        - Drop in your product stills. Keep the grid. -

        -
        -
        - -
        - <.collapsible - :for={cell <- cells()} - id={cell.id} - class="collapsible ui-brand ui-width-full soonex-bento-cell" - open={cell.open} - > - <:trigger> - - {cell.kicker} - - {cell.title} - - - <.heroicon name="hero-chevron-down" /> - - <:content> -

        {cell.body}

        - - -
        -
        - - """ - end - - defp cells do - [ - %{ - id: "soonex-inside-waitlist", - open: true, - kicker: "Waitlist", - title: "Named fields, ready to wire", - body: - "Email, role, interests, team size, and launch notes are already in the form. Submit shows a toast in this demo." - }, - %{ - id: "soonex-inside-journal", - open: false, - kicker: "Journal", - title: "Markdown posts, tags, RSS", - body: "Add a file under _posts, rebuild, and the cover, date, and title show up." - }, - %{ - id: "soonex-inside-skins", - open: false, - kicker: "Skins", - title: "Custom CSS you can delete", - body: - "Each theme lives in assets/css/skins. Host CSS does not copy Corex recipes or set overlay opacity." - }, - %{ - id: "soonex-inside-access", - open: false, - kicker: "Access", - title: "Preferences stay on device", - body: - "Zoom, contrast, motion, cursor, focus, and link underline persist in local storage. Reset returns the defaults." - } - ] - end -end diff --git a/lib/pages/home/stats.ex b/lib/pages/home/stats.ex deleted file mode 100644 index f291857..0000000 --- a/lib/pages/home/stats.ex +++ /dev/null @@ -1,48 +0,0 @@ -defmodule Soonex.HomePage.Stats do - @moduledoc false - - use Phoenix.Component - - import Soonex.Layouts.Section, only: [block: 1] - - alias Soonex.Layouts.Shell - - def stats(assigns) do - ~H""" - <.block - id="stats" - section="stats" - labelledby="soonex-stats-heading" - eyebrow="Numbers" - tone={:surface} - align={:center} - compact - > - <:title> - Small surface. Finished chrome. - - <:lede> - Counts you can stand behind when you fork the template. Replace them when you have real ones. - -
        -
        -
        {caption}
        -
        {stat}
        -
        -
        - - """ - end - - defp stat_items do - [ - {"4", "Theme skins"}, - {"5", "Waitlist fields"}, - {Soonex.Launch.label(), "Launch day"}, - {"0", "npm packages"} - ] - end -end diff --git a/lib/pages/home/steps.ex b/lib/pages/home/steps.ex deleted file mode 100644 index 597f185..0000000 --- a/lib/pages/home/steps.ex +++ /dev/null @@ -1,80 +0,0 @@ -defmodule Soonex.HomePage.Steps do - @moduledoc false - - use Phoenix.Component - use Corex - - import Soonex.Layouts.Section, only: [block: 1] - - def steps(assigns) do - ~H""" - <.block - id="steps" - section="steps" - labelledby="soonex-steps-heading" - eyebrow="How it works" - tone={:root} - > - <:title> - Up and running in three commands. - - <:lede> - No npm project. Mix, Tailwind, and esbuild are already in the repo. Clone, set up, and serve. - -
          -
        1. -

          - {step.index} -

          -

          {step.title}

          -

          {step.body}

          - <.clipboard - id={step.clip_id} - class="clipboard ui-brand ui-solid ui-width-fit ui-size-sm mt-6 self-start" - value={step.command} - > - <:label class="sr-only">{step.title} - <:copy> - <.heroicon name="hero-clipboard" /> - Copy - - <:copied> - <.heroicon name="hero-check" /> - Copied - - -
        2. -
        - - """ - end - - defp step_items do - [ - %{ - index: "01", - clip_id: "soonex-step-clone", - title: "Clone the template", - body: - "Grab the repo. Photography, copy, and the launch date are the first things you replace.", - command: "git clone https://github.com/corex-ui/soonex.git" - }, - %{ - index: "02", - clip_id: "soonex-step-setup", - title: "mix setup", - body: - "Fetches Hex deps and builds Corex design CSS from config. Fonts are already self-hosted.", - command: "mix setup" - }, - %{ - index: "03", - clip_id: "soonex-step-server", - title: "mix server", - body: - "Serves the site on localhost:4999. Template Options in the corner switches theme and mode.", - command: "mix server" - } - ] - end -end diff --git a/lib/pages/home/trust.ex b/lib/pages/home/trust.ex new file mode 100644 index 0000000..22a3fd8 --- /dev/null +++ b/lib/pages/home/trust.ex @@ -0,0 +1,36 @@ +defmodule Soonex.HomePage.Trust do + @moduledoc false + + use Phoenix.Component + use Corex + + alias Soonex.Layouts.Shell + + def trust(assigns) do + assigns = assign(assigns, :items, trust_items()) + + ~H""" +
        +
        +

        + Built with the stack you already ship +

        +
          +
        • + {item} +
        • +
        +
        +
        + """ + end + + defp trust_items do + ["Corex", "Phoenix LiveView", "Tableau", "Tailwind CSS", "Elixir", "Hex"] + end +end diff --git a/lib/pages/home/voices.ex b/lib/pages/home/voices.ex deleted file mode 100644 index 308be14..0000000 --- a/lib/pages/home/voices.ex +++ /dev/null @@ -1,77 +0,0 @@ -defmodule Soonex.HomePage.Voices do - @moduledoc false - - use Phoenix.Component - - import Soonex.Layouts.Media, only: [photo: 1] - import Soonex.Layouts.Section, only: [block: 1] - - alias Soonex.Layouts.Shell - - def voices(assigns) do - ~H""" - <.block - id="voices" - section="voices" - labelledby="soonex-voices-heading" - eyebrow="Voices" - tone={:root} - > - <:title> - Real workflows, not a component tour. - - <:lede> - These quotes are placeholders. Keep the grid. Skins restyle the cards; the markup stays. - -
          -
        • -
          -

          - “{voice.quote}” -

          -
          - <.photo - src={voice.src} - alt={voice.name} - width={72} - height={72} - class="soonex-avatar soonex-avatar-lg" - /> -
          - {voice.name} - {voice.role} -
          -
          -
          -
        • -
        - - """ - end - - defp voice_items do - [ - %{ - src: "/images/people/amira.jpg", - quote: - "We forked Soonex on a Thursday and had a waitlist on a custom domain before the weekend. The skins were the part we did not have to argue about.", - name: "Amira N.", - role: "Founder, studio launch" - }, - %{ - src: "/images/people/jonas.jpg", - quote: - "I changed the launch date in one module, ran mix corex.design.build after a seed tweak, and the contrast still held. That is the whole job.", - name: "Jonas K.", - role: "Staff engineer" - }, - %{ - src: "/images/people/leah.jpg", - quote: - "Leo does not look like a recolor of neo. That is the bar. If a theme switch is invisible, the template failed.", - name: "Leah M.", - role: "Product designer" - } - ] - end -end diff --git a/lib/pages/home/waitlist.ex b/lib/pages/home/waitlist.ex index f42829a..db577e7 100644 --- a/lib/pages/home/waitlist.ex +++ b/lib/pages/home/waitlist.ex @@ -17,22 +17,21 @@ defmodule Soonex.HomePage.Waitlist do section="waitlist" labelledby="soonex-waitlist-heading" eyebrow="Waitlist" - tone={:surface} + tone={:root} align={:center} - reveal={false} > <:title> - Be on the list when {Soonex.Launch.label()} lands. + Get notified when Corex templates expand. <:lede> - This demo does not store addresses. Submit still shows the success toast so you can see the - path before you wire a real list. + Demo form — no addresses are stored. Submit still fires the success toast so you can validate + the flow before connecting your provider.
        <.native_input @@ -41,10 +40,10 @@ defmodule Soonex.HomePage.Waitlist do id="soonex-waitlist-email" required autocomplete="email" - placeholder="you@studio.dev" + placeholder="you@company.com" class="native-input ui-size-md ui-width-full" > - <:label class="sr-only">Email + <:label>Email <.select @@ -71,41 +70,6 @@ defmodule Soonex.HomePage.Waitlist do - <.tags_input - id="soonex-waitlist-interests" - name="waitlist[interests]" - class="tags-input ui-brand ui-width-full" - value={[]} - translation={ - %Corex.TagsInput.Translation{ - placeholder: "Add an interest and press Enter" - } - } - > - <:label>Interests - <:close> - <.heroicon name="hero-x-mark" /> - - - - <.number_input - id="soonex-waitlist-team" - name="waitlist[team_size]" - class="number-input ui-brand ui-width-full" - min={1.0} - max={200.0} - step={1.0} - value="2" - > - <:label>Team size - <:decrement_trigger> - <.heroicon name="hero-chevron-down" class="icon" /> - - <:increment_trigger> - <.heroicon name="hero-chevron-up" class="icon" /> - - - <.switch id="soonex-waitlist-notes" name="waitlist[notes]" diff --git a/lib/pages/home_page.ex b/lib/pages/home_page.ex index 48591a9..2810d4b 100644 --- a/lib/pages/home_page.ex +++ b/lib/pages/home_page.ex @@ -5,14 +5,10 @@ defmodule Soonex.HomePage do use Corex import Soonex.HomePage.Hero, only: [hero: 1] - import Soonex.HomePage.Logos, only: [logos: 1] + import Soonex.HomePage.Trust, only: [trust: 1] import Soonex.HomePage.Features, only: [features: 1] - import Soonex.HomePage.Product, only: [product: 1] - import Soonex.HomePage.Steps, only: [steps: 1] + import Soonex.HomePage.HowItWorks, only: [how_it_works: 1] import Soonex.HomePage.Pricing, only: [pricing: 1] - import Soonex.HomePage.LaunchBand, only: [launch_band: 1] - import Soonex.HomePage.Voices, only: [voices: 1] - import Soonex.HomePage.Stats, only: [stats: 1] import Soonex.HomePage.Journal, only: [journal: 1] import Soonex.HomePage.Faq, only: [faq: 1] import Soonex.HomePage.Waitlist, only: [waitlist: 1] @@ -25,14 +21,10 @@ defmodule Soonex.HomePage do ~H"""
        <.hero /> - <.logos /> + <.trust /> <.features /> - <.product /> - <.steps /> + <.how_it_works /> <.pricing /> - <.launch_band /> - <.voices /> - <.stats /> <.journal posts={@posts} /> <.faq /> <.waitlist /> diff --git a/lib/pages/privacy_page.ex b/lib/pages/privacy_page.ex index 24babe9..4c277d7 100644 --- a/lib/pages/privacy_page.ex +++ b/lib/pages/privacy_page.ex @@ -59,7 +59,7 @@ defmodule Soonex.PrivacyPage do
        • Theme and color mode - — which Corex theme (neo, uno, duo, leo) and light or dark mode you selected. + — light or dark color mode you selected.
        • Accessibility settings diff --git a/lib/soonex/palette/config.ex b/lib/soonex/palette/config.ex index 50a4d21..e41199f 100644 --- a/lib/soonex/palette/config.ex +++ b/lib/soonex/palette/config.ex @@ -1,7 +1,7 @@ defmodule Soonex.Palette.Config do @moduledoc false - @theme_order ~w(neo uno duo leo) + @theme_order ~w(neo) def defaults do nl = neo_light() @@ -23,18 +23,7 @@ defmodule Soonex.Palette.Config do "state_order" => ["muted", "default", "hover", "active"], "themes" => %{ "neo-light" => nl, - "neo-dark" => nd, - "uno-light" => - nl |> merge_overrides(uno_light_overrides()) |> Map.put("seeds", uno_seeds()), - "uno-dark" => - nd |> merge_overrides(uno_dark_overrides()) |> Map.put("seeds", uno_seeds()), - "duo-light" => - nl |> merge_overrides(duo_light_overrides()) |> Map.put("seeds", duo_seeds()), - "duo-dark" => - nd |> merge_overrides(duo_dark_overrides()) |> Map.put("seeds", duo_seeds()), - "leo-light" => - nl |> merge_overrides(leo_light_overrides()) |> Map.put("seeds", leo_seeds()), - "leo-dark" => nd |> merge_overrides(leo_dark_overrides()) |> Map.put("seeds", leo_seeds()) + "neo-dark" => nd }, "ui_ratio_base" => %{"default" => -1.12, "hover" => -1.08, "muted" => -1.2} } @@ -42,48 +31,15 @@ defmodule Soonex.Palette.Config do defp neo_seeds do %{ - "accent" => "#4B4B4B", - "alert" => "#A43C3C", - "base" => "#F0F0F0", - "brand" => "#32479C", - "info" => "#1F77D4", - "success" => "#059669" - } - end - - defp uno_seeds do - %{ - "accent" => "#475569", - "alert" => "#B91C1C", - "base" => "#EEF2F7", - "brand" => "#0E7490", - "info" => "#0369A1", - "success" => "#047857" - } - end - - defp duo_seeds do - %{ - "accent" => "#57534E", - "alert" => "#9F1239", - "base" => "#FAF7F2", - "brand" => "#5B21B6", - "info" => "#1D4ED8", + "accent" => "#161618", + "alert" => "#B42318", + "base" => "#EEEAE4", + "brand" => "#1D4ED8", + "info" => "#0E7490", "success" => "#15803D" } end - defp leo_seeds do - %{ - "accent" => "#3F3F46", - "alert" => "#991B1B", - "base" => "#F4F4F5", - "brand" => "#B45309", - "info" => "#1E40AF", - "success" => "#166534" - } - end - defp neo_light do %{ "seeds" => neo_seeds(), @@ -200,134 +156,6 @@ defmodule Soonex.Palette.Config do } end - defp uno_light_overrides do - %{ - "output" => "tokens/themes/uno/color/light.json", - "surface" => %{ - "layer" => %{"lightness" => 97}, - "root" => %{"lightness" => 100}, - "ui" => %{"lightness" => 94} - }, - "utility" => %{ - "border" => %{"ratio" => 1.32}, - "shadow" => %{"ratio" => 1.1} - }, - "ink" => %{"default" => %{"ratio" => 8.5}}, - "semantic" => %{ - "accent" => %{ - "bg" => "base", - "lightness" => 89, - "ink" => %{"color" => "accent", "ratio" => 5.5} - }, - "alert" => %{ - "bg" => "base", - "lightness" => 89, - "ink" => %{"color" => "alert", "ratio" => 5.5} - }, - "brand" => %{ - "bg" => "base", - "lightness" => 95, - "ink" => %{"color" => "brand", "ratio" => 5.5} - }, - "info" => %{ - "bg" => "base", - "lightness" => 89, - "ink" => %{"color" => "info", "ratio" => 5.5} - }, - "selected" => %{ - "bg" => "base", - "lightness" => 26, - "ink" => %{"color" => "accent", "ratio" => 10} - }, - "success" => %{ - "bg" => "base", - "lightness" => 89, - "ink" => %{"color" => "success", "ratio" => 5.5} - } - } - } - end - - defp uno_dark_overrides do - %{ - "output" => "tokens/themes/uno/color/dark.json", - "surface" => %{ - "layer" => %{"lightness" => 14}, - "root" => %{"lightness" => 7}, - "ui" => %{"lightness" => 19} - }, - "utility" => %{ - "border" => %{"ratio" => 1.42}, - "shadow" => %{"ratio" => 1.22} - }, - "ink" => %{"default" => %{"ratio" => 12.25}} - } - end - - defp duo_light_overrides do - %{ - "output" => "tokens/themes/duo/color/light.json", - "surface" => %{ - "layer" => %{"lightness" => 97}, - "root" => %{"lightness" => 99}, - "ui" => %{"lightness" => 92} - }, - "utility" => %{ - "border" => %{"ratio" => 1.34}, - "shadow" => %{"ratio" => 1.07} - }, - "ink" => %{"default" => %{"ratio" => 8.25}} - } - end - - defp duo_dark_overrides do - %{ - "output" => "tokens/themes/duo/color/dark.json", - "surface" => %{ - "layer" => %{"lightness" => 16}, - "root" => %{"lightness" => 10}, - "ui" => %{"lightness" => 21} - }, - "utility" => %{ - "border" => %{"ratio" => 1.43}, - "shadow" => %{"ratio" => 1.23} - }, - "ink" => %{"default" => %{"ratio" => 12.1}} - } - end - - defp leo_light_overrides do - %{ - "output" => "tokens/themes/leo/color/light.json", - "surface" => %{ - "layer" => %{"lightness" => 96}, - "root" => %{"lightness" => 98}, - "ui" => %{"lightness" => 92} - }, - "utility" => %{ - "border" => %{"ratio" => 1.34}, - "shadow" => %{"ratio" => 1.07} - }, - "ink" => %{"default" => %{"ratio" => 8.25}} - } - end - - defp leo_dark_overrides do - %{ - "output" => "tokens/themes/leo/color/dark.json", - "surface" => %{ - "layer" => %{"lightness" => 16}, - "root" => %{"lightness" => 9}, - "ui" => %{"lightness" => 21} - }, - "utility" => %{ - "border" => %{"ratio" => 1.43}, - "shadow" => %{"ratio" => 1.23} - }, - "ink" => %{"default" => %{"ratio" => 12.1}} - } - end - def merge_overrides(base, overrides), do: deep_merge(base, overrides) defp deep_merge(a, b) do diff --git a/lib/soonex/theme.ex b/lib/soonex/theme.ex index 0107791..692142c 100644 --- a/lib/soonex/theme.ex +++ b/lib/soonex/theme.ex @@ -1,7 +1,7 @@ defmodule Soonex.Theme do @moduledoc false - @themes ~w(neo uno duo leo) + @themes ~w(neo) @default_theme "neo" def themes, do: @themes @@ -9,38 +9,18 @@ defmodule Soonex.Theme do def default_theme, do: @default_theme def head_script do - themes_json = Jason.encode!(themes()) - default_theme_json = Jason.encode!(default_theme()) + script = + "" - Phoenix.HTML.raw(""" - - """) + Phoenix.HTML.raw(script) end def current(assigns) do - list = themes() - d = default_theme() - case Map.get(assigns, :theme) do - t when is_binary(t) -> - if(t in list, do: t, else: d) - - _ -> - d + t when t in @themes -> t + _ -> @default_theme end end - def select_items do - themes() - |> Enum.map(fn t -> %{value: t, label: String.capitalize(t)} end) - |> Corex.List.new() - end + def select_items, do: Corex.List.new([]) end diff --git a/mix.exs b/mix.exs index 12ca8ac..600d014 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Soonex.MixProject do def project do [ app: :soonex, - version: "0.2.2", + version: "0.3.0", elixir: "~> 1.17", start_permanent: Mix.env() == :prod, elixirc_paths: elixirc_paths(Mix.env()), From 6975d9e2dd4661675f0457651736a7ddb02c2705 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 03:46:45 +0000 Subject: [PATCH 61/65] Align Soonex home with new-gen visual rhythm Adapt the premium SaaS reference pack: warm ivory tokens, charcoal inverse bands, left-aligned hero with asymmetric CTA, Corex UI showcase panels, three-column capabilities and scale sections with dividers, split spotlight band, and a closing waitlist CTA band before the form. Typography shifts to medium-weight display type at ~48-56px hero scale. Primary actions use ui-accent solid buttons. Section/nav/footer links updated for the new information architecture. Co-authored-by: Karim Semmoud --- CHANGELOG.md | 2 +- assets/css/chrome.css | 67 +++++++++++++++-- config/config.exs | 20 ++--- lib/layouts/brand.ex | 2 +- lib/layouts/root/footer.ex | 58 +++++++------- lib/layouts/root/nav.ex | 25 ++++--- lib/layouts/section.ex | 59 +++++++++++---- lib/layouts/shell.ex | 38 +++++++--- lib/pages/home/faq.ex | 12 +-- lib/pages/home/features.ex | 56 +++++--------- lib/pages/home/hero.ex | 48 ++++++------ lib/pages/home/how_it_works.ex | 22 +++--- lib/pages/home/journal.ex | 8 +- lib/pages/home/pricing.ex | 107 ++++++++++---------------- lib/pages/home/showcase.ex | 73 ++++++++++++++++++ lib/pages/home/waitlist.ex | 133 ++++++++++++++++----------------- lib/pages/home_page.ex | 4 +- lib/soonex/palette/config.ex | 6 +- 18 files changed, 437 insertions(+), 303 deletions(-) create mode 100644 lib/pages/home/showcase.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index aa8ef2b..37eca3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.3.0 -- Full home redesign: premium SaaS layout (hero, trust badges, features, how-it-works tabs, pricing, journal, FAQ, waitlist) with Corex-focused copy and generous whitespace. +- Full home redesign inspired by premium SaaS landing rhythm: warm ivory canvas, charcoal feature bands, left-aligned hero, Corex UI showcase panels, scale section with closing CTA band, and compact waitlist footer block. - Simplify customization to Corex Design only: single `neo` theme with light/dark modes; remove per-theme skin CSS/JS, Framer photography, frosted scroll chrome, marquee, and carousel. - Drop `assets/css/layout.css`, `assets/css/skins/*`, `assets/js/skins/*`, `assets/js/landing*.js`, and `assets/js/theme.js`. - Demo controls: light/dark mode toggle and accessibility panel only (no theme switcher). diff --git a/assets/css/chrome.css b/assets/css/chrome.css index edaec79..f9c3ed4 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -8,14 +8,69 @@ html[data-motion="reduce"] { scroll-behavior: auto; } -.soonex-plan[data-featured="true"] { - background: var(--color-brand); - color: var(--color-brand-contrast); - border-color: var(--color-brand); +html[data-motion="reduce"] .soonex-showcase-grid > * { + translate: none; } -.soonex-plan[data-featured="true"] :is(.soonex-eyebrow, .display, p, li) { - color: inherit; +.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-band-inverse.soonex-columns > * + * { + border-inline-start-color: color-mix(in oklab, var(--color-accent-contrast) 18%, var(--color-accent)); + } +} + +.soonex-showcase-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; +} + +@media (min-width: 1024px) { + .soonex-showcase-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1.25rem; + } + + .soonex-showcase-grid > :nth-child(2) { + translate: 0 -1.25rem; + } + + .soonex-showcase-grid > :nth-child(3) { + translate: 0 1.25rem; + } +} + +.soonex-closing-band { + border: 1px solid var(--color-border); + border-radius: var(--radius-2xl, 1rem); + background: var(--color-surface); } .tabs.tabs--wide { diff --git a/config/config.exs b/config/config.exs index 69ea711..d44c66d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -134,9 +134,9 @@ config :corex_design, themes: %{ neo: %{ seeds: %{ - neutral: "#EEEAE4", - accent: "#161618", - brand: "#1D4ED8", + neutral: "#FAF9F6", + accent: "#141414", + brand: "#141414", alert: "#B42318", info: "#0E7490", success: "#15803D" @@ -194,21 +194,21 @@ config :corex_design, typography: %{ "h1" => %{ font_family: {:font, :display}, - font_weight: {:weight, :bold}, - letter_spacing: {:tracking, :tighter} + font_weight: {:weight, :medium}, + letter_spacing: {:tracking, :tight} }, "h2" => %{ font_family: {:font, :display}, - font_weight: {:weight, :bold}, + font_weight: {:weight, :medium}, letter_spacing: {:tracking, :tight} }, - "h3" => %{font_family: {:font, :display}, font_weight: {:weight, :semibold}}, - "h4" => %{font_family: {:font, :sans}, font_weight: {:weight, :semibold}}, + "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, :bold}, - letter_spacing: {:tracking, :tighter} + font_weight: {:weight, :medium}, + letter_spacing: {:tracking, :tight} }, "blockquote" => %{font_family: {:font, :serif}, font_style: :italic}, "code" => %{font_family: {:font, :mono}}, diff --git a/lib/layouts/brand.ex b/lib/layouts/brand.ex index c611f18..0823689 100644 --- a/lib/layouts/brand.ex +++ b/lib/layouts/brand.ex @@ -19,7 +19,7 @@ defmodule Soonex.Layouts.Brand do <.mark class="soonex-mark" /> - Soonex + Soonex """ end diff --git a/lib/layouts/root/footer.ex b/lib/layouts/root/footer.ex index 3671553..de46ef0 100644 --- a/lib/layouts/root/footer.ex +++ b/lib/layouts/root/footer.ex @@ -21,53 +21,46 @@ defmodule Soonex.Layouts.Root.Footer do |> assign(:hexdocs, @hexdocs) ~H""" -
          +
          -
          -
          +
          +
          <.lockup /> -

          - Tableau + Corex coming-soon template with accessible Phoenix UI, a waitlist, and a journal. +

          + Tableau + Corex coming-soon template.

          +

          Accessible Phoenix UI

          -
          +
          -

          Product

          -
            +

            Product

            +
            • <.navigate - to={Soonex.Public.path("/") <> "#features"} + to={Soonex.Public.path("/") <> "#capabilities"} class="link ui-nav ui-size-sm" > - Features - -
            • -
            • - <.navigate to={Soonex.Public.path("/") <> "#pricing"} class="link ui-nav ui-size-sm"> - Plans + Capabilities
            • <.navigate - to={Soonex.Public.path("/") <> "#how-it-works"} + to={Soonex.Public.path("/") <> "#spotlight"} class="link ui-nav ui-size-sm" > How it works
            • - <.navigate - to={Soonex.Public.path("/") <> "#waitlist"} - class="link ui-nav ui-size-sm" - > - Waitlist + <.navigate to={Soonex.Public.path("/") <> "#scale"} class="link ui-nav ui-size-sm"> + Scale
          -

          Journal

          -
            +

            Journal

            +
            • <.navigate to={Soonex.Public.path("/blog")} class="link ui-nav ui-size-sm"> All posts @@ -86,8 +79,8 @@ defmodule Soonex.Layouts.Root.Footer do
          -

          Elsewhere

          -
            +

            Elsewhere

            +
            • <.navigate to={@github} class="link ui-nav ui-size-sm" external> GitHub @@ -98,6 +91,14 @@ defmodule Soonex.Layouts.Root.Footer do Hexdocs
            • +
            • + <.navigate + to={Soonex.Public.path("/") <> "#waitlist"} + class="link ui-nav ui-size-sm" + > + Waitlist + +
          @@ -119,11 +120,8 @@ defmodule Soonex.Layouts.Root.Footer do <.cookie_dialog />
          - <.navigate - to={Soonex.Public.path("/") <> "#waitlist"} - class="button ui-brand ui-solid ui-size-sm w-fit" - > - Join waitlist + <.navigate to="#main-content" class="link ui-nav ui-size-sm text-ink-muted"> + Back to top ↑
          diff --git a/lib/layouts/root/nav.ex b/lib/layouts/root/nav.ex index ec4afe1..9867ea6 100644 --- a/lib/layouts/root/nav.ex +++ b/lib/layouts/root/nav.ex @@ -15,8 +15,8 @@ defmodule Soonex.Layouts.Root.Nav do assigns = assign(assigns, :nav_select_items, nav_select_items()) ~H""" -
          -
          +
          +
          <.select id="soonex-mobile-nav" @@ -52,7 +52,7 @@ defmodule Soonex.Layouts.Root.Nav do - <.navigate to={Public.path("/") <> "#waitlist"} class="button ui-brand ui-solid ui-size-sm"> + <.navigate to={Public.path("/") <> "#waitlist"} class={Shell.primary_button()}> Join waitlist
          @@ -77,9 +77,9 @@ defmodule Soonex.Layouts.Root.Nav do defp desktop_links do [ - %{id: :product, label: "Product", to: Public.path("/") <> "#features"}, - %{id: :how, label: "How it works", to: Public.path("/") <> "#how-it-works"}, - %{id: :pricing, label: "Plans", to: Public.path("/") <> "#pricing"}, + %{id: :product, label: "Product", to: Public.path("/") <> "#capabilities"}, + %{id: :how, label: "How it works", to: Public.path("/") <> "#spotlight"}, + %{id: :scale, label: "Scale", to: Public.path("/") <> "#scale"}, %{id: :journal, label: "Journal", to: Public.path("/blog")}, %{id: :questions, label: "FAQ", to: Public.path("/") <> "#questions"} ] @@ -87,14 +87,19 @@ defmodule Soonex.Layouts.Root.Nav do defp nav_select_items do Corex.List.new([ - %{label: "Product", value: "features", to: Public.path("/") <> "#features", redirect: :href}, + %{ + label: "Product", + value: "capabilities", + to: Public.path("/") <> "#capabilities", + redirect: :href + }, %{ label: "How it works", - value: "how", - to: Public.path("/") <> "#how-it-works", + value: "spotlight", + to: Public.path("/") <> "#spotlight", redirect: :href }, - %{label: "Plans", value: "pricing", to: Public.path("/") <> "#pricing", redirect: :href}, + %{label: "Scale", value: "scale", to: Public.path("/") <> "#scale", redirect: :href}, %{label: "Journal", value: "blog", to: Public.path("/blog"), redirect: :href}, %{label: "FAQ", value: "questions", to: Public.path("/") <> "#questions", redirect: :href}, %{label: "Join waitlist", value: "waitlist", to: Public.path("/") <> "#waitlist", redirect: :href} diff --git a/lib/layouts/section.ex b/lib/layouts/section.ex index e01d111..a9f3ed4 100644 --- a/lib/layouts/section.ex +++ b/lib/layouts/section.ex @@ -7,12 +7,13 @@ defmodule Soonex.Layouts.Section do attr(:id, :string, required: true) attr(:labelledby, :string, required: true) - attr(:eyebrow, :string, required: true) + attr(:eyebrow, :string, default: nil) attr(:section, :string, default: nil) - attr(:tone, :atom, default: :root) + attr(:tone, :atom, default: :root, values: [:root, :surface, :inverse]) attr(:align, :atom, default: :start) - attr(:layout, :atom, default: :stack, values: [:stack, :sticky]) + attr(:layout, :atom, default: :stack, values: [:stack, :sticky, :split]) attr(:compact, :boolean, default: false) + attr(:heading_size, :atom, default: :default, values: [:default, :large]) slot(:title, required: true) slot(:lede) @@ -27,13 +28,13 @@ defmodule Soonex.Layouts.Section do class={"#{section_class(@compact)} #{tone_class(@tone)}"} aria-labelledby={@labelledby} > -
          -
          -

          {@eyebrow}

          -

          +
          +
          +

          {@eyebrow}

          +

          {render_slot(@title)}

          -

          +

          {render_slot(@lede)}

          -
          +
          +
          +

          {@eyebrow}

          +

          + {render_slot(@title)} +

          +
          +
          +

          + {render_slot(@lede)} +

          +
          + {render_slot(@actions)} +
          +
          +
          +
          {render_slot(@inner_block)}
          @@ -56,14 +73,27 @@ defmodule Soonex.Layouts.Section do defp tone_class(:root), do: "bg-root" defp tone_class(:surface), do: "border-y border-border bg-surface" + defp tone_class(:inverse), do: "soonex-band-inverse bg-accent text-accent-contrast" - defp stage_class(:sticky), do: "#{Shell.stage()} #{Shell.sticky_grid()}" - defp stage_class(:stack), do: Shell.stage() + defp stage_class(:split, _tone), do: Shell.stage() + defp stage_class(:sticky, _tone), do: "#{Shell.stage()} #{Shell.sticky_grid()}" + defp stage_class(:stack, _tone), do: Shell.stage() defp intro_class(:sticky, _align), do: Shell.sticky_intro() defp intro_class(:stack, :center), do: Shell.intro_center() defp intro_class(:stack, :start), do: Shell.intro() + defp eyebrow_class(:inverse), do: "#{Shell.eyebrow()} soonex-on-inverse" + defp eyebrow_class(_tone), do: Shell.eyebrow() + + defp heading_class(:inverse, :large), do: "#{Shell.section_heading_lg()} soonex-on-inverse" + defp heading_class(:inverse, _size), do: "#{Shell.section_heading()} soonex-on-inverse" + defp heading_class(_tone, :large), do: Shell.section_heading_lg() + defp heading_class(_tone, _size), do: Shell.section_heading() + + defp lede_class(:inverse), do: "#{Shell.lede()} soonex-muted-on-inverse" + defp lede_class(_tone), do: Shell.lede() + defp lede_align(:sticky, _align), do: "" defp lede_align(:stack, :center), do: "mx-auto" defp lede_align(:stack, :start), do: "" @@ -72,7 +102,8 @@ defmodule Soonex.Layouts.Section do defp actions_align(:stack, :center), do: "justify-center" defp actions_align(:stack, :start), do: "" - defp body_class(:sticky, _compact), do: Shell.sticky_body() - defp body_class(:stack, true), do: Shell.body_tight() - defp body_class(:stack, false), do: Shell.body() + defp body_class(:split, _compact, _tone), do: Shell.body_tight() + defp body_class(:sticky, _compact, _tone), do: Shell.sticky_body() + defp body_class(:stack, true, _tone), do: Shell.body_tight() + defp body_class(:stack, false, _tone), do: Shell.body() end diff --git a/lib/layouts/shell.ex b/lib/layouts/shell.ex index f4c8730..1dacfc9 100644 --- a/lib/layouts/shell.ex +++ b/lib/layouts/shell.ex @@ -1,39 +1,51 @@ defmodule Soonex.Layouts.Shell do @moduledoc false - def section, do: "scroll-mt-24 w-full py-24 sm:py-32" + def section, do: "scroll-mt-24 w-full py-28 sm:py-36" def section_compact, do: "scroll-mt-24 w-full py-16 sm:py-20" def section_hero, - do: "relative flex min-h-[80svh] w-full flex-col justify-center py-20 sm:py-28" + do: "relative flex min-h-[88svh] w-full flex-col justify-end pb-16 pt-28 sm:pb-20 sm:pt-32" def stage, do: "mx-auto w-full max-w-6xl px-6 lg:px-8" - def intro, do: "mx-auto max-w-2xl lg:mx-0" + def stage_wide, do: "mx-auto w-full max-w-7xl px-6 lg:px-8" + + def intro, do: "max-w-2xl lg:max-w-3xl" def intro_center, do: "mx-auto max-w-2xl text-center" - def body, do: "mx-auto mt-16 w-full sm:mt-20 lg:mt-24" + def body, do: "mt-16 w-full sm:mt-20 lg:mt-24" - def body_tight, do: "mx-auto mt-10 w-full sm:mt-12" + def body_tight, do: "mt-10 w-full sm:mt-12" - def sticky_grid, do: "grid grid-cols-1 items-start gap-16 lg:grid-cols-12 lg:gap-12" + def sticky_grid, do: "grid grid-cols-1 items-start gap-16 lg:grid-cols-12 lg:gap-16" - def sticky_intro, do: "mx-auto max-w-2xl lg:sticky lg:top-28 lg:col-span-5 lg:mx-0" + def sticky_intro, do: "max-w-xl lg:sticky lg:top-28 lg:col-span-5" def sticky_body, do: "w-full min-w-0 lg:col-span-7" def eyebrow, - do: "soonex-eyebrow m-0 text-sm/6 font-semibold tracking-wide text-brand-text uppercase" + do: "soonex-eyebrow m-0 text-xs/6 font-medium tracking-wide text-ink-muted uppercase" + + def hero_heading, + do: + "display max-w-3xl text-pretty text-5xl font-medium tracking-tight text-ink lg:text-[3.5rem] lg:leading-[1.05]" def section_heading, - do: "display mt-3 text-pretty text-4xl font-semibold tracking-tight text-ink sm:text-5xl" + do: "display mt-2 text-pretty text-2xl font-medium tracking-tight text-ink sm:text-3xl" + + def section_heading_lg, + do: + "display text-pretty text-3xl font-medium tracking-tight text-ink sm:text-4xl lg:max-w-2xl lg:text-[2.75rem] lg:leading-tight" def page_heading, - do: "display mt-3 text-pretty text-4xl font-semibold tracking-tight text-ink sm:text-5xl" + do: "display mt-2 text-pretty text-2xl font-medium tracking-tight text-ink sm:text-3xl" - def lede, do: "mt-5 max-w-2xl text-pretty text-lg/8 text-ink-muted" + def lede, do: "mt-4 max-w-xl text-pretty text-sm/6 text-ink-muted sm:text-base/7" + + def lede_wide, do: "mt-4 max-w-2xl text-pretty text-sm/6 text-ink-muted sm:text-base/7" def panel, do: "relative overflow-hidden rounded-2xl border border-border bg-surface p-6 sm:p-8" @@ -43,4 +55,8 @@ defmodule Soonex.Layouts.Shell do def frame, do: "relative overflow-hidden rounded-2xl border border-border bg-surface" def data_list, do: "data-list ui-accent ui-size-md w-full max-w-none" + + def primary_button, do: "button ui-accent ui-solid ui-size-sm" + + def column_grid, do: "soonex-columns grid grid-cols-1 gap-12 lg:grid-cols-3 lg:gap-0" end diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index 8717c74..0af2f7c 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -14,7 +14,7 @@ defmodule Soonex.HomePage.Faq do labelledby="soonex-faq-heading" eyebrow="Questions" layout={:sticky} - tone={:surface} + tone={:root} > <:title> Before you fork Soonex. @@ -24,7 +24,7 @@ defmodule Soonex.HomePage.Faq do framework to maintain. <:actions> - <.navigate to="#waitlist" class="link ui-brand"> + <.navigate to="#waitlist" class="link ui-accent"> Join waitlist <.heroicon name="hero-arrow-down" /> @@ -41,17 +41,17 @@ defmodule Soonex.HomePage.Faq do <:content :let={item}>
          -

          {item.content}

          - <.navigate :if={item.value == "customize"} to="#how-it-works" class="link ui-brand w-fit"> +

          {item.content}

          + <.navigate :if={item.value == "customize"} to="#spotlight" class="link ui-accent w-fit"> See how it works <.heroicon name="hero-arrow-down" /> - <.navigate :if={item.value == "waitlist"} to="#waitlist" class="link ui-brand w-fit"> + <.navigate :if={item.value == "waitlist"} to="#waitlist" class="link ui-accent w-fit"> Open the waitlist <.heroicon name="hero-arrow-down" /> <.clipboard :if={item.value == "toolchain"} id="soonex-faq-setup" - class="clipboard ui-brand ui-size-sm ui-width-fit self-start" + class="clipboard ui-accent ui-size-sm ui-width-fit self-start" value="mix setup" > <:label class="sr-only">mix setup diff --git a/lib/pages/home/features.ex b/lib/pages/home/features.ex index 9539c1f..f69c11a 100644 --- a/lib/pages/home/features.ex +++ b/lib/pages/home/features.ex @@ -2,63 +2,45 @@ defmodule Soonex.HomePage.Features do @moduledoc false use Phoenix.Component - use Corex - - import Soonex.Layouts.Section, only: [block: 1] alias Soonex.Layouts.Shell def features(assigns) do ~H""" - <.block - id="features" - section="features" - labelledby="soonex-features-heading" - eyebrow="Product" - tone={:root} - align={:center} +
          - <:title> - Corex components with contrast you can trust. - - <:lede> - Tokens, modes, and accessibility are solved in config — not in one-off CSS. Customize seeds, - radius, and typography, then ship the same markup in light and dark. - -
            -
          • - - <.heroicon name={feature.icon} class="size-5" /> - -

            - {feature.title} -

            -

            {feature.body}

            -
          • -
          - +
          +
            +
          • +

            {feature.title}

            +

            {feature.body}

            +
          • +
          +
          +
          """ end defp claims do [ %{ - icon: "hero-swatch", - title: "Design tokens that stay readable", + title: "Tokens that stay readable", body: - "Light and dark palettes are generated from seeds in config :corex_design. Brand, accent, and ink keep WCAG-friendly contrast without hand-tuned hex stacks." + "Light and dark palettes regenerate from seeds in config :corex_design. Ink, surfaces, and CTAs keep WCAG-friendly contrast without hand-tuned hex stacks." }, %{ - icon: "hero-command-line", - title: "Phoenix UI without the app server", + title: "Static HTML, live components", body: - "Tableau renders static HTML. Corex hooks hydrate selects, tabs, accordions, and forms on the client — the same components you use in LiveView apps." + "Tableau renders the page. Corex hooks hydrate tabs, accordions, selects, and forms — the same primitives you use in LiveView apps." }, %{ - icon: "hero-heart", title: "Accessibility built in", body: - "The demo ships Corex accessibility controls: text size, contrast, motion, focus, and link underline — persisted locally with no backend." + "The demo ships Corex accessibility controls for text size, contrast, motion, focus, and link underline — persisted locally with no backend." } ] end diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index d1cb09e..ece891f 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -13,39 +13,41 @@ defmodule Soonex.HomePage.Hero do aria-labelledby="soonex-headline" data-section="hero" > -
          -

          - Launching {Soonex.Launch.year_label()} -

          +
          +
          +
          +

          + Launching {Soonex.Launch.year_label()} +

          -

          - Accessible Phoenix UI, ready for your launch page. -

          +

          + Accessible Phoenix UI for your coming-soon launch. +

          -

          - Soonex is a Tableau + Corex coming-soon template. Ship a waitlist, journal, and - production-grade components with tokens, light/dark modes, and built-in accessibility. -

          +

          + Soonex is a Tableau + Corex template. Ship a waitlist, journal, and production-grade + components with design tokens, light/dark modes, and built-in accessibility — no + parallel CSS framework. +

          +
          -
          - <.navigate to="#waitlist" class="button ui-brand ui-solid ui-size-lg"> - Join waitlist - - <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-lg"> - Read the journal - +
          + <.navigate to="#waitlist" class={Shell.primary_button()}> + Join waitlist + + <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-sm"> + Read journal + +
          -
          +
          <.timer id="soonex-hero-timer" countdown start_ms={Soonex.Launch.countdown_ms()} target_ms={0} - class="timer ui-brand ui-size-md" + class="timer ui-accent ui-size-sm" > <:day_label>days <:hour_label>hours diff --git a/lib/pages/home/how_it_works.ex b/lib/pages/home/how_it_works.ex index f7f9457..4a7cd83 100644 --- a/lib/pages/home/how_it_works.ex +++ b/lib/pages/home/how_it_works.ex @@ -9,29 +9,31 @@ defmodule Soonex.HomePage.HowItWorks do def how_it_works(assigns) do ~H""" <.block - id="how-it-works" - section="how-it-works" - labelledby="soonex-how-heading" - eyebrow="How it works" - tone={:surface} + id="spotlight" + section="spotlight" + labelledby="soonex-spotlight-heading" + tone={:inverse} + layout={:split} + heading_size={:large} > <:title> - From clone to launch in three commands. + Config-driven UI, shipped as static HTML. <:lede> - Mix handles dependencies, Corex design CSS, Tailwind, and esbuild. You edit config and - content — not a parallel design system. + Customize seeds, radius, and typography under config :corex_design, then edit HEEx content + modules. Mix handles Corex design CSS, Tailwind, and esbuild — you do not maintain a + parallel design system. <.tabs id="soonex-how-tabs" - class="tabs tabs--wide ui-brand ui-width-full" + class="tabs tabs--wide ui-accent ui-width-full" value="setup" items={tab_items()} />
          <.clipboard id="soonex-how-copy" - class="clipboard ui-brand ui-solid ui-width-fit ui-size-sm" + class="clipboard ui-accent ui-solid ui-width-fit ui-size-sm" value="mix setup" > <:label class="sr-only">mix setup diff --git a/lib/pages/home/journal.ex b/lib/pages/home/journal.ex index e3d03f9..24222ee 100644 --- a/lib/pages/home/journal.ex +++ b/lib/pages/home/journal.ex @@ -34,12 +34,12 @@ defmodule Soonex.HomePage.Journal do Markdown posts under _posts. The index paginates; the home page shows the latest three. <:actions> - <.navigate to={Soonex.Public.path("/blog")} class="link ui-brand"> + <.navigate to={Soonex.Public.path("/blog")} class="link ui-accent"> All posts <.heroicon name="hero-arrow-up-right" />
          -

          No posts yet.

          +

          No posts yet.

          {date_label(post)}

          -

          +

          {post[:title] || "Untitled"}

          @@ -56,7 +56,7 @@ defmodule Soonex.HomePage.Journal do

          <.navigate to={Soonex.Public.path(post.permalink)} - class="button ui-ghost ui-brand ui-size-sm mt-6 w-fit" + class="button ui-ghost ui-size-sm mt-6 w-fit" > Read more <.heroicon name="hero-arrow-up-right" /> diff --git a/lib/pages/home/pricing.ex b/lib/pages/home/pricing.ex index 4828cdd..7b2b028 100644 --- a/lib/pages/home/pricing.ex +++ b/lib/pages/home/pricing.ex @@ -4,90 +4,61 @@ defmodule Soonex.HomePage.Pricing do use Phoenix.Component use Corex - import Soonex.Layouts.Section, only: [block: 1] - alias Soonex.Layouts.Shell def pricing(assigns) do ~H""" - <.block - id="pricing" - section="pricing" - labelledby="soonex-pricing-heading" - eyebrow="Plans" - tone={:root} - align={:center} +
          - <:title> - Start free. Scale when you ship. - - <:lede> - Placeholder tiers you can rename for your product. The middle option is the default fork - path for teams who want the full template. - -
          -
          -

          {plan.name}

          -

          {plan.headline}

          -

          {plan.price}

          -

          {plan.body}

          -
            -
          • - <.heroicon name="hero-check" class="mt-0.5 size-4 shrink-0" /> - {item} -
          • -
          - <.navigate - to="#waitlist" - class={ - if plan.featured, - do: "button ui-size-md mt-8 w-full bg-root text-ink", - else: "button ui-brand ui-solid ui-size-md mt-8 w-full" - } - > - {plan.cta} +
          +

          + Fast to fork, easy to ship. +

          +

          + A coming-soon template you can rename, retheme, and deploy without rebuilding sections + from scratch. +

          + +
            +
          • +

            {item.title}

            +

            {item.body}

            +
          • +
          + +
          +

          + Want early access to Corex templates? +

          + <.navigate to="#waitlist" class={"#{Shell.primary_button()} shrink-0"}> + Yes, join waitlist -
          +
          - +

  • """ end - defp plan_class(%{featured: true}), do: Shell.frame() - defp plan_class(_plan), do: Shell.panel() - - defp plans do + defp scale_items do [ %{ - name: "Launch", - headline: "Fork and customize", - price: "Open", - featured: false, - cta: "Join waitlist", - body: "Waitlist landing, journal, and light/dark modes with Corex tokens.", - items: ["Waitlist + toast demo", "Journal + tags", "GitHub Pages workflow"] + title: "Instant activation", + body: + "Run mix setup and mix server to preview the full landing, journal, and waitlist flow locally." }, %{ - name: "Studio", - headline: "Full template", - price: "Open", - featured: true, - cta: "Start with Studio", - body: "Everything in Launch plus accessibility controls and richer form fields.", - items: ["Corex a11y dialog", "Tabs, accordion, selects", "mix project.rename task"] + title: "Adaptable approach", + body: + "Use Soonex as a launch page, a client handoff, or the starting point for a Corex marketing site." }, %{ - name: "Team", - headline: "Ship for clients", - price: "Open", - featured: false, - cta: "Join waitlist", - body: "Hand off a static site that already passes axe and documents customization in README.", - items: ["Self-hosted fonts", "Config-driven theming", "Tableau + Corex docs linked"] + title: "Effortless integration", + body: + "Wire the waitlist form to your provider, swap copy in lib/pages/home, and publish _site/ to any static host." } ] end diff --git a/lib/pages/home/showcase.ex b/lib/pages/home/showcase.ex new file mode 100644 index 0000000..51097f8 --- /dev/null +++ b/lib/pages/home/showcase.ex @@ -0,0 +1,73 @@ +defmodule Soonex.HomePage.Showcase do + @moduledoc false + + use Phoenix.Component + use Corex + + alias Soonex.Layouts.Shell + + def showcase(assigns) do + ~H""" +
    +
    +
    +
    +

    Actions

    +
    +
    + Primary + Secondary + Corex +
    +
    +
    + +
    +

    Forms

    +
    + <.native_input + type="email" + name="showcase[email]" + id="soonex-showcase-email" + placeholder="you@company.com" + class="native-input ui-size-sm ui-width-full" + readonly + > + <:label>Email + + <.switch id="soonex-showcase-switch" name="showcase[notes]" checked class="switch ui-accent"> + <:label>Launch notes + +
    +
    + +
    +

    Structure

    +
    + <.tabs + id="soonex-showcase-tabs" + class="tabs tabs--wide ui-accent ui-width-full" + value="tokens" + items={tab_items()} + /> +
    +
    +
    +
    +
    + """ + end + + defp tab_items do + Corex.Content.new([ + %{value: "tokens", label: "Tokens", content: "Seeds and modes from config :corex_design."}, + %{value: "hooks", label: "Hooks", content: "Tabs, accordion, and selects hydrate on the client."}, + %{value: "a11y", label: "A11y", content: "Contrast, motion, and focus controls ship in the demo."} + ]) + end +end diff --git a/lib/pages/home/waitlist.ex b/lib/pages/home/waitlist.ex index db577e7..da5f446 100644 --- a/lib/pages/home/waitlist.ex +++ b/lib/pages/home/waitlist.ex @@ -4,87 +4,86 @@ defmodule Soonex.HomePage.Waitlist do use Phoenix.Component use Corex - import Soonex.Layouts.Section, only: [block: 1] - alias Soonex.Layouts.Shell def waitlist(assigns) do assigns = assign(assigns, :role_items, role_items()) ~H""" - <.block +
    - <:title> - Get notified when Corex templates expand. - - <:lede> - Demo form — no addresses are stored. Submit still fires the success toast so you can validate - the flow before connecting your provider. - - -
    - <.native_input - type="email" - name="waitlist[email]" - id="soonex-waitlist-email" - required - autocomplete="email" - placeholder="you@company.com" - class="native-input ui-size-md ui-width-full" +
    +
    +

    + Join the waitlist +

    +

    + Demo form — no addresses are stored. Submit fires the success toast so you can validate + the flow before connecting your provider. +

    + + - <:label>Email - + <.native_input + type="email" + name="waitlist[email]" + id="soonex-waitlist-email" + required + autocomplete="email" + placeholder="you@company.com" + class="native-input ui-size-md ui-width-full" + > + <:label>Email + - <.select - id="soonex-waitlist-role" - name="waitlist[role]" - class="select ui-brand ui-size-md ui-width-full" - value={["founder"]} - items={@role_items} - positioning={ - %Corex.Positioning{ - strategy: "absolute", - placement: "bottom-start", - same_width: true, - gutter: 8, - slide: false, - fit_viewport: false, - flip: false + <.select + id="soonex-waitlist-role" + name="waitlist[role]" + class="select ui-accent ui-size-md ui-width-full" + value={["founder"]} + items={@role_items} + positioning={ + %Corex.Positioning{ + strategy: "absolute", + placement: "bottom-start", + same_width: true, + gutter: 8, + slide: false, + fit_viewport: false, + flip: false + } } - } - > - <:label>Role - <:trigger> - <.heroicon name="hero-chevron-down" /> - - + > + <:label>Role + <:trigger> + <.heroicon name="hero-chevron-down" /> + + - <.switch - id="soonex-waitlist-notes" - name="waitlist[notes]" - checked - class="switch ui-brand" - > - <:label>Email me launch notes - + <.switch + id="soonex-waitlist-notes" + name="waitlist[notes]" + checked + class="switch ui-accent" + > + <:label>Email me launch notes + - + +
    - - +
    +
    """ end diff --git a/lib/pages/home_page.ex b/lib/pages/home_page.ex index 2810d4b..e849d32 100644 --- a/lib/pages/home_page.ex +++ b/lib/pages/home_page.ex @@ -5,7 +5,7 @@ defmodule Soonex.HomePage do use Corex import Soonex.HomePage.Hero, only: [hero: 1] - import Soonex.HomePage.Trust, only: [trust: 1] + import Soonex.HomePage.Showcase, only: [showcase: 1] import Soonex.HomePage.Features, only: [features: 1] import Soonex.HomePage.HowItWorks, only: [how_it_works: 1] import Soonex.HomePage.Pricing, only: [pricing: 1] @@ -21,7 +21,7 @@ defmodule Soonex.HomePage do ~H"""
    <.hero /> - <.trust /> + <.showcase /> <.features /> <.how_it_works /> <.pricing /> diff --git a/lib/soonex/palette/config.ex b/lib/soonex/palette/config.ex index e41199f..cdd78e6 100644 --- a/lib/soonex/palette/config.ex +++ b/lib/soonex/palette/config.ex @@ -31,10 +31,10 @@ defmodule Soonex.Palette.Config do defp neo_seeds do %{ - "accent" => "#161618", + "accent" => "#141414", "alert" => "#B42318", - "base" => "#EEEAE4", - "brand" => "#1D4ED8", + "base" => "#FAF9F6", + "brand" => "#141414", "info" => "#0E7490", "success" => "#15803D" } From b77e00b36598c37916af230f1990b6e88601585c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 03:51:53 +0000 Subject: [PATCH 62/65] Restore Corex theme switcher and tech marquee; keep new-gen layout Bring back four Corex themes (neo/uno/duo/leo) in config :corex_design with Template Options theme select and theme.js persistence. Restore the tech stack Corex marquee below the hero with eager Marquee hook loading to fix the grey stuck-loading state. Keep the new-gen-inspired home rhythm: left-aligned hero, showcase panels, charcoal capability band, split spotlight, scale section with closing CTA, and compact waitlist. No Framer skin CSS or scroll chrome. Co-authored-by: Karim Semmoud --- CHANGELOG.md | 3 +- README.md | 4 +- assets/css/chrome.css | 16 +++ assets/css/fonts.css | 90 +++++++++++++- assets/css/hosts.css | 8 ++ assets/js/site.js | 3 + assets/js/theme.js | 33 +++++ config/config.exs | 235 +++++++++++++++++++++++++++++++++++ lib/layouts/root/demo.ex | 72 ++++++++--- lib/pages/home/logos.ex | 54 ++++++++ lib/pages/home_page.ex | 2 + lib/pages/privacy_page.ex | 2 +- lib/soonex/palette/config.ex | 178 +++++++++++++++++++++++++- lib/soonex/theme.ex | 34 +++-- 14 files changed, 695 insertions(+), 39 deletions(-) create mode 100644 assets/js/theme.js create mode 100644 lib/pages/home/logos.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index 37eca3a..4732cb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ## 0.3.0 - Full home redesign inspired by premium SaaS landing rhythm: warm ivory canvas, charcoal feature bands, left-aligned hero, Corex UI showcase panels, scale section with closing CTA band, and compact waitlist footer block. -- Simplify customization to Corex Design only: single `neo` theme with light/dark modes; remove per-theme skin CSS/JS, Framer photography, frosted scroll chrome, marquee, and carousel. +- Simplify customization to Corex Design only: four stock themes (neo/uno/duo/leo) with light/dark modes via Template Options; no per-theme skin CSS/JS. Restore Corex tech marquee with eager hook loading. +- Drop Framer photography, frosted scroll chrome, and carousel; keep marquee as a Corex component demo. - Drop `assets/css/layout.css`, `assets/css/skins/*`, `assets/js/skins/*`, `assets/js/landing*.js`, and `assets/js/theme.js`. - Demo controls: light/dark mode toggle and accessibility panel only (no theme switcher). - Update README, privacy copy, and blog index lede for the new direction. diff --git a/README.md b/README.md index 6801316..aca3ebc 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Rebuild assets: `mix assets.build`. - **Brand / SEO:** lockup in [`lib/layouts/brand.ex`](lib/layouts/brand.ex) and [`extra/images/logo.svg`](extra/images/logo.svg); titles in [`lib/layouts/root_layout.ex`](lib/layouts/root_layout.ex). - **Launch date:** [`lib/soonex/launch.ex`](lib/soonex/launch.ex). Hero badge and countdown timer both read it. -- **Theme:** overlay allowed keys in [`config/config.exs`](config/config.exs) under `config :corex_design` (`seeds`, `colors.light` / `colors.dark`, `dimensions.radius`, `dimensions.font`, `typography`, top-level `scales:`). Contrast stays calculated; `*_scale` / duration / opacity keys are rejected. Then `mix corex.design.build`. Light/dark mode toggles via [`lib/soonex/mode.ex`](lib/soonex/mode.ex). +- **Theme:** overlay allowed keys in [`config/config.exs`](config/config.exs) under `config :corex_design` for each theme (`neo`, `uno`, `duo`, `leo`): `seeds`, `colors.light` / `colors.dark`, `dimensions.radius`, `dimensions.font`, `typography`, top-level `scales:`. Then `mix corex.design.build`. Switch themes and light/dark in **Template Options** (demo FAB) — no skin CSS required. - **Accessibility:** Corex `--a11y` dialog in the demo FAB ([`lib/soonex/accessibility.ex`](lib/soonex/accessibility.ex)). Preferences live in `localStorage` (`phx:a11y`); run `mix corex.design.build` after changing accessibility config. - **Fonts:** self-hosted woff2 in [`extra/fonts/`](extra/fonts/), faces in [`assets/css/fonts.css`](assets/css/fonts.css). Default stack: Outfit (display) + Manrope (sans) + JetBrains Mono. - **Chrome:** sticky header in [`lib/layouts/root/nav.ex`](lib/layouts/root/nav.ex); minimal host polish in [`assets/css/hosts.css`](assets/css/hosts.css) and [`assets/css/chrome.css`](assets/css/chrome.css). Prefer Corex `ui-*` modifiers over custom CSS. @@ -60,7 +60,7 @@ MDX-style Tableau extras (tags, `include_dir`, sitemap) are summarized in **Tabl - `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/mode.js`](assets/js/mode.js) for light/dark mode, a11y head script, waitlist toast in [`assets/js/waitlist.js`](assets/js/waitlist.js). +- Client UI: [`assets/js/theme.js`](assets/js/theme.js) and [`assets/js/mode.js`](assets/js/mode.js) for theme/mode persistence; a11y head script; waitlist toast in [`assets/js/waitlist.js`](assets/js/waitlist.js). Corex marquee loads eagerly in [`assets/js/site.js`](assets/js/site.js). ## Production and hosting diff --git a/assets/css/chrome.css b/assets/css/chrome.css index f9c3ed4..c591d9c 100644 --- a/assets/css/chrome.css +++ b/assets/css/chrome.css @@ -73,6 +73,22 @@ html[data-motion="reduce"] .soonex-showcase-grid > * { background: var(--color-surface); } +.soonex-tech-chip { + display: inline-flex; + flex-shrink: 0; + align-items: center; + gap: 0.75rem; + white-space: nowrap; + color: var(--color-ink); +} + +.soonex-tech-chip img { + display: block; + width: 2rem; + height: 2rem; + object-fit: contain; +} + .tabs.tabs--wide { max-width: none; } diff --git a/assets/css/fonts.css b/assets/css/fonts.css index 65c2d1a..286daa4 100644 --- a/assets/css/fonts.css +++ b/assets/css/fonts.css @@ -1,23 +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-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; - src: url("/fonts/manrope-latin-wght-normal.woff2") format("woff2"); + 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: "Outfit"; + font-family: Sora; font-style: normal; - font-weight: 100 900; font-display: swap; - src: url("/fonts/outfit-latin-wght-normal.woff2") format("woff2"); + 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; - src: url("/fonts/jetbrains-mono-latin-wght-normal.woff2") format("woff2"); + 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/hosts.css b/assets/css/hosts.css index aa79f41..98f5180 100644 --- a/assets/css/hosts.css +++ b/assets/css/hosts.css @@ -17,6 +17,7 @@ .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 { @@ -45,6 +46,13 @@ 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; diff --git a/assets/js/site.js b/assets/js/site.js index 04d579d..e583f38 100644 --- a/assets/js/site.js +++ b/assets/js/site.js @@ -1,4 +1,5 @@ import "./mode.js" +import "./theme.js" import { Socket } from "phoenix" import { LiveSocket } from "phoenix_live_view" import { hooks } from "corex/hooks" @@ -10,6 +11,7 @@ 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" @@ -29,6 +31,7 @@ const liveSocket = new LiveSocket("/live", Socket, { ToggleGroup, Menu, Clipboard, + Marquee, ...hooks({ Tabs: () => import("corex/tabs"), Timer: () => import("corex/timer"), diff --git a/assets/js/theme.js b/assets/js/theme.js new file mode 100644 index 0000000..feae714 --- /dev/null +++ b/assets/js/theme.js @@ -0,0 +1,33 @@ +import { + bindStoredPreference, + documentRoot, + firstDetailValue, + parseList, +} from "./controls-shared.js" + +const themes = () => parseList("data-themes") + +const defaultTheme = () => + documentRoot().getAttribute("data-default-theme") || themes()[0] || "neo" + +const resolveTheme = (raw) => { + const allowed = themes() + return raw && allowed.includes(raw) ? raw : defaultTheme() +} + +bindStoredPreference({ + attr: "data-theme", + controlId: "theme-switcher", + resolve: resolveTheme, + syncControl: (theme) => { + const root = document.getElementById("theme-switcher") + if (!root || !theme) return + const current = root.getAttribute("data-value") || "" + if (current.split(",").map((s) => s.trim()).includes(theme)) return + root.dispatchEvent( + new CustomEvent("corex:select:set-value", { detail: { value: [theme] } }), + ) + }, + clientEvent: "corex:set-theme", + parseClientEvent: (event) => firstDetailValue(event) || defaultTheme(), +}) diff --git a/config/config.exs b/config/config.exs index d44c66d..decd4d5 100644 --- a/config/config.exs +++ b/config/config.exs @@ -214,6 +214,238 @@ config :corex_design, "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], @@ -244,10 +476,13 @@ config :corex_design, :checkbox, :"data-list", :timer, + :marquee, :accordion, + :avatar, :"floating-panel", :scrollbar, :pagination, + :carousel, :switch, :"data-table", :collapsible, diff --git a/lib/layouts/root/demo.ex b/lib/layouts/root/demo.ex index 6609b5a..ad7ebf9 100644 --- a/lib/layouts/root/demo.ex +++ b/lib/layouts/root/demo.ex @@ -20,7 +20,7 @@ defmodule Soonex.Layouts.Root.Demo do id="site-controls" class="floating-panel" dir="ltr" - size={%{width: 220, height: 96}} + size={%{width: 250, height: 120}} positioning={ %Corex.Positioning{ placement: "bottom-end", @@ -31,29 +31,63 @@ defmodule Soonex.Layouts.Root.Demo do translation={%Corex.FloatingPanel.Translation{close: "Close"}} > <:trigger class="button ui-size-sm"> - <.heroicon name="hero-cog-6-tooth" /> Options + <.heroicon name="hero-cog-6-tooth" /> Template Options - <:title>Display + <:title>Template Options <:close_trigger> <.heroicon name="hero-x-mark" /> <:content> - <.toggle - id="mode-switcher" - class="toggle ui-size-sm" - data-toggle-dual-label - pressed={@mode == "dark"} - dir="ltr" - on_pressed_change_client="corex:set-mode" - > - Color mode - - <.heroicon name="hero-moon" /> - - - <.heroicon name="hero-sun" /> - - +
    +
    + <.select + id="theme-switcher" + class="select ui-size-sm w-full min-w-0" + dir="ltr" + items={Soonex.Theme.select_items()} + value={[]} + 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 + <:trigger> + <.heroicon name="hero-chevron-down" /> + + <:item_indicator> + <.heroicon name="hero-check" /> + + + + <.toggle + id="mode-switcher" + class="toggle ui-size-sm" + data-toggle-dual-label + pressed={@mode == "dark"} + dir="ltr" + on_pressed_change_client="corex:set-mode" + > + Color mode + + <.heroicon name="hero-moon" /> + + + <.heroicon name="hero-sun" /> + + +
    +
    <.navigate diff --git a/lib/pages/home/logos.ex b/lib/pages/home/logos.ex new file mode 100644 index 0000000..619b345 --- /dev/null +++ b/lib/pages/home/logos.ex @@ -0,0 +1,54 @@ +defmodule Soonex.HomePage.Logos do + @moduledoc false + + use Phoenix.Component + use Corex + + alias Soonex.Layouts.Shell + + def logos(assigns) do + assigns = assign(assigns, :tech, tech_items()) + + ~H""" +
    +
    +

    + The same stack you already ship with +

    + <.marquee + id="soonex-tech-marquee" + class="marquee ui-width-full mt-6" + duration={24} + spacing="2rem" + pause_on_interaction + items={@tech} + > + <:item :let={item}> + + + {item.name} + + + +
    +
    + """ + end + + defp tech_items do + [ + %{name: "Elixir", src: "/images/tech/elixir.svg"}, + %{name: "Phoenix", src: "/images/tech/phoenixframework.svg"}, + %{name: "Tailwind", src: "/images/tech/tailwindcss.svg"}, + %{name: "HTML5", src: "/images/tech/html5.svg"}, + %{name: "JavaScript", src: "/images/tech/javascript.svg"}, + %{name: "TypeScript", src: "/images/tech/typescript.svg"}, + %{name: "Hex", src: "/images/tech/hex.svg"} + ] + end +end diff --git a/lib/pages/home_page.ex b/lib/pages/home_page.ex index e849d32..66d5a59 100644 --- a/lib/pages/home_page.ex +++ b/lib/pages/home_page.ex @@ -5,6 +5,7 @@ defmodule Soonex.HomePage do use Corex import Soonex.HomePage.Hero, only: [hero: 1] + import Soonex.HomePage.Logos, only: [logos: 1] import Soonex.HomePage.Showcase, only: [showcase: 1] import Soonex.HomePage.Features, only: [features: 1] import Soonex.HomePage.HowItWorks, only: [how_it_works: 1] @@ -21,6 +22,7 @@ defmodule Soonex.HomePage do ~H"""
    <.hero /> + <.logos /> <.showcase /> <.features /> <.how_it_works /> diff --git a/lib/pages/privacy_page.ex b/lib/pages/privacy_page.ex index 4c277d7..24babe9 100644 --- a/lib/pages/privacy_page.ex +++ b/lib/pages/privacy_page.ex @@ -59,7 +59,7 @@ defmodule Soonex.PrivacyPage do diff --git a/lib/layouts/root/nav.ex b/lib/layouts/root/nav.ex index 9867ea6..c10d4cb 100644 --- a/lib/layouts/root/nav.ex +++ b/lib/layouts/root/nav.ex @@ -15,8 +15,8 @@ defmodule Soonex.Layouts.Root.Nav do assigns = assign(assigns, :nav_select_items, nav_select_items()) ~H""" -
    -
    +
    +
    <.select id="soonex-mobile-nav" @@ -78,8 +78,8 @@ defmodule Soonex.Layouts.Root.Nav do defp desktop_links do [ %{id: :product, label: "Product", to: Public.path("/") <> "#capabilities"}, - %{id: :how, label: "How it works", to: Public.path("/") <> "#spotlight"}, - %{id: :scale, label: "Scale", to: Public.path("/") <> "#scale"}, + %{id: :how, label: "Workflow", to: Public.path("/") <> "#spotlight"}, + %{id: :proof, label: "Why Soonex", to: Public.path("/") <> "#proof"}, %{id: :journal, label: "Journal", to: Public.path("/blog")}, %{id: :questions, label: "FAQ", to: Public.path("/") <> "#questions"} ] @@ -94,15 +94,20 @@ defmodule Soonex.Layouts.Root.Nav do redirect: :href }, %{ - label: "How it works", + label: "Workflow", value: "spotlight", to: Public.path("/") <> "#spotlight", redirect: :href }, - %{label: "Scale", value: "scale", to: Public.path("/") <> "#scale", redirect: :href}, + %{label: "Why Soonex", value: "proof", to: Public.path("/") <> "#proof", redirect: :href}, %{label: "Journal", value: "blog", to: Public.path("/blog"), redirect: :href}, %{label: "FAQ", value: "questions", to: Public.path("/") <> "#questions", redirect: :href}, - %{label: "Join waitlist", value: "waitlist", to: Public.path("/") <> "#waitlist", redirect: :href} + %{ + label: "Join waitlist", + value: "waitlist", + to: Public.path("/") <> "#waitlist", + redirect: :href + } ]) end end diff --git a/lib/layouts/root_layout.ex b/lib/layouts/root_layout.ex index d01dfab..b58d15d 100644 --- a/lib/layouts/root_layout.ex +++ b/lib/layouts/root_layout.ex @@ -211,13 +211,13 @@ defmodule Soonex.RootLayout do else kind_description(page[:page_kind], site_name) || page_description(page) || - "Tableau + Corex coming-soon template with accessible Phoenix UI components." + "Soonex: the launch page kit for Phoenix teams, built with Tableau and Corex." end end defp kind_description(:home, _site_name), do: - "Tableau + Corex coming-soon template: accessible Phoenix UI, waitlist, journal, and config-driven design tokens." + "Soonex is the launch page kit for Phoenix teams: static HTML, accessible Corex components, four themes, a waitlist, and a journal." defp kind_description(:blog_index, site_name), do: "Shipping notes from #{site_name}." defp kind_description(:not_found, site_name), do: "That page is not on #{site_name}." diff --git a/lib/layouts/section.ex b/lib/layouts/section.ex index a9f3ed4..c2c8c4a 100644 --- a/lib/layouts/section.ex +++ b/lib/layouts/section.ex @@ -9,7 +9,8 @@ defmodule Soonex.Layouts.Section do attr(:labelledby, :string, required: true) attr(:eyebrow, :string, default: nil) attr(:section, :string, default: nil) - attr(:tone, :atom, default: :root, values: [:root, :surface, :inverse]) + attr(:tone, :atom, default: :root, values: [:root, :surface, :inverse, :photo]) + attr(:photo, :string, default: nil, doc: "image path, required when tone is :photo") attr(:align, :atom, default: :start) attr(:layout, :atom, default: :stack, values: [:stack, :sticky, :split]) attr(:compact, :boolean, default: false) @@ -20,6 +21,40 @@ defmodule Soonex.Layouts.Section do slot(:actions) slot(:inner_block, required: true) + def block(%{tone: :photo} = assigns) do + ~H""" +
    +
    +
    + + +
    +

    {@eyebrow}

    +

    + {render_slot(@title)} +

    +

    {render_slot(@lede)}

    +
    +
    + {render_slot(@inner_block)} +
    +
    +
    +
    + """ + end + def block(assigns) do ~H"""
    -
    +

    {render_slot(@lede)}

    @@ -75,9 +110,9 @@ defmodule Soonex.Layouts.Section do defp tone_class(:surface), do: "border-y border-border bg-surface" defp tone_class(:inverse), do: "soonex-band-inverse bg-accent text-accent-contrast" - defp stage_class(:split, _tone), do: Shell.stage() - defp stage_class(:sticky, _tone), do: "#{Shell.stage()} #{Shell.sticky_grid()}" - defp stage_class(:stack, _tone), do: Shell.stage() + defp stage_class(:split, _tone), do: Shell.stage_wide() + defp stage_class(:sticky, _tone), do: "#{Shell.stage_wide()} #{Shell.sticky_grid()}" + defp stage_class(:stack, _tone), do: Shell.stage_wide() defp intro_class(:sticky, _align), do: Shell.sticky_intro() defp intro_class(:stack, :center), do: Shell.intro_center() diff --git a/lib/layouts/shell.ex b/lib/layouts/shell.ex index 1dacfc9..47ce705 100644 --- a/lib/layouts/shell.ex +++ b/lib/layouts/shell.ex @@ -1,12 +1,11 @@ defmodule Soonex.Layouts.Shell do @moduledoc false - def section, do: "scroll-mt-24 w-full py-28 sm:py-36" + def section, do: "scroll-mt-24 w-full py-20 sm:py-28 lg:py-32" - def section_compact, do: "scroll-mt-24 w-full py-16 sm:py-20" + def section_compact, do: "scroll-mt-24 w-full py-14 sm:py-16" - def section_hero, - do: "relative flex min-h-[88svh] w-full flex-col justify-end pb-16 pt-28 sm:pb-20 sm:pt-32" + def section_hero, do: "relative w-full pb-16 pt-16 sm:pb-24 sm:pt-24 lg:pt-28" def stage, do: "mx-auto w-full max-w-6xl px-6 lg:px-8" @@ -16,7 +15,7 @@ defmodule Soonex.Layouts.Shell do def intro_center, do: "mx-auto max-w-2xl text-center" - def body, do: "mt-16 w-full sm:mt-20 lg:mt-24" + def body, do: "mt-16 w-full sm:mt-20" def body_tight, do: "mt-10 w-full sm:mt-12" @@ -27,25 +26,27 @@ defmodule Soonex.Layouts.Shell do def sticky_body, do: "w-full min-w-0 lg:col-span-7" def eyebrow, - do: "soonex-eyebrow m-0 text-xs/6 font-medium tracking-wide text-ink-muted uppercase" + do: "soonex-eyebrow m-0 text-xs/6 font-medium tracking-[0.18em] text-ink-muted uppercase" - def hero_heading, + def display_heading, do: - "display max-w-3xl text-pretty text-5xl font-medium tracking-tight text-ink lg:text-[3.5rem] lg:leading-[1.05]" + "display m-0 text-balance text-5xl font-medium tracking-tighter text-ink sm:text-6xl lg:text-[5.5rem] lg:leading-[0.98]" + + def hero_heading, do: display_heading() def section_heading, - do: "display mt-2 text-pretty text-2xl font-medium tracking-tight text-ink sm:text-3xl" + do: "display mt-3 text-pretty text-2xl font-medium tracking-tight text-ink sm:text-3xl" def section_heading_lg, do: - "display text-pretty text-3xl font-medium tracking-tight text-ink sm:text-4xl lg:max-w-2xl lg:text-[2.75rem] lg:leading-tight" + "display mt-3 text-balance text-4xl font-medium tracking-tighter text-ink sm:text-5xl lg:max-w-3xl lg:text-[3.5rem] lg:leading-[1.02]" def page_heading, do: "display mt-2 text-pretty text-2xl font-medium tracking-tight text-ink sm:text-3xl" - def lede, do: "mt-4 max-w-xl text-pretty text-sm/6 text-ink-muted sm:text-base/7" + def lede, do: "mt-5 max-w-xl text-pretty text-base/7 text-ink-muted sm:text-lg/8" - def lede_wide, do: "mt-4 max-w-2xl text-pretty text-sm/6 text-ink-muted sm:text-base/7" + def lede_wide, do: "mt-5 max-w-2xl text-pretty text-base/7 text-ink-muted sm:text-lg/8" def panel, do: "relative overflow-hidden rounded-2xl border border-border bg-surface p-6 sm:p-8" @@ -54,9 +55,28 @@ defmodule Soonex.Layouts.Shell do def frame, do: "relative overflow-hidden rounded-2xl border border-border bg-surface" + def tile, + do: "relative flex flex-col overflow-hidden rounded-3xl border border-border bg-surface" + + def photo_frame, do: "relative isolate overflow-hidden rounded-3xl bg-ink" + + def photo_fill, do: "absolute inset-0 -z-10 size-full object-cover" + + def scrim, do: "absolute inset-0 -z-10 bg-linear-to-t from-black/80 via-black/45 to-black/10" + + def on_photo_heading, + do: + "display m-0 text-balance text-4xl font-medium tracking-tighter text-white sm:text-5xl lg:text-6xl lg:leading-[1.02]" + + def on_photo_body, do: "mt-5 max-w-xl text-pretty text-base/7 text-white/85 sm:text-lg/8" + def data_list, do: "data-list ui-accent ui-size-md w-full max-w-none" def primary_button, do: "button ui-accent ui-solid ui-size-sm" + def primary_button_md, do: "button ui-accent ui-solid ui-size-md" + + def secondary_button_md, do: "button ui-ghost ui-size-md" + def column_grid, do: "soonex-columns grid grid-cols-1 gap-12 lg:grid-cols-3 lg:gap-0" end diff --git a/lib/pages/home/faq.ex b/lib/pages/home/faq.ex index 0af2f7c..cfa1598 100644 --- a/lib/pages/home/faq.ex +++ b/lib/pages/home/faq.ex @@ -17,11 +17,11 @@ defmodule Soonex.HomePage.Faq do tone={:root} > <:title> - Before you fork Soonex. + Questions, answered. <:lede> - Customization lives in config :corex_design and HEEx content modules. No parallel CSS - framework to maintain. + The short version: one config block for the look, plain HEEx modules for the words, and + static HTML at the end. <:actions> <.navigate to="#waitlist" class="link ui-accent"> @@ -43,7 +43,7 @@ defmodule Soonex.HomePage.Faq do

    {item.content}

    <.navigate :if={item.value == "customize"} to="#spotlight" class="link ui-accent w-fit"> - See how it works <.heroicon name="hero-arrow-down" /> + See the workflow <.heroicon name="hero-arrow-down" /> <.navigate :if={item.value == "waitlist"} to="#waitlist" class="link ui-accent w-fit"> Open the waitlist <.heroicon name="hero-arrow-down" /> @@ -78,9 +78,9 @@ defmodule Soonex.HomePage.Faq do Corex.Content.new([ %{ value: "corex", - label: "What is Corex in this template?", + label: "What is Corex, and why does Soonex use it?", content: - "Corex is an accessible Phoenix UI kit. Soonex uses its static-site integration: design tokens from config :corex_design, ui-* modifiers in HEEx, and client hooks for interactive widgets." + "Corex is an accessible UI kit for Phoenix. Soonex uses its static-site integration: design tokens from config :corex_design, ui-* modifiers in HEEx, and client hooks that bring widgets to life without a server." }, %{ value: "customize", @@ -90,9 +90,9 @@ defmodule Soonex.HomePage.Faq do }, %{ value: "waitlist", - label: "Does the waitlist store email addresses?", + label: "Where do waitlist signups go?", content: - "Not in this demo. Submit shows a toast so you can verify the UX before wiring your list provider. Field names are already structured for a real backend." + "Nowhere until you connect a provider. Out of the box, submitting shows a confirmation toast and stores nothing. The field names are ready to post to Buttondown, ConvertKit, or your own endpoint." }, %{ value: "toolchain", diff --git a/lib/pages/home/features.ex b/lib/pages/home/features.ex index f69c11a..90205d7 100644 --- a/lib/pages/home/features.ex +++ b/lib/pages/home/features.ex @@ -2,45 +2,89 @@ defmodule Soonex.HomePage.Features do @moduledoc false use Phoenix.Component + use Corex + + import Soonex.Layouts.Section, only: [block: 1] alias Soonex.Layouts.Shell def features(assigns) do + assigns = assign(assigns, :tiles, tiles()) + ~H""" -
    -
    -
      -
    • -

      {feature.title}

      -

      {feature.body}

      -
    • -
    + <:title> + Everything a launch page needs. Nothing it doesn't. + + <:lede> + A focused set of building blocks that cover the first impression, the signup, and the + story you tell while you build. + + +
    +
    +
    + +
    +
    + <.heroicon name="hero-swatch" class="size-6 text-ink" /> +

    + Design tokens that stay readable. +

    +

    + Palettes are generated from seeds, so light mode, dark mode, and high contrast all + stay legible without hand-tuned color stacks. +

    +
    +
    + +
    + <.heroicon name={tile.icon} class="size-6 text-ink" /> +

    {tile.title}

    +

    {tile.body}

    +
    -
    + """ end - defp claims do + defp tiles do [ %{ - title: "Tokens that stay readable", - body: - "Light and dark palettes regenerate from seeds in config :corex_design. Ink, surfaces, and CTAs keep WCAG-friendly contrast without hand-tuned hex stacks." + icon: "hero-bolt", + title: "Static and fast", + body: "Tableau renders plain HTML you can host anywhere, from GitHub Pages to a CDN." }, %{ - title: "Static HTML, live components", + icon: "hero-cursor-arrow-rays", + title: "Live components", body: - "Tableau renders the page. Corex hooks hydrate tabs, accordions, selects, and forms — the same primitives you use in LiveView apps." + "Tabs, accordions, selects, and timers hydrate in the browser with full keyboard support." }, %{ - title: "Accessibility built in", + icon: "hero-envelope", + title: "Waitlist built in", body: - "The demo ships Corex accessibility controls for text size, contrast, motion, focus, and link underline — persisted locally with no backend." + "A validated signup form with role select and toast feedback, ready for your provider." + }, + %{ + icon: "hero-newspaper", + title: "A journal that ships", + body: "Markdown posts with tags, RSS, and pagination keep early followers in the loop." } ] end diff --git a/lib/pages/home/hero.ex b/lib/pages/home/hero.ex index ece891f..4067fd3 100644 --- a/lib/pages/home/hero.ex +++ b/lib/pages/home/hero.ex @@ -13,50 +13,60 @@ defmodule Soonex.HomePage.Hero do aria-labelledby="soonex-headline" data-section="hero" > -
    -
    +
    +
    -

    - Launching {Soonex.Launch.year_label()} +

    + Doors open {Soonex.Launch.year_label()}

    - -

    - Accessible Phoenix UI for your coming-soon launch. +

    + Launch pages that feel finished before you ship.

    - -

    - Soonex is a Tableau + Corex template. Ship a waitlist, journal, and production-grade - components with design tokens, light/dark modes, and built-in accessibility — no - parallel CSS framework. -

    -
    - <.navigate to="#waitlist" class={Shell.primary_button()}> - Join waitlist - - <.navigate to={Soonex.Public.path("/blog")} class="button ui-ghost ui-size-sm"> - Read journal - +
    +

    + Soonex turns a Phoenix team's coming-soon page into static HTML with accessible + Corex components, four tuned themes, and a waitlist that works from day one. +

    +
    + <.navigate to="#waitlist" class={Shell.primary_button_md()}> + Join the waitlist + + <.navigate to="#preview" class={Shell.secondary_button_md()}> + See it in action <.heroicon name="hero-arrow-down" /> + +
    -
    - <.timer - id="soonex-hero-timer" - countdown - start_ms={Soonex.Launch.countdown_ms()} - target_ms={0} - class="timer ui-accent ui-size-sm" - > - <:day_label>days - <:hour_label>hours - <:minute_label>min - <:second_label>sec - -

    - Countdown target lives in Soonex.Launch. -

    +
    + +
    +
    +

    Countdown to launch

    + <.timer + id="soonex-hero-timer" + countdown + start_ms={Soonex.Launch.countdown_ms()} + target_ms={0} + class="timer ui-accent ui-size-sm mt-3" + > + <:day_label>days + <:hour_label>hours + <:minute_label>min + <:second_label>sec + +
    +
    diff --git a/lib/pages/home/how_it_works.ex b/lib/pages/home/how_it_works.ex index 609eea1..4edf005 100644 --- a/lib/pages/home/how_it_works.ex +++ b/lib/pages/home/how_it_works.ex @@ -12,17 +12,17 @@ defmodule Soonex.HomePage.HowItWorks do id="spotlight" section="spotlight" labelledby="soonex-spotlight-heading" + eyebrow="Workflow" tone={:inverse} layout={:split} heading_size={:large} > <:title> - Config-driven UI, shipped as static HTML. + From clone to live in an afternoon. <:lede> - Customize seeds, radius, and typography under config :corex_design, then edit HEEx content - modules. Mix handles Corex design CSS, Tailwind, and esbuild — you do not maintain a - parallel design system. + Three steps, all in Mix. No Node toolchain, no parallel design system, and nothing to + maintain beyond your config and your copy. <.tabs id="soonex-how-tabs" @@ -70,11 +70,11 @@ defmodule Soonex.HomePage.HowItWorks do meta: %{ id: "setup", intro: - "Clone Soonex, install Hex dependencies, and build Corex design assets before you open the site locally.", + "Clone the repo, fetch Hex dependencies, and build the design assets in one command.", steps: [ - "Run mix setup from the repo root — it fetches deps and runs mix corex.design.build.", - "Start the dev server with mix server and open http://localhost:4999.", - "Use Template Options to switch neo, uno, duo, or leo and toggle light/dark mode." + "Run mix setup from the repo root. It fetches deps and builds Corex design CSS.", + "Start the dev server with mix server and open localhost:4999.", + "Try neo, uno, duo, or leo and light or dark mode from Template Options." ], command: "mix setup", command_label: "mix setup" @@ -86,12 +86,11 @@ defmodule Soonex.HomePage.HowItWorks do content: "Customize", meta: %{ id: "customize", - intro: - "Brand the template through config :corex_design and HEEx content modules — not a parallel CSS framework.", + intro: "Make it yours with one config block and plain HEEx content modules.", steps: [ - "Edit seeds, radius, fonts, and typography under config :corex_design.", + "Set seeds, radius, fonts, and type scale under config :corex_design.", "Run mix corex.design.build to regenerate tokens and component CSS.", - "Update copy in lib/pages/home and lib/layouts without touching skin files." + "Edit section copy in lib/pages/home and the launch date in Soonex.Launch." ], command: "mix corex.design.build", command_label: "mix corex.design.build" @@ -104,11 +103,11 @@ defmodule Soonex.HomePage.HowItWorks do meta: %{ id: "ship", intro: - "Tableau emits static HTML into _site/. Deploy the folder to GitHub Pages or any static host.", + "Build static HTML into _site/ and publish it to GitHub Pages or any static host.", steps: [ - "Set SOONEX_PUBLIC_URL to your production origin before building.", - "Run MIX_ENV=prod mix build to compile _site/ with prefixed asset paths.", - "Wire the waitlist form to your provider — the demo toast flow is already in place." + "Set SOONEX_PUBLIC_URL to your production origin.", + "Run MIX_ENV=prod mix build to produce _site/ with prefixed asset paths.", + "Point the waitlist form at your email provider and go live." ], command: "MIX_ENV=prod mix build", command_label: "prod build" diff --git a/lib/pages/home/journal.ex b/lib/pages/home/journal.ex index 24222ee..26cd489 100644 --- a/lib/pages/home/journal.ex +++ b/lib/pages/home/journal.ex @@ -26,16 +26,18 @@ defmodule Soonex.HomePage.Journal do labelledby="soonex-journal-heading" eyebrow="Journal" tone={:surface} + heading_size={:large} > <:title> - Notes from building with Corex. + Notes from the build. <:lede> - Markdown posts under _posts. The index paginates; the home page shows the latest three. + Short dispatches on design tokens, accessibility, and shipping static sites with Phoenix + tooling. <:actions> <.navigate to={Soonex.Public.path("/blog")} class="link ui-accent"> - All posts <.heroicon name="hero-arrow-up-right" /> + Read the journal <.heroicon name="hero-arrow-up-right" />
    @@ -43,23 +45,39 @@ defmodule Soonex.HomePage.Journal do
    -
    +
    +
    + +

    {date_label(post)}

    -

    - {post[:title] || "Untitled"} +

    + <.navigate + to={Soonex.Public.path(post.permalink)} + class="after:absolute after:inset-0" + > + {post[:title] || "Untitled"} +

    {post[:description]}

    - <.navigate - to={Soonex.Public.path(post.permalink)} - class="button ui-ghost ui-size-sm mt-6 w-fit" - > - Read more <.heroicon name="hero-arrow-up-right" /> - +
    diff --git a/lib/pages/home/logos.ex b/lib/pages/home/logos.ex index 30a565d..5898117 100644 --- a/lib/pages/home/logos.ex +++ b/lib/pages/home/logos.ex @@ -13,10 +13,13 @@ defmodule Soonex.HomePage.Logos do
    -
    +
    +

    + Built on the tools Phoenix teams already trust +

    <.marquee id="soonex-tech-marquee" class="marquee ui-width-full" @@ -39,7 +42,7 @@ defmodule Soonex.HomePage.Logos do [ %{name: "Elixir", src: "/images/tech/elixir.svg"}, %{name: "Phoenix", src: "/images/tech/phoenixframework.svg"}, - %{name: "Tableau", src: "/images/tech/tableau.svg"}, + %{name: "Tableau", src: "/images/tech/tableau.jpg"}, %{name: "Tailwind", src: "/images/tech/tailwindcss.svg"}, %{name: "HTML5", src: "/images/tech/html5.svg"}, %{name: "JavaScript", src: "/images/tech/javascript.svg"}, diff --git a/lib/pages/home/pricing.ex b/lib/pages/home/pricing.ex deleted file mode 100644 index 7b2b028..0000000 --- a/lib/pages/home/pricing.ex +++ /dev/null @@ -1,65 +0,0 @@ -defmodule Soonex.HomePage.Pricing do - @moduledoc false - - use Phoenix.Component - use Corex - - alias Soonex.Layouts.Shell - - def pricing(assigns) do - ~H""" -
    -
    -

    - Fast to fork, easy to ship. -

    -

    - A coming-soon template you can rename, retheme, and deploy without rebuilding sections - from scratch. -

    - -
      -
    • -

      {item.title}

      -

      {item.body}

      -
    • -
    - -
    -

    - Want early access to Corex templates? -

    - <.navigate to="#waitlist" class={"#{Shell.primary_button()} shrink-0"}> - Yes, join waitlist - -
    -
    -
    - """ - end - - defp scale_items do - [ - %{ - title: "Instant activation", - body: - "Run mix setup and mix server to preview the full landing, journal, and waitlist flow locally." - }, - %{ - title: "Adaptable approach", - body: - "Use Soonex as a launch page, a client handoff, or the starting point for a Corex marketing site." - }, - %{ - title: "Effortless integration", - body: - "Wire the waitlist form to your provider, swap copy in lib/pages/home, and publish _site/ to any static host." - } - ] - end -end diff --git a/lib/pages/home/proof.ex b/lib/pages/home/proof.ex new file mode 100644 index 0000000..296b53a --- /dev/null +++ b/lib/pages/home/proof.ex @@ -0,0 +1,56 @@ +defmodule Soonex.HomePage.Proof do + @moduledoc false + + use Phoenix.Component + + alias Soonex.Layouts.Shell + + def proof(assigns) do + assigns = assign(assigns, :stats, stats()) + + ~H""" +
    +
    +
    +
    +

    Why Soonex

    +

    + Small surface. Serious polish. +

    +
    +
    +

    + A launch page is the first product your users touch. It should be as accessible, + fast, and considered as the thing you are about to ship. +

    +

    The idea behind Soonex

    +
    +
    + +
      +
    • +

      + {stat.value} +

      +

      {stat.label}

      +
    • +
    +
    +
    + """ + end + + defp stats do + [ + %{value: "4", label: "Themes, each with light and dark modes"}, + %{value: "0", label: "npm packages to install or audit"}, + %{value: "1", label: "Config block for your whole brand"}, + %{value: "100%", label: "Static HTML, hostable anywhere"} + ] + end +end diff --git a/lib/pages/home/showcase.ex b/lib/pages/home/showcase.ex index 123d9b8..338fd1d 100644 --- a/lib/pages/home/showcase.ex +++ b/lib/pages/home/showcase.ex @@ -4,89 +4,116 @@ defmodule Soonex.HomePage.Showcase do use Phoenix.Component use Corex + import Soonex.Layouts.Section, only: [block: 1] + alias Soonex.Layouts.Shell def showcase(assigns) do assigns = assign(assigns, :config_snippet, config_snippet()) ~H""" -
    -
    -

    Preview

    -

    - Corex components on a static launch page. -

    -

    - The same tokens, buttons, tabs, and forms you use in LiveView — rendered by Tableau and - hydrated on the client. Switch themes in Template Options to see all four Corex palettes. -

    + <:title> + Real components, not screenshots. + + <:lede> + Every control on this page is a Corex component rendered to static HTML and hydrated in + the browser. Switch themes in Template Options and watch the whole page follow. + -
    -
    -
    -

    Config

    -

    - Seeds and typography live in one place. Rebuild with - mix corex.design.build. -

    - <.clipboard - id="soonex-preview-copy" - class="clipboard ui-accent ui-size-sm ui-width-fit mt-8" - value={"mix corex.design.build"} - > - <:label class="sr-only">mix corex.design.build - <:copy> - <.heroicon name="hero-clipboard" /> - Copy command - - <:copied> - <.heroicon name="hero-check" /> - Copied - - -
    {@config_snippet}
    -
    +
    +
    +
    +

    One config

    +

    + Brand once, everywhere. +

    +

    + Seeds, radius, and type scale live in a single block. Rebuild and every button, + field, and panel picks up the change. +

    +
    {@config_snippet}
    + <.clipboard + id="soonex-preview-copy" + class="clipboard ui-accent ui-size-sm ui-width-fit mt-6" + value="mix corex.design.build" + > + <:label class="sr-only">mix corex.design.build + <:copy> + <.heroicon name="hero-clipboard" /> + Copy build command + + <:copied> + <.heroicon name="hero-check" /> + Copied + + +
    -
    +
    +

    Live UI

    -
    - Join waitlist - Secondary - Accessible -
    -
    - <.tabs - id="soonex-preview-tabs" - class="tabs tabs--wide ui-accent ui-width-full" - value="tokens" - items={tab_items()} - /> -
    -
    - <.native_input - type="email" - name="preview[email]" - id="soonex-preview-email" - placeholder="you@company.com" - class="native-input ui-size-sm ui-width-full" - readonly - > - <:label>Email - - <.switch id="soonex-preview-switch" name="preview[notes]" checked class="switch ui-accent"> - <:label>Launch notes - +
    + WCAG AA + Light and dark
    + + <.tabs + id="soonex-preview-tabs" + class="tabs tabs--wide ui-accent ui-width-full" + value="tokens" + items={tab_items()} + > + <:content :let={item}> +
    +

    {item.meta.intro}

    +
      +
    • + <.heroicon name="hero-check-circle" class="mt-0.5 size-4 shrink-0" /> + {point} +
    • +
    +
    + + + +
    + <.native_input + type="email" + name="preview[email]" + id="soonex-preview-email" + placeholder="you@company.com" + class="native-input ui-size-sm ui-width-full" + readonly + > + <:label>Work email + + <.switch + id="soonex-preview-switch" + name="preview[notes]" + checked + class="switch ui-accent" + > + <:label>Send launch notes + +
    + +
    + Join waitlist + Maybe later +
    -
    + """ end @@ -95,17 +122,42 @@ defmodule Soonex.HomePage.Showcase do %{ value: "tokens", label: "Tokens", - content: "Light and dark palettes regenerate from seeds in config :corex_design." + content: "Tokens", + meta: %{ + intro: "Colors are generated from a handful of seeds, so contrast holds in every mode.", + points: [ + "Light and dark palettes regenerate from the same seeds.", + "Ink, surface, and accent pairs are checked for readable contrast.", + "Four starting themes: neo, uno, duo, and leo." + ] + } }, %{ value: "hooks", - label: "Hooks", - content: "Tabs, accordion, marquee, and forms hydrate without a Phoenix app server." + label: "Interactivity", + content: "Interactivity", + meta: %{ + intro: + "Static pages still get real widgets, with no Phoenix server running behind them.", + points: [ + "Tabs, accordion, select, timer, and marquee hydrate on the client.", + "Keyboard and screen reader behavior comes from Zag state machines.", + "Hooks load lazily, so the first paint stays light." + ] + } }, %{ value: "a11y", - label: "A11y", - content: "Contrast, motion, and focus controls ship in the accessibility panel." + label: "Accessibility", + content: "Accessibility", + meta: %{ + intro: "Visitors can tune the page to how they read, and it remembers their choice.", + points: [ + "Text size, contrast, motion, focus ring, and link underline controls.", + "Preferences persist locally with no account or backend.", + "The test suite runs axe against the home page in headless Chrome." + ] + } } ]) end @@ -114,7 +166,9 @@ defmodule Soonex.HomePage.Showcase do """ config :corex_design, default_theme: :neo, - themes: %{neo: %{seeds: %{...}}} + themes: %{ + neo: %{seeds: %{accent: "#7c5cff"}} + } """ |> String.trim() end diff --git a/lib/pages/home/trust.ex b/lib/pages/home/trust.ex deleted file mode 100644 index 22a3fd8..0000000 --- a/lib/pages/home/trust.ex +++ /dev/null @@ -1,36 +0,0 @@ -defmodule Soonex.HomePage.Trust do - @moduledoc false - - use Phoenix.Component - use Corex - - alias Soonex.Layouts.Shell - - def trust(assigns) do - assigns = assign(assigns, :items, trust_items()) - - ~H""" -
    -
    -

    - Built with the stack you already ship -

    -
      -
    • - {item} -
    • -
    -
    -
    - """ - end - - defp trust_items do - ["Corex", "Phoenix LiveView", "Tableau", "Tailwind CSS", "Elixir", "Hex"] - end -end diff --git a/lib/pages/home/waitlist.ex b/lib/pages/home/waitlist.ex index da5f446..a477d6c 100644 --- a/lib/pages/home/waitlist.ex +++ b/lib/pages/home/waitlist.ex @@ -13,74 +13,97 @@ defmodule Soonex.HomePage.Waitlist do
    -
    -
    -

    - Join the waitlist -

    -

    - Demo form — no addresses are stored. Submit fires the success toast so you can validate - the flow before connecting your provider. -

    +
    +
    + + -
    - <.native_input - type="email" - name="waitlist[email]" - id="soonex-waitlist-email" - required - autocomplete="email" - placeholder="you@company.com" - class="native-input ui-size-md ui-width-full" - > - <:label>Email - +
    +
    +

    Early access

    +

    + Be first through the door. +

    +

    + Join the list for launch day access, release notes, and the occasional + behind-the-scenes post. No spam, unsubscribe anytime. +

    +
    - <.select - id="soonex-waitlist-role" - name="waitlist[role]" - class="select ui-accent ui-size-md ui-width-full" - value={["founder"]} - items={@role_items} - positioning={ - %Corex.Positioning{ - strategy: "absolute", - placement: "bottom-start", - same_width: true, - gutter: 8, - slide: false, - fit_viewport: false, - flip: false - } - } - > - <:label>Role - <:trigger> - <.heroicon name="hero-chevron-down" /> - - +
    +
    + + <.native_input + type="email" + name="waitlist[email]" + id="soonex-waitlist-email" + required + autocomplete="email" + placeholder="you@company.com" + class="native-input ui-size-md ui-width-full" + > + <:label>Work email + - <.switch - id="soonex-waitlist-notes" - name="waitlist[notes]" - checked - class="switch ui-accent" - > - <:label>Email me launch notes - + <.select + id="soonex-waitlist-role" + name="waitlist[role]" + class="select ui-accent ui-size-md ui-width-full" + value={["founder"]} + items={@role_items} + positioning={ + %Corex.Positioning{ + strategy: "absolute", + placement: "bottom-start", + same_width: true, + gutter: 8, + slide: false, + fit_viewport: false, + flip: false + } + } + > + <:label>I am a + <:trigger> + <.heroicon name="hero-chevron-down" /> + + - - + <.switch + id="soonex-waitlist-notes" + name="waitlist[notes]" + checked + class="switch ui-accent" + > + <:label>Email me launch notes + + + +

    + This form is not connected to a provider yet, so nothing you enter is stored. +

    + +
    +
    +
    @@ -91,7 +114,8 @@ defmodule Soonex.HomePage.Waitlist do Corex.List.new([ %{label: "Founder", value: "founder"}, %{label: "Engineer", value: "engineer"}, - %{label: "Designer", value: "designer"} + %{label: "Designer", value: "designer"}, + %{label: "Marketer", value: "marketer"} ]) end end diff --git a/lib/pages/home_page.ex b/lib/pages/home_page.ex index 66d5a59..de3131e 100644 --- a/lib/pages/home_page.ex +++ b/lib/pages/home_page.ex @@ -9,7 +9,7 @@ defmodule Soonex.HomePage do import Soonex.HomePage.Showcase, only: [showcase: 1] import Soonex.HomePage.Features, only: [features: 1] import Soonex.HomePage.HowItWorks, only: [how_it_works: 1] - import Soonex.HomePage.Pricing, only: [pricing: 1] + import Soonex.HomePage.Proof, only: [proof: 1] import Soonex.HomePage.Journal, only: [journal: 1] import Soonex.HomePage.Faq, only: [faq: 1] import Soonex.HomePage.Waitlist, only: [waitlist: 1] @@ -26,7 +26,7 @@ defmodule Soonex.HomePage do <.showcase /> <.features /> <.how_it_works /> - <.pricing /> + <.proof /> <.journal posts={@posts} /> <.faq /> <.waitlist /> diff --git a/mix.lock b/mix.lock index d6072be..ba343bd 100644 --- a/mix.lock +++ b/mix.lock @@ -10,18 +10,18 @@ "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, "date_time_parser": {:hex, :date_time_parser, "1.3.0", "6ba16850b5ab83dd126576451023ab65349e29af2336ca5084aa1e37025b476e", [:mix], [{:kday, "~> 1.0", [hex: :kday, repo: "hexpm", optional: false]}], "hexpm", "93c8203a8ddc66b1f1531fc0e046329bf0b250c75ffa09567ef03d2c09218e8c"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, - "ex_ast": {:hex, :ex_ast, "0.13.1", "b3d80ec163733176f63662ac44d2511445c224f6b5e4e3ce01f5eff83c4a5993", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "bd15f68cde5ec945b859bd67416f26cf5499f1aef9b067eb2163ed244c7e703a"}, - "ex_slop": {:hex, :ex_slop, "0.4.4", "8ca277b902e93193eb3545e414c309026a51204e5eb7d6ada5ff96555de6abcb", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "03ef1b2553ebad45c8ceb3e482eed5f1ccd08fc992215a9da0ef6196935107e2"}, + "ex_ast": {:hex, :ex_ast, "0.16.0", "cc7580f80fe585fd13cbce01b9092fd78100138440929d9f532a44b0182ea8c6", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "700303b0cb693cb36dbac3a2f66a609282b108114fa20179395f36a13a9043b4"}, + "ex_slop": {:hex, :ex_slop, "0.4.5", "4e3ea38397d08d422f8e92e4cef16877e4a31a93c6abd07b494f4458e37c3ce2", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "174c1ce4da612f7d3e93734d08d81d93774bacb6a8e86123e7d6876369a2df81"}, "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, - "finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"}, + "finch": {:hex, :finch, "0.24.0", "4022b6194e907b6d9b597c168001cac38e488a35fe36cef9d8297e3159782510", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "33ba40069c3587c2f99f9125b766e19dad87d6d54be3c6961db2304df04cef00"}, "floki": {:hex, :floki, "0.38.4", "10f98971e892aed2c2f1b3a0f928e488e3797e1c6dd3dfd98db40b14e9a78bcf", [:mix], [], "hexpm", "bdb34645eee8e79845c7edaca2d4099a52804ee4d4a3ecc683a69451f0244973"}, "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"}, "glob_ex": {:hex, :glob_ex, "0.1.12", "7b2d9369c20e2697efcfd185d13d6e84c94cd3bfd2730fbde613141c2e015c00", [:mix], [], "hexpm", "2e2fac83f113514434c7eaf267b4c38af2f91766f1cab2c5db7053b7fc1ee0bb"}, "h2": {:hex, :h2, "0.12.0", "f393539ee2728f8118fb2024b6d5f3e2c45e40ceb31b18b4e9bf5e50d028f80f", [:rebar3], [], "hexpm", "beaafc93c54cdc5d623247334d3970cdf4bc66b6b8b296b74ba1d7c7513c3dfc"}, "hackney": {:hex, :hackney, "4.7.4", "8fe2ddaa3ca27de99d68e682d72b66d07d2331da680f77c8000580a0122c69e6", [:rebar3], [{:certifi, "~> 2.17.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.12.0", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.5", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.5", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "d07d7e1358353ab6cc75132f058c155287f3e013d43f709fbb79d79eeab98195"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, - "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, + "hpax": {:hex, :hpax, "1.1.0", "782931867cc23217c68fb5f68fe1a11f5e7544c7fda82c8a7019a5df5a4a1cdf", [:mix], [], "hexpm", "0b8d0f05832f55571d65ac720f79bf8994138ffbb133209dc4685eae0ad456a8"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "httpoison": {:hex, :httpoison, "3.0.0", "8566a933bb9175236d1ec335978445b67cd1f5b5d3ead6ca4b80be469d41f5d9", [:mix], [{:hackney, "~> 4.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "9130197b7658901c493d6fcfb842fb9676300fa8a6c8ed058c8889cf1a77f3c2"}, "idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"}, @@ -29,7 +29,7 @@ "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "kday": {:hex, :kday, "1.1.0", "64efac85279a12283eaaf3ad6f13001ca2dff943eda8c53288179775a8c057a0", [:mix], [{:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}], "hexpm", "69703055d63b8d5b260479266c78b0b3e66f7aecdd2022906cd9bf09892a266d"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, - "makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"}, + "makeup": {:hex, :makeup, "1.2.3", "c3ef0ff83acf305384acba0f70bff87d8732502baa1eb3eb081d8c154fbe18f5", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "15970751b7021c2bcd7ed56a54b3def23affd3969fb7c214dffdf28e97ce6f76"}, "makeup_css": {:hex, :makeup_css, "0.2.3", "abbe801a520b88a0430684293f98745a7b082fb47c3902c61ec277a0fb7b79d7", [:mix], [{:makeup, "~> 1.1", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "656afcd4e96468311ac9b34ce4c91bb8521a48203afa33982d5f21edc012e79a"}, "makeup_eex": {:hex, :makeup_eex, "2.0.2", "88983b72aadb2e8408b06f7c9413804ce7eae2ca2a5a35cb738c6a9cb393c155", [:mix], [{:makeup, "~> 1.2.1 or ~> 1.3", [hex: :makeup, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.2.0 or ~> 1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "30ac121dda580298ff3378324ffaec94aad5a5b67e0cc6af177c67d5f45629b9"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, @@ -40,27 +40,27 @@ "mdex_native": {:hex, :mdex_native, "0.2.8", "20b7cbf330c1ca81b8da4132b8d01952cded11f6dfc2abe8fef25c13681b15e4", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "004a5565b6c96a06400901eb1e4e603585e00b23262d3f595c3f4aa38b83ef66"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, - "mint": {:hex, :mint, "1.10.0", "85af3353bfc504f5bdfe494bd92b8490f87a306dc659ee1ad0af435107e898dc", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "8b16fb72aaa7531d206a1f05e4cc85509ba531ccec7a17a22736c9c95cbb24d1"}, + "mint": {:hex, :mint, "1.11.0", "a713551624815c0435237b93d90ea8b9b14254690c66f732d0ef8930f76ff1d9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "c6279ba2d6aa3a383a1d4cfbe7b59f42e6efd400f58d8e2acfeac48a438693ab"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "owl": {:hex, :owl, "0.13.1", "1ec4a5dea170465f0e90c502c203079224516bc0cbd599281c8667b3c6ef8848", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "351e768af8f2edc575cdaab1a5a2f6d6381be591758a026c701c703145508a0c"}, "parse_trans": {:hex, :parse_trans, "3.4.2", "c352ddc1a0d5e54f9b1654d45f9c432eef76f9cea371c55ddff769ef688fdb74", [:rebar3], [], "hexpm", "4c25347de3b7c35732d32e69ab43d1ceee0beae3f3b3ade1b59cbd3dd224d9ca"}, - "phoenix": {:hex, :phoenix, "1.8.13", "e33192826d9bed4022bdb3f5a7b36c04362049d9390fd1b581d5ad6261779268", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 2.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ad14e24d10e5a52d5f80429053bbe3a5d124311a2868fceb0a01a2e859c44539"}, + "phoenix": {:hex, :phoenix, "1.8.15", "dcdb304113660ec97e1579458824d3c34b5d2eed07a0fdf25a836a9515b63146", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 2.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "7b83ed6b3d544f24a29277eab7f051be38b76f390bb511bb6ddb7ec6e8e05b95"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.2.11", "1c83fdfd007e8d554076baee00ee6f55bb9c87ad9439f33ee520e47ae4a983bb", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c4087267cbd4dc4ed7ac36512a97830af8e9c857ca7cb22557f8640fc5f9d306"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.3.0", "03916bfbc31a5121945b3cfffe5aec647a5c97fe1dc172a319b94428562359c9", [:mix], [], "hexpm", "eec7be6e9cf02e2551d389b558402d6c637cd3973796326e7ba4bb03c6b2e91d"}, - "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.2.12", "35848150bbab579e9d0aff79525269d7c0801a4f61232b84f635c11222cee88f", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "656810d716e3369545dd63981196a5d68b77fdb253afe02ef0c6fa14cfd8dc2b"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.4.0", "be29b070ec9298df8c3c90a2ae0712d4c9e37c214f6a6ca81341bd19f1bde00b", [:mix], [], "hexpm", "80776f727460ed104c95ecddcacc5098d3cd31f40e5f05741e1219abfb20f4a8"}, + "phoenix_template": {:hex, :phoenix_template, "1.1.0", "b329582281b1e00e4dc664afd60ef5b489cbb356923caa3b1e00abe8651b9a18", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "eba70070de79b2c3501ef205a74a69f98ab352f3785aa15da9ed161f9fe0fd5d"}, "plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"}, "plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"}, "quic": {:hex, :quic, "1.8.2", "c315176d2c4fad0725e52f2a8033b96d01c4fecfc7e6a7333615ff10041142a5", [:rebar3], [], "hexpm", "274d2f41ee9c00d8d6415248df9fb5637381fe69b5387771dc37e91038a65479"}, - "req": {:hex, :req, "0.7.4", "23e9ffec17de032a46a4b15ed65c09793893bf4a7c680f4bbf6227fce6bdf74d", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "4b192d63253e8dcc6221ef992ea9ebef7d3555166e8423aa5b553e86bc3c69a2"}, + "req": {:hex, :req, "0.7.5", "0c3293cbf7717aba20b1b1b3ea13fe8d3fdf6f1557490f390c3f1ef6dd9db955", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "576853c4bf403faa3c1fa5d093e0759227083b78f8a6864c67d62c0a9c45514d"}, "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"}, "schematic": {:hex, :schematic, "0.5.2", "07b666b971b780bcfbae7a5032945886dc9fb7e31834d7438c6e572ef80c2866", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cef597a88260823a806c106071213bd85670b855007cd70e21284a716fe44348"}, "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, - "sourceror": {:hex, :sourceror, "1.12.2", "85bfd48159f020c0cbfc72f289f11456fdc05dc43719b6f2589fb969faefa113", [:mix], [], "hexpm", "da37d3da09c5b890528802c7056a8f585a061973820d7656b6e3649c14f0e9cb"}, - "spitfire": {:hex, :spitfire, "0.4.1", "69e90335d00ca328295e1e1e77cac5d7575aa6d34274e3467ebfc654b8858be3", [:mix], [], "hexpm", "27d86f67681179682b15c6758d64ac2eb2b3637ed8340800c8b885c69754cdcd"}, + "sourceror": {:hex, :sourceror, "1.12.3", "f58eebef0765c7a369a49a755ab2a5ee88d92777f403ea5a08b722cefcc37f51", [:mix], [], "hexpm", "d5f2f37099de794840f08c54ae546d7f6e4ea015e397be64aebe4996fa9f7da7"}, + "spitfire": {:hex, :spitfire, "0.4.2", "5c719208d4eeb810e5b2a2aa1024d1e4b2974b7ca422c274b2486666e5159735", [:mix], [], "hexpm", "9bbbbffe93e6f88ccf193487ef56b83c2646a6dc3875bb0975d4654bfe96c5bb"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "tableau": {:hex, :tableau, "0.30.0", "12b507e6c517b4e87be469edf466fe542a36c6cda57b0f5607a12ae123a7f8e6", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: false]}, {:date_time_parser, "~> 1.2", [hex: :date_time_parser, repo: "hexpm", optional: false]}, {:html_entities, "~> 0.5.2", [hex: :html_entities, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16.0", [hex: :libgraph, repo: "hexpm", optional: false]}, {:mdex, "~> 0.11.1", [hex: :mdex, repo: "hexpm", optional: false]}, {:schematic, "~> 0.5.1", [hex: :schematic, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}, {:tz, "~> 0.28.1", [hex: :tz, repo: "hexpm", optional: false]}, {:web_dev_utils, "~> 0.3", [hex: :web_dev_utils, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "4534c5b74a7702bd64a4015d59bbd40cfc55da022d49afaeaca0ab76b84db42e"}, "tailwind": {:hex, :tailwind, "0.5.1", "35435b13158c90d37da11e1cfc808755fca1d7b6c5ab87b1b19c5de87e2f0a10", [:mix], [], "hexpm", "c4e26302a59fec72abc5610ecb6ad2116d9aa31f31aab2d4b8eb6e95d25a689c"},