From 92aedb96572fc4ed6a251171a09d68dee4e50b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bojl=C3=A9n?= Date: Thu, 18 Jun 2026 18:38:03 +0100 Subject: [PATCH] Add TypeScript hook pipeline with esbuild Introduces the TS source tree for LiveView hooks (assets/ts/), built into the storybook static bundle via esbuild. assets/js/storybook.js is now the source entrypoint that imports the hooks; the built priv bundle is a gitignored artifact. Adds tsconfig.json (tsc --noEmit gate), esbuild config, and wires esbuild into the assets.build alias. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 6 +++-- assets/js/storybook.js | 3 +++ assets/ts/dialog.ts | 38 ++++++++++++++++++++++++++++++ assets/ts/index.ts | 3 +++ assets/tsconfig.json | 12 ++++++++++ config/config.exs | 12 ++++++++++ mix.exs | 2 +- priv/static/assets/js/storybook.js | 3 --- 8 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 assets/js/storybook.js create mode 100644 assets/ts/dialog.ts create mode 100644 assets/ts/index.ts create mode 100644 assets/tsconfig.json delete mode 100644 priv/static/assets/js/storybook.js diff --git a/.gitignore b/.gitignore index bbbced1..97c990e 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,8 @@ assets/node_modules/ # Vendored reference repos (not part of the package) vendor/ -# Storybook dev-harness assets: Tailwind-built CSS is a build artifact -# (source is assets/css/storybook.css). The static JS bundle is committed. +# Storybook dev-harness assets: both the Tailwind-built CSS and the esbuild-built +# JS bundle are build artifacts. Sources live in assets/css/storybook.css and +# assets/js/storybook.js (which imports the TS hooks in assets/ts/). /priv/static/assets/css/storybook.css +/priv/static/assets/js/storybook.js diff --git a/assets/js/storybook.js b/assets/js/storybook.js new file mode 100644 index 0000000..17f1218 --- /dev/null +++ b/assets/js/storybook.js @@ -0,0 +1,3 @@ +import { Hooks } from "../ts/index.ts"; + +window.storybook = { Hooks, Params: {}, Uploaders: {} }; diff --git a/assets/ts/dialog.ts b/assets/ts/dialog.ts new file mode 100644 index 0000000..aa12a5d --- /dev/null +++ b/assets/ts/dialog.ts @@ -0,0 +1,38 @@ +interface DialogHook { + el: HTMLElement; + mounted(): void; + destroyed(): void; +} + +const FOCUSABLE = + 'a[href],area[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),[tabindex]:not([tabindex="-1"])'; + +export const ShadixDialog = { + mounted(this: DialogHook) { + const el = this.el; + let restoreTo: HTMLElement | null = null; + + const open = () => { + restoreTo = document.activeElement as HTMLElement | null; + document.body.style.overflow = "hidden"; + window.requestAnimationFrame(() => { + const content = el.querySelector('[role="dialog"]'); + const first = content?.querySelector(FOCUSABLE); + (first ?? content)?.focus(); + }); + }; + + const close = () => { + document.body.style.overflow = ""; + restoreTo?.focus(); + restoreTo = null; + }; + + el.addEventListener("shadix:dialog-open", open); + el.addEventListener("shadix:dialog-close", close); + }, + + destroyed(this: DialogHook) { + document.body.style.overflow = ""; + }, +}; diff --git a/assets/ts/index.ts b/assets/ts/index.ts new file mode 100644 index 0000000..69a1b46 --- /dev/null +++ b/assets/ts/index.ts @@ -0,0 +1,3 @@ +import { ShadixDialog } from "./dialog"; + +export const Hooks = { ShadixDialog }; diff --git a/assets/tsconfig.json b/assets/tsconfig.json new file mode 100644 index 0000000..eb8c6c2 --- /dev/null +++ b/assets/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"] + }, + "include": ["ts/**/*.ts"] +} diff --git a/config/config.exs b/config/config.exs index 49e4aa3..b06fda2 100644 --- a/config/config.exs +++ b/config/config.exs @@ -12,6 +12,18 @@ config :tailwind, cd: Path.expand("..", __DIR__) ] +# esbuild bundles the storybook JS entrypoint (assets/js/storybook.js), which +# imports the TypeScript LiveView hooks (assets/ts/*), into the static JS bundle +# served by the dev harness. The built file is a build artifact (gitignored). +config :esbuild, + version: "0.23.0", + storybook: [ + args: + ~w(js/storybook.js --bundle --format=iife --target=es2022 --outdir=../priv/static/assets/js), + cd: Path.expand("../assets", __DIR__), + env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} + ] + if File.exists?(Path.join(__DIR__, "#{config_env()}.exs")) do import_config "#{config_env()}.exs" end diff --git a/mix.exs b/mix.exs index 71fdc44..0255cb2 100644 --- a/mix.exs +++ b/mix.exs @@ -52,7 +52,7 @@ defmodule Shadix.MixProject do defp aliases do [ - "assets.build": ["tailwind storybook"], + "assets.build": ["tailwind storybook", "esbuild storybook"], dev: ["run --no-halt dev.exs"] ] end diff --git a/priv/static/assets/js/storybook.js b/priv/static/assets/js/storybook.js deleted file mode 100644 index e8b6b3e..0000000 --- a/priv/static/assets/js/storybook.js +++ /dev/null @@ -1,3 +0,0 @@ -// Minimal storybook JS bundle. Shadix components have no LiveView hooks yet, so -// empty registries suffice. Loaded immediately before phoenix_storybook's own JS. -window.storybook = { Hooks: {}, Params: {}, Uploaders: {} }; -- 2.51.2