├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── deps.edn ├── dev └── example │ └── core.cljs ├── index.html ├── logo.svg ├── package-lock.json ├── package.json ├── pom.xml ├── release.edn ├── shadow-cljs.edn ├── src └── cljs_react_devtools │ └── core.cljs └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: roman01la 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cpcache 3 | .idea 4 | *.iml 5 | .shadow-cljs 6 | out 7 | target 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | _React DevTools for ClojureScript wrappers_ 4 | 5 | > ⚠️ _EXPERIMENTAL_ 6 | 7 | https://github.com/roman01la/cljs-react-devtools/assets/1355501/c3bd8d6d-1127-4459-89ac-3b551d47da36 8 | 9 | [*Live demo*](https://roman01la.github.io/cljs-react-devtools/) 10 | 11 | [![Clojars Project](https://img.shields.io/clojars/v/com.github.roman01la/cljs-react-devtools.svg)](https://clojars.org/com.github.roman01la/cljs-react-devtools) 12 | 13 | ## Features 14 | 15 | - React components & DOM nodes tree 16 | - Visual picking and highlighting 17 | - Props, hooks, Reagent reactions and re-frame subscriptions inspector 18 | - Update reactions and subscriptions from the inspector 19 | - Click on a value in the inspector to log it to the console 20 | - Press a shortcut to toggle DevTools visibility 21 | - Bottom, left, right docking and undocking into a separate window 22 | 23 | ### Supported React wrappers 24 | 25 | - UIx 26 | - Reagent 27 | 28 | ## Setup 29 | 30 | 1. Install the library via Git deps 31 | 32 | ```clojure 33 | {:deps {com.github.roman01la/cljs-react-devtools {:mvn/version "0.2.0"}}} 34 | ``` 35 | 36 | 2. Create preload namespace and initialize DevTools 37 | 38 | ```clojure 39 | (cljs-react-devtools.core/init! 40 | {:root (js/document.getElementById "root") ;; React root 41 | :shortcut "Control-Shift-Meta-R"}) ;; toggles DevTools visibility 42 | ``` 43 | 44 | ## Run example in this repo 45 | 46 | 1. Install NPM deps 47 | 2. Run `clojure -A:examples -M -m shadow.cljs.devtools.cli watch examples` 48 | 3. Open `http://localhost:3000/` 49 | 50 | [_Support development of the project via GitHub Sponsors program_](https://github.com/sponsors/roman01la) 51 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {com.pitch/uix.core {:mvn/version "1.2.0"} 2 | com.pitch/uix.dom {:mvn/version "1.2.0"}} 3 | :paths ["src"] 4 | :aliases {:examples {:extra-paths ["dev"] 5 | :extra-deps {org.clojure/clojure {:mvn/version "1.12.0"} 6 | org.clojure/clojurescript {:mvn/version "1.11.60"} 7 | thheller/shadow-cljs {:mvn/version "2.28.19"} 8 | reagent/reagent {:mvn/version "1.2.0"} 9 | re-frame/re-frame {:mvn/version "1.4.2"}}} 10 | :release {:extra-paths ["dev"] 11 | :extra-deps {appliedscience/deps-library {:mvn/version "0.3.4"} 12 | org.apache.maven/maven-model {:mvn/version "3.6.3"}} 13 | :main-opts ["-m" "release"]}}} -------------------------------------------------------------------------------- /dev/example/core.cljs: -------------------------------------------------------------------------------- 1 | (ns example.core 2 | (:require [cljs-react-devtools.core] 3 | [uix.core :as uix :refer [$ defui]] 4 | [uix.dom] 5 | [uix.dev] 6 | [reagent.core :as r] 7 | [re-frame.core :as rf])) 8 | 9 | 10 | ;; dev setup 11 | (uix.dev/init-fast-refresh!) 12 | 13 | (defn ^:dev/after-load refresh [] 14 | (uix.dev/refresh!)) 15 | 16 | ;; app code 17 | (def tools [:rect :circle :text]) 18 | 19 | (rf/reg-sub :hello/workd 20 | (fn [] 21 | :nothing)) 22 | 23 | (defn tool-button [{:keys [selected? label on-press]}] 24 | (r/with-let [a (r/atom 1)] 25 | @(rf/subscribe [:hello/workd]) 26 | [:div {:on-click on-press 27 | :style {:padding "4px 8px" 28 | :cursor :pointer 29 | :border-radius 3 30 | :color (when selected? "#fff") 31 | :background-color (when selected? "#ff89da")}} 32 | label])) 33 | 34 | (defui toolbar [{:keys [state set-state on-add-shape]}] 35 | (let [{:keys [grid?]} state] 36 | ($ :div {:style {:padding "8px 16px" 37 | :height 46 38 | :display :flex 39 | :align-items :center 40 | :background-color "#fff" 41 | :position :relative 42 | :box-shadow "0 1px 1px rgba(0, 0, 10, 0.2)"}} 43 | ($ :img {:src "https://raw.githubusercontent.com/pitch-io/uix/master/logo.png" 44 | :style {:height "100%" 45 | :margin "0 16px 0 0"}}) 46 | (for [t tools] 47 | (r/as-element [tool-button {:key t :label (name t) :on-press #(on-add-shape t)}])) 48 | ($ :div {:style {:width 1 :height "60%" :background-color "#c1cdd0" :margin "0 8px"}}) 49 | (r/as-element 50 | [tool-button {:label "grid" 51 | :selected? grid? 52 | :on-press #(set-state (update state :grid? not))}])))) 53 | 54 | (defui ^:memo canvas-grid [{:keys [width height size color]}] 55 | (let [wn (Math/ceil (/ width size)) 56 | hn (Math/ceil (/ height size))] 57 | ($ :<> 58 | (for [widx (range wn)] 59 | ($ :line {:key widx 60 | :x1 (* size widx) 61 | :x2 (* size widx) 62 | :y1 0 63 | :y2 height 64 | :stroke color})) 65 | (for [hidx (range hn)] 66 | ($ :line {:key hidx 67 | :y1 (* size hidx) 68 | :y2 (* size hidx) 69 | :x1 0 70 | :x2 width 71 | :stroke color}))))) 72 | 73 | (defui cursor [{:keys [mx my r color]}] 74 | (let [mx (+ mx (/ r 2)) 75 | my (+ my (/ r 2))] 76 | ($ :circle {:cx (- mx (/ r 2)) :cy (- my (/ r 2)) :r r :fill color}))) 77 | 78 | (defui rect [{:keys [x y width height fill-color stroke-width stroke-color 79 | children on-mouse-down on-mouse-up]}] 80 | ($ :rect 81 | {:on-mouse-down on-mouse-down 82 | :on-mouse-up on-mouse-up 83 | :width width 84 | :height height 85 | :x x 86 | :y y 87 | :fill fill-color 88 | :stroke-width stroke-width 89 | :stroke stroke-color} 90 | children)) 91 | 92 | (defui circle [{:keys [x y width height fill-color stroke-width stroke-color on-mouse-down]}] 93 | ($ :ellipse 94 | {:on-mouse-down on-mouse-down 95 | :cx (+ x (/ width 2)) 96 | :cy (+ y (/ height 2)) 97 | :rx (/ width 2) 98 | :ry (/ height 2) 99 | :fill fill-color 100 | :stroke-width stroke-width 101 | :stroke stroke-color})) 102 | 103 | (defui text [{:keys [x y width height fill-color stroke-width stroke-color 104 | value font-size font-family font-style 105 | on-mouse-down]}] 106 | ($ :text 107 | {:on-mouse-down on-mouse-down 108 | :x x 109 | :y y 110 | :font-family font-family 111 | :font-size font-size 112 | :font-style font-style} 113 | value)) 114 | 115 | (defn map-object [object size] 116 | (-> object 117 | (update :x * size) 118 | (update :y * size) 119 | (update :width * size) 120 | (update :height * size))) 121 | 122 | (defui ^:memo objects-layer [{:keys [objects size on-select]}] 123 | (for [{:keys [id] :as object} objects] 124 | (let [idx (.indexOf objects object) 125 | object (-> (map-object object size) 126 | (assoc :key id :on-mouse-down #(on-select idx)))] 127 | (case (:type object) 128 | :rect ($ rect object) 129 | :circle ($ circle object) 130 | :text ($ text object))))) 131 | 132 | (defui ^:memo edit-layer [{:keys [mx my on-object-changed on-select idx selected size]}] 133 | (let [[active? set-active] (uix/use-state false) 134 | selected? (some? selected) 135 | on-move (uix/use-callback 136 | (fn [x y] 137 | (on-object-changed idx (assoc selected :x x :y y))) 138 | [idx selected on-object-changed])] 139 | 140 | (uix/use-effect 141 | #(when active? 142 | (on-move mx my)) 143 | [selected? active? mx my on-move]) 144 | 145 | (uix/use-effect 146 | #(when selected? 147 | (set-active true)) 148 | [selected?]) 149 | 150 | (when selected 151 | ($ rect 152 | (-> (map-object selected size) 153 | (assoc 154 | :on-mouse-down #(set-active true) 155 | :on-mouse-up #(set-active false) 156 | :stroke-width 1 157 | :stroke-color "#0000ff" 158 | :fill-color :transparent)))))) 159 | 160 | (defui ^:memo background-layer [{:keys [width height on-mouse-down]}] 161 | ($ rect 162 | {:on-mouse-down #(on-mouse-down) 163 | :x 0 164 | :y 0 165 | :width width 166 | :height height 167 | :fill-color :transparent 168 | :stroke-color :none})) 169 | 170 | (defui canvas [{:keys [state on-object-changed on-object-select]}] 171 | (let [{:keys [grid? canvas]} state 172 | [[width height] set-size] (uix/use-state [0 0]) 173 | [[ox oy] set-offset] (uix/use-state [0 0]) 174 | [[mx my] set-mouse] (uix/use-state [0 0]) 175 | ref (uix/use-ref) 176 | size 8 177 | mx (quot (- mx ox) size) 178 | my (quot (- my oy) size)] 179 | (uix/use-effect 180 | (fn [] 181 | (set-offset [(.-offsetLeft @ref) (.-offsetTop @ref)]) 182 | (set-size [(.-width js/screen) (.-height js/screen)])) 183 | []) 184 | ($ :div {:ref ref 185 | :on-mouse-move (fn [^js e] 186 | (set-mouse [(.-clientX e) (.-clientY e)])) 187 | :style {:flex 1 188 | :position :relative 189 | :background-color "#ebeff0"}} 190 | ($ :svg {:style {:width width 191 | :height height 192 | :position :absolute 193 | :left 0 194 | :top 0} 195 | :view-box (str "0 0 " width " " height)} 196 | (when grid? 197 | ($ :<> 198 | ($ canvas-grid {:width width :height height :size size :color "#c1cdd0"}) 199 | ($ cursor {:r 2 200 | :color "#4f7f8b" 201 | :mx (* size mx) 202 | :my (* size my)}))) 203 | ($ background-layer 204 | {:width width 205 | :height height 206 | :on-mouse-down on-object-select}) 207 | ($ objects-layer 208 | {:objects (:objects canvas) 209 | :size size 210 | :on-select on-object-select}) 211 | ($ edit-layer 212 | {:size size 213 | :on-select on-object-select 214 | :on-object-changed on-object-changed 215 | :mx mx 216 | :my my 217 | :idx (:selected canvas) 218 | :selected (when (:selected canvas) 219 | (nth (:objects canvas) (:selected canvas)))}))))) 220 | 221 | (def default-styles 222 | {:x 32 223 | :y 32 224 | :width 12 225 | :height 12 226 | :stroke-width 2 227 | :stroke-color "#ff0000" 228 | :fill-color "#00ff00"}) 229 | 230 | (defui app [] 231 | (let [[state set-state] (uix/use-state {:grid? true 232 | :canvas {:selected nil 233 | :objects []}}) 234 | on-add-shape (fn [shape] 235 | (let [id (random-uuid)] 236 | (set-state 237 | (->> (case shape 238 | :rect (merge default-styles {:type :rect :id id}) 239 | :circle (merge default-styles {:type :circle :id id}) 240 | :text (merge default-styles {:type :text :id id 241 | :value "text" :font-family "Inter" 242 | :font-size 32 :font-style :normal})) 243 | (update-in state [:canvas :objects] conj))))) 244 | on-object-select (fn 245 | ([] 246 | (set-state (assoc-in state [:canvas :selected] nil))) 247 | ([idx] 248 | (set-state (assoc-in state [:canvas :selected] idx)))) 249 | on-object-changed (uix/use-callback 250 | (fn [idx object] 251 | (set-state 252 | #(assoc-in % [:canvas :objects idx] object))) 253 | [])] 254 | ($ :div {:style {:font-family "Inter" 255 | :font-size 14 256 | :display :flex 257 | :flex-direction :column 258 | :width "100vw" 259 | :height "100vh"}} 260 | ($ toolbar {:state state :set-state set-state :on-add-shape on-add-shape}) 261 | ($ canvas {:state state 262 | :on-object-select on-object-select 263 | :on-object-changed on-object-changed})))) 264 | 265 | ;; init app 266 | (defonce -init 267 | (let [root (uix.dom/create-root (js/document.getElementById "root"))] 268 | (uix.dom/render-root ($ app) root) 269 | nil)) 270 | 271 | (cljs-react-devtools.core/init! 272 | {:root (js/document.getElementById "root") 273 | :shortcut "Control-Shift-Meta-R"}) 274 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cljs-react-devtools", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0", 10 | "react-refresh": "^0.14.0" 11 | } 12 | }, 13 | "node_modules/js-tokens": { 14 | "version": "4.0.0", 15 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 16 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 17 | "dev": true, 18 | "license": "MIT" 19 | }, 20 | "node_modules/loose-envify": { 21 | "version": "1.4.0", 22 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 23 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 24 | "dev": true, 25 | "license": "MIT", 26 | "dependencies": { 27 | "js-tokens": "^3.0.0 || ^4.0.0" 28 | }, 29 | "bin": { 30 | "loose-envify": "cli.js" 31 | } 32 | }, 33 | "node_modules/react": { 34 | "version": "18.2.0", 35 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", 36 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", 37 | "dev": true, 38 | "license": "MIT", 39 | "dependencies": { 40 | "loose-envify": "^1.1.0" 41 | }, 42 | "engines": { 43 | "node": ">=0.10.0" 44 | } 45 | }, 46 | "node_modules/react-dom": { 47 | "version": "18.2.0", 48 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", 49 | "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", 50 | "dev": true, 51 | "license": "MIT", 52 | "dependencies": { 53 | "loose-envify": "^1.1.0", 54 | "scheduler": "^0.23.0" 55 | }, 56 | "peerDependencies": { 57 | "react": "^18.2.0" 58 | } 59 | }, 60 | "node_modules/react-refresh": { 61 | "version": "0.14.0", 62 | "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", 63 | "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", 64 | "dev": true, 65 | "license": "MIT", 66 | "engines": { 67 | "node": ">=0.10.0" 68 | } 69 | }, 70 | "node_modules/scheduler": { 71 | "version": "0.23.0", 72 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", 73 | "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", 74 | "dev": true, 75 | "license": "MIT", 76 | "dependencies": { 77 | "loose-envify": "^1.1.0" 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "react": "^18.2.0", 4 | "react-dom": "^18.2.0", 5 | "react-refresh": "^0.14.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.github.roman01la 6 | cljs-react-devtools 7 | 0.1.0 8 | cljs-react-devtools 9 | 10 | 11 | Eclipse Public License - Version 2.0 12 | https://www.eclipse.org/legal/epl-2.0/ 13 | 14 | 15 | 16 | scm:git:git@github.com:roman01la/cljs-react-devtools.git 17 | scm:git:git@github.com:roman01la/cljs-react-devtools.git 18 | 62f169e0112adbdad6c34b6b1b913fd024ed9957 19 | https://github.com/roman01la/cljs-react-devtools 20 | 21 | 22 | 23 | com.pitch 24 | uix.core 25 | 1.2.0 26 | 27 | 28 | com.pitch 29 | uix.dom 30 | 1.2.0 31 | 32 | 33 | 34 | src 35 | 36 | 37 | -------------------------------------------------------------------------------- /release.edn: -------------------------------------------------------------------------------- 1 | {:group-id "com.github.roman01la" 2 | :artifact-id "cljs-react-devtools" 3 | :version "0.2.0" 4 | :scm-url "https://github.com/roman01la/cljs-react-devtools"} 5 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:deps true 2 | :dev-http {3000 "./"} 3 | :builds 4 | {:examples {:target :browser 5 | :output-dir "out" 6 | :asset-path "/out" 7 | :modules {:main {:entries [example.core]}}}}} 8 | -------------------------------------------------------------------------------- /src/cljs_react_devtools/core.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-react-devtools.core 2 | (:require [clojure.string :as str] 3 | [uix.core :as uix :refer [$ defui]] 4 | [uix.dom] 5 | [goog.functions :as fns] 6 | [goog.string :as gstr] 7 | [goog.async.nextTick])) 8 | 9 | (defonce popout-window (atom nil)) 10 | 11 | (def color-themes 12 | {:light 13 | {:highlight-text "#8835ff" 14 | :highlight-bg "#eadcff" 15 | :icon-chevron "#b78ff1" 16 | :data-view-primitive "#216aef" 17 | :data-view-string "#388e28" 18 | :data-view-keyword "#c94d22" 19 | :resize-handle "#fcf8ff" 20 | :tool-bar-text "#a769ff" 21 | :devtools-bg "#fefdff" 22 | :devtools-text "#51485f" 23 | :tree-view-bg "#fbfafd"} 24 | :dark 25 | {:highlight-text "#ebe0fb" 26 | :highlight-bg "#4d27f9" 27 | :icon-chevron "#ede2fd" 28 | :data-view-primitive "#7be0ff" 29 | :data-view-string "#5de144" 30 | :data-view-keyword "#fac543" 31 | :resize-handle "#3e2e44" 32 | :tool-bar-text "#ebe0fc" 33 | :devtools-bg "#302b32" 34 | :devtools-text "#ede2ff" 35 | :tree-view-bg "#2d292d"}}) 36 | 37 | (def theme-ctx (uix/create-context (:light color-themes))) 38 | 39 | (defn node->siblings [^js node] 40 | (when node 41 | (lazy-seq 42 | (cons node (when (.-sibling node) 43 | (node->siblings (.-sibling node))))))) 44 | 45 | (declare tree-view) 46 | 47 | (defn fiber->child [fiber] 48 | (or (.-child fiber) (some-> fiber .-alternate .-child))) 49 | 50 | (defn render-children [^js node state set-state] 51 | (let [child (fiber->child node)] 52 | (when child 53 | (for [node (node->siblings child)] 54 | ($ tree-view {:node node 55 | :state state 56 | :set-state set-state 57 | :key (.-index node)}))))) 58 | 59 | (defn reagent-node? [^js node] 60 | (let [el-type (.-elementType node)] 61 | (and (fn? el-type) 62 | (.-cljs$lang$type el-type)))) 63 | 64 | (defn uix-node? [^js node] 65 | (let [el-type (.-elementType node)] 66 | (and (fn? el-type) 67 | (.-uix-component? el-type)))) 68 | 69 | (defn memo-node? [node] 70 | (let [el-type (.-elementType node)] 71 | (and el-type 72 | (= js/Object (.-constructor el-type)) 73 | (= (aget el-type "$$typeof") (.for js/Symbol "react.memo"))))) 74 | 75 | (defn demunge-name [name] 76 | (let [s (str/split (demunge-str name) #"\.")] 77 | (str (str/join "." (butlast s)) "/" (last s)))) 78 | 79 | (defn demunge-fn-name [name] 80 | (let [s (str/split (demunge-str name) #"/")] 81 | (str (str/join "." (butlast s)) "/" (last s)))) 82 | 83 | (defn node->name [^js node & {:keys [lib? file?]}] 84 | (let [el-type (.-elementType node) 85 | memo? (memo-node? (.-return node))] 86 | ($ :div {:style {:display :flex 87 | :justify-content :space-between}} 88 | ($ :span 89 | (cond 90 | (string? el-type) el-type 91 | 92 | (reagent-node? node) 93 | (demunge-name (.-displayName el-type)) 94 | 95 | (fn? el-type) (or (.-displayName el-type) 96 | (demunge-fn-name (.-name el-type)))) 97 | (when memo? 98 | " [memo]") 99 | (when lib? 100 | (cond 101 | (reagent-node? node) " [reagent]" 102 | (uix-node? node) " [uix]" 103 | (fn? el-type) " [react]"))) 104 | ($ :span 105 | (when (and file? 106 | (fn? el-type)) 107 | (when-let [o (.. node -type -_source)] 108 | (str (.-file o) ":" (.-lineNumber o)))))))) 109 | 110 | (defui button [props] 111 | ($ :button 112 | (update props :style 113 | #(merge {:background :transparent 114 | :border :none 115 | :cursor :pointer 116 | :padding 0 117 | :opacity (when (:disabled props) 0.5)} 118 | (filter (comp some? val) %))))) 119 | 120 | (def icon-chevron-down 121 | ($ :svg {:xmlns "http://www.w3.org/2000/svg" :fill "none" :viewBox "0 0 24 24" :stroke-width "4" :stroke "currentColor" 122 | :width 8 :height 8} 123 | ($ :path {:stroke-linecap "round" :stroke-linejoin "round" :d "M19.5 8.25l-7.5 7.5-7.5-7.5"}))) 124 | 125 | (def icon-cursor-rays 126 | ($ :svg {:xmlns "http://www.w3.org/2000/svg" :fill "none" :viewBox "0 0 24 24" :stroke-width "2" :stroke "currentColor" 127 | :width 18 :height 18} 128 | ($ :path {:stroke-linecap "round" :stroke-linejoin "round" :d "M15.042 21.672L13.684 16.6m0 0l-2.51 2.225.569-9.47 5.227 7.917-3.286-.672zM12 2.25V4.5m5.834.166l-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243l-1.59-1.59"}))) 129 | 130 | (def icon-window 131 | ($ :svg {:xmlns "http://www.w3.org/2000/svg" :fill "none" :viewBox "0 0 24 24" :stroke-width "2" :stroke "currentColor" 132 | :width 18 :height 18} 133 | ($ :path {:stroke-linecap "round" :stroke-linejoin "round" :d "M3 8.25V18a2.25 2.25 0 002.25 2.25h13.5A2.25 2.25 0 0021 18V8.25m-18 0V6a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 6v2.25m-18 0h18M5.25 6h.008v.008H5.25V6zM7.5 6h.008v.008H7.5V6zm2.25 0h.008v.008H9.75V6z"}))) 134 | 135 | (def icon-dock-bottom 136 | ($ :svg {:width 18 :height 18 :viewBox "0 0 24 24" :fill "none" :xmlns "http://www.w3.org/2000/svg"} 137 | ($ :path {:d "M3 14H21M4.125 19.5H19.875C20.496 19.5 21 18.996 21 18.375V5.625C21 5.004 20.496 4.5 19.875 4.5H4.125C3.504 4.5 3 5.004 3 5.625V18.375C3 18.996 3.504 19.5 4.125 19.5Z" :stroke "currentColor" :stroke-width "2" :stroke-linecap "round" :stroke-linejoin "round"}) 138 | ($ :path {:d "M3.375 18L3.375 14.5L20.625 14.5L20.625 18C20.625 18.621 20.121 19.125 19.5 19.125L4.5 19.125C3.879 19.125 3.375 18.621 3.375 18Z" :fill "currentColor"}))) 139 | 140 | (def icon-dock-right 141 | ($ :svg {:width 19 :height 19 :viewBox "0 0 24 24" :fill "none" :xmlns "http://www.w3.org/2000/svg"} 142 | ($ :path {:d "M4.125 19.5H19.875C20.496 19.5 21 18.996 21 18.375V5.625C21 5.004 20.496 4.5 19.875 4.5H4.125C3.504 4.5 3 5.004 3 5.625V18.375C3 18.996 3.504 19.5 4.125 19.5Z" :stroke "currentColor" :stroke-width "2" :stroke-linecap "round" :stroke-linejoin "round"}) 143 | ($ :path {:d "M19.875 19.5H15V4.5H19.875C20.496 4.5 21 5.004 21 5.625V18.375C21 18.996 20.496 19.5 19.875 19.5Z" :fill "currentColor"}))) 144 | 145 | (def icon-dock-left 146 | ($ :svg {:width 19 :height 19 :viewBox "0 0 24 24" :fill "none" :xmlns "http://www.w3.org/2000/svg"} 147 | ($ :path {:d "M9 4.5V19.5M4.125 19.5H19.875C20.496 19.5 21 18.996 21 18.375V5.625C21 5.004 20.496 4.5 19.875 4.5H4.125C3.504 4.5 3 5.004 3 5.625V18.375C3 18.996 3.504 19.5 4.125 19.5Z" :stroke "currentColor" :stroke-width "2" :stroke-linecap "round" :stroke-linejoin "round"}) 148 | ($ :path {:d "M4.125 19.5H9V4.5H4.125C3.504 4.5 3 5.004 3 5.625V18.375C3 18.996 3.504 19.5 4.125 19.5Z" :fill "currentColor"}))) 149 | 150 | (def icon-arrow-path 151 | ($ :svg {:xmlns "http://www.w3.org/2000/svg" :fill "none" :viewBox "0 0 24 24" :stroke-width "2" :stroke "currentColor" 152 | :width 14 :height 14} 153 | ($ :path {:stroke-linecap "round" :stroke-linejoin "round" :d "M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"}))) 154 | 155 | (def preview-ctx (uix/create-context)) 156 | 157 | (defn has-non-primitive-children? [node] 158 | (let [children (node->siblings (fiber->child node))] 159 | (some #(nil? (.-elementType %)) children))) 160 | 161 | 162 | (defui tree-view [{:keys [^js node state set-state]}] 163 | (let [memo? (memo-node? node) 164 | node (if memo? (fiber->child node) node) 165 | el-type (.-elementType node) 166 | [closed? set-closed] (uix/use-state false) 167 | {:keys [hide-dom? selected]} state 168 | selected? (= selected node) 169 | set-preview-node (uix/use-context preview-ctx) 170 | colors (uix/use-context theme-ctx)] 171 | (cond 172 | (or (nil? el-type) 173 | (and (string? el-type) hide-dom?)) 174 | (render-children node state set-state) 175 | 176 | :else 177 | ($ :div {:style {:margin "4px 0 4px 8px"}} 178 | (when-not (has-non-primitive-children? node) 179 | ($ :span {:style {:margin "0 4px 0 0" 180 | :color (:icon-chevron colors) 181 | :display :inline-block 182 | :transition "transform 100ms ease-in-out" 183 | :transform (if closed? "rotate(-90deg)" "rotate(0deg)")}} 184 | icon-chevron-down)) 185 | ($ button 186 | {:style {:color (:highlight-text colors) 187 | :user-select :none 188 | :background (when selected? (:highlight-bg colors))} 189 | :on-mouse-enter #(set-preview-node node) 190 | :on-mouse-leave #(set-preview-node nil) 191 | :on-click #(do (set-state (assoc state :selected node)) 192 | (when selected? 193 | (set-closed not)))} 194 | (node->name node)) 195 | (when-not closed? 196 | (render-children node state set-state)))))) 197 | 198 | (def primitive-value? 199 | (some-fn number? nil? boolean? string? uuid? keyword? fn?)) 200 | 201 | (declare data-view closed-data-view) 202 | 203 | (defui data-view-map 204 | [{:keys [data tag entries-fn key-fn open? set-open closing set-open-parent] 205 | :or {entries-fn seq 206 | key-fn identity}}] 207 | (let [entries (entries-fn data)] 208 | (when (seq entries) 209 | (map-indexed 210 | (fn [idx [key val]] 211 | (let [last-idx? (= idx (dec (count entries))) 212 | closing (when last-idx? 213 | ($ :<> "}" closing))] 214 | ($ :div 215 | {:key key 216 | :style {:display :flex 217 | :margin (when-not last-idx? "0 0 4px 0")}} 218 | (when (zero? idx) 219 | ($ :span (str 220 | (when tag 221 | (str "#" tag " ")) 222 | "{"))) 223 | ($ data-view 224 | {:data (key-fn key) 225 | :key? true 226 | :on-click (if (primitive-value? val) 227 | #(set-open-parent false) 228 | #(set-open not)) 229 | :style {:margin-right 8 230 | :margin-left (when (pos? idx) 231 | (if tag 232 | (* 7.5 (+ 3 (count tag))) 233 | 6))}}) 234 | (if open? 235 | ($ data-view {:data val :closing closing}) 236 | ($ :<> 237 | ($ closed-data-view {:data val :set-open set-open}) 238 | closing))))) 239 | entries)))) 240 | 241 | (defui data-view-seq 242 | [{:keys [data tag closing open? set-open] 243 | [open close] :brackets}] 244 | (if (empty? data) 245 | ($ :<> open close closing) 246 | ($ :div 247 | {:style {:display :flex}} 248 | (map-indexed 249 | (fn [idx val] 250 | (let [last-idx? (= idx (dec (count data))) 251 | closing (when last-idx? 252 | ($ :<> close closing))] 253 | ($ :div 254 | {:key idx 255 | :style {:display :flex}} 256 | (when (zero? idx) 257 | ($ :span 258 | (str (when tag (str "#" tag " ")) open))) 259 | (if (= open? idx) 260 | ($ data-view 261 | {:data val 262 | :style (when (zero? idx) {:margin 0}) 263 | :set-open-parent set-open 264 | :closing closing}) 265 | ($ :<> 266 | ($ closed-data-view {:data val 267 | :set-open set-open 268 | :idx idx 269 | :style (when-not last-idx? {:margin-right 8})}) 270 | closing))))) 271 | data)))) 272 | 273 | (defonce hint-ctx (uix/create-context)) 274 | 275 | (defn- fmt-fn [data] 276 | (str "fn<" 277 | (cond 278 | (str/blank? (.-name data)) 279 | "anonymous" 280 | 281 | (str/includes? (.-name data) "$") 282 | (let [parts (-> (.-name data) 283 | demunge 284 | (str/split "/")) 285 | name (last parts) 286 | ns (str/join "." (butlast parts))] 287 | (str ns "/" name)) 288 | 289 | :else (.-name data)) 290 | ">")) 291 | 292 | (defui data-view-primitive [{:keys [data data-raw closing color]}] 293 | (let [data (or data-raw (pr-str data))] 294 | ($ :<> 295 | ($ :span {:title data 296 | :style {:color color 297 | :max-width 180 298 | :display :inline-block 299 | :overflow :hidden 300 | :text-overflow :ellipsis}} 301 | data) 302 | closing))) 303 | 304 | (defn atomic-data-view [{:keys [data colors]}] 305 | (cond 306 | (number? data) ($ data-view-primitive {:data data :color (:data-view-primitive colors)}) 307 | (nil? data) ($ data-view-primitive {:data data :color (:data-view-primitive colors)}) 308 | (boolean? data) ($ data-view-primitive {:data data :color (:data-view-primitive colors)}) 309 | (string? data) ($ data-view-primitive {:data data :color (:data-view-string colors)}) 310 | (uuid? data) ($ data-view-primitive {:data data :color (:data-view-string colors)}) 311 | (keyword? data) ($ data-view-primitive {:data data :color (:data-view-keyword colors)}) 312 | (fn? data) ($ data-view-primitive {:data-raw (fmt-fn data) :color (:data-view-primitive colors)}))) 313 | 314 | (defn- constructor [o] 315 | (some-> o .-constructor)) 316 | 317 | (def atomic? (some-fn number? nil? boolean? string? uuid? keyword? fn?)) 318 | 319 | (defui closed-data-view 320 | [{:keys [data style key? set-open idx]}] 321 | (let [set-active (uix/use-context hint-ctx) 322 | colors (uix/use-context theme-ctx)] 323 | ($ :pre 324 | {:style (merge {:margin 0 325 | :cursor :pointer 326 | :font-size "12px"} 327 | style) 328 | :on-mouse-enter #(set-active true) 329 | :on-mouse-leave #(set-active false) 330 | :on-click (fn [event] 331 | (when-not (atomic? data) 332 | (set-open #(if % false (or idx true)))) 333 | (when-not key? 334 | (.stopPropagation event))) 335 | :on-double-click #(when-not key? 336 | (js/console.dir data))} 337 | (cond 338 | (map? data) (if (seq data) "{...}" "{}") 339 | (vector? data) (if (seq data) "[...]" "[]") 340 | (set? data) (if (seq data) "#{...}" "#{}") 341 | (seq? data) (if (seq data) "(...)" "()") 342 | (= js/Object (constructor data)) (if (pos? (.-length (js/Object.keys data))) 343 | "#js {...}" 344 | "#js {}") 345 | (= js/Array (constructor data)) (if (pos? (.-length data)) 346 | "#js [...]" 347 | "#js []") 348 | :else (or (atomic-data-view {:data data :colors colors}) 349 | "..."))))) 350 | 351 | (defui ^:memo data-view 352 | [{:keys [data style key? on-click open? closing set-open-parent]}] 353 | (let [set-active (uix/use-context hint-ctx) 354 | colors (uix/use-context theme-ctx) 355 | [open? set-open] (uix/use-state open?)] 356 | ($ :pre 357 | {:style (merge {:margin 0 358 | :cursor :pointer 359 | :font-size "12px"} 360 | style) 361 | :on-mouse-enter #(set-active true) 362 | :on-mouse-leave #(set-active false) 363 | :on-click (fn [e] 364 | (when on-click (on-click)) 365 | (when-not key? 366 | (.stopPropagation e))) 367 | :on-double-click #(when-not key? 368 | (js/console.dir data))} 369 | (cond 370 | (map? data) ($ data-view-map {:data data :open? open? :set-open set-open :closing closing :set-open-parent set-open-parent}) 371 | (vector? data) ($ data-view-seq {:data data :brackets ["[" "]"] :open? open? :set-open set-open :closing closing}) 372 | (set? data) ($ data-view-seq {:data data :brackets ["#{" "}"] :open? open? :set-open set-open :closing closing}) 373 | (seq? data) ($ data-view-seq {:data data :brackets ["(" ")"] :open? open? :set-open set-open :closing closing}) 374 | (= js/Object (constructor data)) ($ data-view-map 375 | {:data data 376 | :tag "js" 377 | :entries-fn js/Object.entries 378 | :key-fn keyword 379 | :open? open? 380 | :set-open set-open 381 | :closing closing}) 382 | (= js/Array (constructor data)) ($ data-view-seq {:data data :tag "js" :brackets ["[" "]"] :open? open? :set-open set-open :closing closing}) 383 | :else (or (atomic-data-view {:data data :colors colors}) 384 | ($ :<> (pr-str data) closing)))))) 385 | 386 | (defn node->props [^js node] 387 | (let [el-type (.-elementType node)] 388 | (cond 389 | (string? el-type) 390 | ($ data-view {:data (.. node -memoizedProps) 391 | :style {:margin 0 :overflow-x :auto}}) 392 | 393 | (reagent-node? node) 394 | ($ data-view {:data (let [props (rest (some-> node .-memoizedProps .-argv))] 395 | (when (seq props) (vec props))) 396 | :style {:margin 0 :overflow-x :auto}}) 397 | 398 | (uix-node? node) 399 | ($ data-view {:data (.. node -memoizedProps -argv) 400 | :style {:margin 0 :overflow-x :auto}})))) 401 | 402 | (defn node->hooks [^js mem-state] 403 | (when (and mem-state (some? (.-memoizedState mem-state))) 404 | (lazy-seq 405 | (cons (.-memoizedState mem-state) 406 | (when (.-next mem-state) 407 | (node->hooks (.-next mem-state))))))) 408 | 409 | (defn node->captured-state [node] 410 | (some-> node .-stateNode ^js (.-cljsRatom) .-captured)) 411 | 412 | (defn- rf-sub [^js node] 413 | (.-__devtools-label node)) 414 | 415 | (defn node->rf-subs [^js node] 416 | (->> (node->captured-state node) 417 | (keep #(when-let [label (rf-sub %)] 418 | [($ data-view {:data label :style {:margin 0 :overflow-x :auto}}) 419 | %])))) 420 | 421 | (defn node->reactions [^js node] 422 | (->> (node->captured-state node) 423 | (keep #(when (and (not (some-> ^js % .-state .-generation)) 424 | (not (rf-sub %))) 425 | ["ratom" %])))) 426 | 427 | (defn camel-case->kebab-case [s] 428 | (->> (str/split s #"(?<=[a-z])(?=[A-Z])") 429 | (map str/lower-case) 430 | (str/join "-"))) 431 | 432 | (defui section-header [{:keys [children]}] 433 | (let [colors (uix/use-context theme-ctx)] 434 | ($ :div 435 | {:style {:color (:highlight-text colors) 436 | :background (:highlight-bg colors) 437 | :margin "0 0 4px 0" 438 | :padding "0 4px"}} 439 | children))) 440 | 441 | (defui editable-ref [{:keys [ref set-hint label type]}] 442 | (let [[active? set-active] (uix/use-state false) 443 | value (.-state ref)] 444 | ($ :div 445 | {:on-double-click #(set-active true) 446 | :on-mouse-enter (when-not active? 447 | #(do (set-hint (str "double click on the value to update the " label)) 448 | (.stopPropagation %))) 449 | :on-mouse-leave #(set-hint nil)} 450 | (if active? 451 | ($ :input 452 | {:default-value value 453 | :type (if (number? value) :number :text) 454 | :auto-focus true 455 | :on-blur #(set-active false) 456 | :on-key-down (fn [^js e] 457 | (when (= (.-key e) "Enter") 458 | (when (= :sub type) 459 | (set! (.-on-set ^js ref) identity)) 460 | (if (number? value) 461 | (reset! ref (js/parseFloat (.. e -target -value) 10)) 462 | (reset! ref (.. e -target -value))) 463 | (when (= :sub type) 464 | (set! (.-on-set ^js ref) js/undefined)) 465 | (set-active false)))}) 466 | ($ data-view 467 | {:data value 468 | :style {:margin 0 :overflow-x :auto}}))))) 469 | 470 | (defui reactions-view [{:keys [node set-hint]}] 471 | (let [reactions (node->reactions node) 472 | subs (node->rf-subs node)] 473 | ($ :<> 474 | (when (seq reactions) 475 | ($ :div {:style {:margin "8px 0 0 0"}} 476 | ($ section-header "reactions") 477 | (map-indexed 478 | (fn [idx [type reaction]] 479 | ($ :div 480 | {:key idx 481 | :style {:display :flex :justify-content :space-between}} 482 | ($ :div {:style {:display :flex :gap 8}} 483 | ($ :span type) 484 | ($ editable-ref {:ref reaction :set-hint set-hint :label "reaction"})) 485 | #_($ button 486 | {:style {:color (:tool-bar-text colors) 487 | :margin "0 0 0 8px"} 488 | :on-mouse-enter #(set-hint "restore to initial value") 489 | :on-mouse-leave #(set-hint nil) 490 | :title "restore to initial value" 491 | :on-click #(reset! reaction "INITIAL")} 492 | icon-arrow-path))) 493 | reactions))) 494 | (when (seq subs) 495 | ($ :div {:style {:margin "8px 0 0 0"}} 496 | ($ section-header "re-frame subscriptions") 497 | (map-indexed 498 | (fn [idx [type sub]] 499 | ($ :div 500 | {:key idx 501 | :style {:display :flex :justify-content :space-between}} 502 | ($ :div {:style {:display :flex :gap 8}} 503 | ($ :span type) 504 | ($ editable-ref {:ref sub :set-hint set-hint :label "subscription" :type :sub})) 505 | #_($ button 506 | {:style {:color (:tool-bar-text colors) 507 | :margin "0 0 0 8px"} 508 | :on-mouse-enter #(set-hint "restore to initial value") 509 | :on-mouse-leave #(set-hint nil) 510 | :title "restore to initial value" 511 | :on-click #(do 512 | (set! (.-on-set ^js sub) identity) 513 | (reset! sub "INITIAL") 514 | (set! (.-on-set ^js sub) js/undefined))} 515 | icon-arrow-path))) 516 | subs)))))) 517 | 518 | (defui hooks-view [{:keys [node]}] 519 | (let [hooks (node->hooks (.-memoizedState node)) 520 | colors (uix/use-context theme-ctx)] 521 | (when (seq hooks) 522 | ($ :div {:style {:margin "8px 0 0 0"}} 523 | ($ section-header "hooks") 524 | (keep-indexed 525 | (fn [idx hook] 526 | (when-not (and (js/Array.isArray hook) 527 | (js/Array.isArray (aget hook 1)) 528 | (fn? (aget (aget hook 1) 0)) 529 | (= "bound dispatchSetState" (.-name (aget (aget hook 1) 0)))) 530 | (let [name (camel-case->kebab-case (aget (.-_debugHookTypes node) idx))] 531 | ($ :div {:key idx 532 | :style {:margin "8px 0"}} 533 | ($ :span {:style {:color (:highlight-text colors)}} 534 | name) 535 | (case name 536 | "use-callback" 537 | ($ :<> 538 | ($ :div {:style {:display :flex :gap 8}} 539 | ($ :span "callback:") 540 | ($ data-view {:data (aget hook 0) :style {:margin 0 :overflow-x :auto}})) 541 | ($ :div {:style {:display :flex :gap 8}} 542 | ($ :span "deps:") 543 | ($ data-view {:data (vec (aget hook 1)) :style {:margin 0 :overflow-x :auto}}))) 544 | 545 | "use-effect" 546 | ($ :<> 547 | ($ :div {:style {:display :flex :gap 8}} 548 | ($ :span "effect:") 549 | ($ data-view {:data (.-create hook) :style {:margin 0 :overflow-x :auto}})) 550 | ($ :div {:style {:display :flex :gap 8}} 551 | ($ :span "deps:") 552 | ($ data-view {:data (vec (.-deps hook)) :style {:margin 0 :overflow-x :auto}}))) 553 | 554 | "use-ref" 555 | ($ data-view {:data (.. hook -current -current) :style {:margin 0 :overflow-x :auto}}) 556 | 557 | ($ data-view {:data hook :style {:margin 0 :overflow-x :auto}})))))) 558 | hooks))))) 559 | 560 | (uix/defhook use-resize-handler [{:keys [set-size dir max min location] 561 | :or {max 100 min 0}}] 562 | (let [[active? set-active] (uix/use-state false) 563 | ref (uix/use-ref)] 564 | (uix/use-effect 565 | (fn [] 566 | (when active? 567 | (let [move-handler (fn [^js e] 568 | (let [node @ref 569 | bb (.getBoundingClientRect node) 570 | v (* (/ 100 (if (= dir :vertical) js/window.innerHeight js/window.innerWidth)) 571 | (cond 572 | (= dir :vertical) 573 | (- (.-y bb) (.-y e)) 574 | 575 | (= location :left) 576 | (- (.-x e) (+ (.-x bb) (.-width bb))) 577 | 578 | :else (- (.-x bb) (.-x e))))] 579 | (set-size 580 | #(let [v (+ % v)] 581 | (if (>= max v min) 582 | v 583 | %))))) 584 | up-handler #(set-active false)] 585 | (.addEventListener js/document "mousemove" move-handler) 586 | (.addEventListener js/document "mouseup" up-handler) 587 | (fn [] 588 | (.removeEventListener js/document "mousemove" move-handler) 589 | (.removeEventListener js/document "mouseup" up-handler))))) 590 | [active? set-size dir max min location]) 591 | [ref set-active])) 592 | 593 | (defui resize-handle [{:keys [set-size dir max min location] :as props}] 594 | (let [[ref set-active] (use-resize-handler props) 595 | colors (uix/use-context theme-ctx)] 596 | ($ :div {:ref ref 597 | :on-mouse-down #(set-active true) 598 | :style {:height (if (= dir :vertical) "4px" "100%") 599 | :width (if (= dir :vertical) "100%" "4px") 600 | :position :absolute 601 | :left (when (not= location :left) 0) 602 | :right (when (= location :left) 0) 603 | :top 0 604 | :background (:resize-handle colors) 605 | :cursor (if (= dir :vertical) :ns-resize :ew-resize)}}))) 606 | 607 | (uix/defhook use-size [v k] 608 | (let [[size set-size] (uix/use-state #(if-let [n (js/localStorage.getItem (str k))] 609 | (let [n (js/parseFloat n 10)] 610 | (if (js/Number.isNaN n) 611 | v 612 | n)) 613 | v)) 614 | f (uix/use-memo (fn [] 615 | (fns/debounce #(js/localStorage.setItem (str k) %) 100)) 616 | [k])] 617 | (uix/use-effect 618 | #(f size) 619 | [size f]) 620 | [size set-size])) 621 | 622 | (defui inspector [{:keys [state set-hint location]}] 623 | (let [{:keys [selected]} state 624 | [size set-size] (use-size 35 :cljs-devtools-inspector/ui-size) 625 | [active? set-active] (uix/use-state false) 626 | horizontal? (contains? #{:window :bottom} location) 627 | colors (uix/use-context theme-ctx)] 628 | (uix/use-effect 629 | (fn [] 630 | (if active? 631 | (set-hint "double click on the value to log it to console") 632 | (set-hint ""))) 633 | [active? set-hint]) 634 | ($ :div 635 | {:style {:box-sizing :border-box 636 | :width (if horizontal? (str size "%") "100%") 637 | :height (when-not horizontal? (str size "vh")) 638 | :border-left (when horizontal? "1px solid #8632ff75") 639 | :border-top (when-not horizontal? "1px solid #8632ff75") 640 | :padding "0 8px 32px" 641 | :display :flex 642 | :flex-direction :column 643 | :position :relative}} 644 | ($ resize-handle {:set-size set-size 645 | :dir (if horizontal? :horizontal :vertical) 646 | :max 50 647 | :min 20}) 648 | (when selected 649 | ($ (.-Provider hint-ctx) {:value set-active} 650 | ($ :<> 651 | ($ button 652 | {:on-click #(js/console.log (.-elementType selected)) 653 | :on-mouse-enter #(set-active true) 654 | :on-mouse-leave #(set-active false) 655 | :style {:margin "8px 0 0 0" 656 | :display :block 657 | :color (:highlight-text colors)}} 658 | (node->name selected :lib? true :file? true)) 659 | ($ :div {:style {:margin "8px 0 0 0" 660 | :overflow-y :auto 661 | :flex 1}} 662 | ($ section-header "props") 663 | (node->props selected) 664 | (when (reagent-node? selected) 665 | ($ reactions-view {:node selected :set-hint set-hint})) 666 | ($ hooks-view {:node selected})))))))) 667 | 668 | (def error-boundary 669 | (uix/create-error-boundary 670 | {:derive-error-state (fn [error] 671 | {:error error})} 672 | (fn [[{:keys [error]} set-state] {:keys [children]}] 673 | (if error 674 | ($ :div 675 | {:style {:background "#faf0ec" 676 | :color "#ec681f" 677 | :font-size "16px" 678 | :flex 1 679 | :display :flex 680 | :flex-direction :column 681 | :gap 16 682 | :justify-content :center 683 | :align-items :center}} 684 | ($ :div 685 | "Something went wrong") 686 | ($ :div 687 | (if (instance? js/Error error) 688 | (.-message error) 689 | error)) 690 | ($ :a 691 | {:href "https://github.com/roman01la/cljs-react-devtools" 692 | :target "blank_" 693 | :style {:background "#ff784b" 694 | :color "#faf0ec" 695 | :padding "8px 12px" 696 | :border-radius "3px"}} 697 | "report an issue")) 698 | children)))) 699 | 700 | (defonce window-settings (atom {:width 800 :height 400 :top 0 :left 0 701 | :location (let [v (js/localStorage.getItem ":cljs-devtools/window-location")] 702 | (if (str/blank? v) 703 | :bottom 704 | (keyword v)))})) 705 | (declare dock-devtools) 706 | 707 | (defn close-window [location] 708 | (if @popout-window 709 | (do 710 | (swap! window-settings assoc :location location) 711 | (.close @popout-window)) 712 | (dock-devtools :location location))) 713 | 714 | (defui toolbar 715 | [{:keys [state set-state hint set-hint 716 | set-inspecting inspecting? dock-devtools location]}] 717 | (let [{:keys [hide-dom?]} state 718 | colors (uix/use-context theme-ctx)] 719 | ($ :div 720 | {:style {:padding "4px 8px" 721 | :border-bottom "1px solid #8632ff75" 722 | :font-size "12px" 723 | :display :flex 724 | :justify-content :space-between 725 | :gap 32}} 726 | ($ :div 727 | {:on-mouse-enter #(set-hint "toggle DOM nodes in the tree view") 728 | :on-mouse-leave #(set-hint nil)} 729 | ($ :input#cljs-devtools_hide-mo-nodes 730 | {:type :checkbox 731 | :checked hide-dom? 732 | :on-change #(set-state (update state :hide-dom? not)) 733 | :style {:margin "0 4px 0 0"}}) 734 | ($ :label 735 | {:for "cljs-devtools_hide-mo-nodes"} 736 | "Hide DOM nodes")) 737 | ($ :div {:style {:display :flex 738 | :align-items :center}} 739 | ($ :div {:style {:color (:tool-bar-text colors) 740 | :opacity (if (str/blank? hint) 0 1) 741 | :transition "opacity 100ms ease-in-out"}} 742 | hint) 743 | ($ button 744 | {:style {:color (:tool-bar-text colors) 745 | :background (when inspecting? (:highlight-bg colors)) 746 | :margin "0 0 0 8px"} 747 | :on-mouse-enter #(set-hint "select an element to inspect") 748 | :on-mouse-leave #(set-hint nil) 749 | :title "Select an element to inspect" 750 | :on-click #(set-inspecting not)} 751 | icon-cursor-rays) 752 | (when (not= :window location) 753 | ($ button 754 | {:style {:color (:tool-bar-text colors) 755 | :margin "0 0 0 8px"} 756 | :on-mouse-enter #(set-hint "undock into separate window") 757 | :on-mouse-leave #(set-hint nil) 758 | :title "Undock into separate window" 759 | :on-click #(dock-devtools :location :window)} 760 | icon-window)) 761 | ($ button 762 | {:style {:color (:tool-bar-text colors) 763 | :margin "0 0 0 8px"} 764 | :on-mouse-enter #(set-hint "dock to bottom") 765 | :on-mouse-leave #(set-hint nil) 766 | :title "Dock to bottom" 767 | :disabled (= location :bottom) 768 | :on-click #(close-window :bottom)} 769 | icon-dock-bottom) 770 | ($ button 771 | {:style {:color (:tool-bar-text colors) 772 | :margin "0 0 0 8px"} 773 | :on-mouse-enter #(set-hint "dock to the left") 774 | :on-mouse-leave #(set-hint nil) 775 | :title "Dock to the left" 776 | :disabled (= location :left) 777 | :on-click #(close-window :left)} 778 | icon-dock-left) 779 | ($ button 780 | {:style {:color (:tool-bar-text colors) 781 | :margin "0 0 0 8px"} 782 | :on-mouse-enter #(set-hint "dock to the right") 783 | :on-mouse-leave #(set-hint nil) 784 | :title "Dock to the right" 785 | :disabled (= location :right) 786 | :on-click #(close-window :right)} 787 | icon-dock-right))))) 788 | 789 | (defn intersects? [[x y] rect] 790 | (and (<= (.-x rect) x (+ (.-x rect) (.-width rect))) 791 | (<= (.-y rect) y (+ (.-y rect) (.-height rect))))) 792 | 793 | (uix/defhook use-dom-inspector [{:keys [root set-inspecting on-target skip-dom? preview-node]}] 794 | (let [[rect set-rect] (uix/use-state nil) 795 | nodes (uix/use-memo 796 | (fn [] 797 | (->> root 798 | (tree-seq #(some? (.-children %)) #(seq (.-children %))) 799 | (reverse))) 800 | [root])] 801 | (uix/use-effect 802 | (fn [] 803 | (if preview-node 804 | (let [nodes (tree-seq #(some? (fiber->child %)) #(node->siblings (fiber->child %)) 805 | preview-node)] 806 | (when-let [node (some #(when (.-stateNode %) %) nodes)] 807 | (let [dom-node (.-stateNode node)] 808 | (when-let [rect (if (.-getBoundingClientRect dom-node) 809 | ;; DOM node 810 | (.getBoundingClientRect dom-node) 811 | ;; class component 812 | (some-> (uix.dom/find-dom-node dom-node) (.getBoundingClientRect)))] 813 | (set-rect rect))))) 814 | (let [node! (atom nil) 815 | mouse-handler (fn [^js e] 816 | (let [x (.-x e) 817 | y (.-y e)] 818 | (when-let [node (some #(when (intersects? [x y] (.getBoundingClientRect %)) %) 819 | nodes)] 820 | (reset! node! node) 821 | (set-rect (.getBoundingClientRect node))))) 822 | click-handler (fn [] 823 | (when-let [node @node!] 824 | (when-let [target (->> (js/Object.keys node) 825 | (some #(when (str/starts-with? % "__reactFiber") 826 | (if skip-dom? 827 | (.-_debugOwner (aget node %)) 828 | (aget node %)))))] 829 | (on-target target) 830 | (set-inspecting false) 831 | (when-let [w @popout-window] 832 | (.focus w)))))] 833 | (.addEventListener js/document "mousemove" mouse-handler) 834 | (.addEventListener js/document "click" click-handler) 835 | (fn [] 836 | (.removeEventListener js/document "mousemove" mouse-handler) 837 | (.removeEventListener js/document "click" click-handler))))) 838 | [root nodes on-target set-inspecting skip-dom? preview-node]) 839 | rect)) 840 | 841 | (defui inspector-overlay [{:keys [set-inspecting root on-target skip-dom? preview-node] :as props}] 842 | (when-let [rect (use-dom-inspector props)] 843 | ($ :div 844 | {:style {:z-index 9998 845 | :position :fixed 846 | :width "100vw" 847 | :height "100vh" 848 | :top 0 849 | :left 0 850 | :background "#e7c2ff1a" 851 | :on-click #(.stopPropagation %)}} 852 | ($ :div 853 | {:style {:position :absolute 854 | :top (.-y rect) 855 | :left (.-x rect) 856 | :width (.-width rect) 857 | :height (.-height rect) 858 | :background "#cd80ffa6" 859 | :box-sizing :border-box 860 | :border "1px dashed #da33ff" 861 | :pointer-events :none}})))) 862 | 863 | (defui devtools* [{:keys [root location]}] 864 | (let [[tid set-tid] (uix/use-state 0) 865 | fiber (uix/use-memo (fn [] 866 | (when root 867 | tid 868 | (->> (js/Object.keys root) 869 | (some #(when (str/starts-with? % "__reactContainer") (aget root %)))))) 870 | [root tid]) 871 | [state set-state] (uix/use-state {:hide-dom? true 872 | :selected (when (and root fiber) (fiber->child fiber))}) 873 | [size set-size] (use-size 35 :cljs-devtools/ui-size) 874 | [hint set-hint] (uix/use-state "") 875 | [inspecting? set-inspecting] (uix/use-state false) 876 | [preview-node set-preview-node] (uix/use-state false) 877 | on-target (uix/use-callback 878 | (fn [fiber] 879 | (set-state #(assoc % :selected fiber))) 880 | []) 881 | colors (uix/use-context theme-ctx)] 882 | (uix/use-effect 883 | (fn [] 884 | (let [handler (fns/throttle #(set-tid inc) 100) 885 | obs (js/MutationObserver. handler)] 886 | (.observe obs root #js {:childList true :subtree true :attributes true}) 887 | #(.disconnect obs))) 888 | [root]) 889 | ($ :<> 890 | (when (or inspecting? preview-node) 891 | (uix.dom/create-portal 892 | ($ inspector-overlay 893 | {:set-inspecting set-inspecting 894 | :root root 895 | :on-target on-target 896 | :skip-dom? (:hide-dom? state) 897 | :preview-node preview-node}) 898 | (js/document.getElementById "cljs-devtools-inspector-overlay"))) 899 | ($ :div 900 | {:style {:position :fixed 901 | :z-index 9999 902 | :left (case location 903 | (:bottom :left :window) 0 904 | nil) 905 | :right (case location 906 | (:right) 0 907 | nil) 908 | :bottom 0 909 | :width (case location 910 | (:bottom :window) "100vw" 911 | (:left :right) (str size "vw")) 912 | :height (case location 913 | (:left :right :window) "100vh" 914 | :bottom (str size "vh")) 915 | :background (:devtools-bg colors) 916 | :color (:devtools-text colors) 917 | :font "normal 14px sans-serif" 918 | :display :flex 919 | :border-top (when (= location :bottom) "2px solid #8632ff75") 920 | :border-left (when (= location :right) "2px solid #8632ff75") 921 | :border-right (when (= location :left) "2px solid #8632ff75")}} 922 | (when-not (= location :window) 923 | ($ resize-handle 924 | {:set-size set-size 925 | :dir (if (= location :bottom) 926 | :vertical 927 | :horizontal) 928 | :location location 929 | :min 10 930 | :max 90})) 931 | (cond 932 | (or (not root) (not fiber)) 933 | ($ :div 934 | {:style {:display :flex 935 | :flex-direction :column 936 | :gap 8 937 | :flex 1 938 | :justify-content :center 939 | :align-items :center 940 | :color (:highlight-text colors) 941 | :font-size "18px"}} 942 | (if-not root 943 | ($ :<> 944 | "Devtools are not connected to React root" 945 | ($ :span {:style {:font-size "16px"}} 946 | "make sure to pass the root node when initializing devtools") 947 | ($ :pre {:style {:font-size "14px" :margin 0}} 948 | (pr-str 949 | '(cljs-react-devtools.core/init! 950 | {:root (js/document.getElementById "root")})))) 951 | "Provided root node doesn't have React app rendered")) 952 | 953 | :else ($ error-boundary 954 | ($ :div {:style {:flex 1 :max-width "100%"}} 955 | ($ toolbar 956 | {:state state 957 | :set-state set-state 958 | :hint (when (#{:bottom :window} location) hint) 959 | :set-hint set-hint 960 | :inspecting? inspecting? 961 | :set-inspecting set-inspecting 962 | :dock-devtools dock-devtools 963 | :location location}) 964 | ($ :div {:style {:display :flex 965 | :flex-direction (if (#{:window :bottom} location) :row :column) 966 | :flex 1 967 | :max-height "100%" 968 | :min-height "100%" 969 | :width (when (#{:window :bottom} location) "100vw")}} 970 | ($ :div {:style {:flex 1 971 | :overflow-y :auto 972 | :padding "8px 0" 973 | :background (:tree-view-bg colors)}} 974 | ($ (.-Provider preview-ctx) {:value set-preview-node} 975 | (for [node (node->siblings (fiber->child fiber))] 976 | ($ tree-view {:node node 977 | :state state 978 | :set-state set-state 979 | :key (.-index node)})))) 980 | ($ inspector 981 | {:state state 982 | :set-state set-state 983 | :set-hint set-hint 984 | :location location}))))))))) 985 | 986 | (defn matches? [] 987 | (.-matches (js/window.matchMedia "(prefers-color-scheme: dark)"))) 988 | 989 | (defui devtools 990 | [{:keys [shortcut location theme] 991 | :or {theme color-themes} 992 | :as props}] 993 | (let [[visible? set-visible] (uix/use-state #(let [v (js/JSON.parse (js/localStorage.getItem ":cljs-devtools/visible?"))] 994 | (or (nil? v) v))) 995 | [dark-mode? set-dark-mode] (uix/use-state matches?)] 996 | (uix/use-effect 997 | (fn [] 998 | (let [handler #(set-dark-mode (matches?)) 999 | m (js/window.matchMedia "(prefers-color-scheme: dark)")] 1000 | (.addListener m handler) 1001 | #(.removeListener m handler))) 1002 | []) 1003 | (uix/use-effect 1004 | (fn [] 1005 | (when (string? shortcut) 1006 | (let [shortcut (str/split shortcut #"-")] 1007 | (when (seq shortcut) 1008 | (let [down-handler (fn [^js e] 1009 | (when 1010 | (and (not= :window location) 1011 | (->> shortcut 1012 | (every? #(case % 1013 | "Control" (.-ctrlKey e) 1014 | "Alt" (.-altKey e) 1015 | "Meta" (.-metaKey e) 1016 | "Shift" (.-shiftKey e) 1017 | (= % (.-key e)))))) 1018 | (set-visible not)))] 1019 | (.addEventListener js/window "keydown" down-handler) 1020 | (fn [] 1021 | (.removeEventListener js/window "keydown" down-handler))))))) 1022 | [shortcut location]) 1023 | (uix/use-effect 1024 | (fn [] 1025 | (js/localStorage.setItem ":cljs-devtools/visible?" visible?)) 1026 | [visible?]) 1027 | (when visible? 1028 | ($ (.-Provider theme-ctx) {:value (or (if dark-mode? 1029 | (:dark theme) 1030 | (:light theme)) 1031 | (:default theme))} 1032 | ($ devtools* props))))) 1033 | 1034 | (defn hijack-re-frame [] 1035 | (when (exists? js/re-frame.core.subscribe) 1036 | (let [subscribe js/re-frame.core.subscribe] 1037 | (set! js/re-frame.core.subscribe 1038 | (fn 1039 | ([query] 1040 | (let [ret (subscribe query)] 1041 | (set! (.-__devtools-label ^js ret) (first query)) 1042 | ret)) 1043 | ([query dynv] 1044 | (let [ret (subscribe query dynv)] 1045 | (set! (.-__devtools-label ^js ret) (first query)) 1046 | ret))))))) 1047 | 1048 | (defonce opts* (atom nil)) 1049 | 1050 | (defui devtools-popup [{:keys [on-mount location]}] 1051 | (uix/use-effect 1052 | #(on-mount) 1053 | [on-mount]) 1054 | ($ devtools (assoc @opts* :location location))) 1055 | 1056 | ;; https://github.com/day8/re-frame-10x/blob/788bbd8e474c5e61e3cc604d2b01aa2b5a1be75d/src/day8/re_frame_10x/fx/window.cljs 1057 | 1058 | (defn m->str [m] 1059 | (->> m 1060 | (reduce (fn [ret [k v]] 1061 | (let [k (if (keyword? k) (name k) k) 1062 | v (if (keyword? v) (name v) v)] 1063 | (conj ret (str k "=" v)))) 1064 | []) 1065 | (str/join ","))) 1066 | 1067 | (defonce devtools-root* (atom nil)) 1068 | 1069 | (defn mount [popup-window popup-document props] 1070 | ;; When programming here, we need to be careful about which document and window 1071 | ;; we are operating on, and keep in mind that the window can close without going 1072 | ;; through standard react lifecycle, so we hook the beforeunload event. 1073 | (let [node (.createElement popup-document "div") 1074 | _ (set! (.-id node) "cljs-react-devtools-root") 1075 | _ (.append (.-body popup-document) node) 1076 | shadow-root (.attachShadow node #js {:mode "open"}) 1077 | root (uix.dom/create-root shadow-root) 1078 | resize-update-scheduled? (atom false) 1079 | handle-window-resize (fn [_] 1080 | (when-not @resize-update-scheduled? 1081 | (goog.async.nextTick 1082 | (fn [] 1083 | (let [width (.-innerWidth popup-window) 1084 | height (.-innerHeight popup-window)] 1085 | (swap! window-settings merge {:width width :height height})) 1086 | (reset! resize-update-scheduled? false))) 1087 | (reset! resize-update-scheduled? true))) 1088 | handle-window-position (fn [] 1089 | ;; Only update re-frame if the windows position has changed. 1090 | (let [{:keys [left top]} @window-settings 1091 | screen-left (.-screenX popup-window) 1092 | screen-top (.-screenY popup-window)] 1093 | (when (or (not= left screen-left) 1094 | (not= top screen-top)) 1095 | (swap! window-settings merge {:left screen-left :top screen-top})))) 1096 | window-position-interval (atom nil) 1097 | on-unmount (fn [_] 1098 | (.removeEventListener popup-window "resize" handle-window-resize) 1099 | (some-> @window-position-interval js/clearInterval) 1100 | (dock-devtools :location (:location @window-settings) :unload? true) 1101 | nil) 1102 | on-mount (fn [] 1103 | (.addEventListener popup-window "resize" handle-window-resize) 1104 | (.addEventListener popup-window "beforeunload" on-unmount) 1105 | ;; Check the window position every 2 seconds 1106 | (reset! window-position-interval 1107 | (js/setInterval 1108 | handle-window-position 1109 | 2000)))] 1110 | (aset popup-window "onunload" #(reset! popout-window nil)) 1111 | (reset! devtools-root* root) 1112 | (uix.dom/render-root ($ devtools-popup 1113 | (merge 1114 | {:on-mount on-mount} 1115 | props)) 1116 | root))) 1117 | 1118 | (defn open-debugger-window 1119 | "Originally copied from re-frisk.devtool/open-debugger-window" 1120 | [{:keys [width height top left]} props] 1121 | (let [document-title js/document.title 1122 | window-title (gstr/escapeString (str "cljs-react-devtools | " document-title)) 1123 | window-html (str "" 1124 | window-title 1125 | "") 1126 | window-features (m->str 1127 | {:width width 1128 | :height height 1129 | :left left 1130 | :top top 1131 | :resizable :yes 1132 | :scrollbars :yes 1133 | :status :no 1134 | :directories :no 1135 | :toolbar :no 1136 | :menubar :no})] 1137 | ;; We would like to set the windows left and top positions to match the monitor that it was on previously, but Chrome doesn't give us 1138 | ;; control over this, it will only position it within the same display that it was popped out on. 1139 | (if-let [w (js/window.open "about:blank" "re-frame-10x-popout" window-features)] 1140 | (let [d (.-document w)] 1141 | ;; We had to comment out the following unmountComponentAtNode as it causes a React exception we assume 1142 | ;; because React says el is not a root container that it knows about. 1143 | ;; In theory by not freeing up the resources associated with this container (e.g. event handlers) we may be 1144 | ;; creating memory leaks. However, with observation of the heap in developer tools we cannot see any significant 1145 | ;; unbounded growth in memory usage. 1146 | ;(when-let [el (.getElementById d "--re-frame-10x--")] 1147 | ; (r/unmount-component-at-node el))) 1148 | (.open d) 1149 | (.write d window-html) 1150 | (aset w "onload" #(mount w d props)) 1151 | (.close d) 1152 | (reset! popout-window w))))) 1153 | 1154 | (declare render-devtools) 1155 | 1156 | (defn dock-devtools [& {:keys [location theme unload?]}] 1157 | (swap! window-settings assoc :location location) 1158 | (js/localStorage.setItem ":cljs-devtools/window-location" (name location)) 1159 | (if @popout-window 1160 | (do 1161 | (.unmount @devtools-root*) 1162 | (reset! devtools-root* nil) 1163 | (when-not unload? 1164 | (.close @popout-window)) 1165 | (js/setTimeout #(render-devtools {:location location :theme theme}) 50)) 1166 | (do 1167 | (.unmount @devtools-root*) 1168 | (reset! devtools-root* nil) 1169 | (.remove (js/document.getElementById "cljs-react-devtools-root")) 1170 | (if (= location :window) 1171 | (open-debugger-window @window-settings {:location location}) 1172 | (render-devtools {:location location}))))) 1173 | 1174 | (defn render-devtools [{:keys [location theme]}] 1175 | (let [node (js/document.createElement "div") 1176 | shadow-root (.attachShadow node #js {:mode "open"}) 1177 | _ (js/document.body.append node) 1178 | _ (set! (.-id node) "cljs-react-devtools-root") 1179 | root (uix.dom/create-root shadow-root)] 1180 | (reset! devtools-root* root) 1181 | (uix.dom/render-root ($ devtools (assoc @opts* :location location)) root) 1182 | nil)) 1183 | 1184 | (defonce ^:private initialized? (atom false)) 1185 | 1186 | (defn init! [{:keys [root shortcut theme] :as opts}] 1187 | (when-not @initialized? 1188 | (reset! initialized? true) 1189 | (reset! opts* opts) 1190 | (hijack-re-frame) 1191 | (js/setTimeout 1192 | (fn [] 1193 | (let [node (js/document.createElement "div")] 1194 | (set! (.-id node) "cljs-devtools-inspector-overlay") 1195 | (js/document.body.append node) 1196 | (render-devtools {:location (:location @window-settings)}))) 1197 | 100))) 1198 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "js-tokens@^3.0.0 || ^4.0.0": 6 | version "4.0.0" 7 | resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" 8 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 9 | 10 | loose-envify@^1.1.0: 11 | version "1.4.0" 12 | resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" 13 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== 14 | dependencies: 15 | js-tokens "^3.0.0 || ^4.0.0" 16 | 17 | react-dom@^18.2.0: 18 | version "18.2.0" 19 | resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" 20 | integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== 21 | dependencies: 22 | loose-envify "^1.1.0" 23 | scheduler "^0.23.0" 24 | 25 | react-refresh@^0.14.0: 26 | version "0.14.0" 27 | resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz" 28 | integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== 29 | 30 | react@^18.2.0: 31 | version "18.2.0" 32 | resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" 33 | integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== 34 | dependencies: 35 | loose-envify "^1.1.0" 36 | 37 | scheduler@^0.23.0: 38 | version "0.23.0" 39 | resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz" 40 | integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== 41 | dependencies: 42 | loose-envify "^1.1.0" 43 | --------------------------------------------------------------------------------