Git Product home page Git Product logo

phoenix_storybook's Introduction

PhoenixStorybook

github codecov GitHub release GitHub sponsors

📚 Documentation   -   🔎 Demo   -   🎓 Sample app   -   🍿 Getting started video   -   🦊 ElixirCasts

PhoenixStorybook provides a storybook-like UI interface for your Phoenix components.

  • Explore all your components, and showcase them with different variations.
  • Browse your component's documentation, with their supported attributes.
  • Learn how components behave by using an interactive playground.

screenshot screenshot

How does it work?

PhoenixStorybook is mounted in your application router and serves its UI at the mounting point of your choice.

It performs automatic discovery of your storybook content under a specified folder (:content_path) and then automatically generates a storybook navigation sidebar. Every module detected in your content folder will be loaded and identified as a storybook entry.

Three kinds of stories are supported:

  • component to describe your stateless function components or your live_components.
  • page to write & document UI guidelines, or whatever content you want.
  • example to show how your components can be used and mixed in real UI pages.

Installation

To start using PhoenixStorybook in your phoenix application you will need to follow these steps:

  1. Add the phoenix_storybook dependency
  2. Run the generator

1. Add the phoenix_storybook dependency

Add the following to your mix.exs and run mix deps.get:

def deps do
  [
    {:phoenix_storybook, "~> 0.6.0"}
  ]
end

Important

When picking a github version of the library (instead of an official hex.pm release) you need the get the storybook's assets compiled. To do so, please run mix dev.storybook.

2. Run the generator

Run from the root of your application:

$> mix deps.get
$> mix phx.gen.storybook

And you are ready to go!

ℹ️ If you prefer manual setup, please read the setup guide.

Configuration

Of all config settings, only the :otp_app, and :content_path keys are mandatory.

# lib/my_app_web/storybook.ex
defmodule MyAppWeb.Storybook do
  use PhoenixStorybook,
    # OTP name of your application.
    otp_app: :my_app,

    # Path to your storybook stories (required).
    content_path: Path.expand("../storybook", __DIR__),

    # Path to your JS asset, which will be loaded just before PhoenixStorybook's own
    # JS. It's mainly intended to define your LiveView Hooks in `window.storybook.Hooks`.
    # Remote path (not local file-system path) which means this file should be served
    # by your own application endpoint.
    js_path: "/assets/storybook.js",

    # Path to your components stylesheet.
    # Remote path (not local file-system path) which means this file should be served
    # by your own application endpoint.
    css_path: "/assets/storybook.css",

    # This CSS class will be put on storybook container elements where your own styles should
    # prevail. See the `guides/sandboxing.md` guide for more details.
    sandbox_class: "my-app-sandbox",

    # Custom storybook title. Default is "Live Storybook".
    title: "My Live Storybook",

    # Theme settings.
    # Each theme must have a name, and an optional dropdown_class.
    # When set, a dropdown is displayed in storybook header to let the user pick a theme.
    # The dropdown_class is used to render the theme in the dropdown and identify which current
    # theme is active.
    #
    # The chosen theme key will be passed as an assign to all components.
    # ex: <.component theme={:colorful}/>
    #
    # The chosen theme class will also be added to the `.psb-sandbox` container.
    # ex: <div class="psb-sandbox theme-colorful">...</div>
    #
    # If no theme has been selected or if no theme is present in the URL the first one is enabled.
    themes: [
      default: [name: "Default"],
      colorful: [name: "Colorful", dropdown_class: "text-pink-400"]

    # If you want to use custom FontAwesome icons.
    font_awesome_plan: :pro, # default value is :free
    font_awesome_kit_id: "foo8b41bar4625",
    font_awesome_rendering: :webfont, # default value is :svg

    # Story compilation mode, can be either `:eager` or `:lazy`.
    # It defaults to `:lazy` in dev environment, `:eager` in other environments.
    #   - When eager: all .story.exs & .index.exs files are compiled upfront.
    #   - When lazy: only .index.exs files are compiled upfront and .story.exs are compile when the
    #     matching story is loaded in UI.
    compilation_mode: :eager
  ]

All settings can be overridden from your config files.

# config/config.exs
config :my_app, MyAppWeb.Storybook,
  content_path: "overridden/content/path"

ℹ️ Learn more on theming components in the theming guide, on icons in the icons guide.

Contributing

We would love your PRs!

  1. Pull down phoenix_storybook to a directory next to your project (../phoenix_storybook).
  2. Change your mix file to point to this directory:
{:phoenix_storybook, path: "../phoenix_storybook"},
  1. Run dev.storybook mix task from your project
$> mix dev.storybook

And make sure you read the CONTRIBUTING guide. That should get you running against HEAD and ready to dig into the code!

License

MIT License. Copyright (c) 2022

phoenix_storybook's People

Contributors

benregn avatar cblavier avatar crova avatar dependabot[bot] avatar dgigafox avatar feng19 avatar gaiabeatrice avatar gangstead avatar gazler avatar iwarshak avatar jeremylightsmith avatar jtarasovic avatar justlampin avatar lostkobrakai avatar matthewlehner avatar matthieuchabert avatar moogle19 avatar nathany-copia avatar rgraff avatar rudolfman avatar seb3s avatar shirishgoyal avatar sltong avatar tony avatar viniciusmuller avatar woylie avatar zachallaun avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

phoenix_storybook's Issues

Improve code preview of custom structs

Instead of generating the following code:

<.chart height={200} type={:bar}
  data={%{__struct__: PhenixStorybook.Components.Chart.ChartData, datasets: [%{backgroundColor: ["rgba(255, 99, 132, 0.2)", "rgba(54, 162, 235, 0.2)"], borderWidth: 1, data: [12, 19, 3, 5, 2, 3], label: "# of Votes"}], labels: ["Red", "Blue", "Yellow", "Green"]}} 
/>

We should get

<.chart height={200} type={:bar}
  data={%ChartData{datasets: [%{backgroundColor: ["rgba(255, 99, 132, 0.2)", "rgba(54, 162, 235, 0.2)"], borderWidth: 1, data: [12, 19, 3, 5, 2, 3], label: "# of Votes"}], labels: ["Red", "Blue", "Yellow", "Green"]}}
/>

Entries should be ex files again

They currently are .exs files, but still are evaluated at compilation time, so they could be .ex as well.

Dixit José:

I think compiling them is likely better because the compiler is much smarter than code evaluation

They also should be suffixed by default to help with browsing code:

  • typography_page.ex
  • button_stories.ex

The suffix should be ignored when guessing the entry nice name (as a page title, or sidebar label)

Mounting storybook in the root path

👋🏼 Hi there! First of all congrats on this fantastic project 😍 🙌🏼 !

I'm using the main version and trying to mount the storybook in /. However, although following the guides, I cannot make it work since it can't load assets:

[debug] ** (PhxLiveStorybook.EntryNotFound) unknown entry ["assets", "js", "components.js"]
    (phx_live_storybook 0.4.0) lib/phx_live_storybook/live/entry_live.ex:42: PhxLiveStorybook.EntryLive.handle_params/3
    (phoenix_live_view 0.17.11) lib/phoenix_live_view/utils.ex:369: anonymous fn/5 in Phoenix.LiveView.Utils.call_handle_params!/5
    (telemetry 1.1.0) /Users/ricardogarciavega/projects/cabify/elixir/phx_storybook/deps/telemetry/src/telemetry.erl:320: :telemetry.span/3
    (phoenix_live_view 0.17.11) lib/phoenix_live_view/static.ex:271: Phoenix.LiveView.Static.call_mount_and_handle_params!/5
    (phoenix_live_view 0.17.11) lib/phoenix_live_view/static.ex:110: Phoenix.LiveView.Static.render/3
    (phoenix_live_view 0.17.11) lib/phoenix_live_view/controller.ex:39: Phoenix.LiveView.Controller.live_render/3
    (phoenix 1.6.12) lib/phoenix/router.ex:354: Phoenix.Router.__call__/2
    (phx_storybook 0.1.0) lib/phx_storybook_web/endpoint.ex:1: PhxStorybookWeb.Endpoint.plug_builder_call/2
    (phx_storybook 0.1.0) lib/plug/debugger.ex:136: PhxStorybookWeb.Endpoint."call (overridable 3)"/2
    (phx_storybook 0.1.0) lib/phx_storybook_web/endpoint.ex:1: PhxStorybookWeb.Endpoint.call/2
    (phoenix 1.6.12) lib/phoenix/endpoint/cowboy2_handler.ex:54: Phoenix.Endpoint.Cowboy2Handler.init/4
    (cowboy 2.9.0) /Users/ricardogarciavega/projects/cabify/elixir/phx_storybook/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
    (cowboy 2.9.0) /Users/ricardogarciavega/projects/cabify/elixir/phx_storybook/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
    (cowboy 2.9.0) /Users/ricardogarciavega/projects/cabify/elixir/phx_storybook/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
    (stdlib 4.0) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

[info] Sent 404 in 22ms
[debug] ** (PhxLiveStorybook.AssetNotFound) unknown asset %{"asset" => ["js", "app.js"]}
    (phx_live_storybook 0.4.0) lib/phx_live_storybook/controllers/asset_not_found_controller.ex:12: PhxLiveStorybook.AssetNotFoundController.asset/2
    (phx_live_storybook 0.4.0) lib/phx_live_storybook/controllers/asset_not_found_controller.ex:1: PhxLiveStorybook.AssetNotFoundController.action/2
    (phx_live_storybook 0.4.0) lib/phx_live_storybook/controllers/asset_not_found_controller.ex:1: PhxLiveStorybook.AssetNotFoundController.phoenix_controller_pipeline/2
    (phoenix 1.6.12) lib/phoenix/router.ex:354: Phoenix.Router.__call__/2
    (phx_storybook 0.1.0) lib/phx_storybook_web/endpoint.ex:1: PhxStorybookWeb.Endpoint.plug_builder_call/2
    (phx_storybook 0.1.0) lib/plug/debugger.ex:136: PhxStorybookWeb.Endpoint."call (overridable 3)"/2
    (phx_storybook 0.1.0) lib/phx_storybook_web/endpoint.ex:1: PhxStorybookWeb.Endpoint.call/2
    (phoenix 1.6.12) lib/phoenix/endpoint/cowboy2_handler.ex:54: Phoenix.Endpoint.Cowboy2Handler.init/4
    (cowboy 2.9.0) /Users/ricardogarciavega/projects/cabify/elixir/phx_storybook/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
    (cowboy 2.9.0) /Users/ricardogarciavega/projects/cabify/elixir/phx_storybook/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
    (cowboy 2.9.0) /Users/ricardogarciavega/projects/cabify/elixir/phx_storybook/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
    (stdlib 4.0) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

image

Here's the current configuration I'm using:

# config/config.exs

# Storybook config
config :phx_storybook, PhxStorybookWeb.Storybook,
  content_path: Path.expand("../storybook", __DIR__),
  css_path: "/assets/css/components.css",
  js_path: "/assets/js/components.js"
# lib/phx_storybook_web/router.ex

 # ...
  scope "/" do
    storybook_assets()
  end

  scope "/" do
    pipe_through :browser

    live_storybook("/",
      otp_app: :phx_storybook,
      backend_module: PhxStorybookWeb.Storybook
    )
  end

  # ...

And here is what my assets folder looks like:

assets
├── css
│   ├── app.css
│   └── components.css
├── js
│   ├── app.js
│   ├── components.js
│   └── hooks.js
├── tailwind.config.js
└── vendor

Any idea of what is going on?
Thanks in advance and keep it up!

Not all icons are rendered

I just followed the setup instructions of version 0.4.0. Some of the icons in the UI are not rendered correctly. Is there something I need to set up to make it work?

image

Interestingly, only the icon of the active item in this element is rendered.

image

image

image

Icons not rendering correctly

The icons in the sidebar aren't rendering correctly for me.

Screen Shot 2022-09-30 at 11 11 42 AM

<div class="lsb lsb-flex lsb-items-center lsb-py-3 lg:lsb-py-1.5 -lsb-ml-2 lsb-group lsb-cursor-pointer lsb-group hover:lsb-text-indigo-600" phx-click="close-folder" phx-target="1" phx-value-path="/admin/storybook/components/feedback">
  <i class="fa-solid fa-caret-down lsb lsb-pl-1 lsb-pr-2"></i>
</div>

From my investigation, it's because the lsb class defines a font-family property which cascades down to the icon element and overrides the font-family of the fa-* classes. When I added !important to the fa-* font-family property, the icons render properly.

idea: resizable sandbox

Throwing this idea out there...

TailwindUI has a nifty idea where their storybook (for paid users) you to resize the container, showing behaviours at different breakpoints:

e.g.
Peek 2022-08-31 19-25

@cblavier what do you think about this?

Rename attr options to values & values!

Surface is currently supporting attributes options with the values and values! keys :

Currently, we have options values and values! for that.
values is meant to be a common list of values but it does not represent the list of all possible values. For instance, you may have an attribute color where you want to define a set of values to be listed in the catalogue, like red, green, blue or even semantic values like primary secondary, etc. however, the user can also set any other valid color, like #FF0000 and so on.
values! on the other hand, is strict, i.e. it lists all possible values so we can do static validation not only in the catalogue but also in the compiler itself.

These options will soon be upstreamed to LiveView.
The storybook should be consistent with it.

Sidebar rendering

As of #78 storybook content will now be lazy-compiled.

The sidebar will still be reflecting the file hierarchy under the storybook content folder.

But we need enough information to eagerly build a functional sidebar with optional custom folder names, folder icons, and story icons. That's why we will introduce *.index.exs files that will be eagerly compiled (in all envs) to provide all info required by the sidebar.

The storybook content folder will look like this:

- mix.exs
- lib/
- storybook/
  - components/
    - components.index.exs
    - buttons/
      - buttons.index.exs
      - button.story.exs
      - dropdown.story.exs
  - guides/
    - guides.index.ex
    - guidelines.story.exs

*.index.exs files are optional and look like this

defmodule Storybook.Components.Buttons.Index do
  use PhxLiveStorybook.Index
  
  def folder_name, do: "Custom Name"

  # we might use a tuple here to use different icon provides (FontAwesome / Heroicons)
  def icon, do: "fa fa-button" 

  def sidebar_item("button"), do: [name: "custom story name", icon: "fa fa-banana"]

  # we did not define anything for the "dropdown" which will default to [name: "Dropdown", icon: nil]
end

Command palette

A global search menu (tailwind.css.com like) should be globally available within the storybook to search across all the storybook entries (pages & components)

The UI might look like this: https://tailwindui.com/components/application-ui/navigation/command-palettes#component-f55d785e34cd53505459e68e32af879f

The command palette is:

  • reachable by clicking a search input, located at the top of the sidebar
  • reachable with cmd-K global shortcut
  • usable with keyboard: cmd-Kto open, type to search, up/down to select, enter to open, esc to cancel

Feed attributes from live_view declarative assigns

Upcoming liveview 0.18.0 will ship with the ability to declare component assigns.

phx_live_storybook Attr API is intentionally very close with declarative component assigns, so the idea is that:

  • if using liveview 0.17 or less, only storybook attributes are used
  • if using liveview 0.18+, then component attributes and storybook attributes are merged (storybook ones have higher priority)

Improve generated storybook.css & Tailwind/Esbuild instructions

Unless --no-tailwind option is passed to generator:

  • generate a CSS file with tailwind layers
  • put a code placeholder in the @utilities layer explaining how their styling will be sandboxed
  • prompt user to add a new config profile in config :tailwind, config/config.exs
  • prompt user to add the matching watcher in config/dev.exs

Plus (no matter the --no-tailwind-option)

  • set the sandboxing configuration key in backend module to the current application name
  • prompt user to add js/storybook.js in config/config.exs esbuild args
  • prompt user to add ~r"storybook/.*(exs)$" in config/dev.exs live_reload patterns

New templating options

Depending on the component, you may want:

  • to show or not, the template code around the story's code.
  • for story group, to wrap all stories in a single template or to wrap each story in its template.

There should be some options to enable or disable these behaviors.

Open a story group in the component Playground

This ticket must be done after #35

The component playground should now be able to render all stories of a single StoryGroup at once. As in #35 one should be able:

  • to open a story group in the playground from the Stories
  • to pick a story group from the story select input from the Playground

When the playground is initialized from the StoryGroup all attributes that have different values across stories are locked and cannot be edited in the attributes tab.

For instance, with the following StoryGroup the color attribute will be locked whereas text and icon will still be editable.
When updating an editable field, all stories are updated at once.

%StoryGroup{
  id: :colors,
  stories:
    for color <- ~w(info primary success warning danger)a do
      %Story{
        id: color,
        attributes: %{
          text: "My badge",
          icon: "fa fa-icon",
          color: color
        }
      }
    end
}

nested components in Story.block ?

Hi!

Any ideas how we should document this kind of %Story{}?

<.tabs>
  <.tab is_active to="/">Home</.tab>
  <.tab link_type="a" to="/" label="About" />
</.tabs>

I've tried block:

defmodule BrmblWeb.Storybook.Components.Navigation.Tabs do
  use PhxLiveStorybook.Entry, :component

  import BrmblWeb.UI.Navigation.Tabs

  def function, do: &BrmblWeb.UI.Navigation.Tabs.tabs/1

  def stories do
    [
      %Story{
        id: :tabs,
        attributes: %{},
        block: """
        <.tab is_active to="/" label="Tab1" />
        <.tab to="/hi" label="Tab2" />
        """
      }
    ]
  end
end

Which yields an opaque error:

Compiling 2 files (.ex)

== Compilation error in file lib/brmbl_web/storybook.ex ==
** (CompileError) lib/brmbl_web/storybook.ex: an error occured while rendering story tabs
    (stdlib 3.17.2) lists.erl:1358: :lists.mapfoldl/3
    (stdlib 3.17.2) lists.erl:1359: :lists.mapfoldl/3
    (stdlib 3.17.2) lists.erl:1358: :lists.mapfoldl/3
    (phoenix_live_view 0.17.11) expanding macro: Phoenix.LiveView.Helpers.inner_block/2

Confirmed that plain old HTML inside a block works fine:

  def stories do
    [
      %Story{
        id: :tabs,
        attributes: %{},
        block: """
        <a>hi</a>
        <a>hi</a>
        """
      }
    ]
  end

Can we (do we need to) use sigil_H inside our story definitions?

Component and function callback for function component entry

It feels strange to duplicate the module for a function component on both the component/0 and the function/0 callback. I'm not sure I completely understand the system yet, but isn't the function/0 return value enough to figure out the module it's on?

How do we pass `let` variable to a block in a story

If I have a component that can pass an argument to its inner block that can be accessed via a let property, how do we do it in a story?

For example, my component looks like this:

~H"""
<.my_component let={m} ...>
  <div class={m.classes}>...</div>
</.my_component>
"""

I tried something like:

def stories do
    [
      %Story{
        id: :default,
        attributes: %{
          id: "default",
          let: m
        },
        block: """
        <div class={m.classes}>...</div>
        """
      }
    ]
  end

But of course, this will produce an undefined function m/0.

css conflicts in `Variation`s (0.2.0)

Hi!

LSB version: 0.2.0

I've noticed the LSB styles are fighting with app styles imported via css_path

It appears LSB combines its own, and my_app's css together in the document <head>. This may cause components rendered inside %Variation elements to inherit styling from LSB, and not the app.

In my specific case, the use of a LSB text classes on <body> (lsb-font-inter lsb-text-base etc) are overriding my Tailwind base styles, which are added to <html> in preflight in my css bundle.

What are your plans here? I see that Storybook renders each Story in its own iframe.

Anything we can help with?

lsb-sandbox applied to theme switcher

I thought I could prevent my global styles from bleeding into storybook with this:

html:not(.lsb),
body:not(.lsb),
.lsb-sandbox {
  /* ... */
}

However, the storybook theme dropdown also has the sandbox classes:

<div class="... lsb-sandbox myapp-web">

Is that by design? I would have expected the sandbox classes to only be added to the containers around my own components.

Lazy loading of stories

Component & pages .exs files are compiled as a whole at compilation time (triggered by the compilation of the storybook backend module defined in the user application).

As the codebase grows, this could induce increased compilation times.

Components & pages exs files should instead:

  • be eagerly compiled in prod env
  • be lazily compiled at page load in any other environment

Compilation error if :folders not in config.exs

The readme states "All config settings, only the :content_path key is mandatory."

but with this config

config :brmbl, BrmblWeb.Storybook,
  content_path: Path.expand("../storybook", __DIR__),
  css_path: "/css/app.css",
  # js_path: "/js/app.js",
  title: "My Rad storybook"

The project doesn't compile.

app1_1  | == Compilation error in file lib/brmbl_web/storybook.ex ==
app1_1  | ** (FunctionClauseError) no function clause matching in Keyword.get/3    
app1_1  |     
app1_1  |     The following arguments were given to Keyword.get/3:
app1_1  |     
app1_1  |         # 1
app1_1  |         nil
app1_1  |     
app1_1  |         # 2
app1_1  |         :"/components"
app1_1  |     
app1_1  |         # 3
app1_1  |         []
app1_1  |     
app1_1  |     Attempted function clauses (showing 1 out of 1):
app1_1  |     
app1_1  |         def get(+keywords+, +key+, +default+) when -is_list(keywords)- and +is_atom(key)+
app1_1  |     
app1_1  |     (elixir 1.13.4) lib/keyword.ex:352: Keyword.get/3
app1_1  |     (phx_live_storybook 0.3.0) lib/phx_live_storybook/entries.ex:62: anonymous fn/5 in PhxLiveStorybook.Entries.recursive_scan/3
app1_1  |     (elixir 1.13.4) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
app1_1  |     (phx_live_storybook 0.3.0) lib/phx_live_storybook/entries.ex:55: PhxLiveStorybook.Entries.recursive_scan/3
app1_1  |     (phx_live_storybook 0.3.0) expanding macro: PhxLiveStorybook.__using__/1
app1_1  |     lib/brmbl_web/storybook.ex:3: BrmblWeb.Storybook (module)
app1_1  |     (elixir 1.13.4) expanding macro: Kernel.use/2
app1_1  |     lib/brmbl_web/storybook.ex:3: BrmblWeb.Storybook (module)

cmd+opt+i overrides inspector shortcut

If I press cmd+opt+i on a mac, the quick search panel is opened instead of the browser inspector (Firefox and Chrome).

The search panel is also opened when I only type / (opt + i in my keyboard layout). I don't know whether that's intended, but the shortcut rendered in the UI is cmd+k, and putting shortcuts on the option or alt key always leads to trouble, since the option and alt key are used to access characters and symbols. I can't type a / in the search input now, which isn't too bad, but I also cannot enter a / in any input I render in a component, which is kind of bad when it comes to testing input validation and sanitation.

How to add a modal to the storybook

First of all, thank you guys for your work on this!

I am having a hard time adding a modal to the storybook which is supposed to be something like this https://petal.build/components/modals. For a modal to show/hide, it needs a button, so in the sandbox it must also show a button and a modal that is hidden by default.

I already tried doing something like this:

alias MyComponents.Modal

def function, do: &my_modal/1
def description, do: "Modals sit on top of an applications main window"

  def stories do
    [
      %StoryGroup{
        id: :sizes,
        description: "Different sizes of modal",
        stories: [
          %Story{
            id: :default_modal,
            attributes: %{title: "Testing"}
          }
        ]
      }
    ]
  end

  # For storybook purposes only
  def my_modal(assigns) do
    ~H"""
    <button
      class="block text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
      type="button"
      data-modal-toggle="default-modal"
    >
      Toggle modal
    </button>

    <Modal.modal title="Testing123">Some testing</Modal.modal>
    """
  end

This should work, although the documentation now points to my_modal/1 instead of just Modal.modal/1.

Are there any plans to pass a layout option to the sandbox so it will house the component instead of just a div/iframe option? Or in the current version, do you suggest any workaround to make this work?

Open a story in the component Playground

Currently, the component playground is initialized from the first Story declared in the component entry file (or, if the first item is a group, from the first story in the first group).

The idea would be to keep this behavior as a default, but to be also able to open the component playground with any story:

  • in the component Stories tab, there is a link below each story to open it in the playground
  • in the component Playground tab, there is a select input below the attributes tab to pick another story
  • the story id the playground has been opened with must be reflected in the URL (?story_id=my_story)

Handling of StoryGroup is not part of this issue; it will be described in another ticket.

Rewrite set-assign / toggle-assign with JS commands

Instead of writing

phx-click="set-assign/:story_id/show/true"
phx-click="toggle-assign/:story_id/show"

we should write

phx-click={JS.push("assign", values: %{story_id: :story_id, show: true})}
phx-click={JS.push("toggle", values: %{story_id: :story_id, attr: :show})}

Allow to set a custom CSS class on sandbox items

By using TailwindCSS important selector strategy with the .lsb-sandbox class, we force users into putting the lsb-sandbox class on their own application layout.

This should be a custom class, set in configuration.
Sandboxing guide should be amended

Storybook has to be refreshed to show Date Picker

I want to say thank you for your great work on phx_live_storybook!

We have one issue when using Date Picker in the Storybook. The date picker is not shown, unless storybook page is refreshed once.

We are using https://flowbite.com/docs/plugins/datepicker which is basically a Tailwind CSS date picker.

Is it because of the components are rendered during compile time and refreshing the page re-renders the component and activating the hook?

Thank you.

How to support >1 function in a :component

I'd like to mix different components together in the same Entry. Something like this:

defmodule BrmblWeb.Storybook.Components.Typography do
  use PhxLiveStorybook.Entry, :component

  alias BrmblWeb.UI.Typography

  def description, do: "Typography components"

  def stories do
    [
      %Story{
        id: :h1,
        function: &Typography.h1/1,
        attributes: %{},
        slots: [~s|.h1 heading|]
      },
      %Story{
        id: :h2,
        function: &Typography.h2/1,
        attributes: %{},
        slots: [~s|.h2 heading|]
      }
    ]
  end
end

That doesn't work since a :component assumes all the same :function:

app1_1  | == Compilation error in file lib/brmbl_web/storybook.ex ==
app1_1  | ** (KeyError) key :function not found
app1_1  |     (phx_live_storybook 0.3.0) expanding struct: PhxLiveStorybook.Story.__struct__/1
app1_1  |     storybook/components/typography.exs:16: BrmblWeb.Storybook.Components.Typography.stories/0

Is this a good idea? If so, do you have any plans to add this already (or can you share any tips on how you might like to see it implemented?)

Use iframe srcdoc instead of src

We are currently using src when using (optional) iframe rendering.
This triggers one additional HTTP request for every story rendered (1 story = 1 iframe = 1 HTTP GET)

We could instead use iframe srcdoc which allows inline rendering.

UI issues

  • Theme icon is too big (see screenshot)
  • Search window is flickering when triggered (it seems there is a scale transition on the background overlay)

CleanShot 2022-09-01 at 17 48 56

Event log in the component playground

In the component Playground a new tab (next to the Attributes tab) should be available for all components.

This tab will print, in a <pre>...</pre> monospace font fashion:

  • all messages (handle_info & handle_event) received by the component
  • all messages (handle_info & handle_event) received by the parent LiveView (PlaygroundPreviewLive) excepted the {:new_attributes, pid, attrs} which is for internal use
  • all messages are prefixed with the recipient (component or Liveview)
  • all messages are prefixed with their kind (info or event)

If messages are received when the Logs tab is not active, a badge will appear next to the tab name with the number of unread logs. The badge is cleared as soon as the tab is opened.

Story files as .exs

I just looked through the README and noticed that the story files are .ex files, but one also needs to supply a path for where those files are. I'm wondering if those files should rather be .exs files, so they're not compiled by default, but only used by the storybook. I feel like this is a similar setup to test files, which also are .exs files.

Improve router API

I'm seeing a 403 error on GET http://localhost:4000/storybook/assets/js/app.js.

This results in deeply nested menu items not being expandable:

image

Request details:

GET /storybook/assets/js/app.js HTTP/1.1
Host: localhost:4000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: */*
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost:4000/storybook/components/button/icon
Connection: keep-alive
Cookie: NOPE
Sec-Fetch-Dest: script
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same-origin

Response

HTTP/1.1 403 Forbidden
cache-control: max-age=0, private, must-revalidate
content-length: 76286
content-type: text/html; charset=utf-8
cross-origin-window-policy: deny
date: Thu, 18 Aug 2022 11:53:59 GMT
permissions-policy: accelerometer=(),  ambient-light-sensor=(),  autoplay=(),  battery=(),  camera=(),  cross-origin-isolated=(),  display-capture=(),  document-domain=(),  encrypted-media=(),  geolocation=(),  gyroscope=(),  magnetometer=(),  microphone=(),  midi=(),  navigation-override=(self),  payment=(),  picture-in-picture=(),  publickey-credentials-get=(),  screen-wake-lock=(),  sync-xhr=(self),  usb=(),  web-share=(),  xr-spatial-tracking=(),  clipboard-read=(),  clipboard-write=(self),  gamepad=(),  speaker-selection=(),  conversion-measurement=(),  focus-without-user-activation=(),  hid=(),  idle-detection=(),  serial=(),  sync-script=(),  trust-token-redemption=(),  vertical-scroll=()
referrer-policy: strict-origin-when-cross-origin
server: Cowboy
x-content-type-options: nosniff
x-download-options: noopen
x-frame-options: SAMEORIGIN
x-permitted-cross-domain-policies: none
x-request-id: FwxuTP1B_mn4B1UAAM4F
x-xss-protection: 1; mode=block

Phoenix shows this error page:

Plug.CSRFProtection.InvalidCrossOriginRequestError at GET /storybook/assets/js/app.js

security warning: an embedded <script> tag on another site requested protected JavaScript (if you know what you're doing, disable forgery protection for this route)

Strangely phoenix is responding content-type: text/html; charset=utf-8

but in lib/phx_live_storybook/templates/layout/root.html.heex, indeed the correct mime type is set:

15:      <script phx-track-static type="text/javascript" src={application_static_path(@conn, path)}></script>
17:    <script type="text/javascript" src={asset_path(@conn, "js/app.js")}></script> 
Stacktrace
[debug] ** (Plug.CSRFProtection.InvalidCrossOriginRequestError) security warning: an embedded <script> ta
g on another site requested protected JavaScript (if you know what you're doing, disable forgery protecti
on for this route)                                                                                       
    (plug 1.13.6) lib/plug/csrf_protection.ex:392: Plug.CSRFProtection.ensure_same_origin_and_csrf_token!/3                                                                                                       
    (elixir 1.13.4) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3                                
    (plug 1.13.6) lib/plug/conn.ex:1690: Plug.Conn.run_before_send/2                                     
    (plug 1.13.6) lib/plug/conn.ex:450: Plug.Conn.send_file/5                                            
    (plug 1.13.6) lib/plug/static.ex:308: Plug.Static.send_entire_file/3                                 
    (brmbl 0.1.0) BrmblWeb.Router.storybook_assets/2                                                     
    (brmbl 0.1.0) lib/brmbl_web/router.ex:1: BrmblWeb.Router.__pipe_through9__/1                         
    (phoenix 1.6.11) lib/phoenix/router.ex:346: Phoenix.Router.__call__/2                                
    (brmbl 0.1.0) lib/brmbl_web/endpoint.ex:1: BrmblWeb.Endpoint.plug_builder_call/2                     
    (brmbl 0.1.0) lib/brmbl_web/endpoint.ex:3: anonymous fn/3 in BrmblWeb.Endpoint."call (overridable 3)"/2                                                                                                       
    (appsignal 2.1.7) lib/appsignal/instrumentation.ex:10: Appsignal.Instrumentation.instrument/1        
    (brmbl 0.1.0) lib/plug/debugger.ex:136: BrmblWeb.Endpoint."call (overridable 4)"/2                   
    (brmbl 0.1.0) lib/brmbl_web/endpoint.ex:1: BrmblWeb.Endpoint."call (overridable 5)"/2                
    (brmbl 0.1.0) lib/plug/error_handler.ex:80: BrmblWeb.Endpoint.call/2                                 
    (phoenix 1.6.11) lib/phoenix/endpoint/cowboy2_handler.ex:54: Phoenix.Endpoint.Cowboy2Handler.init/4  
    (cowboy 2.9.0) /home/g/src/brmbl/app/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
    (cowboy 2.9.0) /home/g/src/brmbl/app/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3                                                                                                      
    (cowboy 2.9.0) /home/g/src/brmbl/app/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3                                                                                              
    (stdlib 3.17.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3

Generator

Write mix phx_storybook.install task that will:

  • create the backend module
  • create router entry
  • create a storybook folder at the root of the project with a dummy page & a dummy component
  • create a dummy storybook.js file
  • create a dummy storybook.css file

Working with forms and inputs

Hello, how should I go about using forms and inputs in the storybook? using the Phoenix.HTML.Form struct directly causes browser errors which are not likely to be very useful to share.

Uncaught TypeError: e is null

Here is how I'm currently supporting my inputs in storybook:

      %Story{
        id: :select_error_state,
        attributes: %{
          type: :select,
          select_options: ["Foo", "Bar", "Fizz", "Buzz"],
          prompt: "Select a variable name",
          class: "",
          form: %Phoenix.HTML.Form{data: %{foo: "Foo"}, errors: [foo: {"can't be set to Foo", []}]},
          field: :foo
        }
      },

Theming support

One should be able to render its storybook components with different themes (to theme the components, not the storybook itself).

Setup themes with

config :phx_live_storybook, MyApp,
  themes: [default: "Default Theme", colorful: "Colorful"]

Once configured a dropdown will be visible in the header with different themes. When updated:

  • the current theme is reflected in the URL (?theme=colorful) and persistent across the navigation
  • the current theme is passed to all components preview as an assign (%{theme: :colorful})
  • the current theme is not passed to code rendering (it should not be visible in code snippets)

Stories always passing an inner_block

Given this component

   def custom_link(%{link_type: "a"} = assigns) do
     ~H"""
     <%= Phoenix.HTML.Link.link([to: @to, class: @class] ++ @rest,
       do: if(@inner_block, do: render_slot(@inner_block), else: @label)
     ) %>
     """
   end

and this Story (without a slot)

          %Story{id: :default, attributes: %{label: "Link", to: "/"}},
          %Story{id: :slot, attributes: %{to: "/"}, slots: [~s|Default slot|]},

It wont render anything in the first Story in the storybook.

It appears that the Story is always passing an inner_block, slot or not.

This is a workaround, but it's not ideal

-      do: if(@inner_block, do: render_slot(@inner_block), else: @label)
+      do: if(@label, do: @label, else: render_slot(@inner_block))

Live components state inspection

In the component playground, a new tab (next to the Attributes tab) should be available for all live components (not for stateless) to inspect their current state.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.