From 9805b1509b2f82c3e23bc258e355e6f39c7bcf0d Mon Sep 17 00:00:00 2001 From: Mike Zornek Date: Fri, 4 Oct 2024 09:44:02 -0400 Subject: [PATCH] feat: adding the Phoenix Storyboard project (#88) --- .formatter.exs | 10 +- assets/css/storybook.css | 15 +++ assets/js/storybook.js | 35 ++++++ assets/tailwind.config.js | 1 + config/config.exs | 10 +- config/dev.exs | 6 +- .../components/layouts/root.html.heex | 2 +- lib/flick_web/router.ex | 14 ++- lib/flick_web/storybook.ex | 13 +++ mix.exs | 22 ++-- mix.lock | 8 ++ storybook/_root.index.exs | 16 +++ storybook/core_components/button.story.exs | 28 +++++ storybook/core_components/flash.story.exs | 47 ++++++++ storybook/core_components/header.story.exs | 37 +++++++ storybook/core_components/input.story.exs | 41 +++++++ storybook/core_components/list.story.exs | 17 +++ storybook/core_components/modal.story.exs | 25 +++++ storybook/core_components/table.story.exs | 41 +++++++ storybook/examples/core_components.story.exs | 84 +++++++++++++++ storybook/welcome.story.exs | 100 ++++++++++++++++++ 21 files changed, 559 insertions(+), 13 deletions(-) create mode 100644 assets/css/storybook.css create mode 100644 assets/js/storybook.js create mode 100644 lib/flick_web/storybook.ex create mode 100644 storybook/_root.index.exs create mode 100644 storybook/core_components/button.story.exs create mode 100644 storybook/core_components/flash.story.exs create mode 100644 storybook/core_components/header.story.exs create mode 100644 storybook/core_components/input.story.exs create mode 100644 storybook/core_components/list.story.exs create mode 100644 storybook/core_components/modal.story.exs create mode 100644 storybook/core_components/table.story.exs create mode 100644 storybook/examples/core_components.story.exs create mode 100644 storybook/welcome.story.exs diff --git a/.formatter.exs b/.formatter.exs index ef8840c..7182924 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -2,5 +2,13 @@ import_deps: [:ecto, :ecto_sql, :phoenix], subdirectories: ["priv/*/migrations"], plugins: [Phoenix.LiveView.HTMLFormatter], - inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] + inputs: [ + "*.{heex,ex,exs}", + "{config,lib,test}/**/*.{heex,ex,exs}", + "priv/*/seeds.exs", + "storybook/**/*.exs" + ], + locals_without_parens: [ + live_storybook: 2 + ] ] diff --git a/assets/css/storybook.css b/assets/css/storybook.css new file mode 100644 index 0000000..d80c7e2 --- /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 0000000..9ad9805 --- /dev/null +++ b/assets/js/storybook.js @@ -0,0 +1,35 @@ +// 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 015b01c..f556b44 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -6,6 +6,7 @@ const fs = require("fs") const path = require("path") module.exports = { + important: ".flick-web", content: [ "./js/**/*.js", "../lib/flick_web.ex", diff --git a/config/config.exs b/config/config.exs index eaf26c5..7d405c5 100644 --- a/config/config.exs +++ b/config/config.exs @@ -44,7 +44,7 @@ config :esbuild, version: "0.17.11", flick: [ args: - ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), + ~w(js/app.js js/storybook.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), cd: Path.expand("../assets", __DIR__), env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} ] @@ -59,6 +59,14 @@ config :tailwind, --output=../priv/static/assets/app.css ), cd: Path.expand("../assets", __DIR__) + ], + storybook: [ + args: ~w( + --config=tailwind.config.js + --input=css/storybook.css + --output=../priv/static/assets/storybook.css + ), + cd: Path.expand("../assets", __DIR__) ] # Configures Elixir's Logger diff --git a/config/dev.exs b/config/dev.exs index 53b4ac2..6bb0b30 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -26,7 +26,8 @@ config :flick, FlickWeb.Endpoint, secret_key_base: "3A5gt96GzZt6No8BUZpuicElbO80urqkhj8OWMwEyJDefaDnxm3JfDWkwb8FM/Ak", watchers: [ esbuild: {Esbuild, :install_and_run, [:flick, ~w(--sourcemap=inline --watch)]}, - tailwind: {Tailwind, :install_and_run, [:flick, ~w(--watch)]} + tailwind: {Tailwind, :install_and_run, [:flick, ~w(--watch)]}, + storybook_tailwind: {Tailwind, :install_and_run, [:storybook, ~w(--watch)]} ] # ## SSL Support @@ -58,7 +59,8 @@ config :flick, FlickWeb.Endpoint, patterns: [ ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", ~r"priv/gettext/.*(po)$", - ~r"lib/flick_web/(controllers|live|components)/.*(ex|heex)$" + ~r"lib/flick_web/(controllers|live|components)/.*(ex|heex)$", + ~r"storybook/.*(exs)$" ] ] diff --git a/lib/flick_web/components/layouts/root.html.heex b/lib/flick_web/components/layouts/root.html.heex index a688d4f..057975c 100644 --- a/lib/flick_web/components/layouts/root.html.heex +++ b/lib/flick_web/components/layouts/root.html.heex @@ -11,7 +11,7 @@ - + <%= @inner_content %> diff --git a/lib/flick_web/router.ex b/lib/flick_web/router.ex index 22cfca0..a4296e6 100644 --- a/lib/flick_web/router.ex +++ b/lib/flick_web/router.ex @@ -2,6 +2,7 @@ defmodule FlickWeb.Router do use FlickWeb, :router + import PhoenixStorybook.Router import Plug.BasicAuth pipeline :browser do @@ -14,8 +15,13 @@ defmodule FlickWeb.Router do # Tailwind uses SVG data URLs for icons, # so we need to allow them with `img-src`. # https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP + # + # To avoid web console issues with Phoenix Storybook we've added + # `style-src 'self' 'unsafe-inline'` which feels unfortunate and + # might be reconsidered. plug :put_secure_browser_headers, %{ - "content-security-policy" => "default-src 'self'; img-src 'self' data:" + "content-security-policy" => + "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'" } end @@ -27,6 +33,10 @@ defmodule FlickWeb.Router do plug :accepts, ["json"] end + scope "/" do + storybook_assets() + end + scope "/admin", FlickWeb do pipe_through [:browser, :admin] @@ -41,6 +51,8 @@ defmodule FlickWeb.Router do live "/ballot/:url_slug/:secret", Ballots.ViewerLive, :edit live "/ballot/:url_slug/:secret/edit", Ballots.EditorLive, :edit live "/ballot/:url_slug", Vote.VoteCaptureLive, :new + + live_storybook "/storybook", backend_module: Elixir.FlickWeb.Storybook end # Enable LiveDashboard and Swoosh mailbox preview in development diff --git a/lib/flick_web/storybook.ex b/lib/flick_web/storybook.ex new file mode 100644 index 0000000..20f123e --- /dev/null +++ b/lib/flick_web/storybook.ex @@ -0,0 +1,13 @@ +defmodule FlickWeb.Storybook do + @moduledoc """ + Provides a Phoenix Storybook configuration for the FlickWeb application. + """ + + use PhoenixStorybook, + otp_app: :flick_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: "flick-web" +end diff --git a/mix.exs b/mix.exs index fb31e53..dfb4235 100644 --- a/mix.exs +++ b/mix.exs @@ -54,6 +54,9 @@ defmodule Flick.MixProject do # For security scans. {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false}, + # For UI component documentation. + {:phoenix_storybook, "~> 0.6.0"}, + # Unorganized {:bandit, "~> 1.2"}, {:dns_cluster, "~> 0.1.1"}, @@ -62,13 +65,17 @@ defmodule Flick.MixProject do {:finch, "~> 0.13"}, {:floki, ">= 0.30.0", only: :test}, {:gettext, "~> 0.20"}, - {:heroicons, - github: "tailwindlabs/heroicons", - tag: "v2.1.1", - sparse: "optimized", - app: false, - compile: false, - depth: 1}, + { + :heroicons, + # The `override` setting is needed for `phoenix_storybook`. + github: "tailwindlabs/heroicons", + tag: "v2.1.1", + sparse: "optimized", + app: false, + compile: false, + depth: 1, + override: true + }, {:jason, "~> 1.2"}, {:phoenix_ecto, "~> 4.4"}, {:phoenix_html, "~> 4.0"}, @@ -101,6 +108,7 @@ defmodule Flick.MixProject do "assets.deploy": [ "tailwind flick --minify", "esbuild flick --minify", + "tailwind storybook --minify", "phx.digest" ] ] diff --git a/mix.lock b/mix.lock index a0540d2..bda30c0 100644 --- a/mix.lock +++ b/mix.lock @@ -28,22 +28,30 @@ "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_eex": {:hex, :makeup_eex, "0.1.2", "93a5ef3d28ed753215dba2d59cb40408b37cccb4a8205e53ef9b5319a992b700", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.16 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.1.0 or ~> 1.0", [hex: :makeup_html, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "6140eafb28215ad7182282fd21d9aa6dcffbfbe0eb876283bc6b768a6c57b0c3"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [: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", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_html": {:hex, :makeup_html, "0.1.1", "c3d4abd39d5f7e925faca72ada6e9cc5c6f5fa7cd5bc0158315832656cf14d7f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "44f2a61bc5243645dd7fafeaa6cc28793cd22f3c76b861e066168f9a5b2c26a4"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [: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", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, "mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {: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, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, + "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.17", "f396bbdaf4ba227b82251eb75ac0afa6b3da5e509bc0d030206374237dfc9450", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.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", "a61d741ffb78c85fdbca0de084da6a48f8ceb5261a79165b5a0b59e5f65ce98b"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix_storybook": {:hex, :phoenix_storybook, "0.6.4", "d7bfdf6a20214251ff7453cbcb9de5f6f8a7db606f9c21846a87ba09058d8f0e", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:heroicons, "~> 0.5", [hex: :heroicons, repo: "hexpm", optional: true]}, {:jason, "~> 1.3", [hex: :jason, repo: "hexpm", optional: true]}, {:makeup_eex, "~> 0.1.0", [hex: :makeup_eex, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "> 0.18.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "a63669c010e638882d287aae9c2cfd4a2c64d68e05f85403e213101283a74d3f"}, "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_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [: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", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"}, diff --git a/storybook/_root.index.exs b/storybook/_root.index.exs new file mode 100644 index 0000000..f0bf35c --- /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/core_components/button.story.exs b/storybook/core_components/button.story.exs new file mode 100644 index 0000000..128a6a8 --- /dev/null +++ b/storybook/core_components/button.story.exs @@ -0,0 +1,28 @@ +defmodule Storybook.CoreComponents.Button do + use PhoenixStorybook.Story, :component + + def function, do: &FlickWeb.CoreComponents.button/1 + + def variations do + [ + %Variation{ + id: :default, + slots: ["Button"] + }, + %Variation{ + id: :custom_class, + attributes: %{ + class: "rounded-full bg-indigo-500 hover:bg-indigo-600" + }, + slots: ["Disabled"] + }, + %Variation{ + id: :disabled, + attributes: %{ + disabled: true + }, + slots: ["Disabled"] + } + ] + end +end diff --git a/storybook/core_components/flash.story.exs b/storybook/core_components/flash.story.exs new file mode 100644 index 0000000..85b8290 --- /dev/null +++ b/storybook/core_components/flash.story.exs @@ -0,0 +1,47 @@ +defmodule Storybook.CoreComponents.Flash do + use PhoenixStorybook.Story, :component + alias FlickWeb.CoreComponents + + def function, do: &CoreComponents.flash/1 + def imports, do: [{CoreComponents, [button: 1, show: 1]}] + + def template do + """ + <.button phx-click={show("#:variation_id")} psb-code-hidden> + Open flash + + <.psb-variation/> + """ + end + + def variations do + [ + %Variation{ + id: :info_no_title, + attributes: %{ + kind: :info, + hidden: true + }, + slots: ["Info message"] + }, + %Variation{ + id: :error_with_title, + attributes: %{ + kind: :error, + hidden: true, + title: "Flash title" + }, + slots: ["Error message"] + }, + %Variation{ + id: :no_close_button, + attributes: %{ + kind: :info, + hidden: true, + close: false + }, + slots: ["Info message"] + } + ] + end +end diff --git a/storybook/core_components/header.story.exs b/storybook/core_components/header.story.exs new file mode 100644 index 0000000..594b7f6 --- /dev/null +++ b/storybook/core_components/header.story.exs @@ -0,0 +1,37 @@ +defmodule Storybook.CoreComponents.Header do + use PhoenixStorybook.Story, :component + alias FlickWeb.CoreComponents + + def function, do: &CoreComponents.header/1 + def imports, do: [{CoreComponents, button: 1}] + + def variations do + [ + %Variation{ + id: :default, + slots: [ + "Hello World" + ] + }, + %Variation{ + id: :with_a_subtitle, + slots: [ + "Hello World", + "<:subtitle>I'm a header subtitle" + ] + }, + %Variation{ + id: :with_actions, + slots: [ + "Hello World", + "<:subtitle>I'm a header subtitle", + """ + <:actions> + <.button>Link + + """ + ] + } + ] + end +end diff --git a/storybook/core_components/input.story.exs b/storybook/core_components/input.story.exs new file mode 100644 index 0000000..799fe00 --- /dev/null +++ b/storybook/core_components/input.story.exs @@ -0,0 +1,41 @@ +defmodule Storybook.CoreComponents.Input do + use PhoenixStorybook.Story, :component + alias FlickWeb.CoreComponents + + def function, do: &CoreComponents.input/1 + def imports, do: [{CoreComponents, [simple_form: 1]}] + + def template do + """ + <.simple_form :let={f} for={%{}} as={:story} class="w-full"> + <.psb-variation-group field={f[:field]}/> + + """ + end + + def variations do + [ + %VariationGroup{ + id: :basic_inputs, + variations: + for type <- ~w(text textarea number date color range checkbox)a do + %Variation{ + id: type, + attributes: %{ + type: to_string(type), + label: String.capitalize("#{type} input") + } + } + end + }, + %Variation{ + id: :select, + attributes: %{ + label: "Select input", + type: "select", + options: ["Option 1", "Option 2", "Option 3"] + } + } + ] + end +end diff --git a/storybook/core_components/list.story.exs b/storybook/core_components/list.story.exs new file mode 100644 index 0000000..81a1226 --- /dev/null +++ b/storybook/core_components/list.story.exs @@ -0,0 +1,17 @@ +defmodule Storybook.CoreComponents.List do + use PhoenixStorybook.Story, :component + + def function, do: &FlickWeb.CoreComponents.list/1 + + def variations do + [ + %Variation{ + id: :default, + slots: [ + ~s|<:item title="Title">Elixir|, + ~s|<:item title="Rating">5/5| + ] + } + ] + end +end diff --git a/storybook/core_components/modal.story.exs b/storybook/core_components/modal.story.exs new file mode 100644 index 0000000..281311d --- /dev/null +++ b/storybook/core_components/modal.story.exs @@ -0,0 +1,25 @@ +defmodule Storybook.CoreComponents.Modal do + use PhoenixStorybook.Story, :component + alias FlickWeb.CoreComponents + + def function, do: &CoreComponents.modal/1 + def imports, do: [{CoreComponents, [button: 1, hide_modal: 1, show_modal: 1]}] + + def template do + """ + <.button phx-click={show_modal(":variation_id")} psb-code-hidden> + Open modal + + <.psb-variation/> + """ + end + + def variations do + [ + %Variation{ + id: :default, + slots: ["Modal body"] + } + ] + end +end diff --git a/storybook/core_components/table.story.exs b/storybook/core_components/table.story.exs new file mode 100644 index 0000000..0ab46ea --- /dev/null +++ b/storybook/core_components/table.story.exs @@ -0,0 +1,41 @@ +defmodule Storybook.CoreComponents.Table do + use PhoenixStorybook.Story, :component + alias FlickWeb.CoreComponents + + def function, do: &CoreComponents.table/1 + def aliases, do: [Storybook.CoreComponents.Table.User] + + def variations do + [ + %Variation{ + id: :table, + attributes: %{ + rows: + {:eval, + ~s""" + [ + %User{id: 1, username: "jose"}, + %User{id: 2, username: "chris"} + ] + """} + }, + slots: [ + """ + <:col :let={user} label="Id"> + <%= user.id %> + + """, + """ + <:col :let={user} label="User name"> + <%= user.username %> + + """ + ] + } + ] + end +end + +defmodule Storybook.CoreComponents.Table.User do + defstruct [:id, :username] +end diff --git a/storybook/examples/core_components.story.exs b/storybook/examples/core_components.story.exs new file mode 100644 index 0000000..b0b92e8 --- /dev/null +++ b/storybook/examples/core_components.story.exs @@ -0,0 +1,84 @@ +defmodule Storybook.Examples.CoreComponents do + use PhoenixStorybook.Story, :example + import FlickWeb.CoreComponents + + alias Phoenix.LiveView.JS + + def doc do + "An example of what you can achieve with Phoenix core components." + end + + defstruct [:id, :first_name, :last_name] + + @impl true + def mount(_params, _session, socket) do + {:ok, + assign(socket, + current_id: 2, + users: [ + %__MODULE__{id: 1, first_name: "Jose", last_name: "Valim"}, + %__MODULE__{ + id: 2, + first_name: "Chris", + last_name: "McCord" + } + ] + )} + end + + @impl true + def render(assigns) do + ~H""" + <.header> + List of users + <:subtitle>Feel free to add any missing user! + <:actions> + <.button phx-click={show_modal("new-user-modal")}>Create user + + + <.table id="user-table" rows={@users}> + <:col :let={user} label="Id"> + <%= user.id %> + + <:col :let={user} label="First name"> + <%= user.first_name %> + + <:col :let={user} label="Last name"> + <%= user.last_name %> + + + <.modal id="new-user-modal"> + <.header> + Create new user + <:subtitle>This won't be persisted into DB, memory only + + <.simple_form + :let={f} + for={%{}} + as={:user} + phx-submit={JS.push("save_user") |> hide_modal("new-user-modal")} + > + <.input field={f[:first_name]} label="First name" /> + <.input field={f[:last_name]} label="Last name" /> + <:actions> + <.button>Save user + + + + """ + end + + @impl true + def handle_event("save_user", %{"user" => params}, socket) do + user = %__MODULE__{ + first_name: params["first_name"], + last_name: params["last_name"], + id: socket.assigns.current_id + 1 + } + + {:noreply, + socket + |> update(:users, &(&1 ++ [user])) + |> update(:current_id, &(&1 + 1))} + end +end diff --git a/storybook/welcome.story.exs b/storybook/welcome.story.exs new file mode 100644 index 0000000..8714a3e --- /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