├── test ├── test_helper.exs ├── support │ └── conn_case.ex └── catalogue_test.exs ├── .tool-versions ├── lib └── surface │ └── catalogue │ ├── components │ ├── table │ │ └── column.ex │ ├── tabs │ │ └── tabItem.ex │ ├── table.ex │ ├── component_info.ex │ ├── tabs.ex │ ├── component_tree.ex │ ├── component_api.ex │ ├── state_dialog.ex │ ├── prop_input.ex │ └── playground_tools.ex │ ├── application.ex │ ├── router.ex │ ├── markdown.ex │ ├── live │ ├── example_live.ex │ ├── playground_live.ex │ └── page_live.ex │ ├── layout_view.ex │ ├── util.ex │ └── server.ex ├── priv └── catalogue │ ├── button │ ├── playground.ex │ ├── live_example.ex │ └── examples.ex │ ├── card │ └── playground.ex │ ├── sample_catalogue.ex │ ├── editable_props │ └── playground.ex │ └── sample_components │ ├── card.ex │ ├── editable_props.ex │ └── button.ex ├── .formatter.exs ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── ci.yml ├── dev.exs ├── blend.exs ├── .gitignore ├── LICENSE.md ├── config └── config.exs ├── blend ├── premix.exs ├── lowest.mix.lock ├── local.mix.lock └── github.mix.lock ├── CHANGELOG.md ├── mix.exs ├── assets ├── css │ ├── prism-vsc-dark-plus.css │ └── app.css ├── js │ └── app.js └── vendor │ └── prism.js ├── README.md └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 27.0.1 2 | elixir 1.17.2-otp-27 3 | -------------------------------------------------------------------------------- /lib/surface/catalogue/components/table/column.ex: -------------------------------------------------------------------------------- 1 | defmodule Surface.Catalogue.Components.Table.Column do 2 | @moduledoc false 3 | 4 | use Surface.Component, slot: "cols" 5 | 6 | @doc "Column header text" 7 | prop label, :string, required: true 8 | end 9 | -------------------------------------------------------------------------------- /priv/catalogue/button/playground.ex: -------------------------------------------------------------------------------- 1 | defmodule SurfaceCatalogue.SampleComponents.Button.Playground do 2 | use Surface.Catalogue.Playground, 3 | catalogue: SurfaceCatalogue.SampleCatalogue, 4 | subject: SurfaceCatalogue.SampleComponents.Button, 5 | height: "170px" 6 | 7 | @slots [ 8 | default: "DEFAULT SLOT" 9 | ] 10 | end 11 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | import_deps: [:phoenix, :surface], 4 | plugins: [ 5 | Phoenix.LiveView.HTMLFormatter, 6 | Surface.Formatter.Plugin 7 | ], 8 | inputs: [ 9 | "{mix,.formatter}.exs", 10 | "{config,lib,test}/**/*.{ex,exs,sface}", 11 | "blend.exs", 12 | "blend/*.exs", 13 | "priv/catalogue/**/*.{ex,exs,sface}" 14 | ] 15 | ] 16 | -------------------------------------------------------------------------------- /priv/catalogue/card/playground.ex: -------------------------------------------------------------------------------- 1 | defmodule SurfaceCatalogue.SampleComponents.Card.Playground do 2 | use Surface.Catalogue.Playground, 3 | catalogue: SurfaceCatalogue.SampleCatalogue, 4 | subject: SurfaceCatalogue.SampleComponents.Card, 5 | height: "300px" 6 | 7 | @slots [ 8 | default: "DEFAULT", 9 | header: "HEADER", 10 | footer: "FOOTER" 11 | ] 12 | end 13 | -------------------------------------------------------------------------------- /lib/surface/catalogue/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Surface.Catalogue.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | def start(_type, _args) do 7 | children = [ 8 | {Phoenix.PubSub, name: Surface.Catalogue.PubSub} 9 | ] 10 | 11 | opts = [strategy: :one_for_one, name: Surface.Catalogue.Supervisor] 12 | 13 | Supervisor.start_link(children, opts) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report an issue with Surface Catalogue 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | ## How to reproduce it 13 | 14 | 1. ... 15 | 16 | ## The behavior you expected 17 | 18 | ## Your Environment 19 | 20 | Surface: v0.X.X 21 | SurfaceCatalogue: v0.X.X 22 | LiveView: v0.X.X 23 | Elixir: v1.X.X 24 | -------------------------------------------------------------------------------- /dev.exs: -------------------------------------------------------------------------------- 1 | # iex -S mix dev 2 | 3 | Logger.configure(level: :debug) 4 | 5 | Surface.Catalogue.Server.start( 6 | watchers: [ 7 | esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]} 8 | ], 9 | live_reload: [ 10 | patterns: [ 11 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 12 | ~r"lib/surface/catalogue/(live|components)/.*(ex)$", 13 | ~r"priv/catalogue/.*(ex)$" 14 | ] 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Surface.Catalogue.ConnCase do 2 | use ExUnit.CaseTemplate 3 | 4 | using do 5 | quote do 6 | import Surface.Catalogue.ConnCase 7 | use Surface.LiveViewTest 8 | 9 | alias Surface.Catalogue.Router.Helpers, as: Routes 10 | 11 | @endpoint Surface.Catalogue.Server.Endpoint 12 | end 13 | end 14 | 15 | setup _tags do 16 | {:ok, conn: Phoenix.ConnTest.build_conn()} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/catalogue_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Surface.Catalogue.CatalogueTest do 2 | use Surface.Catalogue.ConnCase, async: true 3 | 4 | setup_all do 5 | Application.put_env(:surface_catalogue, Surface.Catalogue.Server.Endpoint, 6 | secret_key_base: "Hu4qQN3iKzTV4fJxhorPQlA/osH9fAMtbtjVS58PFgfw3ja5Z18Q/WSNR9wP4OfW", 7 | live_view: [signing_salt: "hMegieSe"] 8 | ) 9 | 10 | start_supervised(Surface.Catalogue.Server.Endpoint) 11 | 12 | :ok 13 | end 14 | 15 | catalogue_test :all 16 | end 17 | -------------------------------------------------------------------------------- /priv/catalogue/sample_catalogue.ex: -------------------------------------------------------------------------------- 1 | defmodule SurfaceCatalogue.SampleCatalogue do 2 | @moduledoc """ 3 | Sample catalogue. 4 | """ 5 | 6 | use Surface.Catalogue 7 | 8 | load_asset "assets/bulma.min.css", as: :bulma_css 9 | 10 | @impl true 11 | def config() do 12 | [ 13 | head_css: """ 14 | 15 | """, 16 | playground: [ 17 | body: [ 18 | style: "padding: 1.5rem; height: 100%;", 19 | class: "has-background-light" 20 | ] 21 | ] 22 | ] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/surface/catalogue/components/tabs/tabItem.ex: -------------------------------------------------------------------------------- 1 | defmodule Surface.Catalogue.Components.Tabs.TabItem do 2 | @moduledoc false 3 | 4 | use Surface.Component, slot: "tabs" 5 | 6 | @doc "Item label" 7 | prop label, :string, default: "" 8 | 9 | @doc "Item icon" 10 | prop icon, :string 11 | 12 | @doc "Item is disabled" 13 | prop disabled, :boolean, default: false 14 | 15 | @doc "Item is visible" 16 | prop visible, :boolean, default: true 17 | 18 | @doc "Indicate that the information has changed" 19 | prop changed, :boolean, default: false 20 | end 21 | -------------------------------------------------------------------------------- /blend.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | local: [ 3 | {:surface, path: "../surface"} 4 | ], 5 | github: [ 6 | {:surface, github: "surface-ui/surface"} 7 | ], 8 | lowest: [ 9 | {:surface, "0.10.0"}, 10 | {:earmark, "1.4.21"}, 11 | {:makeup_elixir, "0.16.0"}, 12 | {:html_entities, "0.4.0"}, 13 | {:jason, "1.0.0", optional: true, override: true}, 14 | {:plug_cowboy, "2.3.0", only: :dev}, 15 | {:esbuild, "0.2.0", only: :dev}, 16 | {:floki, "0.35.3", only: :test}, 17 | {:phoenix_live_reload, "1.2.0", optional: true, only: [:prod, :dev]}, 18 | {:ex_doc, "0.31.1", only: :docs} 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /priv/catalogue/editable_props/playground.ex: -------------------------------------------------------------------------------- 1 | defmodule SurfaceCatalogue.SampleComponents.EditableProps.Playground do 2 | use Surface.Catalogue.Playground, 3 | catalogue: SurfaceCatalogue.SampleCatalogue, 4 | subject: SurfaceCatalogue.SampleComponents.EditableProps, 5 | height: "355px" 6 | 7 | @props [ 8 | boolean: true, 9 | string: "some string", 10 | string_choices: "2", 11 | atom: :an_atom, 12 | atom_choices: :a, 13 | css_class: ["css", "class"], 14 | integer: 4, 15 | integer_choices: 2, 16 | number: 3.14, 17 | list: [1, "string", :atom], 18 | keyword: [key1: 1, key2: "2", key3: :three], 19 | any: %{a: :map} 20 | ] 21 | end 22 | -------------------------------------------------------------------------------- /lib/surface/catalogue/router.ex: -------------------------------------------------------------------------------- 1 | defmodule Surface.Catalogue.Router do 2 | defmacro surface_catalogue(path, opts \\ []) do 3 | quote bind_quoted: binding() do 4 | scope path, alias: false, as: false do 5 | import Phoenix.LiveView.Router 6 | 7 | alias Surface.Catalogue.{ 8 | LayoutView, 9 | PageLive, 10 | ExampleLive, 11 | PlaygroundLive 12 | } 13 | 14 | live_session :catalogue, root_layout: {LayoutView, :root} do 15 | live "/", PageLive 16 | live "/components/:component/", PageLive 17 | live "/components/:component/:action", PageLive 18 | end 19 | 20 | live_session :catalogue_playgrounds, root_layout: false do 21 | live "/examples/:example", ExampleLive 22 | live "/playgrounds/:playground", PlaygroundLive 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /priv/catalogue/button/live_example.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(Surface.Catalogue.LiveExample) do 2 | defmodule SurfaceCatalogue.SampleComponents.Button.EventExample do 3 | @moduledoc """ 4 | An example handling events and manipulating data. 5 | """ 6 | 7 | use Surface.Catalogue.LiveExample, 8 | subject: SurfaceCatalogue.SampleComponents.Button, 9 | catalogue: SurfaceCatalogue.SampleCatalogue, 10 | title: "Handling events and manipulating data", 11 | height: "90px", 12 | container: {:div, class: "buttons"}, 13 | assert: "Value: 0" 14 | 15 | data value, :integer, default: 0 16 | 17 | def render(assigns) do 18 | ~F""" 19 | 20 | """ 21 | end 22 | 23 | def handle_event("increment", _, socket) do 24 | {:noreply, update(socket, :value, fn value -> value + 1 end)} 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | surface_catalogue-*.tar 24 | 25 | # Temporary files for e.g. tests 26 | /tmp 27 | 28 | node_modules 29 | 30 | /priv/static/ 31 | 32 | .DS_Store 33 | 34 | # Ignore Surface generated files 35 | assets/js/_hooks/ 36 | assets/css/_components.css 37 | 38 | # Ignore blend compiled artifacts and deps 39 | /blend/_build 40 | /blend/deps 41 | -------------------------------------------------------------------------------- /priv/catalogue/sample_components/card.ex: -------------------------------------------------------------------------------- 1 | defmodule SurfaceCatalogue.SampleComponents.Card do 2 | @moduledoc """ 3 | A sample Card component. 4 | """ 5 | 6 | use Surface.Component 7 | 8 | @doc """ 9 | The main content 10 | """ 11 | slot default 12 | 13 | @doc """ 14 | The header content 15 | """ 16 | slot header 17 | 18 | @doc """ 19 | The footer content 20 | """ 21 | slot footer 22 | 23 | def render(assigns) do 24 | ~F""" 25 |
26 |
27 |

28 | <#slot {@header} /> 29 |

30 |
31 |
32 |
33 | <#slot>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nec iaculis mauris. 34 |
35 |
36 |
37 | 40 |
41 |
42 | """ 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2021 Marlus Saraiva 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | if System.get_env("BLEND") == "lowest" do 4 | config :phoenix, :json_library, Jason 5 | end 6 | 7 | config :phoenix, :stacktrace_depth, 20 8 | 9 | # Initialize plugs at runtime for faster development compilation 10 | config :phoenix, :plug_init_mode, :runtime 11 | 12 | config :logger, level: :warning 13 | config :logger, :console, format: "[$level] $message\n" 14 | 15 | # When running `mix dev` inside `surface_catalogue`, there's no need to have the 16 | # assets in "/assets/catalogue" as they are the same we already have in `/assets`. 17 | config :surface_catalogue, :assets_path, "/assets" 18 | 19 | if Mix.env() == :dev do 20 | # Configure esbuild (the version is required) 21 | config :esbuild, 22 | version: "0.13.8", 23 | default: [ 24 | args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets), 25 | cd: Path.expand("../assets", __DIR__), 26 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 27 | ] 28 | 29 | # Required at compile time 30 | config :surface_catalogue, Surface.Catalogue.Server.Endpoint, 31 | code_reloader: true, 32 | debug_errors: true 33 | end 34 | -------------------------------------------------------------------------------- /priv/catalogue/button/examples.ex: -------------------------------------------------------------------------------- 1 | defmodule SurfaceCatalogue.SampleComponents.Button.Examples do 2 | @moduledoc """ 3 | Examples for ` 20 | """ 21 | end 22 | 23 | @example true 24 | @doc """ 25 | An example with direction `horizontal` with the content larger than the 26 | code area. 27 | """ 28 | def horizontal_with_scroll(assigns) do 29 | ~F""" 30 | 31 | """ 32 | end 33 | 34 | @example direction: "vertical", height: "110px" 35 | @doc "An example with direction `vertical`." 36 | def vertical(assigns) do 37 | ~F""" 38 | 39 | 40 | 41 | 42 | 43 | """ 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/surface/catalogue/markdown.ex: -------------------------------------------------------------------------------- 1 | defmodule Surface.Catalogue.Markdown do 2 | @moduledoc false 3 | 4 | def to_html(text, opts \\ []) 5 | 6 | def to_html(nil, _opts) do 7 | "" 8 | end 9 | 10 | def to_html(text, opts) do 11 | strip = Keyword.get(opts, :strip, false) 12 | class = Keyword.get(opts, :class, "content") 13 | 14 | markdown = 15 | text 16 | |> HtmlEntities.decode() 17 | |> String.trim_leading() 18 | 19 | html = 20 | case Earmark.as_html(markdown, code_class_prefix: "language-") do 21 | {:ok, html, messages} -> 22 | Enum.each(messages, &warn/1) 23 | html 24 | 25 | {:error, html, messages} -> 26 | Enum.each(messages, fn 27 | {:warning, _line, msg} -> 28 | message = """ 29 | #{msg} 30 | 31 | Original code: 32 | 33 | #{text} 34 | """ 35 | 36 | warn(message) 37 | 38 | msg -> 39 | warn(msg) 40 | end) 41 | 42 | html 43 | end 44 | 45 | if strip do 46 | html = html |> String.trim_leading("

") |> String.trim_trailing("

") 47 | {:safe, html} 48 | else 49 | {:safe, ~s(
#{html}
)} 50 | end 51 | end 52 | 53 | defp warn(val) do 54 | if is_binary(val) do 55 | IO.warn(val) 56 | else 57 | val |> inspect() |> IO.warn() 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/surface/catalogue/live/example_live.ex: -------------------------------------------------------------------------------- 1 | defmodule Surface.Catalogue.ExampleLive do 2 | use Surface.LiveView 3 | 4 | @default_body [style: "padding: 1.5rem; height: 100%;"] 5 | 6 | data example, :module 7 | data head_css, :string 8 | data head_js, :string 9 | data body, :keyword 10 | data code, :string 11 | 12 | def handle_params(params, _uri, socket) do 13 | example = Module.safe_concat([params["example"]]) 14 | func = params["func"] 15 | config = Surface.Catalogue.get_config(example) 16 | 17 | socket = 18 | socket 19 | |> assign(:example, example || "") 20 | |> assign(:head_css, config[:head_css] || "") 21 | |> assign(:head_js, config[:head_js] || "") 22 | |> assign(:body, config[:body] || @default_body) 23 | |> assign(:func, func) 24 | 25 | {:noreply, socket} 26 | end 27 | 28 | def render(assigns) do 29 | ~F""" 30 | 31 | 32 | 33 | 34 | 35 | 36 | {raw(@head_css)} 37 | {raw(@head_js)} 38 | 39 | 40 | {live_render(@socket, @example, id: "example", session: %{"func" => @func})} 41 | 42 | 43 | """ 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /priv/catalogue/sample_components/editable_props.ex: -------------------------------------------------------------------------------- 1 | defmodule SurfaceCatalogue.SampleComponents.EditableProps do 2 | @moduledoc """ 3 | A sample component that defines props of all types that can be edited in the Playground. 4 | """ 5 | use Surface.Component 6 | 7 | prop boolean, :boolean 8 | prop string, :string 9 | prop string_choices, :string, values: ["1", "2", "3"] 10 | prop atom, :atom 11 | prop atom_choices, :atom, values: [:a, :b, :c] 12 | prop css_class, :css_class 13 | prop integer, :integer 14 | prop integer_choices, :integer, values: [1, 2, 3] 15 | prop number, :number 16 | prop list, :list 17 | prop keyword, :keyword 18 | prop any, :any 19 | 20 | def render(assigns) do 21 | ~F""" 22 |

Input Fields Available in Playground:

23 |
24 |

boolean: {@boolean}

25 |

string: {@string}

26 |

string_choices: {@string_choices}

27 |

atom: {inspect(@atom)}

28 |

atom_choices: {inspect(@atom_choices)}

29 |

css_class: {inspect(@css_class)}

30 |

integer: {@integer}

31 |

integer_choices: {@integer_choices}

32 |

number: {@number}

33 |

list: {inspect(@list)}

34 |

keyword: {inspect(@keyword)}

35 |

any: {inspect(@any)}

36 | """ 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/surface/catalogue/live/playground_live.ex: -------------------------------------------------------------------------------- 1 | defmodule Surface.Catalogue.PlaygroundLive do 2 | use Surface.LiveView 3 | 4 | alias Surface.Catalogue.Playground 5 | 6 | @default_body [style: "padding: 1.5rem; height: 100%; background-color: #f5f5f5"] 7 | 8 | data playground, :module 9 | data head_css, :string 10 | data head_js, :string 11 | data body, :keyword 12 | data __window_id__, :string 13 | 14 | def mount(params, session, socket) do 15 | window_id = Playground.get_window_id(session, params) 16 | socket = assign(socket, :__window_id__, window_id) 17 | {:ok, socket, temporary_assigns: [event_log_entries: []]} 18 | end 19 | 20 | def handle_params(params, _uri, socket) do 21 | playground = Module.safe_concat([params["playground"]]) 22 | config = Surface.Catalogue.get_config(playground) 23 | 24 | socket = 25 | socket 26 | |> assign(:playground, playground) 27 | |> assign(:head_css, config[:head_css] || "") 28 | |> assign(:head_js, config[:head_js] || "") 29 | |> assign(:body, config[:body] || @default_body) 30 | 31 | {:noreply, socket} 32 | end 33 | 34 | def render(assigns) do 35 | ~F""" 36 | 37 | 38 | 39 | 40 | 41 | 42 | {raw(@head_css)} 43 | {raw(@head_js)} 44 | 45 | 46 | {live_render(@socket, @playground, id: "playground", session: %{"__window_id__" => @__window_id__})} 47 | 48 | 49 | """ 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/surface/catalogue/components/table.ex: -------------------------------------------------------------------------------- 1 | defmodule Surface.Catalogue.Components.Table do 2 | @moduledoc false 3 | 4 | use Surface.Component 5 | 6 | @doc "The data that populates the table" 7 | prop data, :generator, required: true 8 | 9 | @doc "The table is expanded (full-width)" 10 | prop expanded, :boolean, default: true 11 | 12 | @doc "Add borders to all the cells" 13 | prop bordered, :boolean, default: false 14 | 15 | @doc "Add stripes to the table" 16 | prop striped, :boolean, default: false 17 | 18 | @doc "The CSS class for the wrapping `
` element" 19 | prop class, :css_class 20 | 21 | @doc """ 22 | A function that returns a class for the item's underlying `` 23 | element. The function receives the item and index related to 24 | the row. 25 | """ 26 | prop rowClass, :fun 27 | 28 | @doc "The columns of the table" 29 | slot cols, generator_prop: :data, required: true 30 | 31 | def render(assigns) do 32 | ~F""" 33 |
34 | 40 | 41 | 42 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | 54 |
43 | {col.label} 44 |
50 | <#slot {col} generator_value={item} /> 51 |
55 |
56 | """ 57 | end 58 | 59 | defp row_class_fun(nil), do: fn _, _ -> "" end 60 | defp row_class_fun(rowClass), do: rowClass 61 | end 62 | -------------------------------------------------------------------------------- /blend/premix.exs: -------------------------------------------------------------------------------- 1 | # This file is autogenerated by blend package. 2 | # 3 | # Run `mix blend.premix` to update it's contents after 4 | # each blend package version update. 5 | 6 | maybe_put_env = fn varname, value -> 7 | System.put_env(varname, System.get_env(varname, value)) 8 | end 9 | 10 | existing_blend = fn name -> 11 | Code.eval_file("blend.exs") 12 | |> elem(0) 13 | |> Map.fetch!(String.to_atom(name)) 14 | end 15 | 16 | blend = System.get_env("BLEND") 17 | 18 | if blend && String.length(blend) > 0 && existing_blend.(blend) do 19 | blend_path = Path.expand("blend") 20 | maybe_put_env.("MIX_LOCKFILE", Path.join([blend_path, "#{blend}.mix.lock"])) 21 | maybe_put_env.("MIX_DEPS_PATH", Path.join([blend_path, "deps", "#{blend}"])) 22 | maybe_put_env.("MIX_BUILD_ROOT", Path.join([blend_path, "_build", "#{blend}"])) 23 | end 24 | 25 | defmodule Blend.Premix do 26 | def patch_project(project) do 27 | Keyword.merge(project, maybe_lockfile_option()) 28 | end 29 | 30 | def patch_deps(mix_deps) do 31 | patch_deps(System.get_env("BLEND"), mix_deps) 32 | end 33 | 34 | defp patch_deps(nil, mix_deps), do: mix_deps 35 | defp patch_deps("", mix_deps), do: mix_deps 36 | 37 | defp patch_deps(blend, mix_deps) do 38 | blend_deps(blend) 39 | |> Enum.reduce(mix_deps, fn blend_dep, acc -> 40 | verify_requirements!(blend, blend_dep, mix_deps) 41 | List.keystore(acc, elem(blend_dep, 0), 0, blend_dep) 42 | end) 43 | end 44 | 45 | defp blend_deps(name) do 46 | {blends, []} = Code.eval_file("blend.exs") 47 | Map.fetch!(blends, String.to_atom(name)) 48 | end 49 | 50 | defp maybe_lockfile_option() do 51 | case System.get_env("MIX_LOCKFILE") do 52 | nil -> [] 53 | "" -> [] 54 | lockfile -> [lockfile: lockfile] 55 | end 56 | end 57 | 58 | defp verify_requirements!(blend, blend_dep, mix_deps) do 59 | blend_app = elem(blend_dep, 0) 60 | blend_requirement = elem(blend_dep, 1) 61 | mix_dep = List.keyfind!(mix_deps, blend_app, 0) 62 | mix_requirement = elem(mix_dep, 1) 63 | 64 | if is_binary(blend_requirement) and 65 | not Hex.Solver.Constraint.allows_all?( 66 | Hex.Solver.parse_constraint!(mix_requirement), 67 | Hex.Solver.parse_constraint!(blend_requirement) 68 | ) do 69 | Mix.shell().error(""" 70 | Blend requirement for `#{blend}` incompatible with project requirement: 71 | in mix.exs #{inspect(mix_dep)} 72 | in blend.exs #{inspect(blend_dep)}. 73 | """) 74 | 75 | exit({:shutdown, 1}) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/surface/catalogue/components/component_info.ex: -------------------------------------------------------------------------------- 1 | defmodule Surface.Catalogue.Components.ComponentInfo do 2 | @moduledoc false 3 | 4 | use Surface.Component 5 | 6 | alias Surface.Catalogue.Components.ComponentAPI 7 | alias Surface.Catalogue.Markdown 8 | alias Surface.Catalogue.Util 9 | 10 | @doc "The component module" 11 | prop module, :module, required: true 12 | 13 | data full_module_name, :string 14 | data doc_summary, :string 15 | data doc_details, :string 16 | data has_details?, :boolean 17 | data has_docs?, :boolean 18 | data api_anchor_id, :string 19 | 20 | def update(assigns) do 21 | prefix = if assigns.module.component_type() == Surface.MacroComponent, do: "#", else: "" 22 | 23 | module_name = 24 | assigns.module 25 | |> Module.split() 26 | |> List.last() 27 | 28 | full_module_name = String.replace_prefix(module_name, "", prefix) 29 | 30 | {doc_summary, doc_details} = fetch_doc_details(assigns.module) 31 | 32 | has_summary? = doc_summary not in [nil, ""] 33 | has_details? = doc_details not in [nil, ""] 34 | has_docs? = has_summary? or has_details? 35 | api_anchor_id = "#{module_name}-API" 36 | 37 | assigns 38 | |> assign(assigns) 39 | |> assign(:full_module_name, full_module_name) 40 | |> assign(:doc_summary, String.trim_trailing(doc_summary || "", ".")) 41 | |> assign(:doc_details, doc_details) 42 | |> assign(:has_details?, has_details?) 43 | |> assign(:has_docs?, has_docs?) 44 | |> assign(:api_anchor_id, api_anchor_id) 45 | end 46 | 47 | def render(assigns) do 48 | assigns = update(assigns) 49 | 50 | ~F""" 51 |
52 |

{@full_module_name}

53 | {Markdown.to_html(@doc_summary, class: "subtitle")} 54 |
55 | {@doc_details |> Markdown.to_html()} 56 |
No documentation available.
57 |
58 |

59 | # Public API 60 |

61 | 62 |
63 | """ 64 | end 65 | 66 | defp fetch_doc_details(module) do 67 | case Code.fetch_docs(module) do 68 | {:docs_v1, _, _, "text/markdown", %{"en" => doc}, _, _} -> 69 | parts = 70 | doc 71 | |> Util.split_doc_sections() 72 | |> List.first() 73 | |> String.split("\n\n", parts: 2) 74 | 75 | {Enum.at(parts, 0), Enum.at(parts, 1)} 76 | 77 | _ -> 78 | {nil, nil} 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/surface/catalogue/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Surface.Catalogue.LayoutView do 2 | use Phoenix.Template, 3 | namespace: Surface.Catalogue, 4 | root: "lib/surface/catalogue/templates" 5 | 6 | import Surface 7 | 8 | @makeup_css Makeup.stylesheet(Makeup.Styles.HTML.StyleMap.monokai_style(), "makeup-highlight") 9 | 10 | def render("makeup.css", _), do: @makeup_css 11 | 12 | def render(_, assigns) do 13 | ~F""" 14 | 15 | 16 | 17 | 18 | 19 | 20 | Component Catalogue 21 | 22 | 26 | 27 | {Phoenix.HTML.raw("")} 28 |