22 KiB
Tailwind And Phoenix Asset Tooling For bDS2 Desktop
Purpose
This document describes the target styling architecture for a Tailwind-integrated Phoenix LiveView desktop app in this repository.
It is written as a handoff for a coding agent that will perform the implementation.
This is not a migration guide from a public web app. It is a target-state guide for a Phoenix LiveView desktop shell with local assets, dense editor surfaces, overlays, resizable panes, and desktop-specific titlebar behavior.
Verified Current State
The current app does not use the default Phoenix asset pipeline.
- CSS is served directly from
priv/ui/app.css. - JS is served directly from
priv/ui/live.js. - Static assets are served from
priv/uiinlib/bds/desktop/endpoint.ex. - The root layout links
/assets/app.cssinlib/bds/desktop/layouts.ex, but that path is currently backed bypriv/ui/app.css, not by generated Phoenix assets. - There is no
:tailwindor:esbuildsetup inmix.exs.
Relevant files:
lib/bds/desktop/endpoint.exlib/bds/desktop/layouts.exmix.exspriv/ui/app.csspriv/ui/live.js
Official Phoenix Facts
The implementation target should follow Phoenix defaults where they fit the desktop shell.
Official Phoenix references:
- Phoenix Asset Management: https://hexdocs.pm/phoenix/asset_management.html
- Phoenix Components and HEEx: https://hexdocs.pm/phoenix/components.html
- Phoenix.Component reference: https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html
Facts from Phoenix docs and generator templates:
- Phoenix v1.7+ defaults to Tailwind for CSS and esbuild for JS.
- The default CSS source entrypoint is
assets/css/app.css. - The default generated CSS output is
priv/static/assets/css/app.css. - The default generated JS output is
priv/static/assets/js/app.js. - Phoenix promotes function components and HEEx as the main rendering model.
- Phoenix does not prescribe per-component CSS modules. Tailwind-first HEEx plus shared source CSS is a valid Phoenix-default shape.
Target Outcome
The target outcome is a solid Phoenix-default asset setup with Tailwind as the main styling system and a generated production stylesheet.
The target should look like this:
- Tailwind source lives in
assets/css/app.cssand imported CSS modules. - The final stylesheet is generated into
priv/static/assets/css/app.css. - LiveView JS entrypoint lives in
assets/js/app.jsand builds topriv/static/assets/js/app.js. - The desktop endpoint serves static assets from
priv/static. - Layouts reference the generated CSS and JS outputs.
- HEEx markup carries most layout, spacing, typography, responsive, and state classes.
- Authored CSS remains for global tokens, app-region behavior, pseudo-elements, scrollbars, Monaco integration, hard selectors, and overlay mechanics.
Architecture Rules
1. Tailwind Owns The Common Case
Use Tailwind utility classes directly in HEEx for:
- flex and grid layout
- spacing
- typography
- borders and radii
- colors and opacity
- active, selected, disabled, and hover states
- responsive breakpoints
- overflow and truncation
Do not preserve large semantic wrapper classes when they only encode simple layout decisions.
Good examples to move into HEEx classes:
- tab rows
- button rows
- editor header layout
- metadata field layout
- sidebar list row spacing
- status bar item alignment
2. Authored CSS Owns The Desktop-Specific Case
Keep authored CSS source files for:
app-regionand-webkit-app-region- pseudo-elements used for icons, handles, and active markers
- custom scrollbars
- Monaco/editor iframe or host integration
- absolute overlay stacks and backdrops
- drag and drop affordances
- complex attribute selectors
- hard-to-read repeated combinations that should become shared component classes
Do not force these into giant utility strings if readability drops.
3. Keep A Thin Semantic CSS Layer
It is acceptable to keep a small number of semantic component classes when they encode a repeated desktop UI primitive.
Examples of valid semantic classes in the target state:
.window-titlebar.resizable-panel-divider.overlay-root.monaco-host.panel-entry.btn-base,.btn-theme-primary,.btn-theme-danger
These classes should be implemented in Tailwind source CSS using @layer components and should remain small, stable, and reusable.
4. Tokens Must Be Centralized
The current stylesheet relies heavily on VS Code-like CSS variables. Preserve that idea.
The new assets/css/app.css must define the design tokens once, using Tailwind v4 @theme plus any required raw CSS custom properties for runtime-driven values.
Examples from the current app that must survive:
- shell/background colors
- tab active/inactive colors
- status bar colors
- focus border color
- input background and border colors
- sidebar width
- assistant width
- font family and font size
5. Desktop Layout Constraints Must Stay Intact
The styling rewrite must preserve these runtime constraints:
- the app occupies full window width and height
- the shell uses
overflow: hiddenat the top level - the main workbench uses
min-height: 0andmin-width: 0correctly - resizable sidebars remain width-variable
- titlebar remains draggable except for interactive controls
- overlays render above the shell without breaking keyboard focus or pointer behavior
Proposed Asset Layout
Use the standard Phoenix asset layout.
assets/
css/
app.css
shell.css
sidebar.css
tabs.css
editor.css
forms.css
overlays.css
panel.css
assistant.css
menu_editor.css
media_editor.css
utilities.css
js/
app.js
...
priv/
static/
assets/
css/
js/
Recommended responsibilities:
assets/css/app.css: Tailwind import,@source, tokens, base layer, importsassets/css/shell.css: app shell, titlebar, activity bar, pane shells, status barassets/css/sidebar.css: sidebar filters, search, chips, calendar tree, load moreassets/css/tabs.css: workbench tabs and editor tabsassets/css/editor.css: common editor frame, toolbar, meta column, shared form shellassets/css/forms.css: shared input, textarea, tag chip, picker, inline action primitivesassets/css/overlays.css: overlay root, modal backdrop, dialog shells, gallery/lightboxassets/css/panel.css: panel tabs, panel entry cards, tasks, output, git logassets/css/assistant.css: assistant sidebar and chat-specific shared surfacesassets/css/menu_editor.css: menu tree, drag/drop indicators, picker listsassets/css/media_editor.css: media preview, linked post picker, detail formsassets/css/utilities.css: a very small set of custom utilities that are truly reused
Proposed Phoenix Asset Tooling
The implementation should introduce the standard Phoenix aliases and configs.
mix.exs
Add:
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev}{:esbuild, "~> 0.10", runtime: Mix.env() == :dev}
Add aliases similar to:
assets.setupassets.buildassets.deploy
Versioning (mandatory)
The Elixir :tailwind wrapper still defaults to Tailwind v3. The plan in this document assumes Tailwind v4 syntax (@import "tailwindcss", @theme, @source, @layer components). Pin both tools explicitly in config/config.exs:
config :tailwind, version: "4.1.14"(or current 4.1.x)config :esbuild, version: "0.25.4"(or current 0.25.x)
Without an explicit v4 pin the build will silently install Tailwind v3 and v4 directives will not resolve.
No Node.js policy
The Elixir :tailwind and :esbuild wrappers download self-contained binaries and do not require Node.js. The implementation MUST stay Node-free unless a third-party Tailwind plugin is later required (in which case the custom assets/build.js route from the Phoenix asset_management guide is used). No package.json is added under assets/.
config/config.exs
Configure Tailwind input and output paths.
Target output:
assets/css/app.css->priv/static/assets/css/app.css
Configure esbuild for:
assets/js/app.js->priv/static/assets/js/app.js
esbuild profile must include --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* and nodePaths: [Mix.Project.build_path() <> "/../../deps"] so phoenix, phoenix_html, and phoenix_live_view resolve from deps/ without an npm install.
config/dev.exs
Add Phoenix watchers for:
- Tailwind
--watch - esbuild
--watch
No phx.digest in desktop builds
This is a desktop app served through an embedded WebView, not a public web app behind a CDN.
- Do NOT run
mix phx.digestas part ofassets.deploy. - Output filenames stay stable (
app.css,app.js) so the layout can link them by fixed path. assets.deployfor this repo is:tailwind default --minify,esbuild default --minify. Nothing else.
endpoint/layout changes
Update the desktop endpoint and root layout to serve and link generated assets from priv/static/assets instead of priv/ui.
Specifically:
- Replace the existing
Plug.Staticfor/assetswithfrom: {:bds, "priv/static/assets"}andonlylisting the generatedcssandjsdirectories. - Drop the
/vendor/phoenixand/vendor/live_viewPlug.Staticblocks; those scripts are now bundled by esbuild fromdeps/. - Add a dedicated
Plug.Staticfor/monacopointing atpriv/ui/monaco(or move it topriv/static/monaco). Monaco is a prebuilt vendor drop and MUST NOT be passed through esbuild. - Remove the
<script src="/vendor/phoenix/...">and<script src="/vendor/live_view/...">tags fromlib/bds/desktop/layouts.ex. Keep only/assets/app.cssand/assets/app.js.
JS Pipeline
The JS pipeline mirrors the CSS pipeline.
Entrypoint
assets/js/app.js is the single esbuild entrypoint. It:
- imports
phoenix,phoenix_html, andphoenix_live_view(resolved fromdeps/via esbuildnodePaths) - constructs the
LiveSocketwith the desktop CSRF token - registers all hooks currently defined inline in
priv/ui/live.js - wires the native menu / titlebar / shortcut bridges
Module layout
Split the current priv/ui/live.js (1.4k lines) into focused modules under assets/js/:
assets/js/app.js- entrypoint, LiveSocket, hook registrationassets/js/hooks/- one file per hookassets/js/bridges/- native menu, titlebar, shortcut bridgesassets/js/monaco/- thin host glue only; do NOT bundle Monaco itself
Monaco carve-out
Monaco is loaded as prebuilt assets from /monaco/... and is not part of the esbuild graph. The host glue in assets/js/monaco/ only configures the loader URL and posts messages; it does not import Monaco modules.
Vendor stripping
After the switch:
priv/ui/app.cssis deleted (its content has been redistributed underassets/css/).priv/ui/live.jsis deleted.priv/ui/monaco/stays (or moves topriv/static/monaco/).- The
phoenixandphoenix_live_viewdep static drops are no longer served.
Iconography
The app keeps its existing inline SVG icon set. Do NOT adopt the Phoenix 1.8 Heroicons-via-Tailwind-plugin pattern.
Reasons:
- The target visual look is defined by the current bDS UI; its icons are part of that look.
- Inline SVG keeps icons under direct control for sizing, stroke, and currentColor styling via Tailwind utility classes.
- Avoiding the Heroicons git dep keeps the build Node-free and dependency-light.
Rules:
- Existing SVG icons stay where they are (HEEx components / inline
<svg>markup). - Icon size and color are controlled by Tailwind utilities on the wrapping element (
size-4,text-[var(--color-icon)], etc.) usingfill="currentColor"orstroke="currentColor". - No
:heroiconsmix dep, no Tailwind icon plugin, noassets/vendor/heroicons.js. - If a new icon is needed, add the SVG inline in the appropriate component module.
HEEx Styling Strategy
Prefer Utility Classes In Templates
Use utility classes in HEEx for:
- shell flex layout
- editor content spacing
- section stacks
- truncation and overflow
- standard button alignment
- grid column changes at breakpoints
- selected and hovered states that are directly tied to assign state
Keep Dynamic Class Lists Explicit
LiveView templates should build classes with arrays where state is already in assigns.
Example pattern:
class={[
"flex items-center gap-2 px-3 py-2 text-sm",
selected? && "bg-[var(--color-selected-bg)] text-white",
disabled? && "opacity-50 pointer-events-none"
]}
Do Not Hide Structure Behind Giant Custom Class Trees
Avoid re-creating the current app.css by keeping every nested selector and merely moving it into assets/css.
If a selector exists only because the old CSS owned all layout, move that responsibility into HEEx.
Tailwind Source Conventions
app.css
assets/css/app.css should use Tailwind v4 import syntax.
source(none) disables Tailwind's automatic content detection so only directories listed via @source are scanned. Every directory that ships HEEx or class strings MUST be listed. Audit lib/ for additional renderers (preview, generation, MCP surfaces) and add @source lines for any that emit class names consumed at build time.
Runtime-driven values (e.g. resizable panel widths that change via CSS variable assignment from JS) MUST stay as plain CSS custom properties under :root or a scoped selector. @theme values are baked at build time and are not appropriate for values mutated at runtime. Use @theme for stable design tokens (colors, font sizes, spacing scale extensions) that should produce utility classes; use :root { --foo: … } for everything that the app writes to at runtime.
Suggested structure:
@import "tailwindcss" source(none);
@source "../css";
@source "../js";
@source "../../lib/bds/desktop";
@theme {
/* app tokens */
}
@layer base {
/* html, body, root shell defaults */
}
@import "./shell.css";
@import "./sidebar.css";
@import "./tabs.css";
@import "./editor.css";
@import "./forms.css";
@import "./panel.css";
@import "./assistant.css";
@import "./overlays.css";
@import "./menu_editor.css";
@import "./media_editor.css";
@import "./utilities.css";
Component CSS Rules
When writing component CSS in imported files:
- use
@layer componentsfor shared semantic classes - use
@layer utilitiesonly for narrowly reusable custom utilities - keep selectors shallow
- avoid giant descendant chains unless required by generated HTML structure or overlay mechanics
- prefer
@applysparingly and only for stable component classes, not as a substitute for writing HEEx utilities
Current UI Structure Reference
The implementation agent must use the current monolith as a source map, not as the final architecture.
The current stylesheet is priv/ui/app.css and is approximately 8.5k lines.
Use the following region map when carrying styling over.
Current app.css region map
- Lines
1-140: root variables, base element reset, buttons, top-level shell defaults - Lines
141-415: app shell and window titlebar.app,.app-main,.app-content.window-titlebar*
- Lines
416-623: activity bar and sidebar shells.activity-bar*.sidebar-shell*,.assistant-sidebar-shell*.sidebar*,.assistant-sidebar*.resizable-panel-divider
- Lines
624-812: workbench tab bar.tab-bar*,.tab*
- Lines
813-1100: shared editor shell and editor tabs.editor-shell,.editor-frame,.editor-main,.editor-meta.editor-toolbar*.post-editor .editor-header,.editor-tabs,.editor-tab*
- Lines
1100-1700: post editor forms and metadata/media panel.post-editor .editor-content.post-editor .editor-field*.post-editor .post-editor-input,.post-editor-textarea.tag-input*,.tag-chip*- media insertion and post media list
- Line
1691onward: first responsive collapse for editor/media layout - Lines
1736-1833: early shell/gallery overlay and insert-media grid rules - Lines
1833+: more mobile/narrow viewport adjustments - Lines
1950-2263: status bar and shell footer controls.status-bar*.project-selector*
- Lines
2264-2599: overlay root and modal system.overlay-root.ai-suggestions-modal*.insert-modal*.language-picker-modal*.confirm-delete-modal*.gallery-overlay*
- Lines
2600-2876: menu editor.menu-editor-*
- Lines
2889+: media editor[data-testid="media-editor"] *
- Lines
3458+: style/theme picker surface.style-theme-*
- Lines
4958,6722,8268,8514: later breakpoint-specific adjustments for desktop shell and advanced editors
Treat those ranges as source material to be redistributed into the new Tailwind source layout.
Current HEEx And Component Surface Reference
The styling rewrite needs to move with the component structure, not only with CSS selectors.
Important current rendering surfaces:
lib/bds/desktop/shell_live.ex- top-level workbench render entry
lib/bds/desktop/shell_live/sidebar_components.ex- sidebar search, archive tree, tag/category chips, nav/settings lists, load more
lib/bds/desktop/shell_live/panel_renderer.ex- tasks, output, git log, panel toolbars
lib/bds/desktop/shell_live/post_editor.ex- post editor render surface
lib/bds/desktop/shell_live/media_editor.ex- media editor render surface
lib/bds/desktop/shell_live/script_editor.ex- script editor render surface
lib/bds/desktop/shell_live/template_editor.ex- template editor render surface
lib/bds/desktop/shell_live/chat_editor.ex- assistant/chat surface
lib/bds/desktop/shell_live/menu_editor.ex- menu editor tree surface
Implementation rule:
- move simple layout and state styling into these HEEx/component surfaces
- keep authored CSS for shared primitives and complex desktop behavior
Desktop-Specific Styling Rules
The app is a desktop shell, not a normal browser page.
The implementation must preserve:
- draggable titlebar regions
- non-draggable controls inside the titlebar
- local, app-like split-pane behavior
- fixed-height titlebar, tabs, and status bar rails
- overlay stacking over the shell
- editor and sidebar widths controlled by CSS variables where appropriate
- visual parity for assistant/sidebar/panel open and closed states
Do not optimize for tiny public web payloads at the expense of shell clarity. Do optimize for maintainability, explicit component ownership, and predictable desktop behavior.
Implementation Phases
Phase 0: Tests And Validation Baseline
- Add a smoke test that requests
/assets/app.cssand/assets/app.jsand asserts a 200 with non-empty body served frompriv/static/assets. - Add a render snapshot test for the top-level shell HEEx so class-string regressions during the rewrite are caught.
- Establish that every phase ends with clean
mix compile --warnings-as-errors,mix test, andmix dialyzerruns (per repo AGENTS.md).
Phase 1: Install Phoenix Asset Tooling
- add Tailwind and esbuild dependencies
- create
assets/css/app.cssandassets/js/app.js - configure
config/config.exs,config/dev.exs, andmix.exs - switch endpoint/layouts to generated assets
- keep current visuals as close as possible
Phase 2: Split The Monolith Into Source Modules
- move the current
priv/ui/app.cssinto the proposedassets/css/*.cssmodules - keep selectors mostly intact at first
- copy raw selectors only; do NOT rewrite to
@applyin this phase - defer all
@applyand utility-extraction decisions to Phase 3 - verify the desktop shell still renders correctly
Phase 3: Move Common Layout Into HEEx
- rewrite top-level shell markup, tabs, headers, and common forms to use Tailwind classes directly
- reduce selector nesting
- keep only the thin semantic CSS layer
Phase 4: Normalize Shared Primitives
- standardize buttons
- standardize inputs and textareas
- standardize tabs and badges
- standardize panel entries and empty states
Phase 5: Finish Desktop-Specific Surfaces
- overlays
- menu editor drag/drop states
- media preview/detail layouts
- assistant/chat surfaces
- narrow viewport behavior
Acceptance Criteria
The rewrite is successful when:
- assets are built through Phoenix default tooling
- the desktop endpoint serves generated assets from
priv/static - the visual shell matches the existing app closely enough for feature work to continue
- the current
priv/ui/app.cssis no longer the source of truth - the new styling is split into source modules with clear ownership
- HEEx markup owns common layout and state classes
- authored CSS is limited to tokens, desktop primitives, and complex selectors
- Tailwind v4 and esbuild versions are explicitly pinned in
config/config.exs - no Node.js /
package.jsonis introduced underassets/ - no
mix phx.digeststep exists; generated asset filenames remain stable - inline SVG iconography from the existing UI is preserved (no Heroicons dep)
mix compile --warnings-as-errors,mix test, andmix dialyzerare clean after every phase
Non-Goals
These are not goals of the rewrite:
- blindly replacing every old class with utilities in one pass
- preserving the exact old selector tree
- introducing CSS modules or a React-style styling system
- optimizing for generic web landing-page concerns
- changing product behavior unrelated to styling and asset delivery
Guidance For The Coding Agent
When implementing this plan:
- start by wiring Phoenix asset tooling, not by rewriting 8k lines of CSS in place
- preserve runtime behavior first, then simplify
- move layout decisions to HEEx only when they become clearer there
- keep desktop-specific mechanics in CSS
- use the current
priv/ui/app.cssregion map as a source index, not as a blueprint for the final architecture