├── test
├── test_helper.exs
└── click_to_component_test.exs
├── priv
├── demo.gif
└── static
│ ├── click_to_component.min.js
│ ├── click_to_component.esm.js
│ ├── click_to_component.cjs.js
│ ├── click_to_component.js
│ ├── click_to_component.esm.js.map
│ └── click_to_component.cjs.js.map
├── .formatter.exs
├── lib
├── click_to_component
│ ├── components.ex
│ └── hooks.ex
└── click_to_component.ex
├── assets
├── package.json
└── js
│ └── click_to_component
│ ├── index.js
│ ├── dom.js
│ └── menu.js
├── .gitignore
├── package.json
├── config
└── config.exs
├── mix.exs
├── README.md
└── mix.lock
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/priv/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elixir-saas/click_to_component/HEAD/priv/demo.gif
--------------------------------------------------------------------------------
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/test/click_to_component_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ClickToComponentTest do
2 | use ExUnit.Case
3 | doctest ClickToComponent
4 |
5 | test "greets the world" do
6 | assert ClickToComponent.hello() == :world
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/click_to_component/components.ex:
--------------------------------------------------------------------------------
1 | defmodule ClickToComponent.Components do
2 | use Phoenix.Component
3 |
4 | attr(:id, :string, default: "click-to-component")
5 |
6 | def click_to_component(assigns) do
7 | ~H"""
8 |
9 | """
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/assets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "click_to_component",
3 | "version": "0.1.0",
4 | "description": "Click-to-component functionality for LiveView apps.",
5 | "license": "MIT",
6 | "repository": {},
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "dependencies": {},
11 | "devDependencies": {}
12 | }
13 |
--------------------------------------------------------------------------------
/.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 | click_to_component-*.tar
24 |
25 | # Temporary files, for example, from tests.
26 | /tmp/
27 |
--------------------------------------------------------------------------------
/lib/click_to_component/hooks.ex:
--------------------------------------------------------------------------------
1 | defmodule ClickToComponent.Hooks do
2 | import Phoenix.LiveView
3 |
4 | def on_mount(:default, _params, _session, socket) do
5 | {:cont, attach_hook(socket, :click_to_component, :handle_event, &handle_event/3)}
6 | end
7 |
8 | defp handle_event("click_to_component:open", %{"path" => path}, socket) do
9 | {cmd, args} = command_from_config(path)
10 | System.cmd(cmd, args, cd: File.cwd!())
11 |
12 | {:halt, socket}
13 | end
14 |
15 | defp handle_event(_event, _params, socket), do: {:cont, socket}
16 |
17 | @default_command {"code", [".", "--goto", :path]}
18 |
19 | def command_from_config(path) do
20 | {cmd, args} = Application.get_env(:click_to_component, :command, @default_command)
21 | {cmd, Enum.map(args, &if(&1 == :path, do: path, else: &1))}
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/assets/js/click_to_component/index.js:
--------------------------------------------------------------------------------
1 | import { createMenu } from "./menu";
2 | import { findNearestComments } from "./dom";
3 |
4 | export const ClickToComponent = () => {
5 | return {
6 | mounted() {
7 | this.menu = createMenu(this.onMenuClick.bind(this));
8 | this.menu.mount();
9 |
10 | document.addEventListener("click", this.onClick.bind(this));
11 | },
12 |
13 | onClick(event) {
14 | if (event.altKey) {
15 | event.preventDefault();
16 | event.stopPropagation();
17 |
18 | const comments = findNearestComments(event.target);
19 |
20 | this.menu.render(event, comments);
21 | }
22 | },
23 |
24 | onMenuClick(comment) {
25 | this.pushEventTo(this.el, "click_to_component:open", {
26 | path: comment.path,
27 | });
28 | },
29 | };
30 | };
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "click_to_component",
3 | "version": "0.1.0",
4 | "description": "Click-to-component functionality for LiveView apps.",
5 | "license": "MIT",
6 | "module": "./priv/static/click_to_component.esm.js",
7 | "main": "./priv/static/click_to_component.cjs.js",
8 | "unpkg": "./priv/static/click_to_component.min.js",
9 | "jsdelivr": "./priv/static/click_to_component.min.js",
10 | "exports": {
11 | "import": "./priv/static/click_to_component.esm.js",
12 | "require": "./priv/static/click_to_component.cjs.js"
13 | },
14 | "author": "Justin Tormey (https://93software.com)",
15 | "repository": {
16 | "type": "git",
17 | "url": "git://github.com/elixir-saas/click_to_component.git"
18 | },
19 | "files": [
20 | "README.md",
21 | "package.json",
22 | "priv/static/*",
23 | "assets/js/click_to_component/*"
24 | ]
25 | }
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | config :phoenix, :json_library, Jason
4 | config :logger, :level, :debug
5 | config :logger, :backends, []
6 |
7 | if Mix.env() == :dev do
8 | esbuild = fn args ->
9 | [
10 | args: ~w(./js/click_to_component --bundle) ++ args,
11 | cd: Path.expand("../assets", __DIR__),
12 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
13 | ]
14 | end
15 |
16 | config :esbuild,
17 | version: "0.12.15",
18 | module:
19 | esbuild.(~w(--format=esm --sourcemap --outfile=../priv/static/click_to_component.esm.js)),
20 | main:
21 | esbuild.(~w(--format=cjs --sourcemap --outfile=../priv/static/click_to_component.cjs.js)),
22 | cdn:
23 | esbuild.(
24 | ~w(--format=iife --target=es2016 --global-name=LiveView --outfile=../priv/static/click_to_component.js)
25 | ),
26 | cdn_min:
27 | esbuild.(
28 | ~w(--format=iife --target=es2016 --global-name=LiveView --minify --outfile=../priv/static/click_to_component.min.js)
29 | )
30 | end
31 |
--------------------------------------------------------------------------------
/assets/js/click_to_component/dom.js:
--------------------------------------------------------------------------------
1 | const parseCommentString = (comment) => {
2 | const [component, path] = comment.trim().split(" ");
3 |
4 | const closing = component.indexOf("/") === 1;
5 |
6 | return {
7 | component: closing ? component.slice(2, -1) : component.slice(1, -1),
8 | path,
9 | closing,
10 | };
11 | };
12 |
13 | export const findNearestComments = (currentEl) => {
14 | const accumulated = [];
15 | const closed = [];
16 |
17 | while (true) {
18 | let node = currentEl.previousSibling;
19 |
20 | while (true) {
21 | if (node === null) {
22 | break;
23 | }
24 |
25 | if (node.nodeType === document.COMMENT_NODE) {
26 | const comment = parseCommentString(node.nodeValue);
27 |
28 | if (comment.closing) {
29 | closed.unshift(comment);
30 | } else if (closed[0]?.component === comment.component) {
31 | closed.shift();
32 | } else {
33 | accumulated.push(comment);
34 | }
35 | }
36 |
37 | node = node.previousSibling;
38 | }
39 |
40 | if (currentEl.parentElement == null) {
41 | break;
42 | }
43 |
44 | currentEl = currentEl.parentElement;
45 | }
46 |
47 | return accumulated;
48 | };
49 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule ClickToComponent.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :click_to_component,
7 | version: "0.2.2",
8 | elixir: "~> 1.15",
9 | start_permanent: Mix.env() == :prod,
10 | name: "Click To Component",
11 | description: "Click-to-component functionality for LiveView apps.",
12 | package: package(),
13 | deps: deps(),
14 | aliases: aliases()
15 | ]
16 | end
17 |
18 | def application do
19 | [
20 | extra_applications: [:logger]
21 | ]
22 | end
23 |
24 | defp deps do
25 | [
26 | {:phoenix_live_view, ">= 0.20.0"},
27 | {:esbuild, "~> 0.2", only: :dev},
28 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}
29 | ]
30 | end
31 |
32 | defp package do
33 | [
34 | maintainers: ["Justin Tormey"],
35 | licenses: ["MIT"],
36 | links: %{
37 | GitHub: "https://github.com/elixir-saas/click_to_component"
38 | },
39 | files: ~w(assets/js lib priv mix.exs package.json README.md)
40 | ]
41 | end
42 |
43 | defp aliases do
44 | [
45 | "assets.build": ["esbuild module", "esbuild cdn", "esbuild cdn_min", "esbuild main"],
46 | "assets.watch": ["esbuild module --watch"]
47 | ]
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/assets/js/click_to_component/menu.js:
--------------------------------------------------------------------------------
1 | export const createMenu = (onClick) => {
2 | const el = document.createElement("div");
3 |
4 | Object.assign(el.style, {
5 | display: "none",
6 | position: "fixed",
7 | top: "0px",
8 | left: "0px",
9 | zIndex: 999999999,
10 | background: "white",
11 | border: "1px solid #e4e4e7",
12 | borderRadius: "8px",
13 | padding: "8px 0px",
14 | });
15 |
16 | return {
17 | mount() {
18 | document.body.appendChild(el);
19 | document.addEventListener("click", (event) => {
20 | if (!el.contains(event.target)) {
21 | el.style.display = "none";
22 | }
23 | });
24 | },
25 |
26 | render(event, comments) {
27 | el.innerHTML = "";
28 | el.style.top = `${event.clientY}px`;
29 | el.style.left = `${event.clientX}px`;
30 | el.style.display = "block";
31 |
32 | for (const comment of comments) {
33 | const commentEl = document.createElement("div");
34 |
35 | Object.assign(commentEl.style, {
36 | margin: "6px 0px",
37 | padding: "0px 12px",
38 | fontSize: "14px",
39 | cursor: "pointer",
40 | });
41 |
42 | commentEl.innerText = comment.component;
43 | commentEl.onclick = () => onClick(comment);
44 |
45 | el.appendChild(commentEl);
46 | }
47 | },
48 | };
49 | };
50 |
--------------------------------------------------------------------------------
/priv/static/click_to_component.min.js:
--------------------------------------------------------------------------------
1 | var LiveView=(()=>{var c=Object.defineProperty;var r=e=>c(e,"__esModule",{value:!0});var m=(e,n)=>{r(e);for(var t in n)c(e,t,{get:n[t],enumerable:!0})};var u={};m(u,{ClickToComponent:()=>a});var p=e=>{let n=document.createElement("div");return Object.assign(n.style,{display:"none",position:"fixed",top:"0px",left:"0px",zIndex:999999999,background:"white",border:"1px solid #e4e4e7",borderRadius:"8px",padding:"8px 0px"}),{mount(){document.body.appendChild(n),document.addEventListener("click",t=>{n.contains(t.target)||(n.style.display="none")})},render(t,s){n.innerHTML="",n.style.top=`${t.clientY}px`,n.style.left=`${t.clientX}px`,n.style.display="block";for(let i of s){let o=document.createElement("div");Object.assign(o.style,{margin:"6px 0px",padding:"0px 12px",fontSize:"14px",cursor:"pointer"}),o.innerText=i.component,o.onclick=()=>e(i),n.appendChild(o)}}}};var d=e=>{let[n,t]=e.trim().split(" "),s=n.indexOf("/")===1;return{component:s?n.slice(2,-1):n.slice(1,-1),path:t,closing:s}},l=e=>{var s;let n=[],t=[];for(;;){let i=e.previousSibling;for(;i!==null;){if(i.nodeType===document.COMMENT_NODE){let o=d(i.nodeValue);o.closing?t.unshift(o):((s=t[0])==null?void 0:s.component)===o.component?t.shift():n.push(o)}i=i.previousSibling}if(e.parentElement==null)break;e=e.parentElement}return n};var a=()=>({mounted(){this.menu=p(this.onMenuClick.bind(this)),this.menu.mount(),document.addEventListener("click",this.onClick.bind(this))},onClick(e){if(e.altKey){e.preventDefault(),e.stopPropagation();let n=l(e.target);this.menu.render(e,n)}},onMenuClick(e){this.pushEventTo(this.el,"click_to_component:open",{path:e.path})}});return u;})();
2 |
--------------------------------------------------------------------------------
/lib/click_to_component.ex:
--------------------------------------------------------------------------------
1 | defmodule ClickToComponent do
2 | @moduledoc """
3 | Documentation for `ClickToComponent`.
4 | """
5 |
6 | require Logger
7 |
8 | @enabled? Application.compile_env(:click_to_component, :enabled, false)
9 |
10 | if @enabled? and !Application.compile_env(:phoenix_live_view, :debug_heex_annotations) do
11 | Logger.warning("""
12 | ClickToComponent requires :debug_heex_annotations to be enabled. Add the following configuration in your `config/dev.exs` file:
13 |
14 | config :phoenix_live_view, debug_heex_annotations: true
15 | """)
16 | end
17 |
18 | if !@enabled? and Application.compile_env(:phoenix_live_view, :debug_heex_annotations) do
19 | Logger.warning("""
20 | It looks like :debug_heex_annotations is enabled, but ClickToComponent is not. To enable, add the following configuration in your `config/dev.exs` file:
21 |
22 | config :click_to_component, enabled: true
23 | """)
24 | end
25 |
26 | @doc """
27 | Renders the ClickToComponent hook.
28 |
29 | Add to your `lib/my_app_web/layouts/root.html.heex` file as follows:
30 |
31 | ```html
32 |
33 |
34 |
35 |
36 | ```
37 | """
38 | import Phoenix.Component, only: [sigil_H: 2], warn: false
39 |
40 | if @enabled? do
41 | def render(assigns), do: __MODULE__.Components.click_to_component(assigns)
42 | else
43 | def render(assigns), do: ~H""
44 | end
45 |
46 | @doc """
47 | Returns the global `on_mount/1` LiveView hook for handling events as quoted code.
48 |
49 | Add to your `lib/my_app_web.ex` file as follows:
50 |
51 | ```elixir
52 | def live_view(opts \\ []) do
53 | quote do
54 | use Phoenix.LiveView,
55 | layout: {MyAppWeb.Layouts, :app}
56 |
57 | unquote(ClickToComponent.hooks())
58 |
59 | # Rest of live_view quoted code...
60 | end
61 | end
62 | ```
63 | """
64 | if @enabled? do
65 | def hooks() do
66 | quote do
67 | on_mount(unquote(__MODULE__.Hooks))
68 | end
69 | end
70 | else
71 | def hooks(), do: nil
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LiveView Click-to-Component
2 |
3 | Adds a click-to-component interaction, which when triggered opens the file and line number associated with the component in your favorite text editor.
4 |
5 | The key binding to trigger the interaction is `Option+Click`.
6 |
7 | 
8 |
9 | ## Installation
10 |
11 | The package can be installed by adding `click_to_component` to your list of dependencies in `mix.exs`:
12 |
13 | ```elixir
14 | def deps do
15 | [
16 | {:click_to_component, "~> 0.2.2"}
17 | ]
18 | end
19 | ```
20 |
21 | Documentation can be found at .
22 |
23 | ## Configuration
24 |
25 | To enable in dev, add the following to `config/dev.exs`:
26 |
27 | ```elixir
28 | config :click_to_component, enabled: true
29 | ```
30 |
31 | You may also customize the command used to open your editor:
32 |
33 | ```elixir
34 | # This is the default command.
35 | # Use `:path` to substitiute one of the args for the path, in the format `file:line`
36 | config :click_to_component, command: {"code", [".", "--goto", :path]}
37 | ```
38 |
39 | Make sure that you have LiveView >=0.20.0 installed in your project, and that you have enabled debug annotations:
40 |
41 | ```elixir
42 | config :phoenix_live_view, debug_heex_annotations: true
43 | ```
44 |
45 | ## Usage
46 |
47 | First, add the following code to your `lib/my_app_web.ex` module, in the quoted code in your `live_view` function:
48 |
49 | ```elixir
50 | def live_view(opts \\ []) do
51 | quote do
52 | use Phoenix.LiveView,
53 | layout: {MyAppWeb.Layouts, :app}
54 |
55 | # Add this line
56 | unquote(ClickToComponent.hooks())
57 |
58 | # Rest of live_view quoted code...
59 | end
60 | end
61 | ```
62 |
63 | Next, render the component code in your `lib/my_app_web/layouts/root.html.heex` layout file:
64 |
65 | ```html
66 |
67 |
68 |
69 |
70 | ```
71 |
72 | When `click_to_component` is enabled, this will render a JS hook that listens for clicks and renders the component menu on click.
73 |
74 | Finally, import and configure the `ClickToComponent` hook in `assets/js/app.js`:
75 |
76 | ```js
77 | import { ClickToComponent } from "click_to_component";
78 |
79 | // Note the function call!
80 | const hooks = {
81 | ClickToComponent: ClickToComponent(),
82 | };
83 |
84 | // Make sure `hooks` is configured in LiveSocket
85 | let liveSocket = new LiveSocket(socketUrl, Socket, {
86 | hooks,
87 | params: { _csrf_token: csrfToken },
88 | });
89 | ```
90 |
--------------------------------------------------------------------------------
/priv/static/click_to_component.esm.js:
--------------------------------------------------------------------------------
1 | // js/click_to_component/menu.js
2 | var createMenu = (onClick) => {
3 | const el = document.createElement("div");
4 | Object.assign(el.style, {
5 | display: "none",
6 | position: "fixed",
7 | top: "0px",
8 | left: "0px",
9 | zIndex: 999999999,
10 | background: "white",
11 | border: "1px solid #e4e4e7",
12 | borderRadius: "8px",
13 | padding: "8px 0px"
14 | });
15 | return {
16 | mount() {
17 | document.body.appendChild(el);
18 | document.addEventListener("click", (event) => {
19 | if (!el.contains(event.target)) {
20 | el.style.display = "none";
21 | }
22 | });
23 | },
24 | render(event, comments) {
25 | el.innerHTML = "";
26 | el.style.top = `${event.clientY}px`;
27 | el.style.left = `${event.clientX}px`;
28 | el.style.display = "block";
29 | for (const comment of comments) {
30 | const commentEl = document.createElement("div");
31 | Object.assign(commentEl.style, {
32 | margin: "6px 0px",
33 | padding: "0px 12px",
34 | fontSize: "14px",
35 | cursor: "pointer"
36 | });
37 | commentEl.innerText = comment.component;
38 | commentEl.onclick = () => onClick(comment);
39 | el.appendChild(commentEl);
40 | }
41 | }
42 | };
43 | };
44 |
45 | // js/click_to_component/dom.js
46 | var parseCommentString = (comment) => {
47 | const [component, path] = comment.trim().split(" ");
48 | const closing = component.indexOf("/") === 1;
49 | return {
50 | component: closing ? component.slice(2, -1) : component.slice(1, -1),
51 | path,
52 | closing
53 | };
54 | };
55 | var findNearestComments = (currentEl) => {
56 | const accumulated = [];
57 | const closed = [];
58 | while (true) {
59 | let node = currentEl.previousSibling;
60 | while (true) {
61 | if (node === null) {
62 | break;
63 | }
64 | if (node.nodeType === document.COMMENT_NODE) {
65 | const comment = parseCommentString(node.nodeValue);
66 | if (comment.closing) {
67 | closed.unshift(comment);
68 | } else if (closed[0]?.component === comment.component) {
69 | closed.shift();
70 | } else {
71 | accumulated.push(comment);
72 | }
73 | }
74 | node = node.previousSibling;
75 | }
76 | if (currentEl.parentElement == null) {
77 | break;
78 | }
79 | currentEl = currentEl.parentElement;
80 | }
81 | return accumulated;
82 | };
83 |
84 | // js/click_to_component/index.js
85 | var ClickToComponent = () => {
86 | return {
87 | mounted() {
88 | this.menu = createMenu(this.onMenuClick.bind(this));
89 | this.menu.mount();
90 | document.addEventListener("click", this.onClick.bind(this));
91 | },
92 | onClick(event) {
93 | if (event.altKey) {
94 | event.preventDefault();
95 | event.stopPropagation();
96 | const comments = findNearestComments(event.target);
97 | this.menu.render(event, comments);
98 | }
99 | },
100 | onMenuClick(comment) {
101 | this.pushEventTo(this.el, "click_to_component:open", {
102 | path: comment.path
103 | });
104 | }
105 | };
106 | };
107 | export {
108 | ClickToComponent
109 | };
110 | //# sourceMappingURL=click_to_component.esm.js.map
111 |
--------------------------------------------------------------------------------
/priv/static/click_to_component.cjs.js:
--------------------------------------------------------------------------------
1 | var __defProp = Object.defineProperty;
2 | var __markAsModule = (target) => __defProp(target, "__esModule", { value: true });
3 | var __export = (target, all) => {
4 | __markAsModule(target);
5 | for (var name in all)
6 | __defProp(target, name, { get: all[name], enumerable: true });
7 | };
8 |
9 | // js/click_to_component/index.js
10 | __export(exports, {
11 | ClickToComponent: () => ClickToComponent
12 | });
13 |
14 | // js/click_to_component/menu.js
15 | var createMenu = (onClick) => {
16 | const el = document.createElement("div");
17 | Object.assign(el.style, {
18 | display: "none",
19 | position: "fixed",
20 | top: "0px",
21 | left: "0px",
22 | zIndex: 999999999,
23 | background: "white",
24 | border: "1px solid #e4e4e7",
25 | borderRadius: "8px",
26 | padding: "8px 0px"
27 | });
28 | return {
29 | mount() {
30 | document.body.appendChild(el);
31 | document.addEventListener("click", (event) => {
32 | if (!el.contains(event.target)) {
33 | el.style.display = "none";
34 | }
35 | });
36 | },
37 | render(event, comments) {
38 | el.innerHTML = "";
39 | el.style.top = `${event.clientY}px`;
40 | el.style.left = `${event.clientX}px`;
41 | el.style.display = "block";
42 | for (const comment of comments) {
43 | const commentEl = document.createElement("div");
44 | Object.assign(commentEl.style, {
45 | margin: "6px 0px",
46 | padding: "0px 12px",
47 | fontSize: "14px",
48 | cursor: "pointer"
49 | });
50 | commentEl.innerText = comment.component;
51 | commentEl.onclick = () => onClick(comment);
52 | el.appendChild(commentEl);
53 | }
54 | }
55 | };
56 | };
57 |
58 | // js/click_to_component/dom.js
59 | var parseCommentString = (comment) => {
60 | const [component, path] = comment.trim().split(" ");
61 | const closing = component.indexOf("/") === 1;
62 | return {
63 | component: closing ? component.slice(2, -1) : component.slice(1, -1),
64 | path,
65 | closing
66 | };
67 | };
68 | var findNearestComments = (currentEl) => {
69 | const accumulated = [];
70 | const closed = [];
71 | while (true) {
72 | let node = currentEl.previousSibling;
73 | while (true) {
74 | if (node === null) {
75 | break;
76 | }
77 | if (node.nodeType === document.COMMENT_NODE) {
78 | const comment = parseCommentString(node.nodeValue);
79 | if (comment.closing) {
80 | closed.unshift(comment);
81 | } else if (closed[0]?.component === comment.component) {
82 | closed.shift();
83 | } else {
84 | accumulated.push(comment);
85 | }
86 | }
87 | node = node.previousSibling;
88 | }
89 | if (currentEl.parentElement == null) {
90 | break;
91 | }
92 | currentEl = currentEl.parentElement;
93 | }
94 | return accumulated;
95 | };
96 |
97 | // js/click_to_component/index.js
98 | var ClickToComponent = () => {
99 | return {
100 | mounted() {
101 | this.menu = createMenu(this.onMenuClick.bind(this));
102 | this.menu.mount();
103 | document.addEventListener("click", this.onClick.bind(this));
104 | },
105 | onClick(event) {
106 | if (event.altKey) {
107 | event.preventDefault();
108 | event.stopPropagation();
109 | const comments = findNearestComments(event.target);
110 | this.menu.render(event, comments);
111 | }
112 | },
113 | onMenuClick(comment) {
114 | this.pushEventTo(this.el, "click_to_component:open", {
115 | path: comment.path
116 | });
117 | }
118 | };
119 | };
120 | //# sourceMappingURL=click_to_component.cjs.js.map
121 |
--------------------------------------------------------------------------------
/priv/static/click_to_component.js:
--------------------------------------------------------------------------------
1 | var LiveView = (() => {
2 | var __defProp = Object.defineProperty;
3 | var __markAsModule = (target) => __defProp(target, "__esModule", { value: true });
4 | var __export = (target, all) => {
5 | __markAsModule(target);
6 | for (var name in all)
7 | __defProp(target, name, { get: all[name], enumerable: true });
8 | };
9 |
10 | // js/click_to_component/index.js
11 | var click_to_component_exports = {};
12 | __export(click_to_component_exports, {
13 | ClickToComponent: () => ClickToComponent
14 | });
15 |
16 | // js/click_to_component/menu.js
17 | var createMenu = (onClick) => {
18 | const el = document.createElement("div");
19 | Object.assign(el.style, {
20 | display: "none",
21 | position: "fixed",
22 | top: "0px",
23 | left: "0px",
24 | zIndex: 999999999,
25 | background: "white",
26 | border: "1px solid #e4e4e7",
27 | borderRadius: "8px",
28 | padding: "8px 0px"
29 | });
30 | return {
31 | mount() {
32 | document.body.appendChild(el);
33 | document.addEventListener("click", (event) => {
34 | if (!el.contains(event.target)) {
35 | el.style.display = "none";
36 | }
37 | });
38 | },
39 | render(event, comments) {
40 | el.innerHTML = "";
41 | el.style.top = `${event.clientY}px`;
42 | el.style.left = `${event.clientX}px`;
43 | el.style.display = "block";
44 | for (const comment of comments) {
45 | const commentEl = document.createElement("div");
46 | Object.assign(commentEl.style, {
47 | margin: "6px 0px",
48 | padding: "0px 12px",
49 | fontSize: "14px",
50 | cursor: "pointer"
51 | });
52 | commentEl.innerText = comment.component;
53 | commentEl.onclick = () => onClick(comment);
54 | el.appendChild(commentEl);
55 | }
56 | }
57 | };
58 | };
59 |
60 | // js/click_to_component/dom.js
61 | var parseCommentString = (comment) => {
62 | const [component, path] = comment.trim().split(" ");
63 | const closing = component.indexOf("/") === 1;
64 | return {
65 | component: closing ? component.slice(2, -1) : component.slice(1, -1),
66 | path,
67 | closing
68 | };
69 | };
70 | var findNearestComments = (currentEl) => {
71 | var _a;
72 | const accumulated = [];
73 | const closed = [];
74 | while (true) {
75 | let node = currentEl.previousSibling;
76 | while (true) {
77 | if (node === null) {
78 | break;
79 | }
80 | if (node.nodeType === document.COMMENT_NODE) {
81 | const comment = parseCommentString(node.nodeValue);
82 | if (comment.closing) {
83 | closed.unshift(comment);
84 | } else if (((_a = closed[0]) == null ? void 0 : _a.component) === comment.component) {
85 | closed.shift();
86 | } else {
87 | accumulated.push(comment);
88 | }
89 | }
90 | node = node.previousSibling;
91 | }
92 | if (currentEl.parentElement == null) {
93 | break;
94 | }
95 | currentEl = currentEl.parentElement;
96 | }
97 | return accumulated;
98 | };
99 |
100 | // js/click_to_component/index.js
101 | var ClickToComponent = () => {
102 | return {
103 | mounted() {
104 | this.menu = createMenu(this.onMenuClick.bind(this));
105 | this.menu.mount();
106 | document.addEventListener("click", this.onClick.bind(this));
107 | },
108 | onClick(event) {
109 | if (event.altKey) {
110 | event.preventDefault();
111 | event.stopPropagation();
112 | const comments = findNearestComments(event.target);
113 | this.menu.render(event, comments);
114 | }
115 | },
116 | onMenuClick(comment) {
117 | this.pushEventTo(this.el, "click_to_component:open", {
118 | path: comment.path
119 | });
120 | }
121 | };
122 | };
123 | return click_to_component_exports;
124 | })();
125 |
--------------------------------------------------------------------------------
/priv/static/click_to_component.esm.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "sources": ["../../assets/js/click_to_component/menu.js", "../../assets/js/click_to_component/dom.js", "../../assets/js/click_to_component/index.js"],
4 | "sourcesContent": ["export const createMenu = (onClick) => {\n const el = document.createElement(\"div\");\n\n Object.assign(el.style, {\n display: \"none\",\n position: \"fixed\",\n top: \"0px\",\n left: \"0px\",\n zIndex: 999999999,\n background: \"white\",\n border: \"1px solid #e4e4e7\",\n borderRadius: \"8px\",\n padding: \"8px 0px\",\n });\n\n return {\n mount() {\n document.body.appendChild(el);\n document.addEventListener(\"click\", (event) => {\n if (!el.contains(event.target)) {\n el.style.display = \"none\";\n }\n });\n },\n\n render(event, comments) {\n el.innerHTML = \"\";\n el.style.top = `${event.clientY}px`;\n el.style.left = `${event.clientX}px`;\n el.style.display = \"block\";\n\n for (const comment of comments) {\n const commentEl = document.createElement(\"div\");\n\n Object.assign(commentEl.style, {\n margin: \"6px 0px\",\n padding: \"0px 12px\",\n fontSize: \"14px\",\n cursor: \"pointer\",\n });\n\n commentEl.innerText = comment.component;\n commentEl.onclick = () => onClick(comment);\n\n el.appendChild(commentEl);\n }\n },\n };\n};\n", "const parseCommentString = (comment) => {\n const [component, path] = comment.trim().split(\" \");\n\n const closing = component.indexOf(\"/\") === 1;\n\n return {\n component: closing ? component.slice(2, -1) : component.slice(1, -1),\n path,\n closing,\n };\n};\n\nexport const findNearestComments = (currentEl) => {\n const accumulated = [];\n const closed = [];\n\n while (true) {\n let node = currentEl.previousSibling;\n\n while (true) {\n if (node === null) {\n break;\n }\n\n if (node.nodeType === document.COMMENT_NODE) {\n const comment = parseCommentString(node.nodeValue);\n\n if (comment.closing) {\n closed.unshift(comment);\n } else if (closed[0]?.component === comment.component) {\n closed.shift();\n } else {\n accumulated.push(comment);\n }\n }\n\n node = node.previousSibling;\n }\n\n if (currentEl.parentElement == null) {\n break;\n }\n\n currentEl = currentEl.parentElement;\n }\n\n return accumulated;\n};\n", "import { createMenu } from \"./menu\";\nimport { findNearestComments } from \"./dom\";\n\nexport const ClickToComponent = () => {\n return {\n mounted() {\n this.menu = createMenu(this.onMenuClick.bind(this));\n this.menu.mount();\n\n document.addEventListener(\"click\", this.onClick.bind(this));\n },\n\n onClick(event) {\n if (event.altKey) {\n event.preventDefault();\n event.stopPropagation();\n\n const comments = findNearestComments(event.target);\n\n this.menu.render(event, comments);\n }\n },\n\n onMenuClick(comment) {\n this.pushEventTo(this.el, \"click_to_component:open\", {\n path: comment.path,\n });\n },\n };\n};\n"],
5 | "mappings": ";AAAO,IAAM,aAAa,CAAC,YAAY;AACrC,QAAM,KAAK,SAAS,cAAc;AAElC,SAAO,OAAO,GAAG,OAAO;AAAA,IACtB,SAAS;AAAA,IACT,UAAU;AAAA,IACV,KAAK;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,SAAS;AAAA;AAGX,SAAO;AAAA,IACL,QAAQ;AACN,eAAS,KAAK,YAAY;AAC1B,eAAS,iBAAiB,SAAS,CAAC,UAAU;AAC5C,YAAI,CAAC,GAAG,SAAS,MAAM,SAAS;AAC9B,aAAG,MAAM,UAAU;AAAA;AAAA;AAAA;AAAA,IAKzB,OAAO,OAAO,UAAU;AACtB,SAAG,YAAY;AACf,SAAG,MAAM,MAAM,GAAG,MAAM;AACxB,SAAG,MAAM,OAAO,GAAG,MAAM;AACzB,SAAG,MAAM,UAAU;AAEnB,iBAAW,WAAW,UAAU;AAC9B,cAAM,YAAY,SAAS,cAAc;AAEzC,eAAO,OAAO,UAAU,OAAO;AAAA,UAC7B,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,UAAU;AAAA,UACV,QAAQ;AAAA;AAGV,kBAAU,YAAY,QAAQ;AAC9B,kBAAU,UAAU,MAAM,QAAQ;AAElC,WAAG,YAAY;AAAA;AAAA;AAAA;AAAA;;;AC5CvB,IAAM,qBAAqB,CAAC,YAAY;AACtC,QAAM,CAAC,WAAW,QAAQ,QAAQ,OAAO,MAAM;AAE/C,QAAM,UAAU,UAAU,QAAQ,SAAS;AAE3C,SAAO;AAAA,IACL,WAAW,UAAU,UAAU,MAAM,GAAG,MAAM,UAAU,MAAM,GAAG;AAAA,IACjE;AAAA,IACA;AAAA;AAAA;AAIG,IAAM,sBAAsB,CAAC,cAAc;AAChD,QAAM,cAAc;AACpB,QAAM,SAAS;AAEf,SAAO,MAAM;AACX,QAAI,OAAO,UAAU;AAErB,WAAO,MAAM;AACX,UAAI,SAAS,MAAM;AACjB;AAAA;AAGF,UAAI,KAAK,aAAa,SAAS,cAAc;AAC3C,cAAM,UAAU,mBAAmB,KAAK;AAExC,YAAI,QAAQ,SAAS;AACnB,iBAAO,QAAQ;AAAA,mBACN,OAAO,IAAI,cAAc,QAAQ,WAAW;AACrD,iBAAO;AAAA,eACF;AACL,sBAAY,KAAK;AAAA;AAAA;AAIrB,aAAO,KAAK;AAAA;AAGd,QAAI,UAAU,iBAAiB,MAAM;AACnC;AAAA;AAGF,gBAAY,UAAU;AAAA;AAGxB,SAAO;AAAA;;;AC3CF,IAAM,mBAAmB,MAAM;AACpC,SAAO;AAAA,IACL,UAAU;AACR,WAAK,OAAO,WAAW,KAAK,YAAY,KAAK;AAC7C,WAAK,KAAK;AAEV,eAAS,iBAAiB,SAAS,KAAK,QAAQ,KAAK;AAAA;AAAA,IAGvD,QAAQ,OAAO;AACb,UAAI,MAAM,QAAQ;AAChB,cAAM;AACN,cAAM;AAEN,cAAM,WAAW,oBAAoB,MAAM;AAE3C,aAAK,KAAK,OAAO,OAAO;AAAA;AAAA;AAAA,IAI5B,YAAY,SAAS;AACnB,WAAK,YAAY,KAAK,IAAI,2BAA2B;AAAA,QACnD,MAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;",
6 | "names": []
7 | }
8 |
--------------------------------------------------------------------------------
/priv/static/click_to_component.cjs.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "sources": ["../../assets/js/click_to_component/index.js", "../../assets/js/click_to_component/menu.js", "../../assets/js/click_to_component/dom.js"],
4 | "sourcesContent": ["import { createMenu } from \"./menu\";\nimport { findNearestComments } from \"./dom\";\n\nexport const ClickToComponent = () => {\n return {\n mounted() {\n this.menu = createMenu(this.onMenuClick.bind(this));\n this.menu.mount();\n\n document.addEventListener(\"click\", this.onClick.bind(this));\n },\n\n onClick(event) {\n if (event.altKey) {\n event.preventDefault();\n event.stopPropagation();\n\n const comments = findNearestComments(event.target);\n\n this.menu.render(event, comments);\n }\n },\n\n onMenuClick(comment) {\n this.pushEventTo(this.el, \"click_to_component:open\", {\n path: comment.path,\n });\n },\n };\n};\n", "export const createMenu = (onClick) => {\n const el = document.createElement(\"div\");\n\n Object.assign(el.style, {\n display: \"none\",\n position: \"fixed\",\n top: \"0px\",\n left: \"0px\",\n zIndex: 999999999,\n background: \"white\",\n border: \"1px solid #e4e4e7\",\n borderRadius: \"8px\",\n padding: \"8px 0px\",\n });\n\n return {\n mount() {\n document.body.appendChild(el);\n document.addEventListener(\"click\", (event) => {\n if (!el.contains(event.target)) {\n el.style.display = \"none\";\n }\n });\n },\n\n render(event, comments) {\n el.innerHTML = \"\";\n el.style.top = `${event.clientY}px`;\n el.style.left = `${event.clientX}px`;\n el.style.display = \"block\";\n\n for (const comment of comments) {\n const commentEl = document.createElement(\"div\");\n\n Object.assign(commentEl.style, {\n margin: \"6px 0px\",\n padding: \"0px 12px\",\n fontSize: \"14px\",\n cursor: \"pointer\",\n });\n\n commentEl.innerText = comment.component;\n commentEl.onclick = () => onClick(comment);\n\n el.appendChild(commentEl);\n }\n },\n };\n};\n", "const parseCommentString = (comment) => {\n const [component, path] = comment.trim().split(\" \");\n\n const closing = component.indexOf(\"/\") === 1;\n\n return {\n component: closing ? component.slice(2, -1) : component.slice(1, -1),\n path,\n closing,\n };\n};\n\nexport const findNearestComments = (currentEl) => {\n const accumulated = [];\n const closed = [];\n\n while (true) {\n let node = currentEl.previousSibling;\n\n while (true) {\n if (node === null) {\n break;\n }\n\n if (node.nodeType === document.COMMENT_NODE) {\n const comment = parseCommentString(node.nodeValue);\n\n if (comment.closing) {\n closed.unshift(comment);\n } else if (closed[0]?.component === comment.component) {\n closed.shift();\n } else {\n accumulated.push(comment);\n }\n }\n\n node = node.previousSibling;\n }\n\n if (currentEl.parentElement == null) {\n break;\n }\n\n currentEl = currentEl.parentElement;\n }\n\n return accumulated;\n};\n"],
5 | "mappings": ";;;;;;;;;AAAA;AAAA;AAAA;;;ACAO,IAAM,aAAa,CAAC,YAAY;AACrC,QAAM,KAAK,SAAS,cAAc;AAElC,SAAO,OAAO,GAAG,OAAO;AAAA,IACtB,SAAS;AAAA,IACT,UAAU;AAAA,IACV,KAAK;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,SAAS;AAAA;AAGX,SAAO;AAAA,IACL,QAAQ;AACN,eAAS,KAAK,YAAY;AAC1B,eAAS,iBAAiB,SAAS,CAAC,UAAU;AAC5C,YAAI,CAAC,GAAG,SAAS,MAAM,SAAS;AAC9B,aAAG,MAAM,UAAU;AAAA;AAAA;AAAA;AAAA,IAKzB,OAAO,OAAO,UAAU;AACtB,SAAG,YAAY;AACf,SAAG,MAAM,MAAM,GAAG,MAAM;AACxB,SAAG,MAAM,OAAO,GAAG,MAAM;AACzB,SAAG,MAAM,UAAU;AAEnB,iBAAW,WAAW,UAAU;AAC9B,cAAM,YAAY,SAAS,cAAc;AAEzC,eAAO,OAAO,UAAU,OAAO;AAAA,UAC7B,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,UAAU;AAAA,UACV,QAAQ;AAAA;AAGV,kBAAU,YAAY,QAAQ;AAC9B,kBAAU,UAAU,MAAM,QAAQ;AAElC,WAAG,YAAY;AAAA;AAAA;AAAA;AAAA;;;AC5CvB,IAAM,qBAAqB,CAAC,YAAY;AACtC,QAAM,CAAC,WAAW,QAAQ,QAAQ,OAAO,MAAM;AAE/C,QAAM,UAAU,UAAU,QAAQ,SAAS;AAE3C,SAAO;AAAA,IACL,WAAW,UAAU,UAAU,MAAM,GAAG,MAAM,UAAU,MAAM,GAAG;AAAA,IACjE;AAAA,IACA;AAAA;AAAA;AAIG,IAAM,sBAAsB,CAAC,cAAc;AAChD,QAAM,cAAc;AACpB,QAAM,SAAS;AAEf,SAAO,MAAM;AACX,QAAI,OAAO,UAAU;AAErB,WAAO,MAAM;AACX,UAAI,SAAS,MAAM;AACjB;AAAA;AAGF,UAAI,KAAK,aAAa,SAAS,cAAc;AAC3C,cAAM,UAAU,mBAAmB,KAAK;AAExC,YAAI,QAAQ,SAAS;AACnB,iBAAO,QAAQ;AAAA,mBACN,OAAO,IAAI,cAAc,QAAQ,WAAW;AACrD,iBAAO;AAAA,eACF;AACL,sBAAY,KAAK;AAAA;AAAA;AAIrB,aAAO,KAAK;AAAA;AAGd,QAAI,UAAU,iBAAiB,MAAM;AACnC;AAAA;AAGF,gBAAY,UAAU;AAAA;AAGxB,SAAO;AAAA;;;AF3CF,IAAM,mBAAmB,MAAM;AACpC,SAAO;AAAA,IACL,UAAU;AACR,WAAK,OAAO,WAAW,KAAK,YAAY,KAAK;AAC7C,WAAK,KAAK;AAEV,eAAS,iBAAiB,SAAS,KAAK,QAAQ,KAAK;AAAA;AAAA,IAGvD,QAAQ,OAAO;AACb,UAAI,MAAM,QAAQ;AAChB,cAAM;AACN,cAAM;AAEN,cAAM,WAAW,oBAAoB,MAAM;AAE3C,aAAK,KAAK,OAAO,OAAO;AAAA;AAAA;AAAA,IAI5B,YAAY,SAAS;AACnB,WAAK,YAAY,KAAK,IAAI,2BAA2B;AAAA,QACnD,MAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;",
6 | "names": []
7 | }
8 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"},
3 | "earmark_parser": {:hex, :earmark_parser, "1.4.37", "2ad73550e27c8946648b06905a57e4d454e4d7229c2dafa72a0348c99d8be5f7", [:mix], [], "hexpm", "6b19783f2802f039806f375610faa22da130b8edc21209d0bff47918bb48360e"},
4 | "esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"},
5 | "ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"},
6 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
7 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
8 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
9 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
10 | "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
11 | "phoenix": {:hex, :phoenix, "1.7.7", "4cc501d4d823015007ba3cdd9c41ecaaf2ffb619d6fb283199fa8ddba89191e0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "8966e15c395e5e37591b6ed0bd2ae7f48e961f0f60ac4c733f9566b519453085"},
12 | "phoenix_html": {:hex, :phoenix_html, "3.3.2", "d6ce982c6d8247d2fc0defe625255c721fb8d5f1942c5ac051f6177bffa5973f", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "44adaf8e667c1c20fb9d284b6b0fa8dc7946ce29e81ce621860aa7e96de9a11d"},
13 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.0", "3f3531c835e46a3b45b4c3ca4a09cef7ba1d0f0d0035eef751c7084b8adb1299", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "29875f8a58fb031f2dc8f3be025c92ed78d342b46f9bbf6dfe579549d7c81050"},
14 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
15 | "phoenix_template": {:hex, :phoenix_template, "1.0.3", "32de561eefcefa951aead30a1f94f1b5f0379bc9e340bb5c667f65f1edfa4326", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"},
16 | "plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"},
17 | "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
18 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
19 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
20 | "websock_adapter": {:hex, :websock_adapter, "0.5.4", "7af8408e7ed9d56578539594d1ee7d8461e2dd5c3f57b0f2a5352d610ddde757", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d2c238c79c52cbe223fcdae22ca0bb5007a735b9e933870e241fce66afb4f4ab"},
21 | }
22 |
--------------------------------------------------------------------------------