├── test ├── test_helper.exs ├── bitstyles_phoenix │ ├── component │ │ ├── card_test.exs │ │ ├── tabs_test.exs │ │ ├── avatar_test.exs │ │ ├── content_test.exs │ │ ├── flash_test.exs │ │ ├── heading_test.exs │ │ ├── sidebar_test.exs │ │ ├── use_svg_test.exs │ │ ├── icon_test.exs │ │ ├── badge_test.exs │ │ ├── modal_test.exs │ │ ├── dropdown_test.exs │ │ ├── breadcrumbs_test.exs │ │ ├── description_list_test.exs │ │ ├── error_test.exs │ │ ├── form_test.exs │ │ └── button_test.exs │ ├── alpine3 │ │ ├── dropdown_test.exs │ │ └── sidebar_test.exs │ └── helper │ │ └── classnames_test.exs ├── support │ └── component_case.ex └── bitstyles_phoenix_test.exs ├── .tool-versions ├── assets ├── custom.css ├── logo.svg └── icons.svg ├── demo ├── assets │ ├── css │ │ └── app.scss │ ├── package.json │ ├── js │ │ └── app.js │ └── package-lock.json ├── priv │ ├── static │ │ ├── favicon.ico │ │ └── robots.txt │ └── gettext │ │ ├── de │ │ └── LC_MESSAGES │ │ │ └── errors.po │ │ ├── errors.pot │ │ └── en │ │ └── LC_MESSAGES │ │ └── errors.po ├── .formatter.exs ├── lib │ ├── bitstyles_phoenix_demo_web │ │ ├── views │ │ │ ├── page_view.ex │ │ │ ├── layout_view.ex │ │ │ ├── error_view.ex │ │ │ └── error_helpers.ex │ │ ├── gettext.ex │ │ ├── live │ │ │ ├── demo_live.ex │ │ │ └── demo_live.html.heex │ │ ├── router.ex │ │ ├── controllers │ │ │ └── page_controller.ex │ │ ├── templates │ │ │ ├── layout │ │ │ │ ├── root.html.heex │ │ │ │ ├── app.html.heex │ │ │ │ └── live.html.heex │ │ │ └── page │ │ │ │ └── index.html.heex │ │ ├── endpoint.ex │ │ └── telemetry.ex │ ├── bitstyles_phoenix_demo.ex │ ├── bitstyles_phoenix_demo │ │ ├── thing.ex │ │ └── application.ex │ └── bitstyles_phoenix_demo_web.ex ├── test │ ├── test_helper.exs │ ├── bitstyles_phoenix_demo_web │ │ ├── views │ │ │ └── error_view_test.exs │ │ └── bitstyles_phoenix_test.exs │ └── support │ │ ├── channel_case.ex │ │ └── conn_case.ex ├── README.md ├── config │ ├── test.exs │ ├── config.exs │ └── dev.exs ├── .gitignore └── mix.exs ├── .formatter.exs ├── config └── config.exs ├── lib ├── bitstyles_phoenix │ ├── live.ex │ ├── alpine3.ex │ ├── helper │ │ ├── test_fixtures.ex │ │ ├── component_rendering.ex │ │ └── classnames.ex │ ├── bitstyles │ │ └── version.ex │ ├── component.ex │ ├── live │ │ ├── dropdown.ex │ │ └── sidebar.ex │ ├── component │ │ ├── use_svg.ex │ │ ├── content.ex │ │ ├── avatar.ex │ │ ├── card.ex │ │ ├── badge.ex │ │ ├── icon.ex │ │ ├── tabs.ex │ │ ├── error.ex │ │ ├── modal.ex │ │ ├── flash.ex │ │ ├── description_list.ex │ │ └── heading.ex │ ├── showcase.ex │ ├── alpine3 │ │ └── dropdown.ex │ └── bitstyles.ex └── bitstyles_phoenix.ex ├── .gitignore ├── LICENSE.txt ├── .github └── workflows │ └── action.yml ├── README.md ├── mix.exs ├── CODE_OF_CONDUCT.md ├── docs └── bitstyles_version_compatibility.md ├── mix.lock ├── scripts └── generate_version_showcase.ex └── CHANGELOG.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.14.5 2 | erlang 25.1.2 3 | nodejs 16.13.0 4 | -------------------------------------------------------------------------------- /assets/custom.css: -------------------------------------------------------------------------------- 1 | .content-inner { 2 | max-width: 1500px; 3 | } 4 | -------------------------------------------------------------------------------- /demo/assets/css/app.scss: -------------------------------------------------------------------------------- 1 | @import "../node_modules/bitstyles/scss/bitstyles.scss"; 2 | -------------------------------------------------------------------------------- /demo/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitcrowd/bitstyles_phoenix/HEAD/demo/priv/static/favicon.ico -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /demo/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb.PageView do 2 | use BitstylesPhoenixDemoWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb.Gettext do 2 | use Gettext, otp_app: :bitstyles_phoenix_demo 3 | end 4 | -------------------------------------------------------------------------------- /demo/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | {:ok, _} = Application.ensure_all_started(:wallaby) 4 | Application.put_env(:wallaby, :base_url, BitstylesPhoenixDemoWeb.Endpoint.url()) 5 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/card_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.CardTest do 2 | use BitstylesPhoenix.ComponentCase 3 | 4 | doctest BitstylesPhoenix.Component.Card 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/tabs_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.TabsTest do 2 | use BitstylesPhoenix.ComponentCase 3 | 4 | doctest BitstylesPhoenix.Component.Tabs 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/avatar_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.AvatarTest do 2 | use BitstylesPhoenix.ComponentCase 3 | 4 | doctest BitstylesPhoenix.Component.Avatar 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/content_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.ContentTest do 2 | use BitstylesPhoenix.ComponentCase 3 | 4 | doctest BitstylesPhoenix.Component.Content 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/flash_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.FlashTest do 2 | use BitstylesPhoenix.ComponentCase, async: true 3 | 4 | doctest BitstylesPhoenix.Component.Flash 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/heading_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.HeadingTest do 2 | use BitstylesPhoenix.ComponentCase 3 | 4 | doctest BitstylesPhoenix.Component.Heading 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/sidebar_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.SidebarTest do 2 | use BitstylesPhoenix.ComponentCase 3 | 4 | doctest BitstylesPhoenix.Component.Sidebar 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/use_svg_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.UseSVGTest do 2 | use BitstylesPhoenix.ComponentCase 3 | 4 | doctest BitstylesPhoenix.Component.UseSVG 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/icon_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.IconTest do 2 | use BitstylesPhoenix.ComponentCase, async: true 3 | 4 | doctest BitstylesPhoenix.Component.Icon 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/badge_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.BadgeTest do 2 | use BitstylesPhoenix.ComponentCase, async: true 3 | 4 | doctest BitstylesPhoenix.Component.Badge 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/modal_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.ModalTest do 2 | use BitstylesPhoenix.ComponentCase, async: true 3 | 4 | doctest BitstylesPhoenix.Component.Modal 5 | end 6 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb.LayoutView do 2 | use BitstylesPhoenixDemoWeb, :view 3 | 4 | alias BitstylesPhoenix.Live.Sidebar, as: LiveSidebar 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/dropdown_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.DropdownTest do 2 | use BitstylesPhoenix.ComponentCase, async: true 3 | 4 | doctest BitstylesPhoenix.Component.Dropdown 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/alpine3/dropdown_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Alpine3.DropdownTest do 2 | use BitstylesPhoenix.ComponentCase, async: true 3 | 4 | doctest BitstylesPhoenix.Alpine3.Dropdown, import: true 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/alpine3/sidebar_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Alpine3.SidebarTest do 2 | use BitstylesPhoenix.ComponentCase, async: true 3 | 4 | doctest BitstylesPhoenix.Alpine3.Sidebar, import: true 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/breadcrumbs_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.BreadcrumbsTest do 2 | use BitstylesPhoenix.ComponentCase, async: true 3 | 4 | doctest BitstylesPhoenix.Component.Breadcrumbs 5 | end 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/description_list_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.DescriptionListTest do 2 | use BitstylesPhoenix.ComponentCase, async: true 3 | 4 | doctest BitstylesPhoenix.Component.DescriptionList 5 | end 6 | -------------------------------------------------------------------------------- /demo/priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/error_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.ErrorTest do 2 | use BitstylesPhoenix.ComponentCase, async: true 3 | import BitstylesPhoenix.Helper.TestFixtures 4 | 5 | doctest BitstylesPhoenix.Component.Error 6 | end 7 | -------------------------------------------------------------------------------- /test/support/component_case.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.ComponentCase do 2 | use ExUnit.CaseTemplate 3 | 4 | @moduledoc false 5 | 6 | using do 7 | quote do 8 | use BitstylesPhoenix.Helper.ComponentRendering 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /demo/assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": "github:bitcrowd/bitstyles_phoenix", 3 | "description": "bitstyles_phoenix demo app assets", 4 | "license": "ISC", 5 | "dependencies": { 6 | "alpinejs": "^3.5.1", 7 | "bitstyles": "^4.0.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :bitstyles_phoenix, 4 | trim_e2e_classes: [enabled: true] 5 | 6 | config :phoenix, :json_library, Jason 7 | 8 | if config_env() === :test do 9 | config :bitstyles_phoenix, add_version_test_classes: true 10 | end 11 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/form_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.FormTest do 2 | use BitstylesPhoenix.ComponentCase, async: true 3 | import BitstylesPhoenix.Helper.TestFixtures 4 | import Phoenix.Component 5 | doctest BitstylesPhoenix.Component.Form 6 | end 7 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemo do 2 | @moduledoc """ 3 | BitstylesPhoenixDemo keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo/thing.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemo.Thing do 2 | use Ecto.Schema 3 | 4 | import Ecto.Changeset 5 | 6 | embedded_schema do 7 | field(:name, :string) 8 | end 9 | 10 | def changeset(schema \\ %__MODULE__{}, params \\ %{}) do 11 | schema 12 | |> cast(params, [:name]) 13 | |> validate_required([:name]) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixTest do 2 | use ExUnit.Case, async: true 3 | use BitstylesPhoenix 4 | import Phoenix.Component 5 | 6 | describe "short-cut imports" do 7 | test "can use the ui_* helpers" do 8 | classnames(["foo", "bar"]) 9 | 10 | assigns = %{inner_content: "foo"} 11 | ~H"<.ui_badge />" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/live.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Live do 2 | @moduledoc """ 3 | Use this module to import all `BitstylesPhoenix.Live` components at once. 4 | ``` 5 | use BitstylesPhoenix.Live 6 | ``` 7 | """ 8 | 9 | defmacro __using__(_) do 10 | quote do 11 | import BitstylesPhoenix.Live.Dropdown 12 | import BitstylesPhoenix.Live.Sidebar 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/alpine3.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Alpine3 do 2 | @moduledoc """ 3 | Use this module to import all `BitstylesPhoenix.Alpine3` components at once. 4 | ``` 5 | use BitstylesPhoenix.Alpine 6 | ``` 7 | """ 8 | defmacro __using__(_) do 9 | quote do 10 | import BitstylesPhoenix.Alpine3.Dropdown 11 | import BitstylesPhoenix.Alpine3.Sidebar 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /demo/priv/gettext/de/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## "msgid"s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove "msgid"s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use "mix gettext.extract --merge" or "mix gettext.merge" 8 | ## to merge POT files into PO files. 9 | msgid "can't be blank" 10 | msgstr "muss ausgefüllt werden" 11 | -------------------------------------------------------------------------------- /demo/priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | msgid "can't be blank" 11 | msgstr "" 12 | -------------------------------------------------------------------------------- /demo/priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | msgid "can't be blank" 11 | msgstr "" 12 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/live/demo_live.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb.DemoLive do 2 | use BitstylesPhoenixDemoWeb, :live_view 3 | 4 | def handle_event("show-flash", _, socket) do 5 | socket = 6 | socket 7 | |> put_flash(:info, "Welcome to the Demo !!!") 8 | |> put_flash(:warning, "Let's pretend we have a warning.") 9 | |> put_flash(:error, "Let's pretend we have an error.") 10 | 11 | {:noreply, push_redirect(socket, to: "/live")} 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # BitstylesPhoenix Demo 2 | 3 | To start your Demo server: 4 | 5 | * Install [asdf](https://github.com/asdf-vm/asdf) 6 | * Install Elixir, Erlang & node with asdf `asdf install` 7 | * Install dependencies with `mix deps.get` 8 | * Install node modules with `(cd assets && npm install)` 9 | * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` 10 | 11 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 12 | 13 | # Run integration tests 14 | 15 | ``` elixir 16 | mix test 17 | ``` 18 | 19 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb.Router do 2 | use BitstylesPhoenixDemoWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_live_flash 8 | plug :put_root_layout, {BitstylesPhoenixDemoWeb.LayoutView, :root} 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | end 12 | 13 | scope "/", BitstylesPhoenixDemoWeb do 14 | pipe_through :browser 15 | 16 | get "/", PageController, :index 17 | live "/live", DemoLive 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/helper/classnames_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Helper.ClassnamesTest do 2 | use ExUnit.Case, async: true 3 | import BitstylesPhoenix.Helper.Classnames 4 | doctest BitstylesPhoenix.Helper.Classnames 5 | 6 | describe "classnames/1" do 7 | test "removes e2e- classes from class lists" do 8 | assert classnames("e2e-out o-in") == "o-in" 9 | assert classnames(["e2e-out o-in"]) == "o-in" 10 | assert classnames(["e2e-out o-#{"in"}"]) == "o-in" 11 | assert classnames([{"e2e-out o-in", true}]) == "o-in" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /demo/test/bitstyles_phoenix_demo_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb.ErrorViewTest do 2 | use BitstylesPhoenixDemoWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(BitstylesPhoenixDemoWeb.ErrorView, "404.html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(BitstylesPhoenixDemoWeb.ErrorView, "500.html", []) == 13 | "Internal Server Error" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb.PageController do 2 | alias BitstylesPhoenixDemo.Thing 3 | use BitstylesPhoenixDemoWeb, :controller 4 | 5 | def index(conn, _params) do 6 | changeset = 7 | Thing.changeset() 8 | |> Map.put(:action, "update") 9 | 10 | conn 11 | |> put_flash(:info, "Welcome to the Demo !!!") 12 | |> put_flash(:warning, "Let's pretend we have a warning.") 13 | |> put_flash(:error, "Let's pretend we have an error.") 14 | |> render("index.html", changeset: changeset) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb.ErrorView do 2 | use BitstylesPhoenixDemoWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /demo/config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :bitstyles_phoenix_demo, BitstylesPhoenixDemoWeb.Endpoint, 6 | http: [ip: {127, 0, 0, 1}, port: 4002], 7 | secret_key_base: "rWByyzlqZQabPuGKeXONbDeDQs8Zhgtgw9RALtnzQj1NZO51kFz8pvCsD42KXy3L", 8 | server: true 9 | 10 | config :bitstyles_phoenix, 11 | trim_e2e_classes: [enabled: false] 12 | 13 | # Print only warnings and errors during test 14 | config :logger, level: :warn 15 | 16 | # Initialize plugs at runtime for faster test compilation 17 | config :phoenix, :plug_init_mode, :runtime 18 | 19 | config :wallaby, otp_app: :bitstyles_phoenix_demo 20 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/helper/test_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Helper.TestFixtures do 2 | @moduledoc false 3 | 4 | # Static values helpful in writing doctests. 5 | # This module is in `/lib` and not in `/test` so that it can be used in the dev env 6 | # in the mix task Mix.Tasks.BitstylesPhoenix.GenerateVersionsShowcase 7 | 8 | def form do 9 | Phoenix.Component.to_form(%{}, as: :user) 10 | end 11 | 12 | def form_with_errors do 13 | Phoenix.Component.to_form(%{}, 14 | as: :user, 15 | errors: [ 16 | name: {"is too short", []}, 17 | email: {"is invalid", []}, 18 | email: "must end with @bitcrowd.net" 19 | ] 20 | ) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /.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 | bitstyles_phoenix-*.tar 24 | 25 | /version_showcase 26 | -------------------------------------------------------------------------------- /demo/assets/js/app.js: -------------------------------------------------------------------------------- 1 | import "phoenix_html" 2 | import Alpine from 'alpinejs' 3 | import {Socket} from "phoenix" 4 | import {LiveSocket} from "phoenix_live_view" 5 | 6 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 7 | let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) 8 | 9 | liveSocket.connect() 10 | Alpine.start() 11 | 12 | // expose liveSocket on window for web console debug logs and latency simulation: 13 | // >> liveSocket.enableDebug() 14 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 15 | // >> liveSocket.disableLatencySim() 16 | window.liveSocket = liveSocket 17 | window.Alpine = Alpine 18 | 19 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/templates/layout/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= csrf_meta_tag() %> 8 | <.live_title suffix="- Bitstyles Phoenix Demo"><%= assigns[:page_title] || "Main" %> 9 | 10 | 11 | 12 | 13 | <%= @inner_content %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | 3 | Copyright (c) 2021-2024, Bitcrowd GmbH. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/helper/component_rendering.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Helper.ComponentRendering do 2 | @moduledoc false 3 | 4 | defmacro __using__(_) do 5 | quote do 6 | use BitstylesPhoenix, js_mode: :none 7 | alias Phoenix.HTML.Safe 8 | import Phoenix.Component, only: [sigil_H: 2] 9 | import Phoenix.HTML, only: [safe_to_string: 1] 10 | import BitstylesPhoenix.Helper.TestFixtures 11 | 12 | defp render(%Phoenix.LiveView.Rendered{} = template) do 13 | template 14 | |> Safe.to_iodata() 15 | |> IO.iodata_to_binary() 16 | |> prettify_html() 17 | end 18 | 19 | defp render(template) do 20 | template 21 | |> safe_to_string() 22 | |> prettify_html() 23 | end 24 | 25 | defp prettify_html(html) do 26 | html 27 | |> Floki.parse_fragment!() 28 | |> Floki.raw_html(pretty: true) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /demo/.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 3rd-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 | bitstyles_phoenix_demo-*.tar 24 | 25 | # Ignore assets that are produced by build tools. 26 | /priv/static/assets/ 27 | 28 | # Ignore digested assets cache. 29 | /priv/static/cache_manifest.json 30 | 31 | # In case you use Node.js/npm, you want to ignore these. 32 | npm-debug.log 33 | /assets/node_modules/ 34 | 35 | /screenshots 36 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/bitstyles/version.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Bitstyles.Version do 2 | @moduledoc false 3 | 4 | @default_version "6.0.0" 5 | 6 | def version do 7 | to_tuple(version_string()) 8 | end 9 | 10 | def version_string do 11 | bitstyles_version_override = Process.get(:bitstyles_phoenix_bistyles_version) 12 | 13 | bitstyles_version_override || 14 | Application.get_env(:bitstyles_phoenix, :bitstyles_version, @default_version) 15 | end 16 | 17 | def default_version do 18 | to_tuple(@default_version) 19 | end 20 | 21 | def default_version_string, do: @default_version 22 | 23 | def to_tuple(version) when is_tuple(version), do: version 24 | 25 | def to_tuple(version) when is_binary(version) do 26 | version 27 | |> String.split(".") 28 | |> Enum.map(&String.to_integer/1) 29 | |> List.to_tuple() 30 | end 31 | 32 | def to_string(version) when is_binary(version), do: version 33 | 34 | def to_string(version) when is_tuple(version) do 35 | version 36 | |> Tuple.to_list() 37 | |> Enum.map_join(".", &Kernel.to_string/1) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /demo/test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use BitstylesPhoenixDemoWeb.ChannelCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with channels 23 | import Phoenix.ChannelTest 24 | import BitstylesPhoenixDemoWeb.ChannelCase 25 | 26 | # The default endpoint for testing 27 | @endpoint BitstylesPhoenixDemoWeb.Endpoint 28 | end 29 | end 30 | 31 | setup _tags do 32 | :ok 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/component.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component do 2 | @moduledoc false 3 | 4 | import Phoenix.Component 5 | 6 | defmacro __using__(_) do 7 | quote do 8 | import Phoenix.Component 9 | import BitstylesPhoenix.Helper.Classnames 10 | import BitstylesPhoenix.Showcase 11 | import BitstylesPhoenix.Component 12 | end 13 | end 14 | 15 | @type assigns_from_single_slot_option :: {:exclude, [atom()]} | {:optional, boolean()} 16 | @spec assigns_from_single_slot(assigns :: map(), slot_name :: atom(), [ 17 | assigns_from_single_slot_option 18 | ]) :: {slot :: map(), attributes :: keyword()} | {nil, []} 19 | def assigns_from_single_slot(assigns, slot_name, opts \\ []) do 20 | case assigns[slot_name] do 21 | [slot] -> 22 | extra = assigns_to_attributes(slot, Keyword.get(opts, :exclude, [])) 23 | {slot, extra} 24 | 25 | nil -> 26 | if Keyword.has_key?(opts, :optional) do 27 | {nil, []} 28 | else 29 | raise "please specify #{slot_name} slot" 30 | end 31 | 32 | _ -> 33 | raise "please specify at most one #{slot_name} slot" 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo/application.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemo.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | children = [ 11 | # Start the Telemetry supervisor 12 | BitstylesPhoenixDemoWeb.Telemetry, 13 | # Start the PubSub system 14 | {Phoenix.PubSub, name: BitstylesPhoenixDemo.PubSub}, 15 | # Start the Endpoint (http/https) 16 | BitstylesPhoenixDemoWeb.Endpoint 17 | # Start a worker by calling: BitstylesPhoenixDemo.Worker.start_link(arg) 18 | # {BitstylesPhoenixDemo.Worker, arg} 19 | ] 20 | 21 | # See https://hexdocs.pm/elixir/Supervisor.html 22 | # for other strategies and supported options 23 | opts = [strategy: :one_for_one, name: BitstylesPhoenixDemo.Supervisor] 24 | Supervisor.start_link(children, opts) 25 | end 26 | 27 | # Tell Phoenix to update the endpoint configuration 28 | # whenever the application is updated. 29 | @impl true 30 | def config_change(changed, _new, removed) do 31 | BitstylesPhoenixDemoWeb.Endpoint.config_change(changed, removed) 32 | :ok 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /demo/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use BitstylesPhoenixDemoWeb.ConnCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with connections 23 | import Plug.Conn 24 | import Phoenix.ConnTest 25 | import BitstylesPhoenixDemoWeb.ConnCase 26 | 27 | alias BitstylesPhoenixDemoWeb.Router.Helpers, as: Routes 28 | 29 | # The default endpoint for testing 30 | @endpoint BitstylesPhoenixDemoWeb.Endpoint 31 | end 32 | end 33 | 34 | setup _tags do 35 | {:ok, conn: Phoenix.ConnTest.build_conn()} 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Translates an error message using gettext. 10 | """ 11 | def translate_error({msg, opts}) do 12 | # When using gettext, we typically pass the strings we want 13 | # to translate as a static argument: 14 | # 15 | # # Translate "is invalid" in the "errors" domain 16 | # dgettext("errors", "is invalid") 17 | # 18 | # # Translate the number of files with plural rules 19 | # dngettext("errors", "1 file", "%{count} files", count) 20 | # 21 | # Because the error messages we show in our forms and APIs 22 | # are defined inside Ecto, we need to translate them dynamically. 23 | # This requires us to call the Gettext module passing our gettext 24 | # backend as first argument. 25 | # 26 | # Note we use the "errors" domain, which means translations 27 | # should be written to the errors.po file. The :count option is 28 | # set by Ecto and indicates we should also apply plural rules. 29 | if count = opts[:count] do 30 | Gettext.dngettext(BitstylesPhoenixDemoWeb.Gettext, "errors", msg, msg, count, opts) 31 | else 32 | Gettext.dgettext(BitstylesPhoenixDemoWeb.Gettext, "errors", msg, opts) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/bitstyles_phoenix/component/button_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.ButtonTest do 2 | use BitstylesPhoenix.ComponentCase, async: true 3 | 4 | doctest BitstylesPhoenix.Component.Button 5 | 6 | describe "ui_button/1" do 7 | test "deprecate :to" do 8 | warning = 9 | ExUnit.CaptureIO.capture_io(:stderr, fn -> 10 | assigns = %{} 11 | 12 | result = 13 | render(~H""" 14 | <.ui_button to="/foo"> 15 | Show 16 | 17 | """) 18 | 19 | assert result == 20 | """ 21 | 22 | Show 23 | 24 | """ 25 | end) 26 | 27 | assert String.contains?(warning, "deprecated") 28 | end 29 | 30 | test "deprecate :variant" do 31 | warning = 32 | ExUnit.CaptureIO.capture_io(:stderr, fn -> 33 | assigns = %{} 34 | 35 | result = 36 | render(~H""" 37 | <.ui_button variant="ui"> 38 | Show 39 | 40 | """) 41 | 42 | assert result == 43 | """ 44 | 47 | """ 48 | end) 49 | 50 | assert String.contains?(warning, "deprecated") 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /.github/workflows/action.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | name: unit 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: erlef/setup-beam@v1 10 | with: 11 | otp-version: '25.1' 12 | elixir-version: '1.14.5' 13 | - run: mix deps.get 14 | - run: mix format --check-formatted 15 | - run: mix test 16 | - run: mix credo --strict 17 | - run: mix docs 18 | - name: Check if versions showcase mix script finishes 19 | run: mix run scripts/generate_version_showcase.ex 20 | 21 | test-unlocked-deps: 22 | runs-on: ubuntu-latest 23 | name: unit (unlocked deps) 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: erlef/setup-beam@v1 27 | with: 28 | otp-version: '25.1' 29 | elixir-version: '1.14.5' 30 | - run: mix deps.unlock --all 31 | - run: mix deps.get 32 | - run: mix test 33 | 34 | demo: 35 | runs-on: ubuntu-latest 36 | name: demo 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: erlef/setup-beam@v1 40 | with: 41 | otp-version: '25.1' 42 | elixir-version: '1.14.5' 43 | - uses: actions/setup-node@v4 44 | with: 45 | node-version: 18 46 | - uses: nanasess/setup-chromedriver@v2 47 | - run: cd demo && mix deps.get 48 | - run: cd demo && mix format --check-formatted 49 | - run: cd demo/assets && npm install 50 | - run: cd demo && mix test 51 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/live/demo_live.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 | <.ui_content> 4 | <.ui_page_title> 5 | Live test 6 | <:action> 7 | <.ui_button phx-click="show-flash">Flash messages 8 | 9 | <:action> 10 | <.ui_js_dropdown variant="right"> 11 | <:button label="Test" class="e2e-dropdown-button" /> 12 | <:menu> 13 | <.ui_dropdown_option href={Routes.page_path(@socket, :index)} class="e2e-dropdown-option"> 14 | Funky 15 | 16 | <.ui_dropdown_option> 17 | Flurky 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | <.ui_content> 26 |

<%= Faker.Lorem.paragraph() %>

27 | <.ui_js_dropdown variant={[:top]}> 28 | <:button class="e2e-dropdown-button-id"> 29 | <.ui_icon name="caret-up" class="a-button__icon" /> 30 | Test with ID and custom button content 31 | 32 | <:menu id="a-dropdown"> 33 | <.ui_dropdown_option href={Routes.page_path(@socket, :index)} class="e2e-dropdown-option-id"> 34 | Funky 35 | 36 | <.ui_dropdown_option> 37 | Flurky 38 | 39 | 40 | 41 | 42 |
43 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :bitstyles_phoenix_demo 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | store: :cookie, 9 | key: "_bitstyles_phoenix_demo_key", 10 | signing_salt: "ojAm0O/0" 11 | ] 12 | 13 | socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] 14 | 15 | # Serve at "/" the static files from "priv/static" directory. 16 | # 17 | # You should set gzip to true if you are running phx.digest 18 | # when deploying your static files in production. 19 | plug Plug.Static, 20 | at: "/", 21 | from: :bitstyles_phoenix_demo, 22 | gzip: false, 23 | only: ~w(assets fonts images favicon.ico robots.txt) 24 | 25 | # Code reloading can be explicitly enabled under the 26 | # :code_reloader configuration of your endpoint. 27 | if code_reloading? do 28 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 29 | plug Phoenix.LiveReloader 30 | plug Phoenix.CodeReloader 31 | end 32 | 33 | plug Plug.RequestId 34 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 35 | 36 | plug Plug.Parsers, 37 | parsers: [:urlencoded, :multipart, :json], 38 | pass: ["*/*"], 39 | json_decoder: Phoenix.json_library() 40 | 41 | plug Plug.MethodOverride 42 | plug Plug.Head 43 | plug Plug.Session, @session_options 44 | plug BitstylesPhoenixDemoWeb.Router 45 | end 46 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb.Telemetry do 2 | use Supervisor 3 | import Telemetry.Metrics 4 | 5 | def start_link(arg) do 6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 | end 8 | 9 | @impl true 10 | def init(_arg) do 11 | children = [ 12 | # Telemetry poller will execute the given period measurements 13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 | # Add reporters as children of your supervision tree. 16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | 22 | def metrics do 23 | [ 24 | # Phoenix Metrics 25 | summary("phoenix.endpoint.stop.duration", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.router_dispatch.stop.duration", 29 | tags: [:route], 30 | unit: {:native, :millisecond} 31 | ), 32 | 33 | # VM Metrics 34 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 35 | summary("vm.total_run_queue_lengths.total"), 36 | summary("vm.total_run_queue_lengths.cpu"), 37 | summary("vm.total_run_queue_lengths.io") 38 | ] 39 | end 40 | 41 | defp periodic_measurements do 42 | [ 43 | # A module, function and arguments to be invoked periodically. 44 | # This function must call :telemetry.execute/3 and a metric must be added above. 45 | # {BitstylesPhoenixDemoWeb, :count_users, []} 46 | ] 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/live/dropdown.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Live.Dropdown do 2 | use BitstylesPhoenix.Component 3 | 4 | alias BitstylesPhoenix.Component.Dropdown, as: RawDropdown 5 | alias Phoenix.LiveView.JS 6 | 7 | import Phoenix.LiveView.Utils, only: [random_id: 0] 8 | 9 | @moduledoc """ 10 | Components for rendering a drowdowns powered by LiveView commands. 11 | """ 12 | 13 | @doc """ 14 | Renders a dropdown component with a button, a menu and options with JS commands. 15 | 16 | Supports all attributes and slots from `BitstylesPhoenix.Component.Dropdown.ui_dropdown`. 17 | """ 18 | 19 | def ui_js_dropdown(assigns) do 20 | extra = assigns_to_attributes(assigns, [:menu, :button]) 21 | 22 | {_, button_extra} = assigns_from_single_slot(assigns, :button) 23 | 24 | {_, menu_extra} = assigns_from_single_slot(assigns, :menu) 25 | 26 | menu_extra = Keyword.put_new_lazy(menu_extra, :id, &random_id/0) 27 | 28 | assigns = 29 | assign(assigns, 30 | extra: extra, 31 | button_extra: button_extra, 32 | menu_extra: menu_extra, 33 | menu_id: menu_extra[:id] 34 | ) 35 | 36 | ~H""" 37 | 38 | <:button 39 | phx-click={JS.toggle(to: "##{@menu_id}", 40 | in: {"is-transitioning", "is-off-screen", "is-on-screen"}, 41 | out: {"is-transitioning", "is-on-screen", "is-off-screen"})} 42 | aria-controls={@menu_id} 43 | {@button_extra} 44 | > 45 | <%= render_slot(@button) %> 46 | 47 | <:menu 48 | style="display: none" 49 | phx-click-away={JS.hide(to: "##{@menu_id}",transition: {"is-transitioning", "is-on-screen", "is-off-screen"})} 50 | {@menu_extra} 51 | > 52 | <%= render_slot(@menu) %> 53 | 54 | 55 | """ 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /demo/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | import Config 9 | 10 | config :bitstyles_phoenix, 11 | translate_errors: {BitstylesPhoenixDemoWeb.ErrorHelpers, :translate_error, []}, 12 | icon_file: {BitstylesPhoenixDemoWeb.Endpoint, :static_path, ["/assets/images/icons.svg"]} 13 | 14 | config :bitstyles_phoenix_demo, BitstylesPhoenixDemoWeb.Gettext, 15 | default_locale: "en", 16 | locales: ~w(en de) 17 | 18 | # Configures the endpoint 19 | config :bitstyles_phoenix_demo, BitstylesPhoenixDemoWeb.Endpoint, 20 | url: [host: "localhost"], 21 | render_errors: [view: BitstylesPhoenixDemoWeb.ErrorView, accepts: ~w(html json), layout: false], 22 | pubsub_server: BitstylesPhoenixDemo.PubSub, 23 | live_view: [signing_salt: "38dJuqM3"] 24 | 25 | # Configure esbuild (the version is required) 26 | config :esbuild, 27 | version: "0.12.18", 28 | default: [ 29 | args: 30 | ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), 31 | cd: Path.expand("../assets", __DIR__), 32 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 33 | ] 34 | 35 | config :dart_sass, 36 | version: "1.43.4", 37 | default: [ 38 | args: ~w(css/app.scss ../priv/static/assets/app.css), 39 | cd: Path.expand("../assets", __DIR__) 40 | ] 41 | 42 | # Configures Elixir's Logger 43 | config :logger, :console, 44 | format: "$time $metadata[$level] $message\n", 45 | metadata: [:request_id] 46 | 47 | # Use Jason for JSON parsing in Phoenix 48 | config :phoenix, :json_library, Jason 49 | 50 | # Import environment specific config. This must remain at the bottom 51 | # of this file so it overrides the configuration defined above. 52 | import_config "#{config_env()}.exs" 53 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/templates/page/index.html.heex: -------------------------------------------------------------------------------- 1 | 6 | <.ui_content variant="full"> 7 | <.ui_page_title>Alpine test 8 | 9 | <.ui_icon name="home" class="e2e-external-svg-icon"/> 10 | <.ui_icon name="foo" file={nil} class="e2e-inline-svg-icon"/> 11 | 12 | <.ui_section_title class="u-margin-l-bottom">Form test 13 | 14 |

<%= Faker.Lorem.paragraph() %>

15 | 16 | <.form :let={f} for={@changeset} class="e2e-form"> 17 | <.ui_input form={f} field={:name} required /> 18 | 19 | 20 | <%= Gettext.with_locale("de", fn -> %> 21 | <.form :let={f} for={@changeset} class="e2e-form-de"> 22 | <.ui_input form={f} field={:name} required /> 23 | 24 | <% end) %> 25 | 26 | <.ui_js_dropdown> 27 | <:button label="Menu" class="e2e-dropdown-button" /> 28 | <:menu> 29 | <.ui_dropdown_option href={Routes.live_path(@conn, BitstylesPhoenixDemoWeb.DemoLive)} class="e2e-dropdown-option"> 30 | Live 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/templates/layout/app.html.heex: -------------------------------------------------------------------------------- 1 | <.ui_js_sidebar_layout> 2 | <:large_sidebar> 3 | 4 | 5 | bitcrowd 6 | 7 | 8 | <:small_sidebar :let={s}> 9 |
10 | 11 | 12 | bitcrowd 13 | 14 |
15 | <.ui_js_sidebar_close sidebar={s} class="e2e-sidebar-close"/> 16 |
17 |
18 | 19 | <:sidebar_content> 20 | <.ui_sidebar_nav> 21 | <.ui_sidebar_nav_item><.ui_button href={Routes.page_path(@conn, :index)} class="u-flex-grow-1 e2e-sidebar-alpine" variant="nav">Alpine 3 22 | <.ui_sidebar_nav_item><.ui_button href={Routes.live_path(@conn, BitstylesPhoenixDemoWeb.DemoLive)} class="u-flex-grow-1 e2e-sidebar-live" variant="nav">Live 23 | 24 | 25 | <:main :let={s}> 26 | <.ui_js_sidebar_open sidebar={s} class="u-margin-s e2e-sidebar-open"/> 27 | <%= if content = get_flash(@conn, :info) do %> 28 | <.ui_flash variant={[:positive, :full]}> 29 | <%= content %> 30 | 31 | <% end %> 32 | <%= if content = get_flash(@conn, :warning) do %> 33 | <.ui_flash variant={[:warning, :full]}> 34 | <.ui_icon name="exclamation" class="u-flex-shrink-0 u-margin-m-right" /> 35 | <%= content %> 36 | 37 | <% end %> 38 | <%= if content = get_flash(@conn, :error) do %> 39 | <.ui_flash variant={[:danger, :full]}> 40 | <.ui_icon name="exclamation" class="u-flex-shrink-0 u-margin-m-right" /> 41 | <%= content %> 42 | 43 | <% end %> 44 | <%= @inner_content %> 45 | 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BitstylesPhoenix 2 | 3 | [![Hex pm](http://img.shields.io/hexpm/v/bitstyles_phoenix.svg?style=flat)](https://hex.pm/packages/bitstyles_phoenix) 4 | [![Hex docs](http://img.shields.io/badge/hex.pm-docs-green.svg?style=flat)](https://hexdocs.pm/bitstyles_phoenix) 5 | [![License](https://img.shields.io/hexpm/l/bitstyles_phoenix?style=flat)](./LICENSE.txt) 6 | 7 | Basic helpers for [bitstyles](https://github.com/bitcrowd/bitstyles) for elixir phoenix projects. 8 | 9 | ## Requirements 10 | 11 | Bitstyles must be installed separately into the asset generation. The helpers in this project just output classes for working with bitstyles. 12 | 13 | Bitstyles versions from 6.0.0 down to 1.3.0 are supported. 14 | 15 | ## Installation 16 | 17 | The package can be installed by adding `bitstyles_phoenix` to your list of dependencies in `mix.exs`: 18 | 19 | ```elixir 20 | def deps do 21 | [ 22 | {:bitstyles_phoenix, "~> 2.5"} 23 | ] 24 | end 25 | ``` 26 | 27 | To make use of the various `ui_*` helpers in the project, just add a use statement to the phoenix application view_helpers: 28 | 29 | ```elixir 30 | defp view_helpers do 31 | quote do 32 | # Use all HTML functionality (forms, tags, etc) 33 | use Phoenix.HTML 34 | use BitstylesPhoenix 35 | 36 | # Import basic rendering functionality (render, render_layout, etc) 37 | import Phoenix.View 38 | 39 | ... 40 | end 41 | end 42 | 43 | ``` 44 | 45 | ## Getting started 46 | 47 | Check out the top level `BitstylesPhoenix` module for usage examples, for the `ui_*` helpers. 48 | 49 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 50 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 51 | be found at [https://hexdocs.pm/bitstyles_phoenix](https://hexdocs.pm/bitstyles_phoenix). 52 | 53 | ## Configuration 54 | 55 | Check out the top level `BitstylesPhoenix` module for configuration examples and documentation. 56 | 57 | ## Developing bitstyles_phoenix 58 | 59 | To live update the documentation when you change the `lib` folder you can do: 60 | 61 | ```sh 62 | mix docs && fswatch -o lib | xargs -n1 -I {} mix docs 63 | ``` 64 | 65 | For running the demo app & integration tests check out the [demo README](demo/README.md). 66 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :bitstyles_phoenix, 7 | version: "2.5.2", 8 | elixir: "~> 1.11", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps(), 12 | description: "A collection of Elixir phoenix helpers for bitstyles", 13 | package: package(), 14 | source_url: "https://github.com/bitcrowd/bitstyles_phoenix", 15 | docs: [ 16 | main: "BitstylesPhoenix", 17 | assets: %{"assets" => "assets"}, 18 | logo: "assets/logo.svg", 19 | extras: ["CHANGELOG.md", "README.md", "LICENSE.txt"], 20 | groups_for_modules: [ 21 | Helpers: ~r/Helper/, 22 | Components: ~r/Component/, 23 | "Live components": ~r/Live/, 24 | "Alpine components": ~r/Alpine/ 25 | ], 26 | nest_modules_by_prefix: [ 27 | BitstylesPhoenix.Component, 28 | BitstylesPhoenix.Alpine3, 29 | BitstylesPhoenix.Live, 30 | BitstylesPhoenix.Helper 31 | ], 32 | before_closing_head_tag: &custom_css/1 33 | ] 34 | ] 35 | end 36 | 37 | def custom_css(:html), do: "" 38 | def custom_css(_), do: "" 39 | 40 | # Run "mix help compile.app" to learn about applications. 41 | def application do 42 | [ 43 | extra_applications: [:logger] 44 | ] 45 | end 46 | 47 | defp elixirc_paths(:test), do: ["lib", "test/support"] 48 | 49 | defp elixirc_paths(_), do: ["lib"] 50 | 51 | # Run "mix help deps" to learn about dependencies. 52 | defp deps do 53 | [ 54 | {:jason, "~> 1.0"}, 55 | {:phoenix_live_view, "~> 0.18.12 or ~> 0.19.0 or ~> 0.20.0"}, 56 | {:phoenix_html, "~> 3.3 or ~> 4.0"}, 57 | {:floki, "~> 0.38", only: [:test, :dev]}, 58 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 59 | {:credo, ">= 0.0.0", only: :dev, runtime: false} 60 | ] 61 | end 62 | 63 | defp package() do 64 | [ 65 | # This option is only needed when you don't want to use the OTP application name 66 | name: "bitstyles_phoenix", 67 | licenses: ~w[ISC], 68 | links: %{ 69 | "GitHub" => "https://github.com/bitcrowd/bitstyles_phoenix", 70 | "bitstyles" => "https://github.com/bitcrowd/bitstyles" 71 | } 72 | ] 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemoWeb do 2 | @moduledoc false 3 | 4 | def controller do 5 | quote do 6 | use Phoenix.Controller, namespace: BitstylesPhoenixDemoWeb 7 | 8 | import Plug.Conn 9 | import BitstylesPhoenixDemoWeb.Gettext 10 | alias BitstylesPhoenixDemoWeb.Router.Helpers, as: Routes 11 | end 12 | end 13 | 14 | def view do 15 | quote do 16 | use Phoenix.View, 17 | root: "lib/bitstyles_phoenix_demo_web/templates", 18 | namespace: BitstylesPhoenixDemoWeb 19 | 20 | # Import convenience functions from controllers 21 | import Phoenix.Controller, 22 | only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] 23 | 24 | use BitstylesPhoenix.Alpine3 25 | 26 | # Include shared imports and aliases for views 27 | unquote(view_helpers()) 28 | end 29 | end 30 | 31 | def live_view do 32 | quote do 33 | use Phoenix.LiveView, 34 | layout: {BitstylesPhoenixDemoWeb.LayoutView, :live} 35 | 36 | use BitstylesPhoenix.Live 37 | 38 | unquote(view_helpers()) 39 | end 40 | end 41 | 42 | def live_component do 43 | quote do 44 | use Phoenix.LiveComponent 45 | 46 | use BitstylesPhoenix.Live 47 | 48 | unquote(view_helpers()) 49 | end 50 | end 51 | 52 | def router do 53 | quote do 54 | use Phoenix.Router 55 | 56 | import Plug.Conn 57 | import Phoenix.Controller 58 | import Phoenix.LiveView.Router 59 | end 60 | end 61 | 62 | def channel do 63 | quote do 64 | use Phoenix.Channel 65 | import BitstylesPhoenixDemoWeb.Gettext 66 | end 67 | end 68 | 69 | defp view_helpers do 70 | quote do 71 | # Use all HTML functionality (forms, tags, etc) 72 | use Phoenix.HTML 73 | 74 | use BitstylesPhoenix 75 | # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc) 76 | import Phoenix.Component 77 | 78 | # Import basic rendering functionality (render, render_layout, etc) 79 | import Phoenix.View 80 | 81 | import BitstylesPhoenixDemoWeb.ErrorHelpers 82 | import BitstylesPhoenixDemoWeb.Gettext 83 | alias BitstylesPhoenixDemoWeb.Router.Helpers, as: Routes 84 | end 85 | end 86 | 87 | @doc """ 88 | When used, dispatch to the appropriate controller/view/etc. 89 | """ 90 | defmacro __using__(which) when is_atom(which) do 91 | apply(__MODULE__, which, []) 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at darren@bitcrowd.net. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/component/use_svg.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.UseSVG do 2 | use BitstylesPhoenix.Component 3 | 4 | @moduledoc """ 5 | Components for rendering SVGs. 6 | """ 7 | 8 | @doc ~S""" 9 | Renders an SVG tag with a `use` reference. 10 | """ 11 | 12 | story( 13 | "A referenced SVG (inlined on the page)", 14 | """ 15 | iex> assigns = %{} 16 | ...> render ~H\""" 17 | ...> <.ui_svg use="arrow"/> 18 | ...> \""" 19 | """, 20 | """ 21 | \""" 22 | 23 | 24 | 25 | 26 | \""" 27 | """, 28 | extra_html: """ 29 | 34 | """ 35 | ) 36 | 37 | story( 38 | "A referenced SVG (external file)", 39 | """ 40 | iex> assigns = %{} 41 | ...> render ~H\""" 42 | ...> <.ui_svg use="logo" file="assets/logo.svg" viewbox="0 0 400 280"/> 43 | ...> \""" 44 | """, 45 | """ 46 | \""" 47 | 48 | 49 | 50 | 51 | \""" 52 | """, 53 | background: "white" 54 | ) 55 | 56 | story( 57 | "A referenced SVG (external file with symbols)", 58 | """ 59 | iex> assigns = %{} 60 | ...> render ~H\""" 61 | ...> <.ui_svg file="assets/icons.svg" use="icon-bin"/> 62 | ...> \""" 63 | """, 64 | """ 65 | \""" 66 | 67 | 68 | 69 | 70 | \""" 71 | """, 72 | background: "white" 73 | ) 74 | 75 | def ui_svg(assigns) do 76 | extra = 77 | assigns 78 | |> assigns_to_attributes([:file, :use]) 79 | |> Keyword.put_new(:xmlns, "http://www.w3.org/2000/svg") 80 | 81 | assigns = assign(assigns, href: href(assigns), extra: extra) 82 | 83 | ~H""" 84 | 85 | 86 | 87 | """ 88 | end 89 | 90 | defp href(assigns), do: "#{assigns[:file]}##{assigns[:use]}" 91 | end 92 | -------------------------------------------------------------------------------- /demo/lib/bitstyles_phoenix_demo_web/templates/layout/live.html.heex: -------------------------------------------------------------------------------- 1 | 2 | <:large_sidebar> 3 | 4 | 5 | bitcrowd 6 | 7 | 8 | <:small_sidebar :let={s}> 9 |
10 | 11 | 12 | bitcrowd 13 | 14 |
15 | 16 |
17 |
18 | 19 | <:sidebar_content> 20 | <.ui_sidebar_nav> 21 | <.ui_sidebar_nav_item><.ui_button href={Routes.page_path(@socket, :index)} class="u-flex-grow-1 e2e-sidebar-alpine" variant="nav">Alpine 3 22 | <.ui_sidebar_nav_item><.ui_button href={Routes.live_path(@socket, BitstylesPhoenixDemoWeb.DemoLive)} class="u-flex-grow-1 e2e-sidebar-live" variant="nav">Live 23 | 24 | 25 | <:main :let={s}> 26 | <%= if content = live_flash(@flash, :info) do %> 27 | <.ui_flash variant="positive" phx-click="lv:clear-flash" phx-value-key="info"> 28 | <%= content %> 29 | 30 | <% end %> 31 | <%= if content = live_flash(@flash, :warning) do %> 32 | <.ui_flash variant="warning" phx-click="lv:clear-flash" phx-value-key="warning"> 33 | <.ui_icon name="exclamation" class= "u-flex-shrink-0 u-margin-m-right" /> 34 | <%= content %> 35 | 36 | <% end %> 37 | <%= if content = live_flash(@flash, :error) do %> 38 | <.ui_flash variant="danger" phx-click="lv:clear-flash" phx-value-key="error"> 39 | <.ui_icon name="exclamation" class= "u-flex-shrink-0 u-margin-m-right" /> 40 | <%= content %> 41 | 42 | <% end %> 43 | 50 | <%= @inner_content %> 51 | 52 |
53 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/component/content.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.Content do 2 | use BitstylesPhoenix.Component 3 | 4 | @moduledoc """ 5 | The Content component. 6 | """ 7 | 8 | @doc ~s""" 9 | Renders a content div, to add some spacing to the sides of your content. 10 | 11 | ## Attributes 12 | 13 | - `variant` — Variant of the content you want, from those available in the CSS classes e.g. `full` 14 | - `class` - Extra classes to pass to the content. See `BitstylesPhoenix.Helper.classnames/1` for usage. 15 | - All other attributes are passed to the `div` tag. 16 | 17 | See [bitstyles content docs](https://bitcrowd.github.io/bitstyles/?path=/docs/atoms-content--content) for examples, and for the default variants available. 18 | """ 19 | 20 | story( 21 | "Default content", 22 | """ 23 | iex> assigns = %{} 24 | ...> render ~H\""" 25 | ...> <.ui_content> 26 | ...> Content 27 | ...> 28 | ...> \""" 29 | """, 30 | """ 31 | \""" 32 |
33 | Content 34 |
35 | \""" 36 | """, 37 | width: "100%" 38 | ) 39 | 40 | story( 41 | "Full content", 42 | """ 43 | iex> assigns = %{} 44 | ...> render ~H\""" 45 | ...> <.ui_content variant="full"> 46 | ...> Full Content 47 | ...> 48 | ...> \""" 49 | """, 50 | """ 51 | \""" 52 |
53 | Full Content 54 |
55 | \""" 56 | """, 57 | width: "100%" 58 | ) 59 | 60 | story( 61 | "Extra classes and attributes", 62 | """ 63 | iex> assigns = %{} 64 | ...> render ~H\""" 65 | ...> <.ui_content variant="full" class="u-h2" data-foo="bar"> 66 | ...> Content with extra 67 | ...> 68 | ...> \""" 69 | """, 70 | """ 71 | \""" 72 |
73 | Content with extra 74 |
75 | \""" 76 | """, 77 | width: "100%" 78 | ) 79 | 80 | def ui_content(assigns) do 81 | variant_classes = assigns[:variant] |> List.wrap() |> Enum.map_join(" ", &"a-content--#{&1}") 82 | 83 | class = classnames(["a-content", variant_classes, assigns[:class]]) 84 | 85 | extra = assigns_to_attributes(assigns, [:class, :variant]) 86 | assigns = assign(assigns, class: class, extra: extra) 87 | 88 | ~H""" 89 |
90 | <%= render_slot(@inner_block) %> 91 |
92 | """ 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /demo/config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with esbuild to bundle .js and .css sources. 9 | config :bitstyles_phoenix_demo, BitstylesPhoenixDemoWeb.Endpoint, 10 | # Binding to loopback ipv4 address prevents access from other machines. 11 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. 12 | http: [ip: {127, 0, 0, 1}, port: 4000], 13 | check_origin: false, 14 | code_reloader: true, 15 | debug_errors: true, 16 | secret_key_base: "Hrpu8ebXpVlCvQjFLp6QnEKazpMs/m0wWeyPV7GbNmHPzGRztjIXF8LtWE5FqLxj", 17 | watchers: [ 18 | # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args) 19 | esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}, 20 | sass: { 21 | DartSass, 22 | :install_and_run, 23 | [:default, ~w(--embed-source-map --source-map-urls=absolute --watch)] 24 | }, 25 | bitstyles: {Mix.Task, :run, ["bitstyles.watch"]} 26 | ] 27 | 28 | # ## SSL Support 29 | # 30 | # In order to use HTTPS in development, a self-signed 31 | # certificate can be generated by running the following 32 | # Mix task: 33 | # 34 | # mix phx.gen.cert 35 | # 36 | # Note that this task requires Erlang/OTP 20 or later. 37 | # Run `mix help phx.gen.cert` for more information. 38 | # 39 | # The `http:` config above can be replaced with: 40 | # 41 | # https: [ 42 | # port: 4001, 43 | # cipher_suite: :strong, 44 | # keyfile: "priv/cert/selfsigned_key.pem", 45 | # certfile: "priv/cert/selfsigned.pem" 46 | # ], 47 | # 48 | # If desired, both `http:` and `https:` keys can be 49 | # configured to run both http and https servers on 50 | # different ports. 51 | 52 | # Watch static and templates for browser reloading. 53 | config :bitstyles_phoenix_demo, BitstylesPhoenixDemoWeb.Endpoint, 54 | live_reload: [ 55 | patterns: [ 56 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 57 | ~r"priv/gettext/.*(po)$", 58 | ~r"lib/bitstyles_phoenix_demo_web/(live|views)/.*(ex)$", 59 | ~r"lib/bitstyles_phoenix_demo_web/templates/.*(eex)$" 60 | ] 61 | ] 62 | 63 | # Do not include metadata nor timestamps in development logs 64 | config :logger, :console, format: "[$level] $message\n" 65 | 66 | # Set a higher stacktrace during development. Avoid configuring such 67 | # in production as building large stacktraces may be expensive. 68 | config :phoenix, :stacktrace_depth, 20 69 | 70 | # Initialize plugs at runtime for faster development compilation 71 | config :phoenix, :plug_init_mode, :runtime 72 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/helper/classnames.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Helper.Classnames do 2 | alias BitstylesPhoenix.Bitstyles 3 | 4 | @default_prefix "e2e-" 5 | 6 | @moduledoc """ 7 | The very best of NPM, now for elixir. 8 | """ 9 | 10 | @doc """ 11 | Concatenates lists of class names, with trimming and conditionals. 12 | 13 | Classes prefixes with `e2e-` are removed by default. This behaviour can be configured with 14 | the `trim_e2e_classes` configuration. Check `BitstylesPhoenix` top level documentation for 15 | more information on configuration options. 16 | 17 | ## Examples 18 | 19 | iex> classnames("foo") 20 | "foo" 21 | 22 | iex> classnames("e2e-out") 23 | false 24 | 25 | iex> classnames(nil) 26 | false 27 | 28 | iex> classnames(" foo ") 29 | "foo" 30 | 31 | iex> classnames(" foo bar ") 32 | "foo bar" 33 | 34 | iex> classnames(["foo", "bar"]) 35 | "foo bar" 36 | 37 | iex> classnames(["foo", "bar baz"]) 38 | "foo bar baz" 39 | 40 | iex> classnames(:foo) 41 | "foo" 42 | 43 | iex> classnames({"foo", 1 == 1}) 44 | "foo" 45 | 46 | iex> classnames({"foo", 1 == 2}) 47 | false 48 | 49 | iex> classnames([" foo boing ", {"bar", 1 == 2}, :baz]) 50 | "foo boing baz" 51 | """ 52 | def classnames(arg, opts \\ [backwards_compatible: true]) do 53 | arg 54 | |> List.wrap() 55 | |> Enum.map(&normalize/1) 56 | |> Enum.flat_map(&split/1) 57 | |> Enum.reject(&remove_class?/1) 58 | |> Enum.uniq() 59 | |> Enum.map_join(" ", &bitstyles_version(&1, opts)) 60 | |> case do 61 | "" -> false 62 | classnames -> classnames 63 | end 64 | end 65 | 66 | defp bitstyles_version(name, opts) do 67 | if Keyword.get(opts, :backwards_compatible) do 68 | Bitstyles.classname(name) 69 | else 70 | name 71 | end 72 | end 73 | 74 | defp normalize(nil), do: "" 75 | defp normalize({class, true}), do: classnames(class) 76 | defp normalize({_class, false}), do: "" 77 | defp normalize({_class, nil}), do: "" 78 | defp normalize(class) when is_binary(class), do: String.trim(class) 79 | defp normalize(class) when is_atom(class), do: class |> to_string() |> String.trim() 80 | 81 | defp split(class), do: String.split(class, " ") 82 | 83 | defp remove_class?(""), do: true 84 | 85 | defp remove_class?(class) when is_binary(class) do 86 | config = Application.get_env(:bitstyles_phoenix, :trim_e2e_classes, []) 87 | prefix = Keyword.get(config, :prefix, @default_prefix) 88 | Keyword.get(config, :enabled, true) && String.starts_with?(class, prefix) 89 | end 90 | 91 | defp remove_class?(_value), do: false 92 | end 93 | -------------------------------------------------------------------------------- /demo/assets/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assets", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "license": "ISC", 8 | "dependencies": { 9 | "alpinejs": "^3.5.1", 10 | "bitstyles": "^4.0.0" 11 | } 12 | }, 13 | "node_modules/@vue/reactivity": { 14 | "version": "3.1.5", 15 | "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", 16 | "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", 17 | "dependencies": { 18 | "@vue/shared": "3.1.5" 19 | } 20 | }, 21 | "node_modules/@vue/shared": { 22 | "version": "3.1.5", 23 | "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", 24 | "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" 25 | }, 26 | "node_modules/alpinejs": { 27 | "version": "3.10.5", 28 | "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.10.5.tgz", 29 | "integrity": "sha512-qlvnal44Gof2XVfm/lef8fYpXKxR9fjdSki7aFB/9THyFvbsRKZ6lM5SjxXpIs7B0faJt7bgpK2K25gzrraXJw==", 30 | "dependencies": { 31 | "@vue/reactivity": "~3.1.1" 32 | } 33 | }, 34 | "node_modules/bitstyles": { 35 | "version": "4.3.0", 36 | "resolved": "https://registry.npmjs.org/bitstyles/-/bitstyles-4.3.0.tgz", 37 | "integrity": "sha512-Iw6tnPNAvmxZq/zH7WyOb+U5RQ/7JpijDN0iFvqlLMhdHr0L8yVYJfHF36mZBAO1EBLtpG+Zzhj/KDb2bzgQpg==" 38 | } 39 | }, 40 | "dependencies": { 41 | "@vue/reactivity": { 42 | "version": "3.1.5", 43 | "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", 44 | "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", 45 | "requires": { 46 | "@vue/shared": "3.1.5" 47 | } 48 | }, 49 | "@vue/shared": { 50 | "version": "3.1.5", 51 | "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", 52 | "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" 53 | }, 54 | "alpinejs": { 55 | "version": "3.10.5", 56 | "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.10.5.tgz", 57 | "integrity": "sha512-qlvnal44Gof2XVfm/lef8fYpXKxR9fjdSki7aFB/9THyFvbsRKZ6lM5SjxXpIs7B0faJt7bgpK2K25gzrraXJw==", 58 | "requires": { 59 | "@vue/reactivity": "~3.1.1" 60 | } 61 | }, 62 | "bitstyles": { 63 | "version": "4.3.0", 64 | "resolved": "https://registry.npmjs.org/bitstyles/-/bitstyles-4.3.0.tgz", 65 | "integrity": "sha512-Iw6tnPNAvmxZq/zH7WyOb+U5RQ/7JpijDN0iFvqlLMhdHr0L8yVYJfHF36mZBAO1EBLtpG+Zzhj/KDb2bzgQpg==" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /demo/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixDemo.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :bitstyles_phoenix_demo, 7 | version: "0.1.0", 8 | elixir: "~> 1.12", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: [:gettext] ++ Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | aliases: aliases(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Configuration for the OTP application. 18 | # 19 | # Type `mix help compile.app` for more information. 20 | def application do 21 | [ 22 | mod: {BitstylesPhoenixDemo.Application, []}, 23 | extra_applications: [:logger, :runtime_tools] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [ 36 | {:bitstyles_phoenix, path: '..'}, 37 | {:faker, "~> 0.17"}, 38 | {:ecto, "~> 3.7.1"}, 39 | {:dart_sass, "~> 0.3", runtime: Mix.env() == :dev}, 40 | {:phoenix, "~> 1.6.2"}, 41 | {:phoenix_ecto, "~> 4.0"}, 42 | {:phoenix_html, "~> 3.0"}, 43 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 44 | {:phoenix_live_view, "~> 0.18.0"}, 45 | {:floki, ">= 0.30.0", only: :test}, 46 | {:esbuild, "~> 0.2", runtime: Mix.env() == :dev}, 47 | {:telemetry_metrics, "~> 0.6"}, 48 | {:telemetry_poller, "~> 1.0"}, 49 | {:gettext, "~> 0.18"}, 50 | {:jason, "~> 1.2"}, 51 | {:plug_cowboy, "~> 2.5"}, 52 | {:wallaby, "~> 0.29.0", runtime: false, only: :test} 53 | ] 54 | end 55 | 56 | # Aliases are shortcuts or tasks specific to the current project. 57 | # For example, to install project dependencies and perform other setup tasks, run: 58 | # 59 | # $ mix setup 60 | # 61 | # See the documentation for `Mix` for more info on aliases. 62 | defp aliases do 63 | [ 64 | setup: [ 65 | "deps.get", 66 | "cmd (cd assets && npm install)" 67 | ], 68 | test: [ 69 | "assets.compile", 70 | "test" 71 | ], 72 | bitstyles: [ 73 | "cmd mkdir -p priv/static/assets", 74 | "cmd cp -R assets/node_modules/bitstyles/assets/* priv/static/assets" 75 | ], 76 | "bitstyles.watch": [ 77 | "bitstyles", 78 | "cmd fswatch -o assets/node_modules | xargs -n1 -I {} mix bitstyles" 79 | ], 80 | "assets.compile": [ 81 | "esbuild default --minify", 82 | "sass default --no-source-map --style=compressed", 83 | "bitstyles" 84 | ], 85 | "assets.deploy": [ 86 | "assets.compile", 87 | "phx.digest" 88 | ] 89 | ] 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /demo/test/bitstyles_phoenix_demo_web/bitstyles_phoenix_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenixWebDemo.BitstylesPhoenixTest do 2 | use ExUnit.Case, async: true 3 | use Wallaby.Feature 4 | 5 | feature "renders errors from gettext", %{session: session} do 6 | session 7 | |> visit("/") 8 | |> find(Query.css(".e2e-form", count: 1)) 9 | |> assert_has(Query.css(".u-fg-warning", text: "can't be blank")) 10 | 11 | session 12 | |> find(Query.css(".e2e-form-de", count: 1)) 13 | |> assert_has(Query.css(".u-fg-warning", text: "muss ausgefüllt werden")) 14 | end 15 | 16 | feature "allows icon config and can still render inline", %{session: session} do 17 | session 18 | |> visit("/") 19 | |> find(Query.css(".e2e-external-svg-icon use", count: 1), fn element -> 20 | assert {width, height} = Element.size(element) 21 | assert width > 0 22 | assert height > 0 23 | end) 24 | |> find(Query.css(".e2e-inline-svg-icon use", count: 1), fn element -> 25 | assert {width, height} = Element.size(element) 26 | assert width > 0 27 | assert height > 0 28 | end) 29 | end 30 | 31 | feature "alpine & live dropdowns", %{session: session} do 32 | session 33 | |> visit("/") 34 | |> refute_has(Query.css(".e2e-dropdown-option")) 35 | |> click(Query.css(".e2e-dropdown-button")) 36 | |> assert_has(Query.css(".e2e-dropdown-option")) 37 | |> click(Query.css(".e2e-dropdown-option")) 38 | |> assert_text("Live test") 39 | |> refute_has(Query.css(".e2e-dropdown-option")) 40 | |> click(Query.css(".e2e-dropdown-button")) 41 | |> assert_has(Query.css(".e2e-dropdown-option")) 42 | |> click(Query.css(".e2e-dropdown-button-id")) 43 | |> wait_css_transition() 44 | |> assert_has(Query.css(".e2e-dropdown-option-id")) 45 | |> refute_has(Query.css(".e2e-dropdown-option")) 46 | |> click(Query.css(".e2e-dropdown-option-id")) 47 | |> assert_text("Alpine test") 48 | end 49 | 50 | feature "alpine & live sidebars", %{session: session} do 51 | session 52 | |> visit("/") 53 | |> assert_has(Query.css(".e2e-sidebar-alpine")) 54 | |> assert_has(Query.css(".e2e-sidebar-live")) 55 | |> click(Query.css(".e2e-sidebar-live")) 56 | |> assert_text("Live test") 57 | |> assert_has(Query.css(".e2e-sidebar-alpine")) 58 | |> assert_has(Query.css(".e2e-sidebar-live")) 59 | |> Wallaby.Browser.resize_window(500, 800) 60 | |> refute_has(Query.css(".e2e-sidebar-alpine")) 61 | |> refute_has(Query.css(".e2e-sidebar-live")) 62 | |> click(Query.css(".e2e-sidebar-open")) 63 | |> wait_css_transition() 64 | |> assert_has(Query.css(".e2e-sidebar-alpine")) 65 | |> assert_has(Query.css(".e2e-sidebar-live")) 66 | |> click(Query.css(".e2e-sidebar-close")) 67 | |> wait_css_transition() 68 | |> refute_has(Query.css(".e2e-sidebar-alpine")) 69 | |> refute_has(Query.css(".e2e-sidebar-live")) 70 | |> click(Query.css(".e2e-sidebar-open")) 71 | |> wait_css_transition() 72 | |> click(Query.css(".e2e-sidebar-alpine")) 73 | |> refute_has(Query.css(".e2e-sidebar-alpine")) 74 | |> refute_has(Query.css(".e2e-sidebar-live")) 75 | |> click(Query.css(".e2e-sidebar-open")) 76 | |> assert_has(Query.css(".e2e-sidebar-alpine")) 77 | |> assert_has(Query.css(".e2e-sidebar-live")) 78 | |> click(Query.css(".e2e-sidebar-close")) 79 | |> wait_css_transition() 80 | |> refute_has(Query.css(".e2e-sidebar-alpine")) 81 | |> refute_has(Query.css(".e2e-sidebar-live")) 82 | |> Wallaby.Browser.resize_window(1200, 800) 83 | |> assert_has(Query.css(".e2e-sidebar-alpine")) 84 | |> assert_has(Query.css(".e2e-sidebar-live")) 85 | end 86 | 87 | defp wait_css_transition(session, time \\ 500) do 88 | :timer.sleep(time) 89 | session 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/live/sidebar.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Live.Sidebar do 2 | use BitstylesPhoenix.Component 3 | 4 | alias BitstylesPhoenix.Component.Sidebar, as: RawSidebar 5 | alias Phoenix.LiveView.JS 6 | import BitstylesPhoenix.Component.Button 7 | import Phoenix.LiveView.Utils, only: [random_id: 0] 8 | 9 | @moduledoc """ 10 | Components for rendering a sidebar layout powered by LiveView commands. 11 | """ 12 | 13 | @doc """ 14 | Renders a sidebar layout with LiveView commands. 15 | 16 | Supports all attributes and slots from `BitstylesPhoenix.Component.Sidebar.ui_sidebar_layout/1`. 17 | 18 | The `small_sidebar` and `main`/`inner` blocks will additionally provide a block argument 19 | with the sidebar context for `ui_js_sidebar_open/1` and `ui_js_sidebar_close/1`. 20 | 21 | For examles see `BitstylesPhoenix.Component.Sidebar.ui_sidebar_layout/1`. 22 | """ 23 | 24 | def ui_js_sidebar_layout(assigns) do 25 | extra = assigns_to_attributes(assigns, [:small_sidebar]) 26 | 27 | {_, small_extra} = assigns_from_single_slot(assigns, :small_sidebar, optional: true) 28 | {_, main_extra} = assigns_from_single_slot(assigns, :main, optional: true) 29 | 30 | small_extra = Keyword.put_new_lazy(small_extra, :id, &random_id/0) 31 | 32 | assigns = assign(assigns, small_extra: small_extra, extra: extra, main_extra: main_extra) 33 | 34 | ~H""" 35 | 36 | <:small_sidebar 37 | style="display: none" 38 | {@small_extra}> 39 | <%= render_slot(assigns[:small_sidebar], @small_extra[:id]) %> 40 | 41 | <:main {@main_extra}> 42 | <%= render_slot(assigns[:main] || @inner_block, @small_extra[:id]) %> 43 | 44 | 45 | """ 46 | end 47 | 48 | @doc """ 49 | A sidebar close icon to be rendered on the open sidebar for closing the sidebar. 50 | 51 | ## Attributes 52 | - `label` - A screen reader label for the icon. Defaults to `"Close"`. 53 | - `sidebar` - The reference to the sidebar it controls. This will be the sidebar `id`. 54 | The `ui_js_sidebar_layout/1` can provide this as a block argument in the small sidebar block. 55 | """ 56 | def ui_js_sidebar_close(assigns) do 57 | id = assigns.sidebar 58 | 59 | options = 60 | assigns 61 | |> assigns_to_attributes([:sidebar, :icon]) 62 | |> Keyword.merge( 63 | "phx-click": 64 | JS.hide(to: "##{id}", transition: {"is-transitioning", "is-on-screen", "is-off-screen"}), 65 | "aria-controls": id 66 | ) 67 | |> Keyword.put_new(:reversed, true) 68 | 69 | assigns = assign(assigns, icon: assigns[:icon] || "cross", options: options) 70 | 71 | ~H""" 72 | <.ui_icon_button icon={@icon} label={assigns[:label] || "Close"} {@options} /> 73 | """ 74 | end 75 | 76 | @doc """ 77 | A sidebar open icon to be rendered on the main content for opening the sidebar. 78 | 79 | ## Attributes 80 | - `label` - A screen reader label for the icon. Defaults to `"Open sidebar"`. 81 | - `sidebar` - The reference to the sidebar it controls. This will be the sidebar `id`. 82 | The `ui_js_sidebar_layout/1` can provide this as a block argument in the main or inner block. 83 | """ 84 | def ui_js_sidebar_open(assigns) do 85 | id = assigns.sidebar 86 | 87 | options = 88 | assigns 89 | |> assigns_to_attributes([:sidebar, :icon]) 90 | |> Keyword.merge( 91 | "phx-click": 92 | JS.show(to: "##{id}", transition: {"is-transitioning", "is-off-screen", "is-on-screen"}), 93 | "aria-controls": id 94 | ) 95 | |> Keyword.put(:class, classnames(["u-hidden@l", assigns[:class]])) 96 | 97 | assigns = assign(assigns, icon: assigns[:icon] || "hamburger", options: options) 98 | 99 | ~H""" 100 | <.ui_icon_button icon={@icon} label={assigns[:label] || "Open sidebar"} {@options} /> 101 | """ 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/component/avatar.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.Avatar do 2 | use BitstylesPhoenix.Component 3 | 4 | @moduledoc """ 5 | An Avatar component. 6 | """ 7 | 8 | @doc ~S""" 9 | Renders an avatar component. 10 | 11 | The avatar component can have medium and large sizes, and it defaults to medium. It accepts a slot for text. 12 | Always provide a source and alt. The width and height have 32px default values and can be overidden. 13 | 14 | See the [bitstyles avatar docs](https://bitcrowd.github.io/bitstyles/?path=/docs/atoms-avatar--a-avatar-m) for further info. 15 | """ 16 | 17 | story( 18 | "Default avatar", 19 | """ 20 | iex> assigns = %{} 21 | ...> render ~H\""" 22 | ...> <.ui_avatar src="https://placehold.co/100x100" alt="Username’s avatar"/> 23 | ...> \""" 24 | """, 25 | """ 26 | \""" 27 |
28 |
29 | Username’s avatar 30 |
31 |
32 | \""" 33 | """ 34 | ) 35 | 36 | story( 37 | "With extra class", 38 | """ 39 | iex> assigns = %{} 40 | ...> render ~H\""" 41 | ...> <.ui_avatar src="https://placehold.co/100x100" class="foo bar" alt="Username’s avatar"/> 42 | ...> \""" 43 | """, 44 | """ 45 | \""" 46 |
47 |
48 | Username’s avatar 49 |
50 |
51 | \""" 52 | """ 53 | ) 54 | 55 | story( 56 | "Large avatar", 57 | """ 58 | iex> assigns = %{} 59 | ...> render ~H\""" 60 | ...> <.ui_avatar size="l" src="https://placehold.co/100x100" alt="Username’s avatar" height="46" width="46"/> 61 | ...> \""" 62 | """, 63 | """ 64 | \""" 65 |
66 |
67 | Username’s avatar 68 |
69 |
70 | \""" 71 | """ 72 | ) 73 | 74 | story( 75 | "Avatar with a text", 76 | """ 77 | iex> assigns = %{} 78 | ...> render ~H\""" 79 | ...> <.ui_avatar src="https://placehold.co/100x100" alt="Username’s avatar"> Username 80 | ...> \""" 81 | """, 82 | """ 83 | \""" 84 |
85 |
86 | Username’s avatar 87 |
88 | 89 | Username 90 | 91 |
92 | \""" 93 | """ 94 | ) 95 | 96 | def ui_avatar(assigns) do 97 | class = 98 | classnames([ 99 | "a-avatar", 100 | {"a-avatar--#{assigns[:size]}", assigns[:size] != nil}, 101 | assigns[:class] 102 | ]) 103 | 104 | extra = 105 | assigns 106 | |> assigns_to_attributes([:class, :size]) 107 | |> put_defaults 108 | 109 | assigns = assign(assigns, extra: extra, class: class) 110 | 111 | ~H""" 112 |
113 |
114 | 115 |
116 | <%= if assigns[:inner_block] do %> 117 | 118 | <%= render_slot(@inner_block) %> 119 | 120 | <% end %> 121 |
122 | """ 123 | end 124 | 125 | @default_size 32 126 | defp put_defaults(opts) do 127 | opts 128 | |> Keyword.put_new(:width, @default_size) 129 | |> Keyword.put_new(:height, @default_size) 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/component/card.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.Card do 2 | use BitstylesPhoenix.Component 3 | import BitstylesPhoenix.Component.Flash 4 | 5 | @moduledoc """ 6 | A Card component. 7 | """ 8 | 9 | @doc ~S""" 10 | Renders a card component. The card component can have default and large sizes, 11 | It accepts a slot for content. 12 | """ 13 | 14 | story( 15 | "Default Card", 16 | """ 17 | iex> assigns = %{} 18 | ...> render ~H\""" 19 | ...> <.ui_card>

Hello world

20 | ...> \""" 21 | """, 22 | """ 23 | \""" 24 |
25 |

26 | Hello world 27 |

28 |
29 | \""" 30 | """ 31 | ) 32 | 33 | story( 34 | "Large Card", 35 | """ 36 | iex> assigns = %{} 37 | ...> render ~H\""" 38 | ...> <.ui_card size="l">

Hello world

39 | ...> \""" 40 | """, 41 | """ 42 | \""" 43 |
44 |

45 | Hello world 46 |

47 |
48 | \""" 49 | """ 50 | ) 51 | 52 | story( 53 | "Large Card with header", 54 | """ 55 | iex> assigns = %{} 56 | ...> render ~H\""" 57 | ...> <.ui_card size="l"> 58 | ...> <:card_header variant="danger">Its me mario 59 | ...>

Hello world

60 | ...> 61 | ...> \""" 62 | """, 63 | """ 64 | \""" 65 |
66 |
67 |
68 | Its me mario 69 |
70 |
71 |

72 | Hello world 73 |

74 |
75 | \""" 76 | """ 77 | ) 78 | 79 | story( 80 | "Small Card with header", 81 | """ 82 | iex> assigns = %{} 83 | ...> render ~H\""" 84 | ...> <.ui_card> 85 | ...> <:card_header variant="danger">Its me mario 86 | ...>

Hello world

87 | ...> 88 | ...> \""" 89 | """, 90 | """ 91 | \""" 92 |
93 |
94 |
95 | Its me mario 96 |
97 |
98 |

99 | Hello world 100 |

101 |
102 | \""" 103 | """ 104 | ) 105 | 106 | def ui_card(assigns) do 107 | class = 108 | classnames([ 109 | "a-card", 110 | {"a-card-#{assigns[:size]}", assigns[:size] != nil}, 111 | assigns[:class] 112 | ]) 113 | 114 | extra = 115 | assigns 116 | |> assigns_to_attributes([:class, :size, :card_header]) 117 | 118 | {card_header, card_header_extra} = 119 | assigns_from_single_slot(assigns, :card_header, optional: true) 120 | 121 | card_header_class = 122 | classnames([ 123 | {"a-card-#{assigns[:size]}__header", !is_nil(assigns[:size])}, 124 | {"a-card__header", is_nil(assigns[:size])}, 125 | card_header[:class] 126 | ]) 127 | 128 | assigns = 129 | assign(assigns, 130 | extra: extra, 131 | class: class, 132 | card_header_extra: card_header_extra, 133 | card_header_class: card_header_class 134 | ) 135 | 136 | ~H""" 137 |
138 | <%= if assigns[:card_header] do %> 139 | <.ui_flash class={@card_header_class} {@card_header_extra}> 140 | <%= render_slot(@card_header) %> 141 | 142 | <% end %> 143 | <%= render_slot(@inner_block) %> 144 |
145 | """ 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/component/badge.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.Badge do 2 | use BitstylesPhoenix.Component 3 | alias BitstylesPhoenix.Bitstyles 4 | 5 | @moduledoc """ 6 | The Badge component. 7 | """ 8 | 9 | @doc ~s""" 10 | Render a badge to highlighted small texts, such as an item count or state indicator. 11 | 12 | ## Attributes 13 | 14 | - `variant` — Variant of the badge you want, from those available in the CSS classes e.g. `brand-1`, `danger` 15 | - `class` - Extra classes to pass to the badge. See `BitstylesPhoenix.Helper.classnames/1` for usage. 16 | - All other attributes are passed to the `span` tag. 17 | 18 | See [bitstyles badge docs](https://bitcrowd.github.io/bitstyles/?path=/docs/atoms-badge--badge) for examples, and for the default variants available. 19 | """ 20 | 21 | story( 22 | "Default badge", 23 | """ 24 | iex> assigns = %{} 25 | ...> render ~H\""" 26 | ...> <.ui_badge> 27 | ...> published 28 | ...> 29 | ...> \""" 30 | """, 31 | "6.0.0": """ 32 | \""" 33 | 34 | published 35 | 36 | \""" 37 | """, 38 | "5.0.1": """ 39 | \""" 40 | 41 | published 42 | 43 | \""" 44 | """, 45 | "4.3.0": """ 46 | \""" 47 | 48 | published 49 | 50 | \""" 51 | """ 52 | ) 53 | 54 | story( 55 | "Badge variant brand-1", 56 | """ 57 | iex> assigns = %{} 58 | ...> render ~H\""" 59 | ...> <.ui_badge variant="brand-1"> 60 | ...> new 61 | ...> 62 | ...> \""" 63 | """, 64 | "6.0.0": """ 65 | \""" 66 | 67 | new 68 | 69 | \""" 70 | """, 71 | "5.0.1": """ 72 | \""" 73 | 74 | new 75 | 76 | \""" 77 | """ 78 | ) 79 | 80 | story( 81 | "Badge variant brand-2", 82 | """ 83 | iex> assigns = %{} 84 | ...> render ~H\""" 85 | ...> <.ui_badge variant="brand-2"> 86 | ...> recommended 87 | ...> 88 | ...> \""" 89 | """, 90 | """ 91 | \""" 92 | 93 | recommended 94 | 95 | \""" 96 | """ 97 | ) 98 | 99 | story( 100 | "Badge variant danger", 101 | """ 102 | iex> assigns = %{} 103 | ...> render ~H\""" 104 | ...> <.ui_badge variant="danger"> 105 | ...> deleted 106 | ...> 107 | ...> \""" 108 | """, 109 | """ 110 | \""" 111 | 112 | deleted 113 | 114 | \""" 115 | """ 116 | ) 117 | 118 | story( 119 | "Extra options and classes", 120 | """ 121 | iex> assigns = %{} 122 | ...> render ~H\""" 123 | ...> <.ui_badge class="extra-class" data-foo="bar"> 124 | ...> published 125 | ...> 126 | ...> \""" 127 | """, 128 | """ 129 | \""" 130 | 131 | published 132 | 133 | \""" 134 | """ 135 | ) 136 | 137 | def ui_badge(assigns) do 138 | extra = assigns_to_attributes(assigns, [:class, :variant]) 139 | 140 | {variant_class, extra} = 141 | if Bitstyles.Version.version() >= {6, 0, 0} do 142 | theme = assigns[:variant] || "grayscale" 143 | {nil, Keyword.put_new(extra, :"data-theme", theme)} 144 | else 145 | variant = assigns[:variant] || "text" 146 | {"a-badge--#{variant}", extra} 147 | end 148 | 149 | class = 150 | classnames([ 151 | "a-badge u-h6 u-font-medium", 152 | variant_class, 153 | assigns[:class] 154 | ]) 155 | 156 | assigns = assign(assigns, class: class, extra: extra) 157 | ~H"<%= render_slot(@inner_block) %>" 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/bitstyles_version_compatibility.md: -------------------------------------------------------------------------------- 1 | # Bitstyles version compatibility 2 | 3 | General rules: 4 | 5 | - We don't drop bitstyles version support. Users of bitstyles_phoenix should be able to upgrade bitstyles_phoenix to the newest version without having to upgrade bitstyles. 6 | - We don't skip bitstyles versions. Users of bitstyles_phoenix should be able to upgrade bitstyles to any version between the highest and lowest currently supported bitstyles version. 7 | 8 | ## Doctests ("stories") 9 | 10 | The `story` macro is used to generate doctests, as well as the component preview in docs. 11 | 12 | The first argument is the story's description (`"An error tag"`), the second argument is an iex code snippet, and the third argument is the expected result when the code snippet gets executed. The third argument can be a single charlist, or a keyword lists that maps bitstyles version numbers to charlists. If the third argument is a single charlist, it's assumed that is the expected result for the default bitstyles version. 13 | 14 | ⚠️ Note that the 4 space indentation in the code snippet and the expected result is important. 15 | 16 | ### Multi-version doctest example 17 | 18 | ```elixir 19 | story( 20 | "An error tag", 21 | ''' 22 | iex> assigns = %{} 23 | ...> render ~H""" 24 | ...> <.ui_error error={{"Foo error", []}} /> 25 | ...> """ 26 | ''', 27 | "4.3.0": ''' 28 | """ 29 | 30 | Foo error 31 | 32 | """ 33 | ''', 34 | "3.0.0": ''' 35 | """ 36 | 37 | Foo error 38 | 39 | """ 40 | ''' 41 | ) 42 | ``` 43 | 44 | ## Versions showcase 45 | 46 | A showcase of components in all supported bitstyles versions can be generated by running: 47 | 48 | ```bash 49 | mix run scripts/generate_version_showcase.ex 50 | ``` 51 | 52 | The script accepts multiple `--only` arguments if you want to limit the showcase to only a handful of versions: 53 | 54 | ```bash 55 | mix run scripts/generate_version_showcase.ex --only 4.3.0 --only 4.2.0 56 | ``` 57 | 58 | This script will create a `version_showcase` directory with static web pages. Open the starting page with: 59 | 60 | ```bash 61 | open version_showcase/index.html 62 | ``` 63 | 64 | ## How to upgrade the default bitstyles version in bitstyles_phoenix? 65 | 66 | 1. Choose the smallest possible version jump (do not skip versions). 67 | 2. Read about the changes in version in [the bitstyles Changelog](https://github.com/bitcrowd/bitstyles/blob/main/CHANGELOG.md). 68 | 3. Update the highest supported version mentioned in the [README](../README.md). 69 | 4. Update the `@default_version` in [`BitstylesPhoenix.Bitstyles`](../lib/bitstyles_phoenix/bitstyles.ex). 70 | 5. Add the new version to the version lists in [`generate_version_showcase`](../scripts/generate_version_showcase.ex). 71 | 6. Handle classes renamed by bitstyles by adding new clauses of the `classname/2` function in [`BitstylesPhoenix.Bitstyles`](../lib/bitstyles_phoenix/bitstyles.ex). 72 | 7. If renaming classes is not enough to upgrade correctly, you can perform [a bitstyles version check in a component](#an-example-of-a-bitstyles-version-check-in-a-component). 73 | 8. Run `mix test`. Fix all doctests until `mix test` succeeds. Add new expected values for the new version, and keep the current expected value for the previous version (see [multi-version doctest example](#multi-version-doctest-example)) 74 | 9. Run `mix docs` to preview components after making changes to them. 75 | 10. Use the [versions showcase](#versions-showcase) to test that you didn't break anything for older bitstyles versions. 76 | 77 | ### An example of a bitstyles version check in a component 78 | 79 | ```elixir 80 | def ui_tricky_component(assigns) do 81 | version = BitstylesPhoenix.Bitstyles.Version.version() 82 | 83 | if version >= {5,0,0} do 84 | ~H""" 85 |

...

86 | """ 87 | else 88 | ~H""" 89 |
90 |
91 | ... 92 |
93 |
94 | """ 95 | end 96 | end 97 | ``` 98 | 99 | ## Adding new components 100 | 101 | Ideally, if you're adding a completely new component, make sure it works with all supported bitstyles versions by using the [versions showcase](#versions-showcase) to test it. 102 | 103 | If it's not practical to support it in other version, you can perform [a bitstyles version check in the component](#an-example-of-a-bitstyles-version-check-in-a-component). 104 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/component/icon.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Component.Icon do 2 | use BitstylesPhoenix.Component 3 | import BitstylesPhoenix.Component.UseSVG 4 | 5 | @moduledoc """ 6 | An SVG icon system, that expects the icons to be present on the page, rendered as SVG ``s. 7 | """ 8 | 9 | @doc ~S""" 10 | Renders an icon element. 11 | 12 | This uses `BitstylesPhoenix.Component.UseSVG` to render an icon either inlined in the page or 13 | referenced in an external SVG file. Icons are assumed to have an id prefixed with `icon-` followed 14 | by the name of the icon, which is used to reference the icon. 15 | 16 | ## Attributes 17 | 18 | - `name` *(required)* - The name of the icon. Assumes icons are prefixed with `icon-`. 19 | - `size` - Specify the icon size to use. Available sizes are specified in CSS, and default to `s`, `m`, `l`, `xl`. If you do not specify a size, the icon will fit into a `1em` square. 20 | - `file` - To be set if icons should be loaded from an external resource (see `BitstylesPhoenix.Component.UseSVG.ui_svg/1`). 21 | This can also be configured to a default `icon_file`, see `BitstylesPhoenix` for config options. With the configuration present, inline icons can still be rendered with `file={nil}`. 22 | - `class` - Extra classes to pass to the svg. See `BitstylesPhoenix.Helper.classnames/1` for usage. 23 | 24 | See the [bitstyles icon docs](https://bitcrowd.github.io/bitstyles/?path=/docs/atoms-icon) for examples of icon usage, and available icons in the [bitstyles icon set](https://bitcrowd.github.io/bitstyles/?path=/docs/ui-data-icons). 25 | """ 26 | 27 | story( 28 | "An icon (from inline svg)", 29 | """ 30 | iex> assigns = %{} 31 | ...> render ~H\""" 32 | ...> <.ui_icon name="inline-arrow"/> 33 | ...> \""" 34 | """, 35 | """ 36 | \""" 37 | 41 | \""" 42 | """, 43 | extra_html: """ 44 | 49 | """ 50 | ) 51 | 52 | story( 53 | "An icon with a size", 54 | """ 55 | iex> assigns = %{} 56 | ...> render ~H\""" 57 | ...> <.ui_icon name="hamburger" file="/assets/icons.svg" size="xl"/> 58 | ...> \""" 59 | """, 60 | """ 61 | \""" 62 | 66 | \""" 67 | """ 68 | ) 69 | 70 | story( 71 | "An icon with extra options", 72 | """ 73 | iex> assigns = %{} 74 | ...> render ~H\""" 75 | ...> <.ui_icon name="bin" file="/assets/icons.svg" class="foo bar"/> 76 | ...> \""" 77 | """, 78 | """ 79 | \""" 80 | 84 | \""" 85 | """ 86 | ) 87 | 88 | def ui_icon(assigns) do 89 | icon = "icon-#{assigns.name}" 90 | 91 | class = 92 | classnames([ 93 | "a-icon", 94 | {"a-icon--#{assigns[:size]}", assigns[:size] != nil}, 95 | assigns[:class] 96 | ]) 97 | 98 | extra = 99 | assigns 100 | |> assigns_to_attributes([:class, :name, :size]) 101 | |> put_defaults 102 | 103 | assigns = assign(assigns, extra: extra, class: class, icon: icon) 104 | 105 | ~H""" 106 | <.ui_svg use={@icon} class={@class} aria-hidden="true" focusable="false" {@extra} /> 107 | """ 108 | end 109 | 110 | @default_size 16 111 | defp put_defaults(opts) do 112 | opts 113 | |> Keyword.put_new(:width, @default_size) 114 | |> Keyword.put_new(:height, @default_size) 115 | |> put_icon_file(Application.get_env(:bitstyles_phoenix, :icon_file, :inline)) 116 | end 117 | 118 | defp put_icon_file(opts, :inline), do: opts 119 | 120 | defp put_icon_file(opts, file) when is_binary(file) do 121 | Keyword.put_new(opts, :file, file) 122 | end 123 | 124 | defp put_icon_file(opts, {module, function, arguments}) do 125 | file = apply(module, function, arguments) 126 | put_icon_file(opts, file) 127 | end 128 | 129 | defp put_icon_file(opts, {module, function}) do 130 | file = apply(module, function) 131 | put_icon_file(opts, file) 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/bitstyles_phoenix/showcase.ex: -------------------------------------------------------------------------------- 1 | defmodule BitstylesPhoenix.Showcase do 2 | @moduledoc false 3 | 4 | import Phoenix.Component, only: [sigil_H: 2] 5 | alias Phoenix.HTML.Safe 6 | 7 | defmacro story(name, doctest_iex_code, doctest_expected_results, opts \\ []) do 8 | default_version = 9 | BitstylesPhoenix.Bitstyles.Version.default_version_string() 10 | |> String.to_atom() 11 | 12 | doctest_expected_results = 13 | if is_list(doctest_expected_results) && is_tuple(List.first(doctest_expected_results)) do 14 | doctest_expected_results 15 | else 16 | [{default_version, doctest_expected_results}] 17 | end 18 | 19 | doctest_expected_result_default_version = 20 | Keyword.get(doctest_expected_results, default_version) 21 | 22 | if doctest_expected_result_default_version == nil do 23 | raise """ 24 | The third argument in the story/4 macro, the expected doctest result, must be a string. 25 | It could also be a keyword list of bitstyles versions to expected doctest result, with a required key of #{inspect(default_version)} (default version) 26 | """ 27 | end 28 | 29 | code = 30 | doctest_expected_result_default_version 31 | |> to_string() 32 | |> String.split("\n") 33 | |> Enum.map_join("\n", &String.trim/1) 34 | 35 | storydoc = """ 36 | ## #{name} 37 | 38 | #{sandbox(code, opts)} 39 | 40 | #{generate_doctests(doctest_iex_code, doctest_expected_results, default_version)} 41 | """ 42 | 43 | extra_html = Keyword.get(opts, :extra_html) 44 | 45 | storydoc = 46 | if extra_html && Keyword.get(opts, :show_extra_html, true) do 47 | storydoc <> 48 | """ 49 | *Requires additional content on the page:* 50 | 51 | ``` 52 | #{extra_html} 53 | ``` 54 | """ 55 | else 56 | storydoc 57 | end 58 | 59 | if Keyword.get(opts, :module, false) do 60 | quote do 61 | @moduledoc @moduledoc <> unquote(storydoc) 62 | end 63 | else 64 | quote do 65 | @doc @doc <> unquote(storydoc) 66 | end 67 | end 68 | end 69 | 70 | @default_iframe_style """ 71 | height:1px; \ 72 | border:none; \ 73 | overflow:hidden; \ 74 | padding-left: 1em; \ 75 | """ 76 | 77 | defp sandbox(code, opts) do 78 | extra_html = Keyword.get(opts, :extra_html, "") 79 | transparent = Keyword.get(opts, :transparent, true) 80 | {result, _} = Code.eval_string(code) 81 | dist = BitstylesPhoenix.Bitstyles.cdn_url() 82 | 83 | style = 84 | if transparent do 85 | """ 86 | html{ \ 87 | background-color: transparent !important; \ 88 | } \ 89 | \ 90 | @media (prefers-color-scheme: dark) { \ 91 | body {color: #fff; } \ 92 | } \ 93 | """ 94 | else 95 | "" 96 | end 97 | 98 | iframe_opts = 99 | [ 100 | srcdoc: 101 | ~s(#{Enum.join([extra_html, result]) |> String.replace("\n", "")}), 102 | style: "", 103 | allowtransparency: if(transparent, do: "true", else: "false") 104 | ] 105 | |> Keyword.merge(style_opts(opts)) 106 | 107 | assigns = %{iframe_opts: iframe_opts} 108 | 109 | if dist do 110 | ~H""" 111 |