diff --git a/.formatter.exs b/.formatter.exs index 6e1d668eed..43bc166c7b 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -2,7 +2,7 @@ import_deps: [:phoenix], plugins: [Phoenix.LiveView.HTMLFormatter], inputs: [ - "{config,lib,rel,test}/**/*.{heex,ex,eex,exs}", + "{config,lib,rel,storybook,test}/**/*.{heex,ex,eex,exs}", "*.{heex,ex,exs}" ] ] diff --git a/.gitignore b/.gitignore index 303d60774e..63623e2526 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ node_modules # we ignore certain directories in priv/static. /priv/static/css /priv/static/js +/priv/static/assets # The config/prod.secret.exs file by default contains sensitive # data and you should not commit it into version control. diff --git a/assets/css/storybook.css b/assets/css/storybook.css new file mode 100644 index 0000000000..d80c7e2ff2 --- /dev/null +++ b/assets/css/storybook.css @@ -0,0 +1,15 @@ +/* This is your custom storybook stylesheet. */ +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; + +/* + * Put your component styling within the Tailwind utilities layer. + * See the https://hexdocs.pm/phoenix_storybook/sandboxing.html guide for more info. + */ + +@layer utilities { + * { + font-family: system-ui; + } +} diff --git a/assets/js/storybook.js b/assets/js/storybook.js new file mode 100644 index 0000000000..11daed5548 --- /dev/null +++ b/assets/js/storybook.js @@ -0,0 +1,34 @@ +// If your components require any hooks or custom uploaders, or if your pages +// require connect parameters, uncomment the following lines and declare them as +// such: +// +// import * as Hooks from "./hooks"; +// import * as Params from "./params"; +// import * as Uploaders from "./uploaders"; + +// (function () { +// window.storybook = { Hooks, Params, Uploaders }; +// })(); + +// If your components require alpinejs, you'll need to start +// alpine after the DOM is loaded and pass in an onBeforeElUpdated +// +// import Alpine from 'alpinejs' +// window.Alpine = Alpine +// document.addEventListener('DOMContentLoaded', () => { +// window.Alpine.start(); +// }); + +// (function () { +// window.storybook = { +// LiveSocketOptions: { +// dom: { +// onBeforeElUpdated(from, to) { +// if (from._x_dataStack) { +// window.Alpine.clone(from, to) +// } +// } +// } +// } +// }; +// })(); diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index be9b51dea7..f91c3b445c 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -16,6 +16,7 @@ module.exports = { preflight: false }, blocklist: ["container", "collapse"], + important: ".dotcom-web", content: [ ...content, "./js/**/*.js", diff --git a/config/config.exs b/config/config.exs index 98cae89def..2ff731be08 100644 --- a/config/config.exs +++ b/config/config.exs @@ -37,6 +37,26 @@ config :sentry, config :mbta_metro, custom_icons: ["#{File.cwd!()}/priv/static/icon-svg/*"] +config :esbuild, + version: "0.17.11", + default: [ + # args: ~w(js/app.js js/storybook.js --bundle --target=es2017 --outdir=../priv/static/assets), + args: ~w(js/storybook.js --bundle --target=es2017 --outdir=../priv/static/assets), + cd: Path.expand("../assets", __DIR__), + env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} + ] + +config :tailwind, + version: "3.4.6", + storybook: [ + args: ~w( + --config=tailwind.config.js + --input=css/storybook.css + --output=../priv/static/assets/storybook.css + ), + cd: Path.expand("../assets", __DIR__) + ] + for config_file <- Path.wildcard("config/{deps,dotcom}/*.exs") do import_config("../#{config_file}") end diff --git a/config/deps/endpoint.exs b/config/deps/endpoint.exs index 78f876eedf..0775b9eabd 100644 --- a/config/deps/endpoint.exs +++ b/config/deps/endpoint.exs @@ -31,7 +31,10 @@ if config_env() == :dev do debug_errors: true, code_reloader: true, check_origin: false, - watchers: [npm: ["run", "webpack:watch", cd: Path.expand("../../assets/", __DIR__)]], + watchers: [ + npm: ["run", "webpack:watch", cd: Path.expand("../../assets/", __DIR__)], + storybook_tailwind: {Tailwind, :install_and_run, [:storybook, ~w(--watch)]} + ], live_reload: [ patterns: [ ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, @@ -39,7 +42,8 @@ if config_env() == :dev do ~r{lib/dotcom_web/components/.*(ex)$}, ~r{lib/dotcom_web/views/.*(ex)$}, ~r{lib/dotcom_web/templates/.*(heex|eex)$}, - ~r{lib/dotcom_web/live/.*(heex|ex)$} + ~r{lib/dotcom_web/live/.*(heex|ex)$}, + ~r"storybook/.*(exs)$" ] ] end diff --git a/deploy/dotcom/dev/Dockerfile b/deploy/dotcom/dev/Dockerfile index 2feab98ac8..4a392b2ae6 100644 --- a/deploy/dotcom/dev/Dockerfile +++ b/deploy/dotcom/dev/Dockerfile @@ -19,4 +19,9 @@ RUN mix deps.get RUN mix deps.compile RUN npm install --prefix assets --omit=optional --audit false --fund false --loglevel verbose --ignore-scripts +COPY priv priv +COPY lib lib +COPY assets assets +COPY storybook storybook + CMD mix deps.get && elixir --sname $SNAME --cookie foobarbaz -S mix phx.server diff --git a/lib/dotcom/body_tag.ex b/lib/dotcom/body_tag.ex index 27c473b4c6..621c0a015c 100644 --- a/lib/dotcom/body_tag.ex +++ b/lib/dotcom/body_tag.ex @@ -21,6 +21,7 @@ defmodule Dotcom.BodyTag do defp class_name(conn) do [ + "dotcom-web", javascript_class(), mticket_class(conn), preview_class(conn) diff --git a/lib/dotcom_web.ex b/lib/dotcom_web.ex index d9252994e8..0c19682e33 100644 --- a/lib/dotcom_web.ex +++ b/lib/dotcom_web.ex @@ -16,7 +16,7 @@ defmodule DotcomWeb do below. """ - def static_paths, do: ~w(css js fonts icon-svg images favicon robots.txt) + def static_paths, do: ~w(assets css js fonts icon-svg images favicon robots.txt) def controller do quote do diff --git a/lib/dotcom_web/router.ex b/lib/dotcom_web/router.ex index 4ccb500049..4f502ea6f6 100644 --- a/lib/dotcom_web/router.ex +++ b/lib/dotcom_web/router.ex @@ -4,6 +4,8 @@ defmodule DotcomWeb.Router do use DotcomWeb, :router use Plug.ErrorHandler + import PhoenixStorybook.Router + alias DotcomWeb.ControllerHelpers @impl Plug.ErrorHandler @@ -314,6 +316,16 @@ defmodule DotcomWeb.Router do get("/about_the_mbta/news_events", Redirector, to: "/news") end + scope "/" do + storybook_assets() + end + + scope "/", DotcomWeb do + # pipe_through([:secure, :browser, :browser_live, :basic_auth]) + pipe_through(:browser) + live_storybook("/storybook", backend_module: Elixir.DotcomWeb.Storybook) + end + scope "/", DotcomWeb do pipe_through([:secure, :browser]) diff --git a/lib/dotcom_web/storybook.ex b/lib/dotcom_web/storybook.ex new file mode 100644 index 0000000000..9554beda7f --- /dev/null +++ b/lib/dotcom_web/storybook.ex @@ -0,0 +1,9 @@ +defmodule DotcomWeb.Storybook do + use PhoenixStorybook, + otp_app: :dotcom_web, + content_path: Path.expand("../../storybook", __DIR__), + # assets path are remote path, not local file-system paths + css_path: "/assets/storybook.css", + js_path: "/assets/storybook.js", + sandbox_class: "dotcom-web" +end diff --git a/mix.exs b/mix.exs index 61c74fc75e..1f3439a1f7 100644 --- a/mix.exs +++ b/mix.exs @@ -24,6 +24,7 @@ defmodule DotCom.Mixfile do ignore_warnings: ".dialyzer.ignore-warnings" ], deps: deps(), + aliases: aliases(), # docs name: "MBTA Website", @@ -84,6 +85,7 @@ defmodule DotCom.Mixfile do {:ecto, "3.12.4"}, {:eflame, "1.0.1", only: :dev}, {:ehmon, [github: "mbta/ehmon", only: :prod]}, + {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, {:ex_doc, "0.34.2", only: :dev}, {:ex_machina, "2.8.0", only: [:dev, :test]}, {:ex_unit_summary, "0.1.0", only: [:dev, :test]}, @@ -124,6 +126,7 @@ defmodule DotCom.Mixfile do # currently release candidate, but used in Phoenix 1.7 generator: https://github.com/phoenix-diff/phoenix-diff/blob/f320791d24bc3248fbdde557978235829313aa06/priv/data/sample-app/1.7.14/default/mix.exs#L42 {:phoenix_live_view, "~> 1.0.0-rc.6", override: true}, {:phoenix_pubsub, "2.1.3"}, + {:phoenix_storybook, "~> 0.6.0"}, {:phoenix_view, "~> 2.0"}, {:plug, "1.16.1"}, {:plug_cowboy, "2.7.2"}, @@ -143,6 +146,7 @@ defmodule DotCom.Mixfile do {:server_sent_event_stage, "1.2.1"}, {:sizeable, "1.0.2"}, {:sweet_xml, "0.7.4", only: [:dev, :prod]}, + {:tailwind, "~> 0.2.0", runtime: Mix.env() == :dev}, {:telemetry, "1.3.0", override: true}, {:telemetry_metrics, "1.0.0", override: true}, {:telemetry_metrics_splunk, "0.0.6-alpha"}, @@ -157,4 +161,16 @@ defmodule DotCom.Mixfile do {:ymlr, "5.1.3", only: [:dev]} ] end + + defp aliases do + [ + setup: ["deps.get", "assets.setup", "assets.build"], + "assets.deploy": [ + "tailwind storybook --minify", + "phx.digest" + ], + "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], + "assets.build": ["tailwind default", "esbuild default"] + ] + end end diff --git a/mix.lock b/mix.lock index 2658d87554..d97751dc03 100644 --- a/mix.lock +++ b/mix.lock @@ -27,6 +27,7 @@ "eini": {:hex, :eini_beam, "2.2.4", "02143b1dce4dda4243248e7d9b3d8274b8d9f5a666445e3d868e2cce79e4ff22", [:rebar3], [], "hexpm", "12de479d144b19e09bb92ba202a7ea716739929afdf9dff01ad802e2b1508471"}, "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"}, "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "ex_unit_summary": {:hex, :ex_unit_summary, "0.1.0", "7b0352afc5e6a933c805df0a539b66b392ac12ba74d8b208db7d83f77cb57049", [:mix], [], "hexpm", "8c87d0deade3657102902251d2ec60b5b94560004ce0e2c2fa5b466232716bd6"}, @@ -108,6 +109,7 @@ "slipstream": {:hex, :slipstream, "1.1.1", "7e56f62f1a9ee81351e3c36f57b9b187e00dc2f470e70ba46ea7ad16e80b061f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mint_web_socket, "~> 0.2 or ~> 1.0", [hex: :mint_web_socket, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.1 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c20e420cde1654329d38ec3aa1c0e4debbd4c91ca421491e7984ad4644e638a6"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"}, + "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, "telemetry_metrics_splunk": {:hex, :telemetry_metrics_splunk, "0.0.6-alpha", "76812c1ece239955d1d9c7a0556fd51ccaebb89abcec357b9c9dd67e619057b4", [:mix], [{:finch, "~> 0.18", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:recase, "~> 0.8", [hex: :recase, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "2150e2112b846cf68bf604b871386d68114fc289472d9a4308146ad7b3fbeee6"}, diff --git a/storybook/_root.index.exs b/storybook/_root.index.exs new file mode 100644 index 0000000000..f0bf35c3cb --- /dev/null +++ b/storybook/_root.index.exs @@ -0,0 +1,16 @@ +defmodule Storybook.Root do + # See https://hexdocs.pm/phoenix_storybook/PhoenixStorybook.Index.html for full index + # documentation. + + use PhoenixStorybook.Index + + def folder_icon, do: {:fa, "book-open", :light, "psb-mr-1"} + def folder_name, do: "Storybook" + + def entry("welcome") do + [ + name: "Welcome Page", + icon: {:fa, "hand-wave", :thin} + ] + end +end diff --git a/storybook/components/route_symbol.story.exs b/storybook/components/route_symbol.story.exs new file mode 100644 index 0000000000..6efea583b9 --- /dev/null +++ b/storybook/components/route_symbol.story.exs @@ -0,0 +1,140 @@ +defmodule DotcomWeb.Storybook.RouteSymbol do + use PhoenixStorybook.Story, :component + + alias Routes.Route + + def function do + &DotcomWeb.Components.RouteSymbols.route_symbol/1 + end + + def variations do + [ + %Variation{ + id: :blue_line, + description: "Subway Blue Line", + attributes: %{ + route: %Route{type: 1, id: "Blue"} + } + }, + %Variation{ + id: :green_line, + description: "Subway Green Line", + attributes: %{ + route: %Route{type: 1, id: "Green"} + } + }, + %Variation{ + id: :green_line_b, + description: "Subway Green Line B Branch", + attributes: %{ + route: %Route{type: 1, id: "Green-B"} + } + }, + %Variation{ + id: :green_line_c, + description: "Subway Green Line C Branch", + attributes: %{ + route: %Route{type: 1, id: "Green-C"} + } + }, + %Variation{ + id: :green_line_d, + description: "Subway Green Line D Branch", + attributes: %{ + route: %Route{type: 1, id: "Green-D"} + } + }, + %Variation{ + id: :green_line_e, + description: "Subway Green Line E Branch", + attributes: %{ + route: %Route{type: 1, id: "Green-E"} + } + }, + %Variation{ + id: :orange_line, + description: "Subway Orange Line", + attributes: %{ + route: %Route{type: 1, id: "Orange"} + } + }, + %Variation{ + id: :red_line, + description: "Subway Red Line", + attributes: %{ + route: %Route{type: 1, id: "Red"} + } + }, + %Variation{ + id: :mattapan_line, + description: "Subway Mattapan Line", + attributes: %{ + route: %Route{type: 1, id: "Mattapan"} + } + }, + %Variation{ + id: :bus, + description: "Bus route icon", + attributes: %{ + route: %Route{type: 3, name: "39"} + } + }, + %Variation{ + id: :silver_line, + description: "Silver Line route icon", + attributes: %{ + route: %Route{type: 3, id: "741", name: "SL1"} + } + }, + %Variation{ + id: :commuter_rail, + description: "Commuter Rail route icon", + attributes: %{ + route: %Route{type: 2} + } + }, + %Variation{ + id: :ferry, + description: "Ferry route icon", + attributes: %{ + route: %Route{type: 4} + } + }, + %Variation{ + id: :massport_shuttle, + description: "Massport shuttle route icon", + attributes: %{ + route: %Route{external_agency_name: "Massport", name: "33"} + } + }, + %Variation{ + id: :logan_express_bb, + description: "Logan Express - Back Bay", + attributes: %{ + route: %Route{external_agency_name: "Logan Express", name: "BB"} + } + }, + %Variation{ + id: :logan_express_bt, + description: "Logan Express - Braintree", + attributes: %{ + route: %Route{external_agency_name: "Logan Express", name: "BT"} + } + }, + %Variation{ + id: :logan_express_dv, + description: "Logan Express - Danvers", + attributes: %{ + route: %Route{external_agency_name: "Logan Express", name: "DV"} + } + }, + %Variation{ + id: :logan_express_wo, + description: "Logan Express - Worcester", + attributes: %{ + route: %Route{external_agency_name: "Logan Express", name: "WO"} + } + } + ] + end +end diff --git a/storybook/welcome.story.exs b/storybook/welcome.story.exs new file mode 100644 index 0000000000..8714a3e0f3 --- /dev/null +++ b/storybook/welcome.story.exs @@ -0,0 +1,100 @@ +defmodule Storybook.MyPage do + # See https://hexdocs.pm/phoenix_storybook/PhoenixStorybook.Story.html for full story + # documentation. + use PhoenixStorybook.Story, :page + + def doc, do: "Your very first steps into using Phoenix Storybook" + + # Declare an optional tab-based navigation in your page: + def navigation do + [ + {:welcome, "Welcome", {:fa, "hand-wave", :thin}}, + {:components, "Components", {:fa, "toolbox", :thin}}, + {:sandboxing, "Sandboxing", {:fa, "box-check", :thin}}, + {:icons, "Icons", {:fa, "icons", :thin}} + ] + end + + # This is a dummy function that you should replace with your own HEEx content. + def render(assigns = %{tab: :welcome}) do + ~H""" +
+

+ We generated your storybook with an example of a page and a component. + Explore the generated *.story.exs + files in your /storybook + directory. When you're ready to add your own, just drop your new story & index files into the same directory and refresh your storybook. +

+ +

+ Here are a few docs you might be interested in: +

+ + <.description_list items={[ + {"Create a new Story", doc_link("Story")}, + {"Display components using Variations", doc_link("Stories.Variation")}, + {"Group components using VariationGroups", doc_link("Stories.VariationGroup")}, + {"Organize the sidebar with Index files", doc_link("Index")} + ]} /> + +

+ This should be enough to get you started, but you can use the tabs in the upper-right corner of this page to check out advanced usage guides. +

+
+ """ + end + + def render(assigns = %{tab: guide}) when guide in ~w(components sandboxing icons)a do + assigns = + assign(assigns, + guide: guide, + guide_content: PhoenixStorybook.Guides.markup("#{guide}.md") + ) + + ~H""" +

+ + This and other guides are also available on HexDocs. + +

+
+ <%= Phoenix.HTML.raw(@guide_content) %> +
+ """ + end + + defp description_list(assigns) do + ~H""" +
+
+
+ <%= for {dt, link} <- @items do %> +
+
+ <%= dt %> +
+
+ + <%= link %> + +
+
+ <% end %> +
+
+
+ """ + end + + defp doc_link(page) do + "https://hexdocs.pm/phoenix_storybook/PhoenixStorybook.#{page}.html" + end +end