├── doc └── potential_features.md ├── example-resources └── public │ └── devcards │ ├── css │ ├── devcard-api.css │ └── two-zero.css │ └── index.html ├── CHANGES.md ├── src └── devcards │ ├── system.clj │ ├── util │ ├── utils.clj │ ├── utils.cljs │ ├── edn_renderer.cljs │ └── markdown.cljs │ ├── core.clj │ └── system.cljs ├── .gitignore ├── index.html ├── resources └── public │ └── devcards │ ├── css │ ├── com_rigsomelight_devcards_addons.css │ ├── com_rigsomelight_edn_flex.css │ ├── com_rigsomelight_github_highlight.css │ ├── com_rigsomelight_edn.css │ ├── zenburn.css │ ├── default.css │ └── com_rigsomelight_devcards.css │ └── js │ └── highlight.pack.js ├── example_src └── devdemos │ ├── start_ui.cljs │ ├── edn_render.cljs │ ├── errors.cljs │ ├── om.cljs │ ├── css_opt_out.cljs │ ├── source_code_display.cljs │ ├── extentions.cljs │ ├── reagent.cljs │ ├── testing.cljs │ ├── custom_cards.cljs │ ├── defcard_api.cljs │ ├── two_zero.cljs │ └── core.cljs ├── project.clj └── README.md /doc/potential_features.md: -------------------------------------------------------------------------------- 1 | ## Todo 2 | 3 | Make edn renderer render differences. 4 | -------------------------------------------------------------------------------- /example-resources/public/devcards/css/devcard-api.css: -------------------------------------------------------------------------------- 1 | .red-box { 2 | border: 5px solid red; 3 | } 4 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0-2 First documented release 2 | 3 | * Support for `cljs.test` async testing 4 | 5 | -------------------------------------------------------------------------------- /src/devcards/system.clj: -------------------------------------------------------------------------------- 1 | (ns devcards.system 2 | (:require 3 | [clojure.java.io :as io])) 4 | 5 | (defmacro inline-resouce-file [file-url] 6 | (when-let [f (io/resource file-url)] 7 | (slurp f))) 8 | 9 | -------------------------------------------------------------------------------- /src/devcards/util/utils.clj: -------------------------------------------------------------------------------- 1 | (ns devcards.util.utils 2 | (:require [cljs.env])) 3 | 4 | (defn devcards-active? [] 5 | (and cljs.env/*compiler* 6 | (get-in @cljs.env/*compiler* [:options :devcards]))) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml.asc 2 | .idea 3 | *.iml 4 | pom.xml 5 | *jar 6 | /lib/ 7 | /classes/ 8 | /out/ 9 | /target/ 10 | /site/out 11 | .lein-deps-sum 12 | .lein-repl-history 13 | .lein-plugins/ 14 | checkouts/ 15 | .nrepl-port 16 | .lein-classpath 17 | .rbenv-version 18 | config.ru 19 | example-resources/public/js/compiled/* 20 | example-resources/public/devcards/js/compiled/* 21 | .\#* 22 | \#* 23 | 24 | *-init.clj 25 | figwheel_server.log 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/devcards/util/utils.cljs: -------------------------------------------------------------------------------- 1 | (ns devcards.util.utils 2 | (:require 3 | [cljs.pprint :as pprint]) 4 | (:import 5 | [goog.string StringBuffer])) 6 | 7 | (defn html-env? [] 8 | (if-let [doc js/goog.global.document] 9 | (aget doc "write"))) 10 | 11 | (defn node-env? [] (not (nil? goog/nodeGlobalRequire))) 12 | 13 | (defn pprint-str [obj] 14 | (let [sb (StringBuffer.)] 15 | (pprint/pprint obj (StringBufferWriter. sb)) 16 | (str sb))) 17 | 18 | (defn pprint-code [code] 19 | (pprint/with-pprint-dispatch pprint/code-dispatch (pprint-str code))) 20 | 21 | -------------------------------------------------------------------------------- /resources/public/devcards/css/com_rigsomelight_devcards_addons.css: -------------------------------------------------------------------------------- 1 | /* full width code examples */ 2 | 3 | body { 4 | overflow-x: hidden; 5 | } 6 | 7 | @media (max-width: 1000px) { 8 | .com-rigsomelight-devcards-card-hide-border 9 | .com-rigsomelight-devcards_rendered-card 10 | .com-rigsomelight-devcards-markdown pre { 11 | margin-right: -3000px; 12 | margin-left: -3000px; 13 | padding-right: 3000px; 14 | padding-left: 3000px; 15 | } 16 | } 17 | 18 | /* default typography */ 19 | .com-rigsomelight-devcards-base { 20 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 21 | font-size: 16px; 22 | line-height: 1.42857143; 23 | } 24 | -------------------------------------------------------------------------------- /example_src/devdemos/start_ui.cljs: -------------------------------------------------------------------------------- 1 | (ns devdemos.start-ui 2 | (:require 3 | [devcards.core] 4 | [devdemos.defcard-api] 5 | [devdemos.om] 6 | [devdemos.reagent] 7 | [devdemos.source-code-display] 8 | [devdemos.two-zero] 9 | [devdemos.testing] 10 | [devdemos.errors] 11 | [devdemos.extentions] 12 | [devdemos.edn-render] 13 | [devdemos.css-opt-out] 14 | [devdemos.custom-cards] 15 | [devdemos.core])) 16 | 17 | ;; The main function here is actually used in a documentation 18 | ;; generator that I'm experimenting with. This is not needed 19 | ;; with a standard Devcards setup!! 20 | 21 | (defn ^:export main [] 22 | (devcards.core/start-devcard-ui!)) 23 | -------------------------------------------------------------------------------- /example-resources/public/devcards/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example_src/devdemos/edn_render.cljs: -------------------------------------------------------------------------------- 1 | (ns devdemos.edn-render 2 | (:require [devcards.core]) 3 | (:require-macros 4 | [devcards.core :as dc :refer [defcard edn]])) 5 | 6 | 7 | (defcard some-typical-nested-edn 8 | {:first (range 50) 9 | :sets (set (range 1000 1100)) 10 | :vector (vec (range 1000 1100)) 11 | :second (take 6 (repeat {:first-name "Bruce" 12 | :last-name "Hauman" 13 | :date (js/Date.) 14 | :children (take 6 (repeat {:first-name "Bruce" 15 | :last-name "Hauman" 16 | :date (js/Date.) 17 | :children (take 6 (repeat {:first-name "Bruce" 18 | :last-name "Hauman" 19 | :date (js/Date.)}))}))}))}) 20 | -------------------------------------------------------------------------------- /example_src/devdemos/errors.cljs: -------------------------------------------------------------------------------- 1 | (ns devdemos.errors 2 | (:require 3 | [devcards.core :as devcards] 4 | [sablono.core :as sab :include-macros true] 5 | [cljs.test :as t :include-macros true]) 6 | (:require-macros 7 | [devcards.core :as dc :refer [defcard defcard-doc deftest dom-node]])) 8 | 9 | (defcard 10 | "#Errors 11 | 12 | Below are examples of various failure scenarios for devcards. 13 | 14 | I use this to put various cards in bad states to see how the system 15 | responds. 16 | 17 | This is a good example of how to use devcards to great benefit. 18 | Make an **errors** page of devcards and put your components 19 | through the races.") 20 | 21 | (defcard-doc (dc/doc "#this isn't is an error")) 22 | 23 | (defcard hello) 24 | 25 | (defcard "This should fall back to **pprint**" 26 | #js {:beetle "juice"}) 27 | 28 | (defcard 29 | "This should fall back to **pprint**" 30 | (to-array ["asdf" "asd"])) 31 | 32 | (defcard #js {} #js {}) 33 | 34 | (defcard #js {} #js {} #js {}) 35 | 36 | (defcard (sab/html [:div "hello"]) {} 37 | { :frame 5 38 | :heading 5 39 | :padding 5 40 | :inspect-data 5 41 | :watch-atom 5 42 | :history 5}) 43 | 44 | -------------------------------------------------------------------------------- /resources/public/devcards/css/com_rigsomelight_edn_flex.css: -------------------------------------------------------------------------------- 1 | .com-rigsomelight-rendered-edn .collection { 2 | display: flex; 3 | display: -webkit-flex; 4 | } 5 | 6 | .com-rigsomelight-rendered-edn .keyval { 7 | display: flex; 8 | display: -webkit-flex; 9 | flex-wrap: wrap; 10 | -webkit-flex-wrap: wrap; 11 | } 12 | 13 | .com-rigsomelight-rendered-edn .keyval > .keyword { 14 | color: #a94442; 15 | } 16 | 17 | .com-rigsomelight-rendered-edn .keyval > *:first-child { 18 | margin: 0px 3px; 19 | flex-shrink: 0; 20 | -webkit-flex-shrink: 0; 21 | } 22 | 23 | .com-rigsomelight-rendered-edn .keyval > *:last-child { 24 | margin: 0px 3px; 25 | } 26 | 27 | .com-rigsomelight-rendered-edn .opener { 28 | color: #999; 29 | margin: 0px 4px; 30 | flex-shrink: 0; 31 | -webkit-flex-shrink: 0; 32 | } 33 | 34 | .com-rigsomelight-rendered-edn .closer { 35 | display: flex; 36 | display: -webkit-flex; 37 | flex-direction: column-reverse; 38 | -webkit-flex-direction: column-reverse; 39 | margin: 0px 3px; 40 | color: #999; 41 | } 42 | 43 | .com-rigsomelight-rendered-edn .string { 44 | color: #428bca; 45 | } 46 | 47 | .com-rigsomelight-rendered-edn .string .opener, 48 | .com-rigsomelight-rendered-edn .string .closer { 49 | display: inline; 50 | margin: 0px; 51 | color: #428bca; 52 | } 53 | -------------------------------------------------------------------------------- /resources/public/devcards/css/com_rigsomelight_github_highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | -webkit-text-size-adjust: none; 14 | } 15 | 16 | .hljs-comment, 17 | .diff .hljs-header { 18 | color: #998; 19 | font-style: italic; 20 | } 21 | 22 | .hljs-keyword, 23 | .css .rule .hljs-keyword, 24 | .hljs-winutils, 25 | .nginx .hljs-title, 26 | .hljs-subst, 27 | .hljs-request, 28 | .hljs-status { 29 | color: #333; 30 | font-weight: bold; 31 | } 32 | 33 | .hljs-number, 34 | .hljs-hexcolor, 35 | .ruby .hljs-constant { 36 | color: #008080; 37 | } 38 | 39 | .hljs-string, 40 | .hljs-tag .hljs-value, 41 | .hljs-doctag, 42 | .tex .hljs-formula { 43 | color: #d14; 44 | } 45 | 46 | .hljs-title, 47 | .hljs-id, 48 | .scss .hljs-preprocessor { 49 | color: #900; 50 | font-weight: bold; 51 | } 52 | 53 | .hljs-list .hljs-keyword, 54 | .hljs-subst { 55 | font-weight: normal; 56 | } 57 | 58 | .hljs-class .hljs-title, 59 | .hljs-type, 60 | .vhdl .hljs-literal, 61 | .tex .hljs-command { 62 | color: #458; 63 | font-weight: bold; 64 | } 65 | 66 | .hljs-tag, 67 | .hljs-tag .hljs-title, 68 | .hljs-rule .hljs-property, 69 | .django .hljs-tag .hljs-keyword { 70 | color: #000080; 71 | font-weight: normal; 72 | } 73 | 74 | .hljs-attribute, 75 | .hljs-variable, 76 | .lisp .hljs-body, 77 | .hljs-name { 78 | color: #008080; 79 | } 80 | 81 | .hljs-regexp { 82 | color: #009926; 83 | } 84 | 85 | .hljs-symbol, 86 | .ruby .hljs-symbol .hljs-string, 87 | .lisp .hljs-keyword, 88 | .clojure .hljs-keyword, 89 | .scheme .hljs-keyword, 90 | .tex .hljs-special, 91 | .hljs-prompt { 92 | color: #990073; 93 | } 94 | 95 | .hljs-built_in { 96 | color: #0086b3; 97 | } 98 | 99 | .hljs-preprocessor, 100 | .hljs-pragma, 101 | .hljs-pi, 102 | .hljs-doctype, 103 | .hljs-shebang, 104 | .hljs-cdata { 105 | color: #999; 106 | font-weight: bold; 107 | } 108 | 109 | .hljs-deletion { 110 | background: #fdd; 111 | } 112 | 113 | .hljs-addition { 114 | background: #dfd; 115 | } 116 | 117 | .diff .hljs-change { 118 | background: #0086b3; 119 | } 120 | 121 | .hljs-chunk { 122 | color: #aaa; 123 | } 124 | -------------------------------------------------------------------------------- /resources/public/devcards/css/com_rigsomelight_edn.css: -------------------------------------------------------------------------------- 1 | .com-rigsomelight-rendered-edn .keyval > .keyword { 2 | color: rgb(196,33,0); 3 | padding-right: 10px; 4 | } 5 | 6 | .com-rigsomelight-rendered-edn .collection { 7 | position: relative; 8 | } 9 | 10 | .com-rigsomelight-rendered-edn .vector, 11 | .com-rigsomelight-rendered-edn .set, 12 | .com-rigsomelight-rendered-edn .seq { 13 | padding-left: 0.9em; 14 | padding-right: 0.9em; 15 | } 16 | 17 | .com-rigsomelight-rendered-edn .set { 18 | padding-left: 1.2em; 19 | } 20 | 21 | 22 | .com-rigsomelight-rendered-edn .collection.map { 23 | padding: 1.8em; 24 | display: inline-block; 25 | vertical-align: top; 26 | } 27 | 28 | .com-rigsomelight-rendered-edn .vector, 29 | .com-rigsomelight-rendered-edn .set, 30 | .com-rigsomelight-rendered-edn .seq { 31 | display: inline-block; 32 | vertical-align: top; 33 | } 34 | 35 | .com-rigsomelight-rendered-edn .vector > .contents { 36 | background-color: rgba(0,0,0,0.01); 37 | } 38 | 39 | .com-rigsomelight-rendered-edn .keyval { 40 | display: inline-block; 41 | } 42 | 43 | .com-rigsomelight-rendered-edn .collection.map > .contents > .separator { 44 | padding-right: 10px; 45 | } 46 | 47 | .com-rigsomelight-rendered-edn .collection .collection > .contents { 48 | /* background-color: rgba(0,0,0,0.02); */ 49 | } 50 | 51 | .com-rigsomelight-rendered-edn .collection .contents > .collection:nth-child(even) { 52 | background-color: rgba(0,0,0,0.04); 53 | } 54 | 55 | .com-rigsomelight-rendered-edn .contents { 56 | display: inline-block; 57 | } 58 | 59 | .com-rigsomelight-rendered-edn .opener, 60 | .com-rigsomelight-rendered-edn .closer { 61 | color: #999; 62 | } 63 | 64 | .com-rigsomelight-rendered-edn .collection.map > .opener, 65 | .com-rigsomelight-rendered-edn .collection.vector > .opener, 66 | .com-rigsomelight-rendered-edn .collection.seq > .opener, 67 | .com-rigsomelight-rendered-edn .collection.set > .opener { 68 | position: absolute; 69 | top: 0px; 70 | left: 3px; 71 | } 72 | .com-rigsomelight-rendered-edn .collection.map > .closer { 73 | position: absolute; 74 | bottom: 0px; 75 | left: 3px; 76 | display: block; 77 | } 78 | 79 | .com-rigsomelight-rendered-edn .collection.vector > .closer, 80 | .com-rigsomelight-rendered-edn .collection.seq > .closer, 81 | .com-rigsomelight-rendered-edn .collection.set > .closer { 82 | position: absolute; 83 | bottom: 0px; 84 | right: 0px; 85 | display: block; 86 | } -------------------------------------------------------------------------------- /resources/public/devcards/css/zenburn.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Zenburn style from voldmar.ru (c) Vladimir Epifanov 4 | based on dark.css by Ivan Sagalaev 5 | 6 | */ 7 | 8 | .hljs { 9 | display: block; 10 | overflow-x: auto; 11 | padding: 0.5em; 12 | /* background: #3f3f3f; */ 13 | color: #dcdcdc; 14 | -webkit-text-size-adjust: none; 15 | } 16 | 17 | .hljs-keyword, 18 | .hljs-tag, 19 | .css .hljs-class, 20 | .css .hljs-id, 21 | .lisp .hljs-title, 22 | .nginx .hljs-title, 23 | .hljs-request, 24 | .hljs-status, 25 | .clojure .hljs-attribute { 26 | color: #e3ceab; 27 | } 28 | 29 | .django .hljs-template_tag, 30 | .django .hljs-variable, 31 | .django .hljs-filter .hljs-argument { 32 | color: #dcdcdc; 33 | } 34 | 35 | .hljs-number, 36 | .hljs-date { 37 | color: #8cd0d3; 38 | } 39 | 40 | .dos .hljs-envvar, 41 | .dos .hljs-stream, 42 | .hljs-variable, 43 | .apache .hljs-sqbracket { 44 | color: #efdcbc; 45 | } 46 | 47 | .dos .hljs-flow, 48 | .diff .hljs-change, 49 | .python .exception, 50 | .python .hljs-built_in, 51 | .hljs-literal, 52 | .tex .hljs-special { 53 | color: #efefaf; 54 | } 55 | 56 | .diff .hljs-chunk, 57 | .hljs-subst { 58 | color: #8f8f8f; 59 | } 60 | 61 | .dos .hljs-keyword, 62 | .hljs-decorator, 63 | .hljs-title, 64 | .hljs-type, 65 | .diff .hljs-header, 66 | .ruby .hljs-class .hljs-parent, 67 | .apache .hljs-tag, 68 | .nginx .hljs-built_in, 69 | .tex .hljs-command, 70 | .hljs-prompt { 71 | color: #efef8f; 72 | } 73 | 74 | .dos .hljs-winutils, 75 | .ruby .hljs-symbol, 76 | .ruby .hljs-symbol .hljs-string, 77 | .ruby .hljs-string { 78 | color: #dca3a3; 79 | } 80 | 81 | .diff .hljs-deletion, 82 | .hljs-string, 83 | .hljs-tag .hljs-value, 84 | .hljs-preprocessor, 85 | .hljs-pragma, 86 | .hljs-built_in, 87 | .hljs-javadoc, 88 | .smalltalk .hljs-class, 89 | .smalltalk .hljs-localvars, 90 | .smalltalk .hljs-array, 91 | .css .hljs-rules .hljs-value, 92 | .hljs-attr_selector, 93 | .hljs-pseudo, 94 | .apache .hljs-cbracket, 95 | .tex .hljs-formula, 96 | .coffeescript .hljs-attribute { 97 | color: #cc9393; 98 | } 99 | 100 | .hljs-shebang, 101 | .diff .hljs-addition, 102 | .hljs-comment, 103 | .hljs-annotation, 104 | .hljs-pi, 105 | .hljs-doctype { 106 | color: #7f9f7f; 107 | } 108 | 109 | .coffeescript .javascript, 110 | .javascript .xml, 111 | .tex .hljs-formula, 112 | .xml .javascript, 113 | .xml .vbscript, 114 | .xml .css, 115 | .xml .hljs-cdata { 116 | opacity: 0.5; 117 | } 118 | 119 | -------------------------------------------------------------------------------- /src/devcards/util/edn_renderer.cljs: -------------------------------------------------------------------------------- 1 | (ns devcards.util.edn-renderer 2 | (:require 3 | [sablono.core :as sab] 4 | [devcards.util.utils :as utils])) 5 | 6 | 7 | (declare html) 8 | 9 | (defn literal? [x] 10 | (and (not (seq? x)) 11 | (not (coll? x)))) 12 | 13 | (defn separator* [s] 14 | (sab/html [:span.seperator s])) 15 | 16 | (defn clearfix-separator* [s] 17 | (sab/html [:span (separator* s) [:span.clearfix]])) 18 | 19 | (defn separate-fn [coll] 20 | (if (not (every? literal? coll)) clearfix-separator* separator*)) 21 | 22 | (defn interpose-separator [rct-coll s sep-fn] 23 | (->> (rest rct-coll) 24 | (interleave (repeatedly #(sep-fn s))) 25 | (cons (first rct-coll)) 26 | to-array)) 27 | 28 | (defn literal [class x] 29 | (sab/html [:span { :className class } (utils/pprint-str x)])) 30 | 31 | (defn join-html [separator coll] 32 | (interpose-separator (mapv html coll) 33 | separator 34 | (separate-fn coll))) 35 | 36 | (defn html-keyval [[k v]] 37 | (sab/html 38 | [:span.keyval { :key (prn-str k)} (html k) (html v)])) 39 | 40 | (defn html-keyvals [coll] 41 | (interpose-separator (mapv html-keyval coll) 42 | " " 43 | (separate-fn (vals coll)))) 44 | 45 | (defn open-close [class-str opener closer rct-coll] 46 | (sab/html 47 | [:span {:className class-str} 48 | [:span.opener opener] 49 | [:span.contents rct-coll] 50 | [:span.closer closer]])) 51 | 52 | (defn html-collection [class opener closer coll] 53 | (open-close (str "collection " class ) opener closer (join-html " " coll)) 54 | ;; this speeds things up but fails in om 55 | #_(rct/pure coll ...) 56 | ) 57 | 58 | (defn html-map [coll] 59 | (open-close "collection map" "{" "}" (html-keyvals coll)) 60 | ;; this speeds things up but fails in om 61 | #_(rct/pure coll ...)) 62 | 63 | (defn html-string [s] 64 | (open-close "string" "\"" "\"" s)) 65 | 66 | (defn html [x] 67 | (cond 68 | (number? x) (literal "number" x) 69 | (keyword? x) (literal "keyword" x) 70 | (symbol? x) (literal "symbol" x) 71 | (string? x) (html-string x) 72 | (map? x) (html-map x) 73 | (set? x) (html-collection "set" "#{" "}" x) 74 | (vector? x) (html-collection "vector" "[" "]" x) 75 | (seq? x) (html-collection "seq" "(" ")" x) 76 | :else (literal "literal" x))) 77 | 78 | (defn html-edn [e] 79 | (sab/html [:div.com-rigsomelight-rendered-edn.com-rigsomelight-devcards-typog (html e)])) 80 | -------------------------------------------------------------------------------- /example_src/devdemos/om.cljs: -------------------------------------------------------------------------------- 1 | (ns devdemos.om 2 | (:require 3 | [devcards.core] 4 | [om.core :as om :include-macros true] 5 | [om.dom :as dom :include-macros true] 6 | [reagent.core :as reagent] 7 | [clojure.string :as string] 8 | [sablono.core :as sab :include-macros true] 9 | [cljs.test :as t :include-macros true :refer-macros [testing is]]) 10 | (:require-macros 11 | ;; Notice that I am not including the 'devcards.core namespace 12 | ;; but only the macros. This helps ensure that devcards will only 13 | ;; be created when the :devcards is set to true in the build config. 14 | [devcards.core :as dc :refer [defcard defcard-doc defcard-om noframe-doc deftest dom-node]])) 15 | 16 | (defcard-doc 17 | "## Rendering Om components with `om-root` and `defcard-om` 18 | 19 | The `om-root` will render Om components, much the way `om.core/root` does. 20 | 21 | Please refer to code of this file to see how these Om examples are 22 | built. 23 | ") 24 | 25 | (defn widget [data owner] 26 | (om/component 27 | (sab/html [:h2 "This is an om card, " (:text data)]))) 28 | 29 | (defonce test-om-data test-om-data) 30 | 31 | (defcard omcard-ex 32 | (dc/om-root widget) 33 | {:text "yep"}) 34 | 35 | (defcard om-share-atoms 36 | (dc/doc 37 | "#### You can share an Atom between `om-root-card`s. 38 | 39 | Interact with the counters below.")) 40 | 41 | (defonce om-test-atom (atom {:count 20})) 42 | 43 | (defn counter [owner data f s] 44 | (om/component 45 | (sab/html 46 | [:div 47 | [:h1 (om/get-shared owner :title) (:count data)] 48 | [:div [:a {:onClick #(om/transact! data :count f)} s]] 49 | (dc/edn data)]))) 50 | 51 | (defn om-counter-inc [data owner] (counter owner data inc "inc")) 52 | 53 | (dc/defcard-om omcard-shared-ex-1 54 | om-counter-inc 55 | om-test-atom 56 | {:shared {:title "First counter "}}) 57 | 58 | (defn om-counter-dec [data owner] (counter owner data dec "dec")) 59 | 60 | (dc/defcard-om omcard-shared-ex-2 61 | om-counter-dec 62 | om-test-atom 63 | {:shared {:title "Second counter "}}) 64 | 65 | (dc/defcard om-test-atom-data 66 | "### You can share an Atom with an `edn-card` too:" 67 | om-test-atom) 68 | 69 | (defn unmount-sample [_ _] 70 | (reify 71 | om/IDidMount 72 | (did-mount [_] 73 | (println "mounting")) 74 | om/IWillUnmount 75 | (will-unmount [_] 76 | (println "unmounting this")) 77 | om/IRender 78 | (render [_] 79 | (dom/div nil "unmount")))) 80 | 81 | (defcard-om sample-cardd 82 | unmount-sample 83 | {}) 84 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject devcards "0.2.0-4" 2 | :description "Devcards is a ClojureScript library that provides a lab space to you develop your UI components independently and interactively." 3 | :url "http://github.com/bhauman/devcards" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :dependencies [[org.clojure/clojure "1.7.0"] 8 | [org.clojure/clojurescript "1.7.122"] 9 | [org.clojure/core.async "0.1.346.0-17112a-alpha"] 10 | [sablono "0.3.6"] 11 | [cljsjs/react "0.13.3-1"] 12 | [cljs-react-reload "0.1.1"] 13 | [cljsjs/showdown "0.4.0-1"]] 14 | 15 | :source-paths ["src"] 16 | 17 | :clean-targets ^{:protect false} ["example-resources/public/devcards/js/compiled" 18 | :target-path] 19 | 20 | :scm { :name "git" 21 | :url "https://github.com/bhauman/devcards" } 22 | 23 | :profiles { 24 | :dev { 25 | :dependencies [[org.omcljs/om "0.9.0"] 26 | [reagent "0.5.1"]] 27 | :plugins [#_[lein-cljsbuild "1.0.5"] 28 | [lein-figwheel "0.4.0"]] 29 | :resource-paths ["resources" "example-resources"] 30 | :cljsbuild { 31 | :builds [{:id "devcards-demos" 32 | :source-paths ["example_src" "src"] 33 | :figwheel { :devcards true 34 | #_:websocket-host #_:js-client-host } 35 | :compiler { 36 | :main "devdemos.start-ui" 37 | :asset-path "js/compiled/out" 38 | :output-to "example-resources/public/devcards/js/compiled/devdemos.js" 39 | :output-dir "example-resources/public/devcards/js/compiled/out" 40 | ;:recompile-dependents true 41 | :optimizations :none 42 | :source-map-timestamp true}} 43 | {:id "website" 44 | :source-paths ["example_src" "src"] 45 | ;; :figwheel { :devcards true } 46 | :compiler { 47 | :main "devdemos.start-ui" 48 | :asset-path "site/out" 49 | :output-to "site/devdemos.js" 50 | :output-dir "site/out" 51 | :devcards true 52 | ;; :pseudo-names true 53 | :recompile-dependents true 54 | ;; :optimizations :simple 55 | :optimizations :advanced 56 | }} 57 | ]} 58 | 59 | :figwheel { :css-dirs ["resources/public/devcards/css"] 60 | :open-file-command "emacsclient" 61 | ;;:nrepl-port 7888 62 | } 63 | }}) 64 | 65 | -------------------------------------------------------------------------------- /example_src/devdemos/css_opt_out.cljs: -------------------------------------------------------------------------------- 1 | (ns devdemos.css-opt-out 2 | (:require 3 | [sablono.core :as sab :include-macros true] 4 | [devcards.core :as dc] 5 | [clojure.string :as string]) 6 | (:require-macros 7 | [devcards.core :refer [defcard]])) 8 | 9 | (defcard 10 | "# Devcards CSS 11 | 12 | Devcards inlines its own CSS into the `` of the HTML document that 13 | hosts your cards. This is done because it is awkward to get and 14 | include CSS and other assets from a CLJS library (in a jar file) into an HTML file. 15 | 16 | Inlining CSS into the document makes the initial setup of Devcards much easier. 17 | 18 | There are four CSS files that are included: 19 | 20 | * Devcards main CSS (for card headings, ui and navigation) 21 | * Devcards addons CSS (adding default typography styles to the card body, etc) 22 | * EDN highlighting CSS (for the built in EDN renderer) 23 | * Code highlighting CSS (for highlight.js) 24 | 25 | If you inspect `` tag of this document or of your Devcards UI you 26 | will see four ` 54 | ``` 55 | 56 | _I'm trying to keep extraneous CSS in the addons_ 57 | 58 | 59 | You can find all the orginal CSS files here: 60 | [https://github.com/bhauman/devcards/blob/master/resources/public/devcards/css](https://github.com/bhauman/devcards/blob/master/resources/public/devcards/css) 61 | 62 | It's probably best to copy and edit the original CSS if you have any 63 | tricky CSS issues. 64 | 65 | 66 | ## Highlight.js 67 | 68 | Currently Devcards inlines a 69 | custom [Highlight.js](https://highlightjs.org/) library into the head 70 | of the your Devcards UI document. If you inspect the `` of your 71 | document you will see the element with the id 72 | `com-rigsomelight-code-highlighting`. If you would like to prevent 73 | this or use another custom Highlight.js pack you can use a similar 74 | strategy to the above. 75 | 76 | You just need to include an HTML tag with the id `com-rigsomelight-code-highlighting` 77 | 78 | For example if you don't want the highlight.js code in your document 79 | at all you can add a TAG like the following to the head of 80 | your document. 81 | 82 | ```html 83 | 84 | ``` 85 | " 86 | ) 87 | 88 | -------------------------------------------------------------------------------- /example_src/devdemos/source_code_display.cljs: -------------------------------------------------------------------------------- 1 | (ns devdemos.source-code-display 2 | (:require 3 | [cljs.repl] 4 | [sablono.core :as sab :include-macros true]) 5 | (:require-macros 6 | [devcards.core :as dc :refer [defcard defcard-doc mkdn-pprint-source]])) 7 | 8 | (enable-console-print!) 9 | 10 | (defn foo [x y z] 11 | "Returns the product of x y and z." 12 | (* x y z)) 13 | 14 | (defcard 15 | "# Source Code Display 16 | 17 | There are some situations, such as tutorials like this one, where you want 18 | to display some source code. And while you can certainly cut-and-paste that 19 | code into the `defcard` docstring, there is a better approach. 20 | 21 | Assume that you have defined a function named `foo`, and you would now like 22 | to display the source code for that function. How do you get the code into 23 | your card?") 24 | 25 | (defcard 26 | "## Cut-And-Paste 27 | 28 | Here is an example where we just pasted the code directly into the docstring 29 | as markdown text. 30 | 31 | ``` 32 | (defn foo [x y z] 33 | \"Returns the product of x y and z.\" 34 | (* x y z)) 35 | ``` 36 | 37 | The problem with this approach is that it is extra work and, more 38 | importantly, the code that we have pasted into the docstring will not stay in 39 | sync with any changes we make to the actual function definition.") 40 | 41 | (defcard-doc 42 | "## mkdn-pprint-source 43 | 44 | Here we have used a cool macro to get the source for us." 45 | 46 | (dc/mkdn-pprint-source foo) 47 | 48 | "To get this same result we simply passed the `foo` function to the 49 | `dc/mkdn-pprint-source` macro. This only works inside of a `defcard-doc`, 50 | not a regular `defcard`. But it does solve our problem in that it will 51 | dynamically retrieve the source code for the `foo` function for us. The card 52 | looks something like this: 53 | ```clojure 54 | (defcard-doc 55 | \"## mkdn-pprint-source 56 | 57 | Here we have used a cool macro to get the source for us.\" 58 | 59 | (dc/mkdn-pprint-source foo) 60 | 61 | \"To get this same result ...\" 62 | ``` 63 | 64 | Now we can continue to develop and refine our `foo` function without having 65 | to worry about making corresponding changes in our devcards.") 66 | 67 | (defcard-doc 68 | "## Error Handling for mkdn-pprint-source 69 | 70 | Here we have tried to get the source for a function that does not exist." 71 | 72 | (dc/mkdn-pprint-source bar) 73 | 74 | "Above we simply passed the `bar` function to `dc/mkdn-pprint-source`. But 75 | `bar` is not recognized in this namespace.") 76 | 77 | (defcard-doc 78 | "## Any Available Source 79 | 80 | Because the `mkdn-pprint-source` makes use of the `cljs.repl` to get the 81 | source code for an object, we can use it to display the source code for any 82 | object accessible to our current namespace. For example:" 83 | 84 | (dc/mkdn-pprint-source mkdn-pprint-source) 85 | 86 | "How's that for introspection? Or this:" 87 | 88 | (dc/mkdn-pprint-source defcard) 89 | 90 | "And one final example:" 91 | 92 | (dc/mkdn-pprint-source defcard-doc)) 93 | 94 | (defcard-doc 95 | "## Almost..." 96 | 97 | "For some reason it can find this (`cljs.repl/source`):" 98 | 99 | (dc/mkdn-pprint-source cljs.repl/source) 100 | 101 | "But not this (`cljs.repl/source-fn`):" 102 | 103 | (dc/mkdn-pprint-source cljs.repl/source-fn)) 104 | -------------------------------------------------------------------------------- /resources/public/devcards/css/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original style from softwaremaniacs.org (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #f0f0f0; 12 | -webkit-text-size-adjust: none; 13 | } 14 | 15 | .hljs, 16 | .hljs-subst, 17 | .hljs-tag .hljs-title, 18 | .nginx .hljs-title { 19 | color: black; 20 | } 21 | 22 | .hljs-string, 23 | .hljs-title, 24 | .hljs-constant, 25 | .hljs-parent, 26 | .hljs-tag .hljs-value, 27 | .hljs-rule .hljs-value, 28 | .hljs-preprocessor, 29 | .hljs-pragma, 30 | .hljs-name, 31 | .haml .hljs-symbol, 32 | .ruby .hljs-symbol, 33 | .ruby .hljs-symbol .hljs-string, 34 | .hljs-template_tag, 35 | .django .hljs-variable, 36 | .smalltalk .hljs-class, 37 | .hljs-addition, 38 | .hljs-flow, 39 | .hljs-stream, 40 | .bash .hljs-variable, 41 | .pf .hljs-variable, 42 | .apache .hljs-tag, 43 | .apache .hljs-cbracket, 44 | .tex .hljs-command, 45 | .tex .hljs-special, 46 | .erlang_repl .hljs-function_or_atom, 47 | .asciidoc .hljs-header, 48 | .markdown .hljs-header, 49 | .coffeescript .hljs-attribute, 50 | .tp .hljs-variable { 51 | color: #800; 52 | } 53 | 54 | .smartquote, 55 | .hljs-comment, 56 | .hljs-annotation, 57 | .diff .hljs-header, 58 | .hljs-chunk, 59 | .asciidoc .hljs-blockquote, 60 | .markdown .hljs-blockquote { 61 | color: #888; 62 | } 63 | 64 | .hljs-number, 65 | .hljs-date, 66 | .hljs-regexp, 67 | .hljs-literal, 68 | .hljs-hexcolor, 69 | .smalltalk .hljs-symbol, 70 | .smalltalk .hljs-char, 71 | .go .hljs-constant, 72 | .hljs-change, 73 | .lasso .hljs-variable, 74 | .makefile .hljs-variable, 75 | .asciidoc .hljs-bullet, 76 | .markdown .hljs-bullet, 77 | .asciidoc .hljs-link_url, 78 | .markdown .hljs-link_url { 79 | color: #080; 80 | } 81 | 82 | .hljs-label, 83 | .ruby .hljs-string, 84 | .hljs-decorator, 85 | .hljs-filter .hljs-argument, 86 | .hljs-localvars, 87 | .hljs-array, 88 | .hljs-attr_selector, 89 | .hljs-important, 90 | .hljs-pseudo, 91 | .hljs-pi, 92 | .haml .hljs-bullet, 93 | .hljs-doctype, 94 | .hljs-deletion, 95 | .hljs-envvar, 96 | .hljs-shebang, 97 | .apache .hljs-sqbracket, 98 | .nginx .hljs-built_in, 99 | .tex .hljs-formula, 100 | .erlang_repl .hljs-reserved, 101 | .hljs-prompt, 102 | .asciidoc .hljs-link_label, 103 | .markdown .hljs-link_label, 104 | .vhdl .hljs-attribute, 105 | .clojure .hljs-attribute, 106 | .asciidoc .hljs-attribute, 107 | .lasso .hljs-attribute, 108 | .coffeescript .hljs-property, 109 | .hljs-phony { 110 | color: #88f; 111 | } 112 | 113 | .hljs-keyword, 114 | .hljs-id, 115 | .hljs-title, 116 | .hljs-built_in, 117 | .css .hljs-tag, 118 | .hljs-doctag, 119 | .smalltalk .hljs-class, 120 | .hljs-winutils, 121 | .bash .hljs-variable, 122 | .pf .hljs-variable, 123 | .apache .hljs-tag, 124 | .hljs-type, 125 | .hljs-typename, 126 | .tex .hljs-command, 127 | .asciidoc .hljs-strong, 128 | .markdown .hljs-strong, 129 | .hljs-request, 130 | .hljs-status, 131 | .tp .hljs-data, 132 | .tp .hljs-io { 133 | font-weight: bold; 134 | } 135 | 136 | .asciidoc .hljs-emphasis, 137 | .markdown .hljs-emphasis, 138 | .tp .hljs-units { 139 | font-style: italic; 140 | } 141 | 142 | .nginx .hljs-built_in { 143 | font-weight: normal; 144 | } 145 | 146 | .coffeescript .javascript, 147 | .javascript .xml, 148 | .lasso .markup, 149 | .tex .hljs-formula, 150 | .xml .javascript, 151 | .xml .vbscript, 152 | .xml .css, 153 | .xml .hljs-cdata { 154 | opacity: 0.5; 155 | } 156 | -------------------------------------------------------------------------------- /example_src/devdemos/extentions.cljs: -------------------------------------------------------------------------------- 1 | (ns devdemos.extentions 2 | (:require 3 | [devcards.core] 4 | [sablono.core :as sab :include-macros true] 5 | [cljs.test :as t :include-macros true] 6 | [om.core :as om] 7 | [reagent.core :as rg]) 8 | (:require-macros 9 | [devcards.core :as dc :refer [defcard defcard-doc deftest dom-node]])) 10 | 11 | (defcard string 12 | (str "## **string** type will render as markdown.")) 13 | 14 | (defcard persitent-array-map 15 | "**PersitentArrayMap** will be rendered as edn" 16 | {:hey "there"}) 17 | 18 | (defcard persistent-vector 19 | "**PersitentArrayMap** will be rendered as edn" 20 | [:hi]) 21 | 22 | (defcard persistent-hash-set 23 | "**PersitentHashSet** will be rendered as edn" 24 | #{ 1 2 3 }) 25 | 26 | (defcard list 27 | "**List** will be rendered as edn" 28 | '(1 2 3)) 29 | 30 | (defcard empty-list 31 | "**EmptyList** will be rendered as edn" 32 | '()) 33 | 34 | (defonce sample-atom (atom 25)) 35 | 36 | (defcard atom-card 37 | "Atom will be rendered as edn and will watch and rerender the atom when it changes. 38 | 39 | It will also set `:history true`" 40 | sample-atom) 41 | 42 | (swap! sample-atom inc) 43 | 44 | (defcard checking-meta 45 | ^{:type :ommer} (fn [a b] (sab/html [:div "ommer"])) 46 | {} 47 | {:heading 5}) 48 | 49 | (defcard om-root 50 | (dc/om-root 51 | (fn [data owner] 52 | (reify om/IRender 53 | (render [_] 54 | (sab/html [:h1 "This is om now!!!"])))))) 55 | 56 | 57 | (defn simple-component [] 58 | [:div 59 | [:p "I am a component!"] 60 | [:p.someclass 61 | "I have " [:strong "bold"] 62 | [:span {:style {:color "red"}} " and red "] "text."]]) 63 | 64 | (defcard reagent 65 | (rg/as-element simple-component)) 66 | 67 | 68 | (defonce click-count (rg/atom 0)) 69 | 70 | (defn counting-component [] 71 | [:div 72 | "The atom " [:code "click-count"] " has valuer: " 73 | @click-count ". " 74 | [:input {:type "button" :value "Click me!" 75 | :on-click #(swap! click-count inc)}]]) 76 | 77 | (defcard reagent-counter 78 | (dc/reagent (counting-component))) 79 | 80 | ;; experimenting with reloadable local state 81 | (defn elapsed-template [seconds-elapsed props] 82 | [:div 83 | [:h1 (:name props)] 84 | "Seconds Elapsed yeppers now: " @seconds-elapsed]) 85 | 86 | (defn timer-component [props] 87 | (let [seconds-elapsed (rg/atom 0)] 88 | (js/setInterval #(swap! seconds-elapsed inc) 1000) 89 | (fn [props1] 90 | (elapsed-template seconds-elapsed props)))) 91 | 92 | ;; trick to capture local state through reloads 93 | (defonce timer (rg/reactify-component timer-component)) 94 | 95 | (defn timer-app [_] 96 | [:div [:h1 "I'm a timer app"] 97 | (rg/create-element timer #js {:name "George" })]) 98 | 99 | 100 | (defonce timer-apper (rg/reactify-component timer-app)) 101 | 102 | (defcard reagent-counter-3 103 | (rg/create-element timer-apper)) 104 | 105 | (defcard reagent-locals-try 106 | "A quick way to create some stable local RAtoms" 107 | (dc/reagent 108 | (fn [data-atom _] 109 | (let [{:keys [name age]} @data-atom] 110 | [:div [:h3 "Hi there " @name] 111 | [:p "You are " @age " years old!"]]))) 112 | ;; store the needed locals in the data atom 113 | {:age (rg/atom 55) 114 | :name (rg/atom "George")}) 115 | 116 | (defn counting-component-passing-ratom [ratom] 117 | [:div 118 | "The atom " [:code "click-count"] " has valuer: " 119 | @click-count ". " 120 | [:input {:type "button" :value "Click me!" 121 | :on-click #(swap! click-count inc)}]]) 122 | 123 | (defonce temp-atom (rg/atom {:count 12})) 124 | 125 | (defcard reagent-atom-support 126 | "## We should support anything with the IAtom interface 127 | 128 | This will allow folks to use Reagent's rAtom." 129 | (dc/reagent 130 | (fn [counter-atom _] 131 | [:div "counting away " 132 | [:button {:on-click #(swap! counter-atom update-in [:count] inc)} "inc"] " " (:count @counter-atom)])) 133 | temp-atom 134 | {:inspect-data true 135 | :history true}) 136 | 137 | (defcard direct-ratom-support 138 | temp-atom) 139 | 140 | ;; tried to support reagent cursor but updates are firing during render 141 | 142 | ;; hmmm need to be smarter about watching things for cursors sake 143 | 144 | #_(defonce c (rg/cursor temp-atom [])) 145 | 146 | #_(defcard reagent-cursor c {} {:heading 5}) 147 | -------------------------------------------------------------------------------- /example-resources/public/devcards/css/two-zero.css: -------------------------------------------------------------------------------- 1 | .board-area { 2 | background-color: rgb(187,173,160); 3 | width: 499px; 4 | height: 499px; 5 | border-radius: 6px; 6 | position: relative; 7 | 8 | /* initialize as 3d */ 9 | -webkit-transform: translate3d(0,0,0); 10 | -moz-transform: translate3d(0,0,0); 11 | transform: translate3d(0,0,0); 12 | } 13 | 14 | .board-area-one-row { 15 | height: 136px; 16 | } 17 | 18 | .one-row-board .board-area { 19 | height: 136px; 20 | overflow: hidden; 21 | } 22 | 23 | .cell-pos { 24 | height: 106px; 25 | width: 106px; 26 | position:absolute; 27 | border-radius: 4px; 28 | 29 | -webkit-transition: all 0.12s; 30 | transition: all 0.12s; 31 | 32 | /* initialize as 3d */ 33 | -webkit-transform: translate3d(0,0,0); 34 | -moz-transform: translate3d(0,0,0); 35 | transform: translate3d(0,0,0); 36 | } 37 | 38 | .cell-empty { 39 | background-color: rgb(205,193,180); 40 | } 41 | 42 | .cell { 43 | background-color: rgb(205,193,180); 44 | height: 106px; 45 | width: 106px; 46 | /* position: relative; */ 47 | border-radius: 4px; 48 | font-family: "Helvetica Neue", Arial, sans-serif; 49 | font-size: 55px; 50 | line-height: 102px; 51 | font-weight: bold; 52 | text-align: center; 53 | vertical-align: middle; 54 | 55 | /* initialize as 3d */ 56 | -webkit-transform: translate3d(0,0,0); 57 | -moz-transform: translate3d(0,0,0); 58 | transform: translate3d(0,0,0); 59 | } 60 | 61 | .cell.highlight { 62 | -webkit-animation: highlight 0.1s; 63 | } 64 | 65 | @-webkit-keyframes highlight { 66 | 0% { -webkit-transform: scale3d(1.2,1.2,1.0); 67 | opacity: 0.7;} 68 | 100% { -webkit-transform: scale3d(1.0,1.0,1.0); 69 | opacity: 1.0;} 70 | } 71 | 72 | .cell.reveal { 73 | -webkit-animation: reveal 0.1s; 74 | } 75 | 76 | @-webkit-keyframes reveal { 77 | 0% { -webkit-transform: scale3d(0.1,0.1,1.0); 78 | opacity: 0.1; 79 | } 80 | 100% { -webkit-transform: scale3d(1.0,1.0,1.0); 81 | opacity: 1.0;} 82 | } 83 | 84 | .pos-top-0 { top: 15px; } 85 | .pos-top-1 { top: 136px; } 86 | .pos-top-2 { top: 257px; } 87 | .pos-top-3 { top: 378px; } 88 | 89 | .pos-left-0 { left: 15px; } 90 | .pos-left-1 { left: 136px; } 91 | .pos-left-2 { left: 257px; } 92 | .pos-left-3 { left: 378px; } 93 | 94 | 95 | 96 | .cell-num-2 { 97 | background-color: rgb(238, 228,218); 98 | color: rgb(110,102,93); 99 | } 100 | 101 | .cell-num-4 { 102 | background-color: rgb(237, 224,200); 103 | color: rgb(119,110,101); 104 | } 105 | 106 | .cell-num-8 { 107 | background-color: rgb(242, 177, 121); 108 | color: rgb(249,246,242); 109 | } 110 | 111 | .cell-num-16 { 112 | background-color: rgb(245, 149, 99); 113 | color: rgb(249,246,242); 114 | } 115 | 116 | .cell-num-32 { 117 | background-color: rgb(245, 124, 95); 118 | color: rgb(249,246,242); 119 | } 120 | 121 | .cell-num-64 { 122 | background-color: rgb(246, 94, 59); 123 | color: rgb(249,246,242); 124 | } 125 | 126 | .cell-num-128 { 127 | background-color: rgb(237, 207,114); 128 | color: rgb(249,246,242); 129 | font-size: 48px; 130 | } 131 | 132 | .cell-num-256 { 133 | background-color: rgb(237, 204, 97); 134 | color: rgb(249,246,242); 135 | font-size: 48px; 136 | border: 1px solid rgba(238, 228, 218, 0.5); 137 | box-shadow: 0 0 25px 5px rgb(237, 204, 97); 138 | } 139 | 140 | .cell-num-512 { 141 | background-color: rgb(237, 204, 97); 142 | color: rgb(249,246,242); 143 | font-size: 48px; 144 | border: 1px solid rgba(238, 228, 218, 0.5); 145 | box-shadow: 0 0 25px 5px rgb(237, 204, 97); 146 | } 147 | 148 | .cell-num-1024 { 149 | background-color: rgb(237, 204, 97); 150 | color: rgb(249,246,242); 151 | font-size: 40px; 152 | border: 1px solid rgba(238, 228, 218, 0.5); 153 | box-shadow: 0 0 25px 5px rgb(237, 204, 97); 154 | } 155 | 156 | .cell-num-2048 { 157 | background-color: rgb(237, 204, 97); 158 | color: rgb(249,246,242); 159 | font-size: 40px; 160 | border: 1px solid rgba(238, 228, 218, 0.5); 161 | box-shadow: 0 0 25px 5px rgb(237, 204, 97); 162 | } 163 | 164 | 165 | @media (max-width: 480px){ 166 | .board-area { 167 | width: 280px; 168 | height: 280px; 169 | } 170 | .cell-pos, .cell { 171 | width: 60px; 172 | height: 60px; 173 | } 174 | 175 | .cell { 176 | font-size: 25px; 177 | line-height: 60px; 178 | } 179 | 180 | .cell-num-128, .cell-num-256, .cell-num-512 { 181 | font-size: 26px; 182 | } 183 | 184 | .cell-num-1024, .cell-num-2048 { 185 | font-size: 21px; 186 | } 187 | 188 | .pos-top-0 { top: 8px; } 189 | .pos-top-1 { top: 76px; } 190 | .pos-top-2 { top: 144px; } 191 | .pos-top-3 { top: 212px; } 192 | 193 | .pos-left-0 { left: 8px; } 194 | .pos-left-1 { left: 76px; } 195 | .pos-left-2 { left: 144px; } 196 | .pos-left-3 { left: 212px; } 197 | 198 | } 199 | 200 | 201 | -------------------------------------------------------------------------------- /example_src/devdemos/reagent.cljs: -------------------------------------------------------------------------------- 1 | (ns devdemos.reagent 2 | (:require 3 | [devcards.core] 4 | [reagent.core :as reagent]) 5 | (:require-macros 6 | [devcards.core :as dc :refer [defcard defcard-doc]])) 7 | 8 | ;; util 9 | 10 | (defn on-click [ratom] 11 | (swap! ratom update-in [:count] inc)) 12 | 13 | (defcard-doc 14 | " 15 | ## Rendering Reagent components 16 | 17 | Note: The following examples assume a namespace that looks like this: 18 | 19 | ```clojure 20 | (ns xxx 21 | (:require [devcards.core] 22 | [reagent.core :as reagent]) 23 | (:require-macros [devcards.core :as dc 24 | :refer [defcard]])) 25 | ``` 26 | ") 27 | 28 | ;; counter 1 29 | 30 | (defonce counter1-state (reagent/atom {:count 0})) 31 | 32 | (defn counter1 [] 33 | [:div "Current count: " (@counter1-state :count) 34 | [:div 35 | [:button {:on-click #(on-click counter1-state)} 36 | "Increment"]]]) 37 | 38 | (defcard counter1 39 | " 40 | ## Counter1 (Basic) 41 | 42 | The simplest way to create a reagent devcard is to pass the `defcard` macro a single argument: 43 | 44 | 1) a reagent component (i.e., `counter1`) wrapped by devcard.core's `reagent` *macro* (i.e., `dc/reagent`) 45 | 46 | ```clojure 47 | (defn on-click [ratom] 48 | (swap! ratom update-in [:count] inc)) 49 | 50 | (defonce counter1-state (reagent/atom {:count 0})) 51 | 52 | (defn counter1 [] 53 | [:div \"Current count: \" (@counter1-state :count) 54 | [:div 55 | [:button {:on-click #(on-click counter1-state)} 56 | \"Increment\"]]]) 57 | 58 | (defcard counter1 59 | (dc/reagent counter1)) ;; <--1 60 | ``` 61 | " 62 | (dc/reagent counter1)) 63 | 64 | ;; counter 2 65 | 66 | (defonce counter2-state (reagent/atom {:count 0})) 67 | 68 | (defn counter2 [] 69 | [:div "Current count: " (@counter2-state :count) 70 | [:div 71 | [:button {:on-click #(on-click counter2-state)} 72 | "Increment"]]]) 73 | 74 | (defcard counter2 75 | " 76 | ## Counter 2 (Displaying the state of reagent atom/cursor) 77 | 78 | However, wouldn't it be nice to see the state of our component? To 79 | accomplish this, we can pass the following arguments to the defcard 80 | macro: 81 | 82 | 1) a reagent component (i.e., `counter2`) wrapped by devcard.core's `reagent` *macro* (i.e., `dc/reagent`) 83 | 84 | 2) the reagent atom (or cursor) that holds the state of our component (i.e., `counter2-state`) 85 | 86 | 3) a hash-map of options, where we set inspect-data to true (i.e., `{:inspect-data :true}`} 87 | 88 | ```clojure 89 | (defn on-click [ratom] 90 | (swap! ratom update-in [:count] inc)) 91 | 92 | (defonce counter2-state (reagent/atom {:count 0})) 93 | 94 | (defn counter2 [] 95 | [:div \"Current count: \" (@counter2-state :count) 96 | [:div 97 | [:button {:on-click #(on-click counter2-state)} 98 | \"Increment\"]]]) 99 | 100 | (defcard counter2 101 | (dc/reagent counter2) ;; <-- 1 102 | counter2-state ;; <-- 2 103 | {:inspect-data true} ;; <-- 3 104 | ) 105 | ``` 106 | " 107 | (dc/reagent counter2) ;; <-- 1 108 | counter2-state ;; <-- 2 109 | {:inspect-data true} ;; <-- 3 110 | ) 111 | 112 | ;; counter 3 113 | 114 | (defonce counter3-state (reagent/atom {:count 0})) 115 | 116 | (defn counter3 [ratom] 117 | [:div "Current count: " (@ratom :count) 118 | [:div 119 | [:button {:on-click #(on-click ratom)} 120 | "Increment"]]]) 121 | 122 | (defcard counter3 123 | " 124 | ## Counter 3 (Passing in an argument to the reagent component) 125 | 126 | At this point, you may be wondering, *how do we pass in arguments to 127 | the reagent component itself?* All you have to do is pass in your 128 | reagent component to defcard.core's reagent macro in *square brackets*. 129 | 130 | ```clojure 131 | (defn on-click [ratom] 132 | (swap! ratom update-in [:count] inc)) 133 | 134 | (defonce counter3-state (reagent/atom {:count 0})) 135 | 136 | (defn counter3 [ratom] ;; <-- counter2 expects one argument 137 | [:div \"Current count: \" (@ratom :count) 138 | [:div 139 | [:button {:on-click #(on-click ratom)} 140 | \"Increment\"]]]) 141 | 142 | (defcard counter3 143 | (dc/reagent [counter3 counter3-state]) ;; <-- passing in a ratom (counter3-state) to our reagent component (counter3) 144 | counter3-state ;; <-- notice that we are *still* passing in a 2nd argument to defcard! 145 | {:inspect-data true} 146 | ) 147 | ``` 148 | " 149 | (dc/reagent [counter3 counter3-state]) 150 | counter3-state 151 | {:inspect-data true} 152 | ) 153 | 154 | ;; counter 4 155 | 156 | (defonce counter4-state (reagent/atom {:count 0})) 157 | 158 | (defn counter4 [ratom 159 | {:keys [title button-text]}] 160 | [:div [:h3 title] 161 | [:div "Current count: " (@ratom :count)] 162 | [:div [:button {:on-click #(on-click ratom)} 163 | button-text]]]) 164 | 165 | (defcard counter4 166 | " 167 | ## Counter 4 (Passing in multiple arguments to the reagent component) 168 | 169 | We can pass in an arbitray number of arguments to our reagent component if we wrap it in square brackets. 170 | 171 | ```clojure 172 | (defn on-click [ratom] 173 | (swap! ratom update-in [:count] inc)) 174 | 175 | (defonce counter4-state (reagent/atom {:count 0})) 176 | 177 | (defn counter4 [ratom 178 | {:keys [title button-text]}] ;; <-- counter4 expects two arguments: a ratom, and a hash-map 179 | [:div [:h3 title] 180 | [:div \"Current count: \" (@ratom :count)] 181 | [:div [:button {:on-click #(on-click ratom)} 182 | button-text]]]) 183 | 184 | (defcard counter4 185 | (dc/reagent [counter4 counter4-state 186 | {:title \"Counter 4\" 187 | :button-text \"INCREMENT\"}]) ;; <-- passing in two arguments to our reagent component 188 | counter4-state ;; <-- notice that we are *still* passing in a 2nd argument to defcard! 189 | {:inspect-data true} 190 | ) 191 | ``` 192 | " 193 | (dc/reagent [counter4 counter4-state {:title "Counter 4" 194 | :button-text "INCREMENT"}]) 195 | counter4-state 196 | {:inspect-data true} 197 | ) 198 | -------------------------------------------------------------------------------- /src/devcards/util/markdown.cljs: -------------------------------------------------------------------------------- 1 | (ns devcards.util.markdown 2 | (:require 3 | [clojure.string :as string] 4 | [cljsjs.showdown])) 5 | 6 | (defn leading-space-count [s] 7 | (when-let [ws (second (re-matches #"^([\s]*).*" s))] 8 | (.-length ws))) 9 | 10 | (let [conv-class (.-converter js/Showdown) 11 | converter (conv-class.)] 12 | (defn markdown-to-html 13 | "render markdown" 14 | [markdown-txt] 15 | (.makeHtml converter markdown-txt))) 16 | 17 | (defn matches-delim? [line] 18 | (re-matches #"^[\s]*```(\w*).*" line)) 19 | 20 | (defmulti block-parser 21 | (fn [{:keys [stage]} line] 22 | [(if (matches-delim? line) :delim :line) (:type stage)])) 23 | 24 | (defmethod block-parser [:line :markdown] [{:keys [stage] :as st} line] 25 | (update-in st [:stage :content] conj (string/trim line))) 26 | 27 | (defmethod block-parser [:line :code-block] [{:keys [stage] :as st} line] 28 | (update-in st [:stage :content] conj (subs line (:leading-spaces stage)))) 29 | 30 | (defmethod block-parser [:delim :markdown] [{:keys [stage accum] :as st} line];; enter block 31 | (let [lang (second (matches-delim? line))] 32 | (-> st ;; the beginning 33 | (assoc :accum (conj accum stage)) 34 | (assoc :stage 35 | {:type :code-block 36 | :lang (when-not (string/blank? lang) lang) 37 | :leading-spaces (leading-space-count line) 38 | :content []})))) 39 | 40 | (defmethod block-parser [:delim :code-block] [{:keys [stage accum] :as st} line];; enter block 41 | (-> st ;; the end 42 | (assoc :accum (conj accum stage)) 43 | (assoc :stage {:type :markdown :content []}))) 44 | 45 | (defn parse-out-blocks* [m] 46 | (reduce block-parser 47 | {:stage {:type :markdown :content []} :accum []} 48 | (string/split m "\n"))) 49 | 50 | (defn parse-out-blocks [m] 51 | (let [{:keys [stage accum]} (parse-out-blocks* m)] 52 | (->> (conj accum stage) 53 | (filter (fn [{:keys [content]}] (not-empty content))) 54 | (map (fn [x] (update-in x [:content] #(string/join "\n" %))))))) 55 | 56 | #_(devcards.core/defcard parse-out-code-blocks3 57 | (parse-out-blocks 58 | " ```langer 59 | (defcard bmi-calculator ;; optional symbol name 60 | \"*Code taken from Reagent readme.*\" ;; optional markdown doc 61 | (fn [data-atom _] (bmi-component data-atom)) ;; object of focus 62 | {:height 180 :weight 80} ;; optional initial data 63 | {:inspect-data true :history true}) ;; optional devcard config options 64 | 65 | ``` 66 | # [Devcards](https://github.com/bhauman/devcards): the hard sell 67 | 68 | The Devcards library is intended to make ClojureScript development 69 | a pure joy. 70 | 71 | Devcards are intended to facilitate **interactive live 72 | development**. Devcards can be used in conjunction with figwheel but 73 | will also work with any form of live code reloading (repl, boot-reload, ...) 74 | 75 | Devcards revolves around a multi-purpose macro called `defcard`. 76 | You can think of `defcard` as a powerful form of **pprint** that helps you 77 | interactively lift code examples out of your source files into the 78 | Devcards interface (you are currently looking at the Devcards 79 | interface). 80 | 81 | The Devcards that you create are intended to have no impact on the 82 | size of your production code. You can use Devcards just as you 83 | would use exectuable comments inline with your source code. You 84 | can also keep them separate like a test suite. 85 | 86 | With [figwheel](https://github.com/bhauman/lein-figwheel), Devcards 87 | configuration couldn't be simpler. Just add `[devcards 88 | \"0.2.0-SNAPSHOT\"]` and create a new build config with `:figwheel 89 | {:devcards true}`. See the Quick Start instructions at the end of 90 | this document. 91 | 92 | Let's look at an advanced Devcard: 93 | 94 | ``` 95 | (defcard bmi-calculator ;; optional symbol name 96 | \"*Code taken from Reagent readme.*\" ;; optional markdown doc 97 | (fn [data-atom _] (bmi-component data-atom)) ;; object of focus 98 | {:height 180 :weight 80} ;; optional initial data 99 | {:inspect-data true :history true}) ;; optional devcard config options 100 | 101 | ``` 102 | 103 | The [defcard api](#!/devdemos.defcard_api) 104 | is intended to be small and intuitive. 105 | 106 | And you can see this devcard rendered below:")) 107 | 108 | 109 | #_(devcards.core/defcard parse-out-code-blocks3 110 | (parse-out-blocks 111 | "# [Devcards](https://github.com/bhauman/devcards): the hard sell 112 | 113 | The Devcards library is intended to make ClojureScript development 114 | a pure joy. 115 | 116 | Devcards are intended to facilitate **interactive live 117 | development**. Devcards can be used in conjunction with figwheel but 118 | will also work with any form of live code reloading (repl, boot-reload, ...) 119 | 120 | Devcards revolves around a multi-purpose macro called `defcard`. 121 | You can think of `defcard` as a powerful form of **pprint** that helps you 122 | interactively lift code examples out of your source files into the 123 | Devcards interface (you are currently looking at the Devcards 124 | interface). 125 | 126 | The Devcards that you create are intended to have no impact on the 127 | size of your production code. You can use Devcards just as you 128 | would use exectuable comments inline with your source code. You 129 | can also keep them separate like a test suite. 130 | 131 | With [figwheel](https://github.com/bhauman/lein-figwheel), Devcards 132 | configuration couldn't be simpler. Just add `[devcards 133 | \"0.2.0-SNAPSHOT\"]` and create a new build config with `:figwheel 134 | {:devcards true}`. See the Quick Start instructions at the end of 135 | this document. 136 | 137 | Let's look at an advanced Devcard: 138 | 139 | ``` 140 | (defcard bmi-calculator ;; optional symbol name 141 | \"*Code taken from Reagent readme.*\" ;; optional markdown doc 142 | (fn [data-atom _] (bmi-component data-atom)) ;; object of focus 143 | {:height 180 :weight 80} ;; optional initial data 144 | {:inspect-data true :history true}) ;; optional devcard config options 145 | 146 | ``` 147 | 148 | The [defcard api](#!/devdemos.defcard_api) 149 | is intended to be small and intuitive. 150 | 151 | And you can see this devcard rendered below:")) 152 | -------------------------------------------------------------------------------- /example_src/devdemos/testing.cljs: -------------------------------------------------------------------------------- 1 | (ns devdemos.testing 2 | (:require 3 | [devcards.core :as devcards] 4 | [cljs.test :as t :refer [report] :include-macros true] 5 | [sablono.core :as sab] 6 | [cljs.core.async :refer [ You do not want to interleave testing runs! 35 | 36 | Tests defined with `defcards.core/deftest`run asynchronously and rely 37 | on a scheduler. If you make a call to `cljs.test/run-tests` while the 38 | tests in the cards are running there is a real possibility that your 39 | test runs will trample all over eachother. 40 | 41 | If you want to schedule a standard `cljs.test` test run in the same 42 | process as the Devcards tests are running in, you will need to have 43 | them run on after a safe time interval has passed to prevent 44 | interleaving test executions. (I might include a way to schedule the 45 | running of some standard tests on the scheduler in the future ...) 46 | 47 | The following is an example of using `devcards.core/deftest` 48 | " 49 | (dc/mkdn-pprint-code 50 | '(deftest first-testers 51 | "## This is documentation 52 | It should work well" 53 | (testing "good stuff" 54 | (is (= (+ 3 4 55555) 5) "Testing the adding") 55 | (is (= (+ 1 0 0 0) 1) "This should work") 56 | (is (= 1 3)) 57 | (is false) 58 | (is (throw "heck")) 59 | (is (js/asdf))) 60 | "## And here is more documentation" 61 | (testing "bad stuff" 62 | (is (= (+ 1 0 0 0) 1)) 63 | (is (= (+ 3 4 55555) 4)) 64 | (is false) 65 | (testing "mad stuff" 66 | (is (= (+ 1 0 0 0) 1)) 67 | (is (= (+ 3 4 55555) 4)) 68 | (is false))))) 69 | 70 | "And you can see this rendered below:") 71 | 72 | (dc/deftest first-testers 73 | "## This is documentation 74 | It should work well" 75 | (testing "good stuff" 76 | (is (= (+ 3 4 55555) 5) "Testing the adding") 77 | (is (= (+ 1 0 0 0) 1) "This should work") 78 | (is (= 1 3)) 79 | (is false) 80 | (is (throw "heck")) 81 | (is (js/asdf))) 82 | "## And here is more documentation" 83 | (testing "bad stuff" 84 | (is (= (+ 1 0 0 0) 1)) 85 | (is (= (+ 3 4 55555) 4)) 86 | (is false) 87 | (testing "mad stuff" 88 | (is (= (+ 1 0 0 0) 1)) 89 | (is (= (+ 3 4 55555) 4)) 90 | (is false)))) 91 | 92 | (defcard "## Checking the case where there are no tests 93 | 94 | Just creating an exmple to display the empty case where no tests are 95 | supplied to `devcards.core/deftest`. 96 | 97 | ```clojure 98 | (devcards.core/deftest no-tests) 99 | ``` 100 | 101 | When you pass 0 tests to `devcards.core/deftest` 102 | it should just render a heading with a counter of zero. 103 | ") 104 | 105 | (dc/deftest no-tests) 106 | 107 | (defcard 108 | "# Async testing 109 | 110 | Devcards supports standard `cljs.test` async testing. 111 | 112 | If you look at the source for the examples below you will see async 113 | testing in use. 114 | 115 | During async tests exceptions and errors are much more difficult to 116 | catch in the testing system each. For this reason 117 | `devcards.core/deftest` has an execution timeout, that will add an 118 | error indicating that the execution of the tests did not complete in 119 | time. This allows us to continue on with the rest of the tests even 120 | when an exception interupts the process. There is a chance that the 121 | timeout is too small for your tests, this can cause test run 122 | interleaving which can corrupt your test results. 123 | 124 | In this case you will want to increase the timeout. 125 | 126 | You can set the timeout in milliseconds as so: 127 | 128 | ```clojure 129 | (set! devcards.core/test-timeout 800) ;; 800 is the default value 130 | ``` 131 | 132 | You can see an example of this below. 133 | 134 | Notice that the last test says `Error: Tests timed out.` This normally 135 | indicates that your async tests threw an exception. 136 | 137 | All the tests after that exception will not be run. 138 | ") 139 | 140 | (set! devcards.core/test-timeout 800) 141 | 142 | (dc/deftest async-tester 143 | "## This is an async test 144 | You should see some tests here" 145 | 146 | (t/testing "Let's run async tests!" 147 | (is (= (+ 3 4 55555) 4) "Testing the adding") 148 | (is (= (+ 1 0 0 0) 1) "This should work") 149 | (is (= 1 3)) 150 | (is true) 151 | (async done 152 | (go 153 | ( env :ns :name name munge)) 27 | 28 | (defn name->path [env vname] 29 | [(keyword (get-ns env)) (keyword vname)]) 30 | 31 | ;; it's nice to have this low level card i think 32 | (defmacro defcard* 33 | ([vname expr] 34 | (when (utils/devcards-active?) 35 | `(devcards.core/register-card ~{:path (name->path &env vname) 36 | :func `(fn [] ~expr)})))) 37 | 38 | (defn card 39 | ([vname docu main-obj initial-data options] 40 | `(devcards.core/defcard* ~(symbol (name vname)) 41 | (devcards.core/card-base 42 | { :name ~(name vname) 43 | :documentation ~docu 44 | :main-obj ~main-obj 45 | :initial-data ~initial-data 46 | :options ~options}))) 47 | ([vname docu main-obj initial-data] 48 | (card vname docu main-obj initial-data {})) 49 | ([vname docu main-obj] 50 | (card vname docu main-obj {} {})) 51 | ([vname docu] 52 | (card vname docu nil {} {}))) 53 | 54 | (defn optional-name [exprs default-name] 55 | (if (instance? clojure.lang.Named (first exprs)) [(first exprs) (rest exprs)] 56 | [default-name exprs])) 57 | 58 | (defn optional-doc [xs] 59 | (if (string? (first xs)) [(first xs) (rest xs)] [nil xs])) 60 | 61 | (defn parse-args [xs default-name] 62 | (let [[vname xs] (optional-name xs default-name) 63 | [docu xs] (optional-doc xs)] 64 | (concat [vname docu] xs))) 65 | 66 | (defn merge-options [lit-opt-map options] 67 | `(merge ~lit-opt-map (devcards.core/assert-options-map ~options))) 68 | 69 | (defn parse-card-args [xs default-name] 70 | (let [[vname docu main-obj initial-data options :as res] 71 | (parse-args xs default-name)] 72 | (if (= vname default-name) 73 | [vname docu main-obj initial-data (merge-options {:heading false} options)] 74 | res))) 75 | 76 | (defmacro defcard [& expr] 77 | (when (utils/devcards-active?) 78 | (apply devcards.core/card (parse-card-args expr 'card)))) 79 | 80 | (defmacro dom-node [body] 81 | (when (utils/devcards-active?) 82 | `(devcards.core/dom-node* ~body))) 83 | 84 | (defmacro hist-recorder [body] 85 | (when (utils/devcards-active?) 86 | `(devcards.core/hist-recorder* ~body))) 87 | 88 | ;; should probably get rid of this 89 | ;; there is a prize for a leaner api 90 | (defmacro doc [& body] 91 | (when (utils/devcards-active?) 92 | `(devcards.core/markdown->react ~@body))) 93 | 94 | ;; should probably get rid of this as well 95 | (defmacro edn [body] 96 | (when (utils/devcards-active?) 97 | `(devcards.util.edn-renderer/html-edn ~body))) 98 | 99 | ;; is this really needed now? 100 | (defmacro defcard-doc [& exprs] 101 | (when (utils/devcards-active?) 102 | `(devcards.core/defcard (doc ~@exprs) {} {:hide-border true}))) 103 | 104 | ;; this really needs to go now 105 | (defmacro noframe-doc [& exprs] 106 | (when (utils/devcards-active?) 107 | `(devcards.core/defcard (doc ~@exprs) {} {:frame false}))) 108 | 109 | ;; currently reflects the most common pattern for creating idevcards 110 | ;; currently to meant to only be consumed internally 111 | (defmacro create-idevcard [main-obj-body default-options-literal] 112 | (when (utils/devcards-active?) 113 | `(reify devcards.core/IDevcardOptions 114 | (~'-devcard-options [this# devcard-opts#] 115 | (assoc devcard-opts# 116 | :main-obj ~main-obj-body 117 | 118 | :options (merge ~default-options-literal 119 | (devcards.core/assert-options-map (:options devcard-opts#)))))))) 120 | 121 | ;; testing 122 | 123 | (defmacro tests [& parts] 124 | (when (utils/devcards-active?) 125 | `(devcards.core/test-card 126 | ~@(map (fn [p] (if (string? p) 127 | `(fn [] (devcards.core/test-doc ~p)) 128 | `(fn [] ~p))) parts)))) 129 | 130 | (defmacro deftest [vname & parts] 131 | `(do 132 | ~(when (utils/devcards-active?) 133 | `(devcards.core/defcard ~vname 134 | (devcards.core/tests ~@parts))) 135 | (cljs.test/deftest ~vname 136 | ~@parts))) 137 | 138 | ;; reagent helpers 139 | 140 | (defmacro reagent->react [body] 141 | `(js/React.createElement (reagent.core/reactify-component (fn [_#] ~body)))) 142 | 143 | (defmacro reagent [body] 144 | `(create-idevcard (let [v# ~body] 145 | (if (fn? v#) 146 | (fn [data-atom# owner#] (reagent->react (v# data-atom# owner#))) 147 | (reagent->react v#))) 148 | {:watch-atom false})) 149 | 150 | ;; om helpers 151 | 152 | (defmacro om-root 153 | ([om-comp-fn om-options] 154 | (when (utils/devcards-active?) 155 | `(create-idevcard 156 | (devcards.core/dom-node* 157 | (fn [data-atom# node#] 158 | (om.core/root ~om-comp-fn data-atom# 159 | (merge ~om-options 160 | {:target node#})))) 161 | {:watch-atom true}))) 162 | ([om-comp-fn] 163 | (when (utils/devcards-active?) 164 | `(om-root ~om-comp-fn {})))) 165 | 166 | (defmacro defcard-om [& exprs] 167 | (when (utils/devcards-active?) 168 | (let [[vname docu om-comp-fn initial-data om-options options] (parse-card-args exprs 'om-root-card)] 169 | (card vname docu `(om-root ~om-comp-fn ~om-options) initial-data options)))) 170 | 171 | ;; formatting for markdown cards 172 | 173 | (defmacro pprint-str [obj] 174 | (when (utils/devcards-active?) 175 | `(devcards.util.utils/pprint-str ~obj))) 176 | 177 | (defmacro pprint-code [obj] 178 | (when (utils/devcards-active?) 179 | `(devcards.util.utils/pprint-code ~obj))) 180 | 181 | (defmacro mkdn-code [body] `(str "\n```clojure\n" ~body "\n```\n")) 182 | 183 | (defmacro mkdn-pprint-code [obj] 184 | (when (utils/devcards-active?) 185 | `(mkdn-code 186 | (devcards.util.utils/pprint-code ~obj)))) 187 | 188 | (defmacro mkdn-pprint-source [obj] 189 | (when (utils/devcards-active?) 190 | `(mkdn-code 191 | ~(or (cljs.repl/source-fn &env obj) (str "Source not found"))))) 192 | 193 | (defmacro mkdn-pprint-str [obj] 194 | (when (utils/devcards-active?) 195 | `(mkdn-code 196 | (devcards.util.utils/pprint-str ~obj)))) 197 | 198 | (defmacro all-front-matter-meta [filter-keyword] 199 | (vec 200 | (filter 201 | (or filter-keyword :front-matter) 202 | (map 203 | (fn [x] (assoc (meta x) 204 | :namespace `(quote ~x) 205 | :munged-namespace `(quote ~(munge x)))) 206 | (ana-api/all-ns))))) 207 | 208 | -------------------------------------------------------------------------------- /example_src/devdemos/custom_cards.cljs: -------------------------------------------------------------------------------- 1 | (ns devdemos.custom-cards 2 | (:require 3 | [devcards.core :as dc] 4 | [sablono.core :as sab :include-macros true]) 5 | (:require-macros 6 | [cljs-react-reload.core :refer [defonce-react-class]] 7 | [devcards.core :refer [defcard defcard-doc]])) 8 | 9 | (defcard 10 | "# Extending Devcards 11 | 12 | There are several ways to get custom behavior out of devcards. 13 | 14 | * create a React Component 15 | * create an instance of `devcards.core/IDevcardOptions` 16 | * create an instance of `devcards.core/IDevcard` 17 | 18 | Implementing a React Component is the most straightforward way to 19 | create some tooling on top of Devcards. 20 | 21 | Here is a sketch of such usage: 22 | 23 | ```clojure 24 | (defn graph-state-overtime [state-atom state-filter] 25 | ... 26 | code that does some cool graphing 27 | ...) 28 | 29 | (defcard state-over-time-view 30 | (fn [state _] 31 | ;; return a ReactElement 32 | (sab/html 33 | [:div 34 | (your-component state) 35 | (graph-state-overtime state :x)])) 36 | ;; initial state 37 | {:x 0}) 38 | ``` 39 | 40 | You can create a macro to make the above composition much more 41 | convenient to use. 42 | 43 | ```clojure 44 | ;; in a clj namespace 45 | (defmacro def-state-plot-card [vname component init-state filter-fn] 46 | `(defcard ~vname 47 | (fn [state# _] 48 | (sab/html 49 | [:div 50 | (~component state) 51 | (graph-state-overtime state# ~filter-fn)]]) 52 | ~init-state)) 53 | 54 | ;; use this in a cljs namespace 55 | (def-state-plot-card cool-state-plot 56 | my-component 57 | {:x 0} 58 | :x) 59 | ``` 60 | 61 | ## Reaching into Devcards arguments with IDevcardOptions 62 | 63 | Using React components works great if you are focussing on the content 64 | area of a card. But sometimes you want to leverage the aregument 65 | parsing of the `defcard` macro and the functionality of the base 66 | devcard system itself. 67 | 68 | If you want to intercept the arguments that the `defcard` macro has 69 | parsed and alter them. You can do this with 70 | `devcards.core/IDevcardOptions`. 71 | 72 | Let's use `IDevcardOptions` to discover the options that are passed 73 | to its protocol method `-devcard-options`. 74 | 75 | ```clojure 76 | (defcard devcard-options-example-name 77 | \"Devcard options documentation.\" 78 | (reify dc/IDevcardOptions 79 | (-devcard-options [_ opts] 80 | (assoc opts :main-obj opts))) ;; <-- alter :main-obj to be the passed in opts 81 | {:devcards-options-init-state true}) 82 | ``` 83 | 84 | This is a bit subtle but implementing `IDevcardOptions` allows us to 85 | intercept the arguments that have been passed to the original 86 | `defcard`. You can see resulting card rendered below: 87 | ") 88 | 89 | (defcard devcard-options-example-namee 90 | "Devcard options documentation." 91 | (reify dc/IDevcardOptions 92 | (-devcard-options [_ opts] 93 | (assoc opts :main-obj opts))) 94 | {:devcards-options-init-state true}) 95 | 96 | (defcard 97 | "As you can see above, we are intercepting the options that were 98 | parsed out by the `defcard` macro and we have a chance to operate on 99 | them before they are sent to the underlying system. 100 | 101 | These options are: 102 | 103 | * `:name` - the name of the card (changing this affects nothing) 104 | * `:documentation` - the docs associated with the card 105 | * `:main-obj` - the main object that is subject of display 106 | * `:initial-data` - the initial data for the card state atom 107 | * `:options` - the options for the devcard system (like `:inspect-data true`) 108 | * `:path` - the path to the card in the devcards interface (normally `[ns var]`) 109 | 110 | You can alter any of these args before returning the `opts`. 111 | 112 | Here is an example where we change the path name of a card. 113 | 114 | ```clojure 115 | (defcard example-2 116 | (reify dc/IDevcardOptions 117 | (-devcard-options [_ opts] 118 | (assoc opts :path 119 | [:devdemos.custom_cards 120 | :this-is-a-changed-path-name])))) 121 | ``` 122 | 123 | And you can see this card rendered below: 124 | 125 | ") 126 | 127 | 128 | (defcard example-2 129 | (reify dc/IDevcardOptions 130 | (-devcard-options [_ opts] 131 | (assoc opts 132 | :path 133 | [:devdemos.custom_cards 134 | :this-is-a-changed-path-name])))) 135 | 136 | (defcard 137 | "The above card's heading has been altered from `example-2` to 138 | `this-is-a-changed-path-name`. We could have changed the `ns` part 139 | of the path name but then the card would no longer be on this page! 140 | 141 | You may have noticed that we are getting a JavaScript Object of some sort 142 | rendered in the body of the card. This is because the `:main-obj` is 143 | the reified instance of IDevcardOptions. This is where the magic comes in, you can 144 | specify any `:main-obj` you like. 145 | 146 | Here's an example where we create a `state-reset` control that we can 147 | use in our cards. 148 | 149 | ```clojure 150 | (defn state-reset [component] 151 | (reify dc/IDevcardOptions 152 | (-devcard-options [_ opts] 153 | (assoc opts 154 | :main-obj 155 | (fn [state owner] 156 | (sab/html 157 | [:div 158 | [:button 159 | {:onClick 160 | (fn [] 161 | (reset! state (:initial-data opts)))} 162 | \"reset state\"] 163 | (if (fn? component) 164 | (component state owner) 165 | component)])))))) 166 | 167 | (defn counter [state] 168 | (sab/html 169 | [:div 170 | [:h3 \"Counter : \" (:count @state)] 171 | [:button {:onClick #(swap! state (fn [s] (update-in s [:count] inc)))} 172 | \"inc\"]])) 173 | 174 | (defcard counter-example 175 | (state-reset (fn [state _] (counter state))) 176 | {:count 0} 177 | {:inspect-data true}) 178 | ``` 179 | 180 | You can see the resulting card below. You can increment the example 181 | counter and then easily reset the state of the counter to the initial 182 | value. 183 | ") 184 | 185 | (defn state-reset [component] 186 | (reify dc/IDevcardOptions 187 | (-devcard-options [_ opts] 188 | (assoc opts 189 | :main-obj 190 | (fn [state owner] 191 | (sab/html 192 | [:div 193 | [:button 194 | {:onClick 195 | (fn [] 196 | (reset! state (:initial-data opts)))} 197 | "reset state"] 198 | (if (fn? component) 199 | (component state owner) 200 | component)])))))) 201 | 202 | (defn counter [state] 203 | (sab/html 204 | [:div 205 | [:h3 "Counter : " (:count @state)] 206 | [:button {:onClick #(swap! state (fn [s] (update-in s [:count] inc)))} 207 | "inc"]])) 208 | 209 | (defcard counter-example 210 | (state-reset (fn [state _] (counter state))) 211 | {:count 0} 212 | {:inspect-data true}) 213 | 214 | (defcard 215 | "There are other ways to accomplish this same goal. I just wanted to 216 | demonstrate that `IDevcardOptions` can offer a bit of flexibility 217 | when creating tools for devcards. 218 | 219 | Don't forget you can wrap all of this in a macro to make expressive 220 | consice tools for your workflow.") 221 | 222 | (defcard 223 | "## The `IDevcard` protocol 224 | 225 | If you want to escape the base functionality of Devcards and render 226 | your own card with it's own functionality. The `devcards.core/IDevcard` 227 | protocol can help. 228 | 229 | The protocal is simple you just need to implement a function called 230 | `devcards.core/-devcard` and return a `ReactElement`. 231 | 232 | ```clojure 233 | (defcard 234 | (reify dc/IDevcard 235 | (-devcard [_ opts] 236 | (sab/html [:h3 \"This is a card without any base Devcard functionality\"])))) 237 | ``` 238 | ") 239 | 240 | (defcard idevcard-example 241 | (reify dc/IDevcard 242 | (-devcard [_ opts] 243 | (sab/html [:h3 "This is a card without any base Devcard functionality"])))) 244 | 245 | (defcard 246 | "## Low level api `devcards.core/register-card` 247 | 248 | You can create a card with the low level api 249 | `devcards.core/register-card`. This function takes a map with two keys: 250 | 251 | * `:path` - the path to the card in the devcards interface (normally [ns var]) 252 | * `:func` - a thunk (function of no args) that returns a ReactElement 253 | 254 | Here is an example of using `register-card`: 255 | 256 | ```clojure 257 | (dc/register-card 258 | {:path [:devdemos.custom_cards 259 | :registered-card] 260 | :func (fn [] (sab/html [:h1 \"** Registered card **\"]))}) 261 | ``` 262 | 263 | You can see this in action below 264 | 265 | ") 266 | 267 | (dc/register-card 268 | {:path [:devdemos.custom_cards 269 | :registered-card] 270 | :func (fn [] (sab/html [:h1 "** Registered card **"]))}) 271 | 272 | (defcard 273 | "## The `devcards.core/defcard*` macro 274 | 275 | The `devcard*` macro makes it easy to bypass base `devcard` 276 | functionality. 277 | 278 | It is defined as so: 279 | 280 | ```clojure 281 | (defmacro defcard* 282 | ([vname expr] 283 | (when (utils/devcards-active?) 284 | `(devcards.core/register-card ~{:path (name->path &env vname) 285 | :func `(fn [] ~expr)})))) 286 | ``` 287 | 288 | As you can see it uses the `register-card` function but also captures 289 | path information. The `defcard*` macro can be helpful when composing 290 | your own macros for Devcards. 291 | 292 | 293 | ") 294 | -------------------------------------------------------------------------------- /resources/public/devcards/css/com_rigsomelight_devcards.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; 3 | } 4 | 5 | body .hljs { 6 | padding: 0px; 7 | color: #333; 8 | background: transparent; 9 | } 10 | 11 | #com-rigsomelight-devcards-main { 12 | padding-bottom: 10em; 13 | } 14 | 15 | .com-rigsomelight-devcards_rendered-card { 16 | position: relative; 17 | } 18 | 19 | .com-rigsomelight-devcards-body { 20 | background-color: rgb(233,234,237); 21 | } 22 | 23 | .com-rigsomelight-devcards-markdown pre, 24 | .com-rigsomelight-devcards-test-line.com-rigsomelight-devcards-test-doc .com-rigsomelight-devcards-markdown pre 25 | { 26 | display: block; 27 | padding: 9.5px 14px; 28 | margin: 0px 0px 10px; 29 | font-size: 13px; 30 | line-height: 1.42857143; 31 | word-break: normal; 32 | word-wrap: normal; 33 | overflow-x: scroll; 34 | color: #333; 35 | background-color: rgb(250,250,250); 36 | border: 1px solid #e1e1e1; 37 | margin-left: -14px; 38 | margin-right: -14px; 39 | border-left: 0px; 40 | border-right: 0px; 41 | } 42 | 43 | .com-rigsomelight-devcards-test-line.com-rigsomelight-devcards-test-doc .com-rigsomelight-devcards-markdown pre { 44 | margin-left: -15px; 45 | margin-right: -15px; 46 | } 47 | 48 | /* frameless style for markdown */ 49 | .com-rigsomelight-devcards-framelesss .com-rigsomelight-devcards-markdown { 50 | padding-top: 14px; 51 | padding-left: 14px; 52 | padding-right: 14px; 53 | } 54 | 55 | /* end fremless markdown style */ 56 | 57 | .com-rigsomelight-devcards-padding-top-border { 58 | margin-top: 14px; 59 | padding-top: 14px; 60 | } 61 | 62 | .com-rigsomelight-devcards-markdown code { 63 | padding: 2px 4px; 64 | font-size: 90%; 65 | color: #990073; 66 | background-color: #fafafa; 67 | white-space: nowrap; 68 | border-radius: 4px; 69 | } 70 | 71 | .com-rigsomelight-devcards_rendered-card code { 72 | font-size: 90%; 73 | } 74 | 75 | .com-rigsomelight-devcards-markdown pre code { 76 | padding: 0; 77 | font-size: inherit; 78 | color: inherit; 79 | white-space: pre; 80 | background-color: transparent; 81 | border-radius: 0; 82 | } 83 | 84 | .com-rigsomelight-devcards-base, 85 | .com-rigsomelight-devcards-markdown { 86 | 87 | } 88 | 89 | .com-rigsomelight-devcards-typog { 90 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 91 | font-size: 16px; 92 | line-height: 1.42857143; 93 | } 94 | 95 | 96 | .com-rigsomelight-devcards-markdown h1, 97 | .com-rigsomelight-devcards-markdown h2, 98 | .com-rigsomelight-devcards-markdown h3, 99 | .com-rigsomelight-devcards-markdown h4, 100 | .com-rigsomelight-devcards-markdown h5, 101 | .com-rigsomelight-devcards-base h1, 102 | .com-rigsomelight-devcards-base h2, 103 | .com-rigsomelight-devcards-base h3, 104 | .com-rigsomelight-devcards-base h4, 105 | .com-rigsomelight-devcards-base h5 { 106 | font-weight: 500; 107 | } 108 | 109 | .com-rigsomelight-devcards-markdown h1:first-child, 110 | .com-rigsomelight-devcards-markdown h2:first-child, 111 | .com-rigsomelight-devcards-markdown h3:first-child, 112 | .com-rigsomelight-devcards-markdown h4:first-child, 113 | .com-rigsomelight-devcards-markdown h5:first-child { 114 | margin-top: 14px; 115 | } 116 | 117 | .com-rigsomelight-devcards-base a { 118 | color: #428bca; 119 | text-decoration: none; 120 | } 121 | 122 | .com-rigsomelight-devcards-markdown code, 123 | .com-rigsomelight-devcards-markdown kbd, 124 | .com-rigsomelight-devcards-markdown pre, 125 | .com-rigsomelight-devcards-markdown samp { 126 | font-family: Menlo,Monaco,Consolas,"Courier New",monospace; 127 | } 128 | 129 | .com-rigsomelight-devcards-navbar { 130 | background-color: rgb(60,90,153); 131 | border-color: rgb(50,80,153); 132 | color: #fff; 133 | height: 50px; 134 | } 135 | 136 | .com-rigsomelight-devcards-brand { 137 | color: #ccc; 138 | font-size: 18px; 139 | line-height: 50px; 140 | display: block; 141 | margin-left: 14px; 142 | } 143 | 144 | .com-rigsomelight-devcards-container { 145 | /* margin: auto; 146 | width: 80%;*/ 147 | } 148 | 149 | .com-rigsomelight-devcards-card-base { 150 | background: #fff; 151 | padding: 8px 14px; 152 | margin-top: 20px; 153 | } 154 | 155 | .com-rigsomelight-devcards-card-base-no-pad { 156 | background: #fff; 157 | border: 1px solid rgb(231,234,242); 158 | margin-top: 20px; 159 | border-left: 0px; 160 | border-right: 0px; 161 | } 162 | 163 | .com-rigsomelight-devcards-card-base-no-pad.com-rigsomelight-devcards-card-hide-border { 164 | border: 1px solid transparent; 165 | } 166 | 167 | 168 | .com-rigsomelight-devcards-breadcrumbs { 169 | font-size: 16px; 170 | line-height: 1.5em; 171 | border: none !important; 172 | } 173 | 174 | .com-rigsomelight-devcards-breadcrumb-sep { 175 | display: inline-block; 176 | padding: 0px 5px; 177 | color: #ccc; 178 | } 179 | 180 | .com-rigsomelight-devcards-list-group { 181 | margin-top: 30px; 182 | } 183 | 184 | .com-rigsomelight-devcards-list-group-item { 185 | color: #555; 186 | position: relative; 187 | display: block; 188 | padding: 10px 14px; 189 | margin-bottom: -1px; 190 | border-bottom: 1px solid #eee; 191 | } 192 | 193 | .com-rigsomelight-devcards-badge { 194 | display: inline-block; 195 | min-width: 10px; 196 | padding: 3px 7px; 197 | font-size: 12px; 198 | font-weight: 700; 199 | color: #fff; 200 | line-height: 1; 201 | vertical-align: baseline; 202 | white-space: nowrap; 203 | text-align: center; 204 | background-color: #999; 205 | border-radius: 10px; 206 | } 207 | 208 | button.com-rigsomelight-devcards-badge { 209 | border: none; 210 | padding: 3px 19px; 211 | } 212 | 213 | 214 | .com-rigsomelight-devcards-panel-heading { 215 | padding: 8px 15px; 216 | font-size: 16px; 217 | line-height: 1.5em; 218 | background-color: rgb(142,162,206); 219 | background-color: rgb(239, 237, 237); 220 | } 221 | 222 | .com-rigsomelight-devcards-panel-heading a { 223 | color: #666; 224 | } 225 | 226 | .com-rigsomelight-devcards-devcard-padding { 227 | margin-top: 14px; 228 | padding-left: 14px; 229 | padding-right: 14px; 230 | padding-bottom: 14px; 231 | } 232 | 233 | .com-rigsomelight-devcards-test-line { 234 | position: relative; 235 | display: block; 236 | padding: 10px 14px; 237 | border: none; 238 | border-top: 1px solid #fafafa; 239 | } 240 | 241 | 242 | 243 | 244 | .com-rigsomelight-devcards-test-line.com-rigsomelight-devcards-context { 245 | background-color: #fcfcfc; 246 | border-left: 1px solid #f1f1f1; 247 | border-right: 1px solid #f1f1f1; 248 | } 249 | 250 | .com-rigsomelight-devcards-test-line pre { 251 | margin: 0px; 252 | 253 | word-break: normal; 254 | word-wrap: normal; 255 | overflow-x: scroll; 256 | } 257 | 258 | 259 | .com-rigsomelight-devcards-test-line pre code { 260 | font-size: 80%; 261 | padding: 0px; 262 | background-color:transparent; 263 | } 264 | 265 | .com-rigsomelight-devcards-pass { 266 | color: #3c763d; 267 | border: 1px solid rgb(199, 225, 160); 268 | border-left: 10px solid rgb(199, 225, 160); 269 | } 270 | 271 | .com-rigsomelight-devcards-fail, .com-rigsomelight-devcards-error { 272 | color: #a94442; 273 | border: 1px solid rgb(236, 196, 196); 274 | border-left: 10px solid rgb(236, 196, 196); 275 | } 276 | 277 | .com-rigsomelight-devcards-fail { 278 | background-color: rgb(254, 254, 244); 279 | } 280 | 281 | 282 | .com-rigsomelight-devcards-error { 283 | background-color: rgb(254, 245, 245); 284 | } 285 | 286 | 287 | 288 | .com-rigsomelight-devcards-test-message { 289 | display: block; 290 | margin-top: 2px; 291 | margin-bottom: 8px; 292 | } 293 | 294 | .com-rigsomelight-devcards-pass .com-rigsomelight-devcards-test-message { 295 | color: #386739; 296 | } 297 | 298 | .com-rigsomelight-devcards-fail .com-rigsomelight-devcards-test-message { 299 | color: #994745; 300 | } 301 | 302 | .com-rigsomelight-devcards-history-control-small-arrow { 303 | display: inline-block; 304 | height: 0px; 305 | width: 0px; 306 | border: 8px solid transparent; 307 | border-left-width: 9px; 308 | border-left-color: #666; 309 | margin-right: -10px; 310 | } 311 | 312 | .com-rigsomelight-devcards-history-control-block { 313 | display: inline-block; 314 | height: 16px; 315 | width: 3px; 316 | background-color: #666; 317 | } 318 | 319 | .com-rigsomelight-devcards-history-control-right { 320 | display: inline-block; 321 | height: 0px; 322 | width: 0px; 323 | border: 8px solid transparent; 324 | border-left-width: 16px; 325 | border-left-color: #666; 326 | margin-right: -10px; 327 | } 328 | 329 | .com-rigsomelight-devcards-history-control-left { 330 | display: inline-block; 331 | height: 0px; 332 | width: 0px; 333 | border: 8px solid transparent; 334 | border-right-width: 16px; 335 | border-right-color: #666; 336 | margin-left: -10px; 337 | } 338 | 339 | .com-rigsomelight-devcards-history-stop { 340 | display: inline-block; 341 | height: 17px; 342 | width: 17px; 343 | background-color: #D88282; 344 | border-radius: 3px; 345 | } 346 | 347 | .com-rigsomelight-devcards-history-control-bar { 348 | background-color: rgb(255,252,234); 349 | padding-top: 5px; 350 | padding-bottom: 3px; 351 | margin: 14px 0px; 352 | padding-left: 14px; 353 | padding-right: 14px; 354 | text-align: right; 355 | /* position: absolute; 356 | top: 0px; 357 | right: 0px; */ 358 | } 359 | 360 | .com-rigsomelight-devcards-history-control-bar button { 361 | background: transparent; 362 | border: none; 363 | margin: 0px 4px; 364 | height: 20px; 365 | padding: 1px 28px; 366 | } 367 | 368 | .com-rigsomelight-devcards-history-control-bar + .com-rigsomelight-devcards-padding-top-border { 369 | border: none; 370 | padding-top: 0px; 371 | } 372 | 373 | .com-rigsomelight-devcards-devcard-padding .com-rigsomelight-devcards-history-control-bar { 374 | /* margin-top: -14px; */ 375 | margin: 14px -30px; 376 | } 377 | 378 | 379 | 380 | 381 | @media (min-width: 768px) { 382 | 383 | 384 | .com-rigsomelight-devcards-markdown pre, 385 | .com-rigsomelight-devcards-test-line.com-rigsomelight-devcards-test-doc .com-rigsomelight-devcards-markdown pre { 386 | padding: 9.5px 30px; 387 | margin-left: -30px; 388 | margin-right: -30px; 389 | } 390 | 391 | .com-rigsomelight-devcards-panel-heading { 392 | padding: 8px 30px; 393 | } 394 | 395 | .com-rigsomelight-devcards-brand { 396 | margin-left: 0px; 397 | } 398 | 399 | .com-rigsomelight-devcards-devcard-padding { 400 | padding-left: 30px; 401 | padding-right: 30px; 402 | } 403 | 404 | .com-rigsomelight-devcards-card-hide-border .com-rigsomelight-devcards-devcard-padding { 405 | padding-left: 0px; 406 | padding-right: 0px; 407 | } 408 | 409 | .com-rigsomelight-devcards-breadcrumbs { 410 | padding: 0px 0px; 411 | } 412 | 413 | .com-rigsomelight-devcards-list-group { 414 | margin-top: 30px; 415 | } 416 | 417 | .com-rigsomelight-devcards-list-group-item { 418 | padding-left: 0px; 419 | padding-right: 0px; 420 | } 421 | 422 | .com-rigsomelight-devcards-container { 423 | margin: auto; 424 | width: 750px; 425 | } 426 | 427 | button.com-rigsomelight-devcards-badge { 428 | border: 1px solid #999; 429 | padding: 3px 9px; 430 | background-color: #ccc; 431 | } 432 | 433 | .com-rigsomelight-devcards-history-control-bar button { 434 | padding: 1px 6px; 435 | } 436 | 437 | .com-rigsomelight-devcards-card-base, 438 | .com-rigsomelight-devcards-card-base-no-pad { 439 | border-radius: 3px; 440 | border: 1px solid rgb(231,234,242); 441 | } 442 | 443 | .com-rigsomelight-devcards-test-line { 444 | padding: 10px 30px; 445 | } 446 | 447 | .com-rigsomelight-devcards-pass { 448 | border-left: 25px solid rgb(199, 225, 160); 449 | } 450 | 451 | .com-rigsomelight-devcards-fail { 452 | border-left: 25px solid rgb(236, 196, 196); 453 | } 454 | 455 | .com-rigsomelight-devcards-error { 456 | border-left: 25px solid rgb(236, 196, 196); 457 | } 458 | 459 | } 460 | 461 | @media (min-width: 800px) { 462 | .com-rigsomelight-devcards-card-hide-border .com-rigsomelight-devcards-markdown pre { 463 | border: 1px solid #e1e1e1; 464 | border-radius: 4px; 465 | padding-left: 14px; 466 | padding-right: 14px; 467 | 468 | margin-left: 0px; 469 | margin-right: 0px; 470 | } 471 | } 472 | 473 | @media (min-width: 1200px) { 474 | .com-rigsomelight-devcards-card-hide-border .com-rigsomelight-devcards-devcard-padding { 475 | padding-left: 30px; 476 | padding-right: 30px; 477 | } 478 | .com-rigsomelight-devcards-brand { 479 | margin-left: 30px; 480 | } 481 | .com-rigsomelight-devcards-list-group-item { 482 | margin-left: 30px; 483 | margin-right: 30px; 484 | } 485 | 486 | .com-rigsomelight-devcards-breadcrumbs { 487 | padding: 0px 30px; 488 | } 489 | 490 | .com-rigsomelight-devcards-container { 491 | margin: auto; 492 | width: 970px; 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Devcards 2 | 3 | ### Current release: 4 | 5 | [![Clojars Project](https://clojars.org/devcards/latest-version.svg)](https://clojars.org/devcards) 6 | 7 | Devcards aims to provide ClojureScript developers with an interactive 8 | visual REPL. Devcards makes it simple to interactively surface code 9 | examples that have a visual aspect into a browser interface. 10 | 11 | Devcards is **not** a REPL, as it is driven by code that exists in 12 | your source files, but it attempts to provide a REPL-like experience 13 | by allowing developers to quickly try different code examples and 14 | see how they behave in an actual DOM. 15 | 16 | Devcards is centered around a notion of a *card*. Every card 17 | represents some code to be displayed. Devcards provides an interface 18 | which allows the developer to navigate to different namespaces and view 19 | the *cards* that have been defined in that namespace. 20 | 21 | When used in conjunction with [lein figwheel][leinfigwheel] the cards 22 | can be created and edited **"live"** in one's ClojureScript source 23 | files. Essentially lifting the code example out of the file into the 24 | browser for you to try out immediately. 25 | 26 | 27 | 28 | For example, the following code will create a *card* for a Sablono 29 | template that you might be working on: 30 | 31 | ```clojure 32 | (defcard two-zero-48-view 33 | (sab/html 34 | [:div.board 35 | [:div.cells 36 | [:div {:class "cell xpos-1 ypos-1"} 4] 37 | [:div {:class "cell xpos-1 ypos-2"} 2] 38 | [:div {:class "cell xpos-1 ypos-3"} 8]]])) 39 | ``` 40 | 41 | When used with [lein-figwheel][leinfigwheel], saving the file that 42 | contains this definition will cause this Sablono template to be 43 | rendered into the Devcards interface. 44 | 45 | Read: [The Hard Sell](http://rigsomelight.com/devcards/#!/devdemos.core) 46 | 47 | [See the introduction video.](https://vimeo.com/97078905) 48 | 49 | [See the Strange Loop talk.](https://www.youtube.com/watch?v=G7Z_g2fnEDg) 50 | 51 | # Why??? 52 | 53 | We primarily design and iterate on our front end applications *inside* 54 | the main application itself. In other words, our execution environment 55 | is constrained by the shape and demands of the application we are 56 | working on. This is extremely limiting. 57 | 58 | This doesn't seem like a problem, eh? 59 | 60 | Well think of it this way: the main application and its many 61 | subcomponents can potentially embody a tremendous number of states. But 62 | working against a single instance of the application only lets you 63 | look at one state at a time. What if you could work on the application 64 | or component in several states at the same time? This is a powerful 65 | multiplier. You are **increasing the bandwidth of the feedback** you are 66 | getting while working on your code. 67 | 68 | Another problem is that we often manually place our components into 69 | different **important** states to run them through their paces as we 70 | develop them. But ... these test states are **ephemeral**. Wouldn't 71 | it be better to **keep** a development "page" as a permanent asset 72 | where these components are displayed in these various states as a 73 | 74 | * a lab space for future development 75 | * a code reference for new developers, and your future self 76 | * a tool for QA and application testers 77 | 78 | Developing your components in a different context than your main 79 | application **starkly reveals environmental coupling**, in the same 80 | way that unit tests often do. This can lead to developing components 81 | that are more independent than the ones that are developed inside the 82 | main app. 83 | 84 | One more thing: developing your components in a SPA that isn't your 85 | main application provides you a space to create and use visual 86 | components that are intended to help you understand the code you are 87 | working on. We are UI programmers after all, why wait for IDEs to 88 | create the tools we need? Most problems are unique and can benefit 89 | tremendously from the creation of a very thin layer of custom tooling. 90 | 91 | Developing inside the main application is constraining and it isn't 92 | until you develop inside a **meta application** that you can see this 93 | more clearly. With a meta application, you now have a space to try 94 | things out that **do not have to interface or fit into the main 95 | application**. This is extremely important as it gives you space to 96 | try new things without the cost that is currently associated with 97 | experiments (branching, new html host file, etc). 98 | 99 | ## Examples 100 | 101 | Regardless of which path you take to get started with Devcards please 102 | see the following examples: 103 | 104 | [Introduction examples](http://rigsomelight.com/devcards/#!/devdemos.core) ([src](https://github.com/bhauman/devcards/blob/master/example_src/devdemos/core.cljs)) 105 | 106 | [An example implementation of 2048](http://rigsomelight.com/devcards/#!/devdemos.two_zero) ([src](https://github.com/bhauman/devcards/blob/master/example_src/devdemos/two_zero.cljs)) 107 | 108 | [An introduction to the `defcard` api](http://rigsomelight.com/devcards/#!/devdemos.defcard_api) ([src](https://github.com/bhauman/devcards/blob/master/example_src/devdemos/defcard_api.cljs)) 109 | 110 | ## Super Quick Start 111 | 112 | There is a Devcards Leiningen template to get you up an running quickly. 113 | 114 | Make sure you have the [latest version of leiningen installed](https://github.com/technomancy/leiningen#installation). 115 | 116 | Type the following to create a fresh project with devcards setup for you: 117 | 118 | ``` 119 | lein new devcards hello-world 120 | ``` 121 | 122 | Then 123 | 124 | ``` 125 | cd hello-world 126 | 127 | lein figwheel 128 | ``` 129 | 130 | to start the figwheel interactive devserver. 131 | 132 | Then visit `http://localhost:3449/cards.html` 133 | 134 | ## Quick Trial 135 | 136 | If you want to quickly interact with a bunch of devcards demos: 137 | 138 | ``` 139 | git clone https://github.com/bhauman/devcards.git 140 | 141 | cd devcards 142 | 143 | lein figwheel 144 | ``` 145 | 146 | Then visit `http://localhost:3449/devcards/index.html` 147 | 148 | The code for the cards you are viewing in the devcards interface is 149 | located in the `example_src` directory. 150 | 151 | Go ahead and edit the code in the examples and see how the devcards 152 | interface responds. 153 | 154 | ## Usage 155 | 156 | First make sure you include the following `:dependencies` in your `project.clj` file. 157 | 158 | ```clojure 159 | [org.clojure/clojurescript "1.7.122"] 160 | [devcards "0.2.0-3"] 161 | ``` 162 | 163 | 164 | You will need an HTML file to host the devcards interface. It makes 165 | sense to have a separate file to host devcards. I would 166 | create the following `resources/public/cards.html` file (this is the same 167 | file as in the leiningen template). 168 | 169 | 170 | ```html 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | ``` 183 | 184 | 185 | ##Usage With Figwheel 186 | 187 | [lein-figwheel](https://github.com/bhauman/lein-figwheel) 188 | is not required to use Devcards but it is definitely recommended 189 | if you want to experience interactive coding with Devcards. 190 | See the [lein-figwheel repo](https://github.com/bhauman/lein-figwheel) 191 | for instructions on how to do that. 192 | 193 | Configure your devcards build: 194 | 195 | ```clojure 196 | :cljsbuild { 197 | :builds [ 198 | {:id "devcards" 199 | :source-paths ["src"] 200 | :figwheel { :devcards true } ;; <- note this 201 | :compiler { :main "{{your lib name}}.core" 202 | :asset-path "js/compiled/devcards_out" 203 | :output-to "resources/public/js/{{your lib name}}_devcards.js" 204 | :output-dir "resources/public/js/devcards_out" 205 | :source-map-timestamp true }}] 206 | } 207 | ``` 208 | 209 | 210 | Next you will need to include the Devcards macros into your file: 211 | 212 | ```clojure 213 | (ns example.core 214 | (:require 215 | [sablono.core :as sab]) ; just for example 216 | (:require-macros 217 | [devcards.core :refer [defcard]])) 218 | 219 | (defcard my-first-card 220 | (sab/html [:h1 "Devcards is freaking awesome!"])) 221 | ``` 222 | 223 | This will create a card in the devcards interface. 224 | 225 | Take a look at [the `defcard` api](http://rigsomelight.com/devcards/#!/devdemos.defcard_api) ([src](https://github.com/bhauman/devcards/blob/master/example_src/devdemos/defcard_api.cljs)) 226 | 227 | ## Usage without Figwheel 228 | 229 | Figwheel does some magic so that Devcards can be included or excluded 230 | from your code easily. You can certainly use Devcards without Figwheel, 231 | but there are three things that you will need to do. 232 | 233 | #### You need to specify `:devcards true` **in the build-options** of your ClojureScript build 234 | 235 | ```clojure 236 | { :main "{{name}}.core" 237 | :devcards true ; <- note this 238 | :asset-path "js/compiled/devcards_out" 239 | :output-to "resources/public/js/{{sanitized}}_devcards.js" 240 | :output-dir "resources/public/js/devcards_out" 241 | :source-map-timestamp true } 242 | ``` 243 | 244 | This is important as it is a signal to the `defcard` macro to render 245 | the cards. This is equivalent to adding `:figwheel { :devcards true }` 246 | in our figwheel based build above, but since we aren't using figwheel 247 | in this build adding the figwheel options doesn't help. 248 | 249 | #### You will need to require `devcards.core` in the files that use devcards as such: 250 | 251 | ```clojure 252 | (ns example.core 253 | (:require 254 | [devcards.core :as dc] ; <-- here 255 | [sablono.core :as sab]) ; just for this example 256 | (:require-macros 257 | [devcards.core :refer [defcard]])) ; <-- and here 258 | 259 | (defcard my-first-card 260 | (sab/html [:h1 "Devcards is freaking awesome!"])) 261 | ``` 262 | 263 | This isn't required with Figwheel because it puts `devcards.core` into the 264 | build automatically. 265 | 266 | #### You will need to start the Devcards UI 267 | 268 | ``` 269 | (devcards.core/start-devcard-ui!) 270 | ``` 271 | 272 | Make sure this is included in the file you have specified as `:main` 273 | in your build options. As mentioned above, you don't want the Devcards UI to compete with 274 | your application's UI so you will want to make sure it isn't getting 275 | launched. 276 | 277 | 278 | ## Devcards as a Standalone Website 279 | 280 | Devcards can easily be hosted as a standalone website by following 281 | steps similar to those needed to use it locally without figwheel. 282 | In this example, we will be adding a `hostedcards` profile to build 283 | our site. 284 | 285 | #### Add `:devcards true` **to the build-options** of our ClojureScript build profile 286 | 287 | ```clojure 288 | {:id "hostedcards" 289 | :source-paths ["src"] 290 | :compiler {:main "{{your lib name}}.core" 291 | :devcards true ; <- note this 292 | :asset-path "js/compiled/out" 293 | :output-to "resources/public/js/compiled/{{your lib name}}.js" 294 | :optimizations :advanced}} 295 | ``` 296 | 297 | #### Require `devcards.core`in the files that use devcards 298 | 299 | ```clojure 300 | (ns {{your lib name}}.core 301 | (:require 302 | [devcards.core :as dc]) 303 | (:require-macros 304 | [devcards.core :refer [defcard]])) 305 | ``` 306 | 307 | ### Start the Devcards UI in {{your lib name}}.core 308 | ```clojure 309 | (devcards.core/start-devcard-ui!) 310 | ``` 311 | 312 | ### Include the compiled JS in our HTML 313 | 314 | ```html 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 |
324 | 325 | 326 | 327 | ``` 328 | 329 | 330 | ### Run our Build 331 | 332 | `lein cljsbuild once hostedcards` 333 | 334 | Once the build is complete, simply copy the contents of `resources\public` 335 | to your webserver and serve it up as you would any other page. You 336 | 337 | ## FAQ 338 | 339 | #### Does Devcards only work with React or Om? 340 | 341 | Nope, it can work with arbitrary CLJS code examples as well. Devcards 342 | provides a `dom-node` helper that will give you a node in the DOM to 343 | display stuff in. 344 | 345 | #### Does Devcards require Figwheel? 346 | 347 | Devcards will work automatically with REPL workflow or boot-reload. 348 | 349 | You can also just reload the browser after making a change. 350 | 351 | #### What do I do for deployment? 352 | 353 | Devcards has been rewritten so that you can write Devcards alongside 354 | your code with no impact on your production code. 355 | 356 | That being said it is often helpful to move the bulk of your cards to 357 | a different buildpath that is only built when working on the **devcards** 358 | build. 359 | 360 | When working with devcards I oftern have three builds "devcards", 361 | "dev", "prod". 362 | 363 | 364 | 365 | 366 | [leinfigwheel]: https://github.com/bhauman/lein-figwheel 367 | 368 | -------------------------------------------------------------------------------- /example_src/devdemos/defcard_api.cljs: -------------------------------------------------------------------------------- 1 | (ns devdemos.defcard-api 2 | (:require 3 | [devcards.core] 4 | [om.core :as om :include-macros true] 5 | [om.dom :as dom :include-macros true] 6 | [reagent.core :as reagent] 7 | [clojure.string :as string] 8 | [sablono.core :as sab :include-macros true] 9 | [cljs.test :as t :include-macros true :refer-macros [testing is]]) 10 | (:require-macros 11 | ;; Notice that I am not including the 'devcards.core namespace 12 | ;; but only the macros. This helps ensure that devcards will only 13 | ;; be created when the :devcards is set to true in the build config. 14 | [devcards.core :as dc :refer [defcard defcard-doc noframe-doc deftest dom-node]])) 15 | 16 | (defcard-doc 17 | "#It all starts with `defcard` 18 | 19 | Once you have Devcards setup and have required the devcards macros as below 20 | ```clojure 21 | (:require-macros 22 | [devcards.core :as dc :refer [defcard])) 23 | ``` 24 | You can then use the `defcard` macro. `defcard` is a multipurpose 25 | macro which is designed to take what you are working on elevate 26 | live into the browser. It can handle many types of data but 27 | primarily takes any type of ReactElement. 28 | 29 | So this would be the \"Hello World\" of Devcards," 30 | 31 | '(defcard (sab/html [:h3 "Hello world"])) 32 | 33 | "You can see this devcard rendered below:") 34 | 35 | (defcard (sab/html [:h3 "Hello World!"])) 36 | 37 | (defcard 38 | "# These cards are hot ... loaded 39 | 40 | One thing that isn't easy to see from reading this page is that when 41 | you define a Devcard in your code and save it, a card instantly 42 | appears in the Devcards interface. It shows up on the page in the 43 | order of its definition, and when you comment out or delete the 44 | card from your code it dissapears from the interface. 45 | 46 | ## `defcard` takes 5 arguments 47 | 48 | * **name** an optional symbol name for the card to be used as a heading and to 49 | locate it in the Devcards interface 50 | * **documentation** an optional string literal of markdown documentation 51 | * **main object** a required object for the card to display 52 | * **initial data** an optional Atom, RAtom or Clojure data structure (normally 53 | a Map), used to initialize the a state that the devcard will pass back to 54 | your code examples. More on this later ... 55 | * **devcard options** an optional map of options for the underlying devcard 56 | 57 | ``` 58 | (defcard hello-world ;; optional symbol name 59 | \"**Optional Mardown documentation**\" ;; optional literal string doc 60 | {:object \"of focus\"} ;; required object of focus 61 | {} ;; optional intial data 62 | {} ;; optional devcard config 63 | ) 64 | ``` 65 | 66 | We are going to explore these arguments and examples of how they work 67 | below. 68 | 69 | This is pretty *meta* as I am using Devcards to document Devcards. 70 | So please take this page as an example of **interactive literate 71 | programming**. Where you create a story about your code, supported 72 | by live examples of how it works. 73 | 74 | ## What's in a name? 75 | 76 | The first optional arg to `defcard` is the **name**. This is a 77 | symbol and it is used to provide a distinct key for the card 78 | you are creating. 79 | 80 | For cards that aren't stateful like documentation and such the name 81 | really isn't necessary but when you create cards that are displaying 82 | stateful running widgets then this key will help ensure that the 83 | underlying state is mapped back to the correct card. 84 | 85 | The name will be used to create a header on the card. The header can 86 | be clicked to display and work on the card by itself. 87 | 88 | For instance here is a card with a name: 89 | 90 | ``` 91 | (defcard first-example) 92 | ``` 93 | 94 | You can see this card with its header just below. If you click on 95 | the `first-example` card header, you will be presented with the card 96 | by itself, so that you can work on the card in isolation. 97 | 98 | 99 | ") 100 | 101 | (defcard first-example 102 | (sab/html [:div]) 103 | {} 104 | {:heading true}) 105 | 106 | (defcard-doc 107 | "## Name absentia 108 | 109 | In the absense of a name, the heading of the card will not be displayed. 110 | 111 | Devcards generate's a card name in the order that it shows up on 112 | the page. You can see this autogenerated name by setting the 113 | `:heading` option to `true`. 114 | 115 | ``` 116 | (defcard {} ; main obj 117 | {} ; initial data 118 | {:heading true}) ; devcard options: forces header to show 119 | ``` 120 | 121 | Which is displayed as so:") 122 | 123 | (defcard {} {} {:heading true}) 124 | 125 | (defcard 126 | "The generated card name above is *card-4*. This makes sense 127 | because it's the fouth card on the page that has no name. 128 | 129 | The generated card name will work in many cases but not for all. 130 | It's best to have a name for cards with state.") 131 | 132 | (defcard 133 | "## Optional markdown doc 134 | 135 | You can also add an optional markdown documentation to your card like this: 136 | ``` 137 | (defcard example-2 \"## Example: This is optional markdown\") 138 | ``` 139 | ") 140 | 141 | (defcard example-2 "## Example: This is optional markdown") 142 | 143 | (defcard 144 | "Since the name `example-2` is optional you can write docs just like this: 145 | 146 | ``` 147 | (defcard 148 | \"## Example: writing markdown docs is intended to be easy. 149 | 150 | You should be able to add docs to your code examples easily.\") 151 | ```") 152 | 153 | (defcard-doc 154 | "# The object of our attention 155 | 156 | The main object that we are displaying comes after the optional 157 | **name** and **documentation** and it will be displayed in the 158 | **body** of the card. 159 | 160 | As mentioned before this object can be many things but perhaps most 161 | importantly it can be a ReactElement. 162 | 163 | For example this is valid:" 164 | 165 | (dc/mkdn-pprint-code 166 | '(defcard react-example (sab/html [:h3 "Example: Rendering a ReactElement"]))) 167 | 168 | "Above we simply passed a ReactElement created by `sablono` to `defcard` 169 | and it gets rendered as the following card:") 170 | 171 | (defcard react-example (sab/html [:h3 "Example: Rendering a ReactElement"])) 172 | 173 | (defcard 174 | "## A string is interpreted as markdown 175 | 176 | In the example below we are not using a string literal so the first 177 | arg is really the main object and because it is of type `string` it 178 | will be interpreted as markdown. 179 | 180 | ``` 181 | (defcard (str \"This is the main object and it **will** be interpreted as markdown\")) 182 | ``` 183 | ") 184 | 185 | 186 | (defcard (str "This is the main object and **will** be interpreted as markdown")) 187 | 188 | (defcard 189 | "## Many types are displayed as edn 190 | 191 | As we are programming we often want to see the result of an 192 | evaluation, for this reason `defcard` will display many types of 193 | data as edn. 194 | 195 | This is a growing list of items but right now look at the following examples:") 196 | 197 | (defcard 198 | "**Map**s are displayed as edn: 199 | ``` 200 | (defcard {:this \"is a map\"}) 201 | ```" 202 | {:this "is a map"}) 203 | 204 | (defcard 205 | "**Vector**s are displayed as edn: 206 | 207 | ``` 208 | (defcard [\"This\" \"is\" \"a\" \"vector\"]) 209 | ```" 210 | (string/split "This is a vector" #"\s" )) 211 | 212 | (defcard 213 | "**Set**s are displayed as edn 214 | 215 | ``` 216 | (defcard #{1 2 3}) 217 | ```" 218 | #{1 2 3}) 219 | 220 | (defcard 221 | "**List**s are displayed as edn 222 | 223 | ``` 224 | (defcard (list 1 2 3)) 225 | ```" 226 | (list 1 2 3)) 227 | 228 | (defcard 229 | "## Atoms are displayed as observed edn 230 | 231 | When you pass an atom to `defcard` as the main object it's contents 232 | will be rendered as edn. And when the atom changes so will the 233 | displayed edn. 234 | 235 | ``` 236 | (defonce observed-atom 237 | (let [a (atom 0)] 238 | (js/setInterval (fn [] (swap! observed-atom inc)) 1000) 239 | a)) 240 | 241 | (defcard atom-observing-card observed-atom) 242 | ``` 243 | 244 | This will produce the timer card that you that you can see below:") 245 | 246 | (defonce observed-atom 247 | (let [a (atom {:time 0})] 248 | (js/setInterval (fn [] (swap! observed-atom update-in [:time] inc)) 1000) 249 | a)) 250 | 251 | (defcard atom-observing-card observed-atom {} {:history false}) 252 | 253 | (defcard-doc 254 | "## A function as a main object 255 | 256 | The main point of devcards is to get your code out of the source 257 | file and up and running in front of you as soon as possible. To this 258 | end devcards tries to provide several generic ways for you to run 259 | your code in the devcards interface. The main way is to pass a 260 | function to the `defcard` macro as the main object. 261 | 262 | Instead of a ReactElement you can provide a function the takes two 263 | parameters and returns a ReactElement like so:" 264 | 265 | (dc/mkdn-pprint-code 266 | '(defcard (fn [data-atom owner] 267 | (sab/html [:div [:h2 "Example: fn that returns React"] 268 | (prn-str data-atom)])))) 269 | "In this example the `data-atom` is a ClojureScript Atom and 270 | the`owner` is the enclosing cards ReactElement.") 271 | 272 | (defcard 273 | (fn [data-atom owner] 274 | (sab/html [:div [:h3 "Example: fn that returns React"] 275 | (prn-str data-atom)]))) 276 | 277 | (defcard-doc 278 | "If `data-atom` in the above example changes then the card will be re-rendered. 279 | 280 | Let's make a quick example counter:" 281 | (dc/mkdn-pprint-code 282 | '(defcard 283 | (fn [data-atom owner] 284 | (sab/html [:div [:h3 "Example Counter: " (:count @data-atom)] 285 | [:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "inc"]]))))) 286 | 287 | (defcard 288 | (fn [data-atom owner] 289 | (sab/html [:div [:h3 "Example Counter: " (:count @data-atom)] 290 | [:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "inc"]]))) 291 | 292 | (defcard-doc 293 | "## Initial state 294 | 295 | The counter example above was very interesting but what if you want 296 | to introduce some initial state? 297 | 298 | Well the next option after the main object is the **initial-data** 299 | parameter. You can use it like so:" 300 | (dc/mkdn-pprint-code 301 | '(defcard 302 | (fn [data-atom owner] 303 | (sab/html [:div [:h3 "Example Counter w/Initial Data: " (:count @data-atom)] 304 | [:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "inc"]])) 305 | {:count 50}))) 306 | 307 | (defcard 308 | (fn [data-atom owner] 309 | (sab/html [:div [:h3 "Example Counter w/Initial Data: " (:count @data-atom)] 310 | [:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "inc"]])) 311 | {:count 50}) 312 | 313 | (defcard-doc 314 | "## Initial state can be an Atom 315 | 316 | You can also pass an Atom as the initial state. This is a very 317 | important feature of devcards as it allows you to share state 318 | between cards. 319 | 320 | The following examples share state:" 321 | 322 | (dc/mkdn-pprint-code 323 | '(defonce first-example-state (atom {:count 55}))) 324 | 325 | (dc/mkdn-pprint-code 326 | '(defcard example-counter 327 | (fn [data-atom owner] 328 | (sab/html [:h3 "Example Counter w/Shared Initial Atom: " (:count @data-atom)])) 329 | first-example-state)) 330 | 331 | (dc/mkdn-pprint-code 332 | '(defcard example-incrementer 333 | (fn [data-atom owner] 334 | (sab/html [:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "increment"])) 335 | first-example-state)) 336 | 337 | (dc/mkdn-pprint-code 338 | '(defcard example-decrementer 339 | (fn [data-atom owner] 340 | (sab/html [:button {:onClick (fn [] (swap! data-atom update-in [:count] dec))} "decrement"])) 341 | first-example-state)) 342 | "As you can see, we created three cards that all share the same state. 343 | 344 | If you try these example cards below you will see that they are all wired together:") 345 | 346 | (defonce first-example-state (atom {:count 55})) 347 | 348 | (defcard example-counter 349 | (fn [data-atom owner] 350 | (sab/html [:h3 "Example Counter w/Shared Initial Atom: " (:count @data-atom)])) 351 | first-example-state) 352 | 353 | (defcard example-incrementer 354 | (fn [data-atom owner] 355 | (sab/html [:button {:onClick (fn [] (swap! data-atom update-in [:count] inc)) } "increment"])) 356 | first-example-state) 357 | 358 | (defcard example-decrementer 359 | (fn [data-atom owner] 360 | (sab/html [:button {:onClick (fn [] (swap! data-atom update-in [:count] dec)) } "decrement"])) 361 | first-example-state) 362 | 363 | (defcard 364 | "# Reseting the state of a card 365 | 366 | The **initial state** is just the initial state of the card. What if 367 | you want to reset the card and start from the initial state or some 368 | new initial state? 369 | 370 | There is a simple trick: you just change the name of the card. I 371 | often add and remove a `*` at the end of a card name to bump the 372 | state out of the card in read in a new initial state. 373 | 374 | I am debating adding knobs for these things to the heading panel of 375 | the card. A knob to reset the state, a knob to turn on history, a 376 | knob to display the data in the atom. Let me know if you think this 377 | is a good idea.") 378 | 379 | (defcard 380 | "# Devcard options 381 | 382 | The last argument to `defcard` is an optional map of options. 383 | 384 | Here are the available options with their defaults: 385 | 386 | ``` 387 | { 388 | :frame true ;; wether to enclose the card in a padded frame 389 | :heading true ;; wether to add a heading panel to the card 390 | :padding true ;; wether to have padding around the body of the card 391 | :hidden false ;; wether to diplay the card or not 392 | :inspect-data false ;; wether to display the data in the card atom 393 | :watch-atom true ;; wether to watch the atom and render on change 394 | :history false ;; wether to record a change history of the atom 395 | :classname "" ;; provide card with a custom classname 396 | } 397 | ``` 398 | 399 | Most of these are farily straight forward. Whats important to know 400 | is that you can change any of these live and the card will respond 401 | with the new behavior. 402 | 403 | Here are some cards that exercise these options:") 404 | 405 | (defcard no-framed 406 | (str "## This is a devcard 407 | 408 | And it doesn't have a frame") 409 | {} 410 | {:frame false}) 411 | 412 | (defcard no-heading 413 | (str "# this card is hiding it's heading") 414 | {} 415 | {:heading false}) 416 | 417 | (defcard no-padding 418 | (str " this card is has no padding on its body") 419 | {} 420 | {:padding false}) 421 | 422 | (defcard custom-classname 423 | (str " this card has a custom class `.red-box`") 424 | {} 425 | {:classname "red-box"}) 426 | 427 | (defcard inspect-data 428 | (fn [data-atom owner] 429 | (sab/html [:div [:h3 "Inspecting data on this Counter: " (:count @data-atom)] 430 | [:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "inc"]])) 431 | {:count 50} 432 | {:inspect-data true}) 433 | 434 | (defcard inspect-data-and-record-history 435 | (fn [data-atom owner] 436 | (sab/html [:div [:h3 "Inspecting data and recording history this Counter: " (:count @data-atom)] 437 | [:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "inc"]])) 438 | {:count 50} 439 | {:inspect-data true :history true}) 440 | 441 | 442 | (defcard-doc 443 | "## Accessing the DOM with `dom-node` 444 | 445 | While Devcards was written in and are very easy to use in 446 | conjunction with React. You may want to write something that writes 447 | directly to the DOM. 448 | 449 | The helper macro `dom-node` takes a function that accepts a DOM 450 | node and ClojureScript Atom and returns a ReactElement." 451 | 452 | (dc/mkdn-pprint-code 453 | '(defcard example-dom-node 454 | (dom-node (fn [data-atom node] 455 | (set! (.-innerHTML node) "

Example Dom Node

")))))) 456 | 457 | (defcard example-dom-node 458 | (dom-node 459 | (fn [data-atom node] 460 | (set! (.-innerHTML node) "

Example Dom Node

")))) 461 | -------------------------------------------------------------------------------- /example_src/devdemos/two_zero.cljs: -------------------------------------------------------------------------------- 1 | (ns devdemos.two-zero 2 | (:require 3 | [devcards.core] 4 | [devcards.util.utils :refer [html-env?]] 5 | [clojure.string :as string] 6 | [clojure.set :refer [difference union]] 7 | [sablono.core :as sab :include-macros true] 8 | 9 | [om.core :as om :include-macros true] 10 | [om.dom :as dom :include-macros true] 11 | [cljs.core.async :refer [timeout]] 12 | [goog.labs.userAgent.device :as device] 13 | [cljs.test :as t :include-macros true]) 14 | (:require-macros 15 | [cljs.core.async.macros :refer [go]] 16 | [devcards.core :as dc :refer [defcard defcard-doc deftest]])) 17 | 18 | (defn lh [x] (prn-str x) x) 19 | 20 | (defn lc [x] (.log js/console x) x) 21 | 22 | (def is-mobile? 23 | (when (html-env?) 24 | (or (device/isMobile) 25 | ;; we could hook into a callback to set this on resize 26 | (< (.-offsetWidth 27 | (aget (.getElementsByTagName js/document "body") 0)) 28 | 490)))) 29 | 30 | (defcard 31 | "# 2048 32 | Let's build 2048 interactively with devcards") 33 | 34 | (def to-pixel-pos 35 | (if is-mobile? 36 | (fn [x] (+ 8 (* x (+ 60 8)))) 37 | (fn [x] (+ 15 (* x (+ 106 15)))))) 38 | 39 | (defn pixel-pos [pos] 40 | (str (to-pixel-pos pos) "px")) 41 | 42 | (defn board-cell [{:keys [top left id v highlight reveal]}] 43 | (let [translate (str "translate3d(" 44 | (pixel-pos left) "," 45 | (pixel-pos top) ", 0px)")] 46 | (sab/html 47 | [:div.cell-pos { :style { "-webkit-transform" translate 48 | "-moz-transform" translate 49 | "transform" translate } 50 | :key (str (name id)) } 51 | [:div { :class (str "cell cell-num-" v 52 | (when highlight " highlight") 53 | (when reveal " reveal")) } v]]))) 54 | 55 | ;; These are the background cells of the board, they never change. 56 | (defn background-cells [] 57 | (for [top (range 4) left (range 4)] 58 | (sab/html 59 | [:div.cell-pos.cell-empty 60 | { :style { :top (pixel-pos top) 61 | :left (pixel-pos left)}}]))) 62 | 63 | (defn one-row-board-static 64 | "Only used for the demo process" 65 | [data] 66 | (sab/html 67 | [:div.board-area.board-area-one-row 68 | [:div.background (take 4 (background-cells))] 69 | [:div.cells (map board-cell data)]])) 70 | 71 | (defn game-board [data] 72 | (sab/html 73 | [:div.board-area 74 | [:div.background (background-cells)] 75 | [:div.cells (map board-cell data)]])) 76 | 77 | (defcard board-style 78 | "## Board Style 79 | 80 | Let's start by creating the style for the board. 81 | 82 | The board is a 4x4 board. It will have one container with 16 83 | absolutely positioned cells in it. These cells will mark where the 84 | potential locations for the game tiles." 85 | (game-board [])) 86 | 87 | (defcard board-with-cells 88 | "### Cell Style 89 | Then we'll work on the style for the cells. The hard part is 90 | getting the colors and the font sizes correct." 91 | (game-board [{ :top 0 :left 0 :v 2 :id :t1} 92 | { :top 0 :left 1 :v 4 :id :t2} 93 | { :top 0 :left 2 :v 8 :id :t3} 94 | { :top 0 :left 3 :v 16 :id :t4} 95 | { :top 1 :left 0 :v 32 :id :t5} 96 | { :top 1 :left 1 :v 64 :id :t6} 97 | { :top 1 :left 2 :v 128 :id :t7} 98 | { :top 1 :left 3 :v 256 :id :t8} 99 | { :top 2 :left 0 :v 512 :id :t9} 100 | { :top 2 :left 1 :v 1024 :id :t10} 101 | { :top 2 :left 2 :v 2048 :id :t11}])) 102 | 103 | #_(defcard animation-work 104 | "## Checking basic tile movement animation" 105 | (dc/slider-card 106 | identity 107 | { :left (range 4) } 108 | :value-render-func 109 | (fn [{:keys [left]}] 110 | (one-row-board-static [{:left left :top 0 :v 2 :id :t1}])))) 111 | 112 | 113 | (defcard-doc 114 | "## Main data structures 115 | #### Tile Map 116 | 117 | The only data structure we are going to hold in our atom is a map 118 | of the tiles on the page. The list will look like this:" 119 | 120 | (dc/mkdn-pprint-str 121 | {:t1 { :top 0 :left 0 :v 2 :id :t1} 122 | :t2 { :top 1 :left 0 :v 4 :id :t2} 123 | :t3 { :top 2 :left 0 :v 8 :id :t3} 124 | :t4 { :top 3 :left 0 :v 4 :id :t4}}) 125 | 126 | "Each tile will have an `:id`, a value `:v` and a position on the board. 127 | 128 | Data handling is complicated because we will have to manage the 129 | state in phases to support animation. Tiles will be marked with the 130 | following flags: 131 | 132 | * `:double` - this flag marks the tiles value to be doubled 133 | * `:remove` - this flag marks the tile to be removed 134 | * `:reveal` - this flag marks the tile to be rendered with the **reveal** class 135 | * `:highlight` - this flag marks the tile to be rendered with the **highlight** class 136 | 137 | All of these flags are temporary markings that will be removed in 138 | a couple of phases of animation right after a move is made. 139 | 140 | #### Board view 141 | 142 | The tile map will be converted to a board view that will look like this:" 143 | 144 | (dc/mkdn-pprint-str 145 | [[{:v 2, :id :t1} :_ :_ :_] 146 | [{:v 8, :id :t2} :_ :_ :_] 147 | [{:v 4, :id :t3} :_ :_ :_] 148 | [{:v 4, :id :t4} :_ :_ :_]]) 149 | 150 | "The board view removes the position data as it is implicit. 151 | 152 | The board view is used to compute the board transformation given a 153 | `:left`, `:right`, `:up`, or `:down` move. For instance if we move 154 | the above board to the `:right` it will end up looking like this:" 155 | 156 | (dc/mkdn-pprint-str 157 | [[:_ :_ :_ {:v 2, :id :t1}] 158 | [:_ :_ :_ {:v 8, :id :t2}] 159 | [:_ :_ :_ {:v 4, :id :t3}] 160 | [:_ :_ :_ {:v 4, :id :t4}]]) 161 | 162 | "And then if we move it `:down` it will look like this." 163 | 164 | (dc/mkdn-pprint-str 165 | [[:_ :_ :_ :_] 166 | [:_ :_ :_ {:v 2, :id :t1}] 167 | [:_ :_ :_ {:v 8, :id :t2}] 168 | [:_ :_ :_ {:v 4, :id :t3 :double true :replaces :t4}]]) 169 | 170 | "Once we have transformed the board we will turn it back into the tile map." 171 | 172 | (dc/mkdn-pprint-str 173 | {:t1 { :top 1 :left 3 :v 2 :id :t1} 174 | :t2 { :top 0 :left 3 :v 8 :id :t2} 175 | :t3 { :top 0 :left 3 :v 4 :id :t3 :double true } 176 | :t4 { :top 0 :left 3 :v 4 :id :t4 :remove true }})) 177 | 178 | (defcard-doc 179 | "### Transforming one row 180 | 181 | We need to get a transformation for one row. From there we can get 182 | all the other transformations. 183 | 184 | We will be transforming one row for a move `:left`.") 185 | 186 | (def remove-blanks (partial filterv #(not= % :_))) 187 | 188 | (defn pad-with [length item xs] 189 | (vec (concat xs (take (- length (count xs)) (repeat item))))) 190 | 191 | (def pad-blanks (partial pad-with 4 :_)) 192 | 193 | (defn combine? [tile-1 tile-2] 194 | (cond 195 | (nil? tile-1) false 196 | (nil? tile-2) false 197 | (:double tile-1) false 198 | (:double tile-2) false 199 | (= (:v tile-1) (:v tile-2)) true 200 | :else false)) 201 | 202 | (defn combine [tile-1 tile-2] 203 | (assoc tile-2 :double true :replaces (:id tile-1))) 204 | 205 | (defn tile-reducer [accum tile] 206 | (if (combine? (last accum) tile) 207 | (conj (vec (butlast accum)) (combine (last accum) tile)) 208 | (conj accum tile))) 209 | 210 | ;; this function reduces one row or the board to the left 211 | (def transform-row 212 | (comp pad-blanks 213 | (partial reduce tile-reducer []) 214 | remove-blanks)) 215 | 216 | (deftest transform-row-tests 217 | "In a move `:left` all the blanks will end up on the right. 218 | So let's `remove-blanks` first" 219 | (t/is (= (remove-blanks [:_ :_ :_ :_]) [])) 220 | 221 | (t/is (= (remove-blanks [:_ :_ :_ {:v 8}]) [{:v 8}])) 222 | "We should also be able to `pad-blanks` back to finish the move." 223 | (t/is (= (pad-blanks [{:v 8}]) [{:v 8} :_ :_ :_])) 224 | "Next we are going to create a function to reduce the row. And to 225 | help us we are going to create a predicate `combine?` to tell us if we 226 | can combine two tiles." 227 | 228 | (t/is (combine? {:v 2} {:v 2})) 229 | (t/is (not (combine? {:v 2 :double true} {:v 2}))) 230 | (t/is (not (combine? {:v 2} {:v 2 :double true}))) 231 | (t/is (not (combine? {:v 2} {:v 4}))) 232 | "Then we need a function to help us `combine` the two tiles." 233 | (t/is (= (combine {:id :t1 :v 2} {:id :t2 :v 2}) 234 | {:id :t2, :v 2, :double true, :replaces :t1})) 235 | "We can then assemble the above functions into `transform-row` 236 | which will take a row and do a 2048 move to the left." 237 | 238 | (t/is (= (transform-row [:_ :_ :_ :_]) [:_ :_ :_ :_])) 239 | (t/is (= (transform-row [{:v 8} :_ :_ :_]) [{:v 8} :_ :_ :_])) 240 | (t/is (= (transform-row [ :_ {:v 8} :_ :_]) [{:v 8} :_ :_ :_])) 241 | (t/is (= (transform-row [ :_ :_ {:v 8} :_]) [{:v 8} :_ :_ :_])) 242 | (t/is (= (transform-row [ :_ :_ :_ {:v 8}]) [{:v 8} :_ :_ :_])) 243 | (t/is (= (transform-row [ {:v 4 :id 1} {:v 4 :id 2} :_ :_]) 244 | [{:v 4 :id 2 :double true :replaces 1} :_ :_ :_])) 245 | (t/is (= (transform-row [ {:v 4 :id 1} :_ :_ {:v 4 :id 2}]) 246 | [{:v 4 :id 2 :double true :replaces 1} :_ :_ :_])) 247 | (t/is (= (transform-row [ :_ {:v 4 :id 1} :_ {:v 4 :id 2}]) 248 | [{:v 4 :id 2 :double true :replaces 1} :_ :_ :_])) 249 | (t/is (= (transform-row [ :_ :_ {:v 4 :id 1} {:v 4 :id 2}]) 250 | [{:id 2 :v 4 :double true :replaces 1} :_ :_ :_])) 251 | (t/is (= (transform-row [ :_ {:id 1 :v 4} {:id 2 :v 4} {:id 3 :v 4}]) 252 | [{:id 2 :v 4 :double true :replaces 1} {:id 3 :v 4} :_ :_])) 253 | (t/is (= (transform-row [ {:id 1 :v 4} {:id 2 :v 4} {:id 3 :v 4} {:id 4 :v 4}]) 254 | [{:id 2 :v 4 :double true :replaces 1} {:id 4 :v 4 :double true :replaces 3} :_ :_])) 255 | (t/is (= (transform-row [ {:id 1 :v 4} {:id 2 :v 8} {:id 3 :v 8} {:id 4 :v 4}]) 256 | [{:id 1 :v 4} {:id 3 :v 8 :double true :replaces 2 } {:id 4 :v 4} :_])) 257 | (t/is (= (transform-row [ {:id :a :v 4} { :id :b :v 4} 258 | {:id :c :v 8} { :id :d :v 8}]) 259 | [{:id :b :v 4 :double true :replaces :a } 260 | {:id :d :v 8 :double true :replaces :c } :_ :_]))) 261 | 262 | (defn convert-to-visible-tiles [board] 263 | (vec (keep identity 264 | (for [row (range 4) 265 | col (range 4)] 266 | (let [t (get-in board [row col])] 267 | (when (not= :_ t) (assoc t :top row :left col))))))) 268 | 269 | (deftest convert-to-visible-tiles-card 270 | (t/is (= (convert-to-visible-tiles 271 | [[:_ :_ :_ {:v 2 :id "t1"}] 272 | [:_ :_ :_ {:v 4 :id "t2"}] 273 | [:_ :_ :_ {:v 8 :id "t3"}] 274 | [:_ :_ :_ {:v 16 :id "t4"}]]) 275 | [{:v 2, :id "t1", :top 0, :left 3} 276 | {:v 4, :id "t2", :top 1, :left 3} 277 | {:v 8, :id "t3", :top 2, :left 3} 278 | {:v 16, :id "t4", :top 3, :left 3}])) 279 | (t/is (= (convert-to-visible-tiles 280 | [[:_ :_ :_ :_] 281 | [:_ :_ :_ :_] 282 | [:_ :_ :_ :_] 283 | [:_ :_ :_ :_]]) 284 | []))) 285 | 286 | (defn select-row-key [row key] 287 | (keep (fn [x] (when (not= x :_) (key x))) row)) 288 | 289 | (def reverse-rows (partial mapv (comp vec reverse))) 290 | 291 | (defn reversed [f] (comp reverse-rows f reverse-rows)) 292 | 293 | (defn transpose [matrix] 294 | (vec (apply map vector matrix))) 295 | 296 | (defn transposed [f] (comp transpose f transpose)) 297 | 298 | (def transform-rows* (partial mapv transform-row)) 299 | (def transform-rows-right* (reversed transform-rows*)) 300 | (def transform-rows-up* (transposed transform-rows*)) 301 | (def transform-rows-down* (transposed transform-rows-right*)) 302 | 303 | (defmulti transform-rows identity) 304 | (defmethod transform-rows :left [_ rows] (transform-rows* rows)) 305 | (defmethod transform-rows :right [_ rows] (transform-rows-right* rows)) 306 | (defmethod transform-rows :up [_ rows] (transform-rows-up* rows)) 307 | (defmethod transform-rows :down [_ rows] (transform-rows-down* rows)) 308 | 309 | (def base-board [[:_ :_ :_ :_] 310 | [:_ :_ :_ :_] 311 | [:_ :_ :_ :_] 312 | [:_ :_ :_ :_]]) 313 | 314 | (defn tiles->board [tiles] 315 | (reduce (fn [accum {:keys [top left] :as x}] 316 | (assoc-in accum [top left] 317 | (dissoc x :top :left))) 318 | base-board (vals tiles))) 319 | 320 | (defn get-replaced-tiles [new-tiles] 321 | (keep (fn [x] 322 | (when (:replaces x) 323 | (-> x 324 | (dissoc :replaces :double) 325 | (assoc :id (:replaces x)) 326 | (assoc :remove true)))) 327 | new-tiles)) 328 | 329 | (defn transform-board [direction tiles] 330 | (let [new-tiles (->> tiles 331 | tiles->board 332 | (transform-rows direction) 333 | convert-to-visible-tiles)] 334 | (merge tiles 335 | (into {} (map (juxt :id #(dissoc % :replaces)) new-tiles)) 336 | (into {} (map (juxt :id identity) (get-replaced-tiles new-tiles)))))) 337 | 338 | (defn double-tiles [tiles] 339 | (let [tiles' (filter (fn [[_ v]] (not (:remove v))) tiles)] 340 | (into {} 341 | (map (fn [[k x]] 342 | [k (if (:double x) 343 | (-> x 344 | (assoc :v (* 2 (:v x)) 345 | :highlight true) 346 | (dissoc :double)) 347 | x)]) 348 | tiles')))) 349 | 350 | (defn remove-highlight-and-reveal [tiles] 351 | (into {} 352 | (map (fn [[k v]] [k (dissoc v :highlight :reveal)]) tiles))) 353 | 354 | (defn create-tile [opts] 355 | (merge 356 | { :v (if (< (rand) 0.9) 2 4) 357 | :top (rand-int 4) 358 | :left (rand-int 4) 359 | :id (keyword (gensym "t")) 360 | :reveal true} 361 | opts)) 362 | 363 | (defn empty-slots [board] 364 | (vec (keep 365 | identity 366 | (for [top (range 4) 367 | left (range 4)] 368 | (when (= :_ (get-in board [top left])) 369 | {:top top :left left}))))) 370 | 371 | (defn add-random-tile [tiles] 372 | (->> tiles 373 | tiles->board 374 | empty-slots 375 | rand-nth 376 | create-tile 377 | ((juxt :id identity)) 378 | (apply assoc tiles))) 379 | 380 | (deftest transform-row-left-test 381 | (t/is (= (transform-row [{:v 2 :id "t1"} :_ :_ {:v 2 :id "t2"}]) 382 | [{:v 2 :double true :id "t2" :replaces "t1"} :_ :_ :_])) 383 | (t/is (= (transform-rows :left [[{:v 2 :id "t6"} :_ :_ {:v 2 :id "t1"}] 384 | [:_ :_ :_ {:v 4 :id "t2"}] 385 | [:_ :_ :_ {:v 8 :id "t3"}] 386 | [{:v 16 :id "t7"} :_ :_ {:v 16 :id "t4"}]]) 387 | [[{:v 2 :id "t1" :double true :replaces "t6"} :_ :_ :_] 388 | [{:v 4 :id "t2"} :_ :_ :_] 389 | [{:v 8 :id "t3"} :_ :_ :_] 390 | [{:v 16 :id "t4" :double true :replaces "t7"} :_ :_ :_]])) 391 | (t/is (= (transform-board :left {:t1 {:id :t1 :top 0 :left 0 :v 2} 392 | :t2 {:id :t2 :top 0 :left 3 :v 2}}) 393 | {:t1 {:id :t1, :v 2, :top 0, :left 0, :remove true} 394 | :t2 {:id :t2, :v 2, :double true, :top 0, :left 0}} 395 | )) 396 | (t/is (= (transform-board :right {:t1 {:id :t1 :top 0 :left 0 :v 2} 397 | :t2 {:id :t2 :top 0 :left 3 :v 2}}) 398 | {:t1 {:id :t1, :v 2, :double true, :top 0, :left 3} 399 | :t2 {:id :t2, :v 2, :top 0, :left 3, :remove true}} 400 | )) 401 | (t/is (= (transform-board :up {:t1 {:id :t1 :top 0 :left 0 :v 2} 402 | :t2 {:id :t2 :top 3 :left 0 :v 2}}) 403 | {:t2 {:id :t2, :v 2, :double true, :top 0, :left 0} 404 | :t1 {:id :t1, :v 2, :top 0, :left 0, :remove true}} 405 | )) 406 | (t/is (= (transform-board :down {:t1 {:id :t1 :top 0 :left 0 :v 2} 407 | :t2 {:id :t2 :top 3 :left 0 :v 2}}) 408 | {:t1 {:id :t1, :v 2, :double true, :top 3, :left 0} 409 | :t2 {:id :t2, :v 2, :top 3, :left 0, :remove true}}))) 410 | 411 | (defn move [dir data] 412 | (let [prev @data] 413 | (swap! data (partial transform-board dir)) 414 | (when (not= prev @data) 415 | (go 416 | (/gm,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0==t.index}function a(e){var n=(e.className+" "+(e.parentNode?e.parentNode.className:"")).split(/\s+/);return n=n.map(function(e){return e.replace(/^lang(uage)?-/,"")}),n.filter(function(e){return N(e)||/no(-?)highlight/.test(e)})[0]}function o(e,n){var t={};for(var r in e)t[r]=e[r];if(n)for(var r in n)t[r]=n[r];return t}function i(e){var n=[];return function r(e,a){for(var o=e.firstChild;o;o=o.nextSibling)3==o.nodeType?a+=o.nodeValue.length:1==o.nodeType&&(n.push({event:"start",offset:a,node:o}),a=r(o,a),t(o).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:o}));return a}(e,0),n}function c(e,r,a){function o(){return e.length&&r.length?e[0].offset!=r[0].offset?e[0].offset"}function c(e){l+=""}function u(e){("start"==e.event?i:c)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=o();if(l+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g==e){f.reverse().forEach(c);do u(g.splice(0,1)[0]),g=o();while(g==e&&g.length&&g[0].offset==s);f.reverse().forEach(i)}else"start"==g[0].event?f.push(g[0].node):f.pop(),u(g.splice(0,1)[0])}return l+n(a.substr(s))}function u(e){function n(e){return e&&e.source||e}function t(t,r){return RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var c={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");c[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):Object.keys(a.k).forEach(function(e){u(e,a.k[e])}),a.k=c}a.lR=t(a.l||/\b[A-Za-z0-9_]+\b/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),void 0===a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"==e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function s(e,t,a,o){function i(e,n){for(var t=0;t";return o+=e+'">',o+n+i}function d(){if(!w.k)return n(y);var e="",t=0;w.lR.lastIndex=0;for(var r=w.lR.exec(y);r;){e+=n(y.substr(t,r.index-t));var a=g(w,r);a?(B+=a[1],e+=p(a[0],n(r[0]))):e+=n(r[0]),t=w.lR.lastIndex,r=w.lR.exec(y)}return e+n(y.substr(t))}function h(){if(w.sL&&!R[w.sL])return n(y);var e=w.sL?s(w.sL,y,!0,L[w.sL]):l(y);return w.r>0&&(B+=e.r),"continuous"==w.subLanguageMode&&(L[w.sL]=e.top),p(e.language,e.value,!1,!0)}function v(){return void 0!==w.sL?h():d()}function b(e,t){var r=e.cN?p(e.cN,"",!0):"";e.rB?(M+=r,y=""):e.eB?(M+=n(t)+r,y=""):(M+=r,y=t),w=Object.create(e,{parent:{value:w}})}function m(e,t){if(y+=e,void 0===t)return M+=v(),0;var r=i(t,w);if(r)return M+=v(),b(r,t),r.rB?0:t.length;var a=c(w,t);if(a){var o=w;o.rE||o.eE||(y+=t),M+=v();do w.cN&&(M+=""),B+=w.r,w=w.parent;while(w!=a.parent);return o.eE&&(M+=n(t)),y="",a.starts&&b(a.starts,""),o.rE?0:t.length}if(f(t,w))throw new Error('Illegal lexeme "'+t+'" for mode "'+(w.cN||"")+'"');return y+=t,t.length||1}var x=N(e);if(!x)throw new Error('Unknown language: "'+e+'"');u(x);for(var w=o||x,L={},M="",k=w;k!=x;k=k.parent)k.cN&&(M=p(k.cN,"",!0)+M);var y="",B=0;try{for(var C,j,I=0;;){if(w.t.lastIndex=I,C=w.t.exec(t),!C)break;j=m(t.substr(I,C.index-I),C[0]),I=C.index+j}m(t.substr(I));for(var k=w;k.parent;k=k.parent)k.cN&&(M+="");return{r:B,value:M,language:e,top:w}}catch(A){if(-1!=A.message.indexOf("Illegal"))return{r:0,value:n(t)};throw A}}function l(e,t){t=t||E.languages||Object.keys(R);var r={r:0,value:n(e)},a=r;return t.forEach(function(n){if(N(n)){var t=s(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}}),a.language&&(r.second_best=a),r}function f(e){return E.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,n){return n.replace(/\t/g,E.tabReplace)})),E.useBR&&(e=e.replace(/\n/g,"
")),e}function g(e,n,t){var r=n?x[n]:t,a=[e.trim()];return e.match(/(\s|^)hljs(\s|$)/)||a.push("hljs"),r&&a.push(r),a.join(" ").trim()}function p(e){var n=a(e);if(!/no(-?)highlight/.test(n)){var t;E.useBR?(t=document.createElementNS("http://www.w3.org/1999/xhtml","div"),t.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):t=e;var r=t.textContent,o=n?s(n,r,!0):l(r),u=i(t);if(u.length){var p=document.createElementNS("http://www.w3.org/1999/xhtml","div");p.innerHTML=o.value,o.value=c(u,i(p),r)}o.value=f(o.value),e.innerHTML=o.value,e.className=g(e.className,n,o.language),e.result={language:o.language,re:o.r},o.second_best&&(e.second_best={language:o.second_best.language,re:o.second_best.r})}}function d(e){E=o(E,e)}function h(){if(!h.called){h.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",h,!1),addEventListener("load",h,!1)}function b(n,t){var r=R[n]=t(e);r.aliases&&r.aliases.forEach(function(e){x[e]=n})}function m(){return Object.keys(R)}function N(e){return R[e]||R[x[e]]}var E={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},R={},x={};return e.highlight=s,e.highlightAuto=l,e.fixMarkup=f,e.highlightBlock=p,e.configure=d,e.initHighlighting=h,e.initHighlightingOnLoad=v,e.registerLanguage=b,e.listLanguages=m,e.getLanguage=N,e.inherit=o,e.IR="[a-zA-Z][a-zA-Z0-9_]*",e.UIR="[a-zA-Z_][a-zA-Z0-9_]*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such)\b/},e.CLCM={cN:"comment",b:"//",e:"$",c:[e.PWM]},e.CBCM={cN:"comment",b:"/\\*",e:"\\*/",c:[e.PWM]},e.HCM={cN:"comment",b:"#",e:"$",c:[e.PWM]},e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e});hljs.registerLanguage("ruby",function(e){var b="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",r="and false then defined module in return redo if BEGIN retry end for true self when next until do begin unless END rescue nil else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",c={cN:"yardoctag",b:"@[A-Za-z]+"},a={cN:"value",b:"#<",e:">"},s={cN:"comment",v:[{b:"#",e:"$",c:[c]},{b:"^\\=begin",e:"^\\=end",c:[c],r:10},{b:"^__END__",e:"\\n$"}]},n={cN:"subst",b:"#\\{",e:"}",k:r},t={cN:"string",c:[e.BE,n],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]},i={cN:"params",b:"\\(",e:"\\)",k:r},d=[t,a,s,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{cN:"inheritance",b:"<\\s*",c:[{cN:"parent",b:"("+e.IR+"::)?"+e.IR}]},s]},{cN:"function",bK:"def",e:" |$|;",r:0,c:[e.inherit(e.TM,{b:b}),i,s]},{cN:"constant",b:"(::)?(\\b[A-Z]\\w*(::)?)+",r:0},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":",c:[t,{b:b}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{cN:"variable",b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{b:"("+e.RSR+")\\s*",c:[a,s,{cN:"regexp",c:[e.BE,n],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}],r:0}];n.c=d,i.c=d;var l="[>?]>",u="[\\w#]+\\(\\w+\\):\\d+:\\d+>",N="(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>",o=[{b:/^\s*=>/,cN:"status",starts:{e:"$",c:d}},{cN:"prompt",b:"^("+l+"|"+u+"|"+N+")",starts:{e:"$",c:d}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:r,c:[s].concat(o).concat(d)}});hljs.registerLanguage("javascript",function(r){return{aliases:["js"],k:{keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document"},c:[{cN:"pi",r:10,v:[{b:/^\s*('|")use strict('|")/},{b:/^\s*('|")use asm('|")/}]},r.ASM,r.QSM,r.CLCM,r.CBCM,r.CNM,{b:"("+r.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[r.CLCM,r.CBCM,r.RM,{b:/;/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[r.inherit(r.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,c:[r.CLCM,r.CBCM],i:/["'\(]/}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+r.IR,r:0}]}});hljs.registerLanguage("clojure",function(e){var t={built_in:"def cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},r="a-zA-Z_\\-!.?+*=<>&#'",n="["+r+"]["+r+"0-9/;:]*",a="[-+]?\\d+(\\.\\d+)?",o={b:n,r:0},s={cN:"number",b:a,r:0},c=e.inherit(e.QSM,{i:null}),i={cN:"comment",b:";",e:"$",r:0},d={cN:"literal",b:/\b(true|false|nil)\b/},l={cN:"collection",b:"[\\[\\{]",e:"[\\]\\}]"},m={cN:"comment",b:"\\^"+n},p={cN:"comment",b:"\\^\\{",e:"\\}"},u={cN:"attribute",b:"[:]"+n},f={cN:"list",b:"\\(",e:"\\)"},h={eW:!0,r:0},y={k:t,l:n,cN:"keyword",b:n,starts:h},b=[f,c,m,p,i,u,l,s,d,o];return f.c=[{cN:"comment",b:"comment"},y,h],h.c=b,l.c=b,{aliases:["clj"],i:/\S/,c:[f,c,m,p,i,u,l,s,d]}});hljs.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)\}/}]},s={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]},a={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/-?[a-z\.]+/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",operator:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"shebang",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,e.NM,s,a,t]}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",a={cN:"function",b:c+"\\(",rB:!0,eE:!0,e:"\\("};return{cI:!0,i:"[=/|']",c:[e.CBCM,{cN:"id",b:"\\#[A-Za-z0-9_-]+"},{cN:"class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"attr_selector",b:"\\[",e:"\\]",i:"$"},{cN:"pseudo",b:":(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\\\"\\']+"},{cN:"at_rule",b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{cN:"at_rule",b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[a,e.ASM,e.QSM,e.CSSNM]}]},{cN:"tag",b:c,r:0},{cN:"rules",b:"{",e:"}",i:"[^\\s]",r:0,c:[e.CBCM,{cN:"rule",b:"[^\\s]",rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:!0,i:"[^\\s]",starts:{cN:"value",eW:!0,eE:!0,c:[a,e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"hexcolor",b:"#[0-9A-Fa-f]+"},{cN:"important",b:"!important"}]}}]}]}]}});hljs.registerLanguage("clojure-repl",function(){return{c:[{cN:"prompt",b:/^([\w.-]+|\s*#_)=>/,starts:{e:/$/,sL:"clojure",subLanguageMode:"continuous"}}]}});hljs.registerLanguage("markdown",function(){return{aliases:["md","mkdown","mkd"],c:[{cN:"header",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"blockquote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{cN:"horizontal_rule",b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"link_label",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link_url",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"link_reference",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:"^\\[.+\\]:",rB:!0,c:[{cN:"link_reference",b:"\\[",e:"\\]:",eB:!0,eE:!0,starts:{cN:"link_url",e:"$"}}]}]}});hljs.registerLanguage("xml",function(){var t="[A-Za-z0-9\\._:-]+",e={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php",subLanguageMode:"continuous"},c={eW:!0,i:/]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xsl","plist"],cI:!0,c:[{cN:"doctype",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"|$)",e:">",k:{title:"style"},c:[c],starts:{e:"",rE:!0,sL:"css"}},{cN:"tag",b:"|$)",e:">",k:{title:"script"},c:[c],starts:{e:"",rE:!0,sL:"javascript"}},e,{cN:"pi",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"",c:[{cN:"title",b:/[^ \/><\n\t]+/,r:0},c]}]}});hljs.registerLanguage("java",function(e){var a=e.UIR+"(<"+e.UIR+">)?",t="false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private",c="(\\b(0b[01_]+)|\\b0[xX][a-fA-F0-9_]+|(\\b[\\d_]+(\\.[\\d_]*)?|\\.[\\d_]+)([eE][-+]?\\d+)?)[lLfF]?",r={cN:"number",b:c,r:0};return{aliases:["jsp"],k:t,i:/<\//,c:[{cN:"javadoc",b:"/\\*\\*",e:"\\*/",r:0,c:[{cN:"javadoctag",b:"(^|\\s)@[A-Za-z]+"}]},e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:!0,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},e.UTM]},{bK:"new throw return",r:0},{cN:"function",b:"("+a+"\\s+)+"+e.UIR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:t,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"params",b:/\(/,e:/\)/,k:t,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},r,{cN:"annotation",b:"@[A-Za-z]+"}]}});hljs.registerLanguage("json",function(e){var t={literal:"true false null"},i=[e.QSM,e.CNM],l={cN:"value",e:",",eW:!0,eE:!0,c:i,k:t},c={b:"{",e:"}",c:[{cN:"attribute",b:'\\s*"',e:'"\\s*:\\s*',eB:!0,eE:!0,c:[e.BE],i:"\\n",starts:l}],i:"\\S"},n={b:"\\[",e:"\\]",c:[e.inherit(l,{cN:null})],i:"\\S"};return i.splice(i.length,0,c,n),{c:i,k:t,i:"\\S"}}); -------------------------------------------------------------------------------- /example_src/devdemos/core.cljs: -------------------------------------------------------------------------------- 1 | (ns 2 | ^{:description "Devcards: A high level introduction." 3 | :rigsomelight-post true} 4 | devdemos.core 5 | (:require 6 | [om.core :as om :include-macros true] 7 | [om.dom :as dom :include-macros true] 8 | [reagent.core :as reagent] 9 | [clojure.string :as string] 10 | [sablono.core :as sab :include-macros true] 11 | [devcards.core] 12 | [cljs.test :as t :include-macros true :refer-macros [testing is]]) 13 | (:require-macros 14 | ;; Notice that I am not including the 'devcards.core namespace 15 | ;; but only the macros. This helps ensure that devcards will only 16 | ;; be created when the :devcards is set to true in the build config. 17 | [devcards.core :as dc :refer [defcard defcard-doc deftest dom-node]])) 18 | 19 | 20 | (def ^:export front-matter 21 | {:layout false 22 | :title "The Hard Sell" 23 | :slug "devcards-the-hard-sell" 24 | :date "2015-06-06" 25 | :draft true 26 | :published false 27 | :base-card-options {:frame false}}) 28 | 29 | (enable-console-print!) 30 | 31 | (defcard 32 | "# [Devcards](https://github.com/bhauman/devcards): the hard sell 33 | 34 | The Devcards library is intended to make ClojureScript development 35 | a pure joy. 36 | 37 | Devcards are intended to facilitate **interactive live 38 | development**. Devcards can be used in conjunction with figwheel but 39 | will also work with any form of live code reloading (repl, boot-reload, ...) 40 | 41 | Devcards revolves around a multi-purpose macro called `defcard`. 42 | You can think of `defcard` as a powerful form of **pprint** that helps you 43 | interactively lift code examples out of your source files into the 44 | Devcards interface (you are currently looking at the Devcards 45 | interface). 46 | 47 | The Devcards that you create are intended to have no impact on the 48 | size of your production code. You can use Devcards just as you 49 | would use exectuable comments inline with your source code. You 50 | can also keep them separate like a test suite. 51 | 52 | With [figwheel](https://github.com/bhauman/lein-figwheel), Devcards 53 | configuration couldn't be simpler. Just add 54 | 55 | [![Clojars Project](https://clojars.org/devcards/latest-version.svg)](https://clojars.org/devcards) 56 | 57 | to your dependencies and create a new build config with `:figwheel 58 | {:devcards true}`. See the Quick Start instructions at the end of 59 | this document. 60 | 61 | Let's look at an advanced Devcard: 62 | 63 | ``` 64 | (defcard bmi-calculator ;; optional symbol name 65 | \"*Code taken from Reagent readme.*\" ;; optional markdown doc 66 | (fn [data-atom _] (bmi-component data-atom)) ;; object of focus 67 | {:height 180 :weight 80} ;; optional initial data 68 | {:inspect-data true :history true}) ;; optional devcard config options 69 | ``` 70 | 71 | The [defcard api](#!/devdemos.defcard_api) 72 | is intended to be small and intuitive. 73 | 74 | You can see this devcard rendered below:") 75 | 76 | ;; code from the reagent page adapted to plain reagent 77 | (defn calc-bmi [bmi-data] 78 | (let [{:keys [height weight bmi] :as data} bmi-data 79 | h (/ height 100)] 80 | (if (nil? bmi) 81 | (assoc data :bmi (/ weight (* h h))) 82 | (assoc data :weight (* bmi h h))))) 83 | 84 | (defn slider [bmi-data param value min max] 85 | (sab/html 86 | [:input {:type "range" :value value :min min :max max 87 | :style {:width "100%"} 88 | :on-change (fn [e] 89 | (swap! bmi-data assoc param (.-target.value e)) 90 | (when (not= param :bmi) 91 | (swap! bmi-data assoc :bmi nil)))}])) 92 | 93 | (defn bmi-component [bmi-data] 94 | (let [{:keys [weight height bmi]} (calc-bmi @bmi-data) 95 | [color diagnose] (cond 96 | (< bmi 18.5) ["orange" "underweight"] 97 | (< bmi 25) ["inherit" "normal"] 98 | (< bmi 30) ["orange" "overweight"] 99 | :else ["red" "obese"])] 100 | (sab/html 101 | [:div 102 | [:h3 "BMI calculator"] 103 | [:div 104 | [:span (str "Height: " (int height) "cm")] 105 | (slider bmi-data :height height 100 220)] 106 | [:div 107 | [:span (str "Weight: " (int weight) "kg")] 108 | (slider bmi-data :weight weight 30 150)] 109 | [:div 110 | [:span (str "BMI: " (int bmi) " ")] 111 | [:span {:style {:color color}} diagnose] 112 | (slider bmi-data :bmi bmi 10 50)]]))) 113 | 114 | (defcard bmi-calculator 115 | "*Code taken from the Reagent readme.*" 116 | (fn [data-atom _] (bmi-component data-atom)) 117 | {:height 180 :weight 80} 118 | {:inspect-data true 119 | :frame true 120 | :history true}) 121 | 122 | (defcard-doc 123 | "## Time travel 124 | 125 | Please interact with **the BMI calculator above**. As you change 126 | the sliders you will notice that a 127 |    shows up. 128 | 129 | This is the integrated history control widget which be enabled by 130 | adding `{:history true}` to the devcard options. 131 | 132 | Go ahead and move the sliders and play with the history control. 133 | 134 | You can move forward with the control. 135 | 136 | You can continue from where you left off with the fast forward control 137 | 138 | 139 | 140 | 141 | 142 | 143 | You can reify the current point in history and start working with your app from this point with the 144 | control or by simply interacting with the app. 145 | 146 | ## Data display 147 | 148 | You will also notice that the data from the main data store is 149 | being displayed. This is enabled by adding `{:inspect-data true}` 150 | to the devcard options. 151 | 152 | If you interact with the calculator above you will see that the 153 | integers are being stored as strings in the data atom. This is a 154 | smell that you will see immediately when the data is displayed 155 | front and center like this. 156 | 157 | ## Markdown docs 158 | 159 | The documentation string \"*Code taken from the Reagent readme*\" 160 | in the example above is optional and allows for the easy display of 161 | contextual information. 162 | 163 | ## Auto-detection and dispatch 164 | 165 | The `defcard` macro does its best to display the given object. 166 | You can pass `defcard` a **string** (will be interpreted as 167 | markdown), a **function** that takes a data-atom and an React owner, a 168 | **ReactElement**, a **Map**, a **Vector**, a **List**, an **Atom**, 169 | an **RAtom**, an **IDeref**, anything that implements 170 | **IDevcardOptions** or **IDevcard**, and I'm hoping to get various 171 | cursor implementations working as well. 172 | 173 | Implementing your own cards is easy. You can simply create an 174 | arbitrary ReactElement and `defcard` will render it. If you want to 175 | create a completely custom card there are the [**IDevcardOptions** 176 | and **IDevcard** protocols](#!/devdemos.custom_cards). 177 | " ) 178 | 179 | (defcard 180 | "# cljs.test integration 181 | 182 | Devcards provides a `deftest` macro that behaves very similarly to 183 | the `cljs.test/deftest` macro. This makes it easy to define tests 184 | that both show up in the Devcards display and can be run 185 | using `(run-tests)` as well. 186 | 187 | The test card has controls in the upper right hand corner that not 188 | only summerize testing status but also allow you to focus on passing or 189 | failing tests. 190 | 191 | Go ahead and click on the numbers in the header of this card. 192 | 193 | The test card will display the testing context as well as the 194 | messages for the various tests that run. 195 | 196 | For example the following tests are defined like this: 197 | 198 | ``` 199 | (deftest cljs-test-integration 200 | \"## Here are some example tests\" 201 | (testing \"testing context 1\" 202 | (is (= (+ 3 4 55555) 4) \"This is the message arg to an 'is' test\") 203 | (is (= (+ 1 0 0 0) 1) \"This should work\") 204 | (is (= 1 3)) 205 | (is false) 206 | (is (throw \"errors get an extra red line on the side\"))) 207 | \"Top level strings are interpreted as markdown for inline documentation.\" 208 | (testing \"testing context 2\" 209 | (is (= (+ 1 0 0 0) 1)) 210 | (is (= (+ 3 4 55555) 4)) 211 | (is false) 212 | (testing \"nested context\" 213 | (is (= (+ 1 0 0 0) 1)) 214 | (is (= (+ 3 4 55555) 4)) 215 | (is false)))) 216 | ``` 217 | 218 | The `testing` and is macros are the ones from `cljs.test` 219 | 220 | These tests are rendered below:") 221 | 222 | (deftest cljs-test-integration 223 | "## Here are some example tests" 224 | (testing "testing context 1" 225 | (is (= (+ 3 4 55555) 4) "This is the message arg to an 'is' test") 226 | (is (= (+ 1 0 0 0) 1) 227 | "This should work") 228 | (is (= 1 3)) 229 | (is false) 230 | (is (throw "errors get an extra red line on the side"))) 231 | "Top level strings are interpreted as markdown for inline documentation." 232 | (t/testing "testing context 2" 233 | (is (= (+ 1 0 0 0) 1)) 234 | (t/is (= (+ 3 4 55555) 4)) 235 | (t/is false) 236 | (t/testing "nested context" 237 | (is (= (+ 1 0 0 0) 1)) 238 | (t/is (= (+ 3 4 55555) 4)) 239 | (t/is false)))) 240 | 241 | (defcard 242 | "You can learn more about testing with devcards [here](#!/devdemos.testing)" 243 | ) 244 | 245 | 246 | 247 | (defn om-slider [bmi-data param value min max] 248 | (sab/html 249 | [:input {:type "range" :value value :min min :max max 250 | :style {:width "100%"} 251 | :on-change (fn [e] 252 | (om/update! bmi-data param (.-target.value e)) 253 | (when (not= param :bmi) 254 | (om/update! bmi-data :bmi nil)))}])) 255 | 256 | (defn om-bmi-component [bmi-data owner] 257 | (let [{:keys [weight height bmi]} (calc-bmi bmi-data) 258 | [color diagnose] (cond 259 | (< bmi 18.5) ["orange" "underweight"] 260 | (< bmi 25) ["inherit" "normal"] 261 | (< bmi 30) ["orange" "overweight"] 262 | :else ["red" "obese"])] 263 | (om/component 264 | (sab/html 265 | [:div 266 | [:h3 "BMI calculator"] 267 | [:div 268 | [:span (str "Height: " (int height) "cm")] 269 | (om-slider bmi-data :height height 100 220)] 270 | [:div 271 | [:span (str "Weight: " (int weight) "kg")] 272 | (om-slider bmi-data :weight weight 30 150)] 273 | [:div 274 | [:span (str "BMI: " (int bmi) " ")] 275 | [:span {:style {:color color}} diagnose] 276 | (om-slider bmi-data :bmi bmi 10 50)]])))) 277 | 278 | (defcard 279 | "# Om support 280 | 281 | Here is the same calculator being rendered as an Om application. 282 | 283 | ``` 284 | (defcard om-support 285 | (dc/om-root om-bmi-component) 286 | {:height 180 :weight 80} ;; initial data 287 | {:inspect-data true :history true }) 288 | ``` 289 | ") 290 | 291 | (defcard om-support 292 | (dc/om-root om-bmi-component) 293 | {:height 180 :weight 80} ;; initial data 294 | {:inspect-data true 295 | :frame true 296 | :history true }) 297 | 298 | (defonce re-bmi-data (reagent/atom {:height 180 :weight 80})) 299 | 300 | (defn re-slider [param value min max] 301 | [:input {:type "range" :value value :min min :max max 302 | :style {:width "100%"} 303 | :on-change (fn [e] 304 | (swap! re-bmi-data assoc param (.-target.value e)) 305 | (when (not= param :bmi) 306 | (swap! re-bmi-data assoc :bmi nil)))}]) 307 | 308 | (defn re-bmi-component [] 309 | (let [{:keys [weight height bmi]} (calc-bmi @re-bmi-data) 310 | [color diagnose] (cond 311 | (< bmi 18.5) ["orange" "underweight"] 312 | (< bmi 25) ["inherit" "normal"] 313 | (< bmi 30) ["orange" "overweight"] 314 | :else ["red" "obese"])] 315 | [:div 316 | [:h3 "BMI calculator"] 317 | [:div 318 | "Height: " (int height) "cm" 319 | [re-slider :height height 100 220]] 320 | [:div 321 | "Weight: " (int weight) "kg" 322 | [re-slider :weight weight 30 150]] 323 | [:div 324 | "BMI: " (int bmi) " " 325 | [:span {:style {:color color}} diagnose] 326 | [re-slider :bmi bmi 10 50]]])) 327 | 328 | (defcard 329 | "# There is also built-in support for Reagent 330 | 331 | Below is the same BMI calculator in Reagent: 332 | ``` 333 | (defcard reagent-support 334 | (dc/reagent re-bmi-component) 335 | re-bmi-data ;; reagent atom 336 | {:inspect-data true :history true }) 337 | ```") 338 | 339 | (defcard reagent-support 340 | 341 | (dc/reagent re-bmi-component) 342 | re-bmi-data 343 | {:inspect-data true 344 | :frame true 345 | :history true }) 346 | 347 | (defcard 348 | "# Not cool enough? 349 | 350 | Well there is a bunch more, but that's what the docs are for. 351 | 352 | For quick documentation please see the source for these devcards here. 353 | 354 | ## Quick Start 355 | 356 | These are brief instructions for the curious. These will not be 357 | helpful if you are not an experienced ClojureScript developer. 358 | 359 | You can generate a new devcards project with: 360 | 361 | ```bash 362 | $ lein new devcards hello-world 363 | ``` 364 | 365 | ## Existing project 366 | 367 | Integrating devcards into an existing is fairly straightforward and 368 | requires a seperate build, similar to how you would create a test 369 | build. 370 | 371 | Add 372 | 373 | [![Clojars Project](https://clojars.org/devcards/latest-version.svg)](https://clojars.org/devcards) 374 | 375 | as a dependency. 376 | 377 | Require the devcards macros: 378 | 379 | ``` 380 | (ns example.core 381 | (:require-macros 382 | ;; Notice that I am not including the 'devcards.core namespace 383 | ;; but only the macros. This helps ensure that devcards will only 384 | ;; be created when the :devcards is set to true in the build config. 385 | [devcards.core :as dc :refer [defcard deftest]])) 386 | ``` 387 | 388 | If you are using figwheel you can create a build from your current 389 | figwheel dev build and add `:devcards true` (figwheel >= 0.3.7) to 390 | your `:figwheel` build config like so: 391 | 392 | ```clojure 393 | :cljsbuild { 394 | :builds [{:id :devcards 395 | :source-paths [\"src\"] 396 | :figwheel { :devcards true } 397 | :compiler { 398 | :main \"example.core\" 399 | :asset-path \"js/out\" 400 | :output-to \"resources/public/js/example.js\" 401 | :output-dir \"resources/public/js/out\" 402 | }}]} 403 | ``` 404 | 405 | It's important to make sure that your application isn't launching 406 | itself on load. We don't want your application to run. We want the 407 | Devards application to run. So having a seperate HTML file for the 408 | devcards build is the best solution. 409 | 410 | ``` 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | ``` 421 | 422 | A quick way to prevent your main application from running is to make 423 | it conditional on the presence of the DOM node it's expecting to 424 | mount and then just include that DOM node on HTML pages where your 425 | app is going to launch. 426 | 427 | ``` 428 | (defn main [] 429 | ;; conditionally start the app based on the presence of #main-app-area 430 | ;; node is on the page 431 | (if-let [node (.getElementById js/document \"main-app-area\")] 432 | (js/React.render (sab/html [:div \"This is main app is ruunning.\"]) node))) 433 | 434 | (main) 435 | ``` 436 | 437 | For now refer to the 438 | [devcards-template](https://github.com/bhauman/devcards-template) 439 | for a more complete picture of setting up decards. 440 | 441 | ## Devcards without Figwheel 442 | 443 | Figwheel does some magic so that Devcards can be included or excluded 444 | from your code easily. You can certainly use Devcards without Figwheel, 445 | but there are three things that you will need to do. 446 | 447 | #### You need to specify `:devcards true` **in the build-options** of your ClojureScript build 448 | 449 | ```clojure 450 | { :main \"{{name}}.core\" 451 | :devcards true 452 | :asset-path \"js/compiled/devcards_out\" 453 | :output-to \"resources/public/js/{{sanitized}}_devcards.js\" 454 | :output-dir \"resources/public/js/devcards_out\" 455 | :source-map-timestamp true } 456 | ``` 457 | 458 | This is important as it is a signal to the `defcard` macro to render 459 | the cards. 460 | 461 | #### You will need to require `devcards.core` in the files that use devcards as such: 462 | 463 | ```clojure 464 | (ns example.core 465 | (:require 466 | [devcards.core :as dc] ; <-- here 467 | [sablono.core :as sab]) ; just for this example 468 | (:require-macros 469 | [devcards.core :refer [defcard]])) ; <-- and here 470 | 471 | (defcard my-first-card 472 | (sab/html [:h1 \"Devcards is freaking awesome!\"])) 473 | ``` 474 | 475 | This isn't required with Figwheel because it puts `devcards.core` into the 476 | build automatically. 477 | 478 | #### You will need to start the Devcards UI 479 | 480 | ``` 481 | (devcards.core/start-devcard-ui!) 482 | ``` 483 | 484 | As mentioned above, you don't want the Devcards UI to compete with 485 | your application's UI so you will want to make sure it isn't getting 486 | launched. 487 | 488 | 489 | 490 | ") 491 | -------------------------------------------------------------------------------- /src/devcards/system.cljs: -------------------------------------------------------------------------------- 1 | (ns devcards.system 2 | (:require 3 | [clojure.string :as string] 4 | [cljs.core.async :refer [put! [f] (fn [e] (.preventDefault e) (f e))) 27 | 28 | (defn get-element-by-id [id] (.getElementById js/document id)) 29 | 30 | (defn devcards-app-node [] (get-element-by-id devcards-app-element-id)) 31 | 32 | (defn path->unique-card-id [path] 33 | (string/join "." (map (fn [x] (str "[" x "]")) 34 | (map name (cons :cardpath path))))) 35 | 36 | #_(defn unique-card-id->path [card-id] 37 | (mapv keyword 38 | (-> (subs card-id 1 39 | (dec (count card-id))) 40 | (string/split #"\].\[") 41 | rest))) 42 | 43 | (defn create-element* [tag id style-text] 44 | (let [el (js/document.createElement tag)] 45 | (set! (.-id el) id) 46 | (.appendChild el (js/document.createTextNode style-text)) 47 | el)) 48 | 49 | (def create-style-element (partial create-element* "style")) 50 | (def create-script-element (partial create-element* "script")) 51 | 52 | (defn prepend-child [node node2] 53 | (if-let [first-child (.-firstChild node)] 54 | (.insertBefore node node2 first-child) 55 | (.appendChild node node2))) 56 | 57 | (defn add-css-if-necessary! [] 58 | (if-let [heads (.getElementsByTagName js/document "head")] 59 | (let [head (aget heads 0)] 60 | (when-not (get-element-by-id "com-rigsomelight-code-highlight-css") 61 | (.appendChild head 62 | (create-style-element "com-rigsomelight-code-highlight-css" 63 | (inline-resouce-file "public/devcards/css/com_rigsomelight_github_highlight.css")))) 64 | 65 | 66 | (when-not (get-element-by-id "com-rigsomelight-devcards-css") 67 | (.appendChild head (create-style-element "com-rigsomelight-devcards-css" 68 | (inline-resouce-file "public/devcards/css/com_rigsomelight_devcards.css")))) 69 | (when-not (get-element-by-id "com-rigsomelight-devcards-addons-css") 70 | (.appendChild head (create-style-element "com-rigsomelight-devcards-css" 71 | (inline-resouce-file "public/devcards/css/com_rigsomelight_devcards_addons.css")))) 72 | (when-not (get-element-by-id "com-rigsomelight-edn-css") 73 | (.appendChild head 74 | (create-style-element "com-rigsomelight-edn-css" 75 | (inline-resouce-file "public/devcards/css/com_rigsomelight_edn_flex.css")))) 76 | 77 | ;; we are injecting conditionally so that we can skip mobile 78 | ;; and skip node 79 | ;; really not diggin this but ... 80 | (when-not (or (get-element-by-id "com-rigsomelight-code-highlighting") 81 | (device/isMobile)) 82 | (.appendChild head 83 | (create-script-element "com-rigsomelight-code-highlighting" 84 | (inline-resouce-file "public/devcards/js/highlight.pack.js"))))))) 85 | 86 | (defn render-base-if-necessary! [] 87 | (add-css-if-necessary!) 88 | (when-not (devcards-app-node) 89 | (let [el (js/document.createElement "div")] 90 | (set! (.-id el) devcards-app-element-id) 91 | (prepend-child (.-body js/document) el)))) 92 | 93 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 94 | ;;; Hashbang routing 95 | 96 | (declare set-current-path history) 97 | 98 | (defonce history 99 | (when (utils/html-env?) 100 | (let [h (History.)] 101 | (.setEnabled h true) 102 | h))) 103 | 104 | (defn path->token [path] 105 | (str "!/" (string/join "/" (map name path)))) 106 | 107 | (defn token->path [token] 108 | (vec (map keyword 109 | (-> token 110 | (string/replace-first #"#" "") 111 | (string/replace-first #"!/" "") 112 | (string/split #"/"))))) 113 | 114 | #_(prn (token->path (.getToken history))) 115 | 116 | #_(prn (token->path (aget js/location "hash"))) 117 | 118 | (defn hash-navigate [path] 119 | (.setToken history (path->token path))) 120 | 121 | (defn hash-routing-init [state-atom] 122 | (events/listen history EventType/NAVIGATE 123 | #(swap! state-atom set-current-path (token->path (.-token %)))) 124 | ;; we should probably just get the location and parse this out to 125 | ;; avoid the initial race condition where .getToken isn't populated 126 | (when-let [token (aget js/location "hash")] 127 | (swap! state-atom set-current-path (token->path token)))) 128 | 129 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 130 | 131 | (defn devcard? [d] 132 | (and (map? d) 133 | #_(:data-atom d) 134 | (:func d) 135 | (:path d) 136 | (:position d) 137 | d)) 138 | 139 | (defn path-collision [state path] 140 | (if-let [c (get (:path-collision-count state) path)] 141 | (vec (concat (butlast (vec path)) 142 | [(keyword (str (name (last path)) "-" c))])) 143 | path)) 144 | 145 | (defn register-collision [state path] 146 | (update-in state [:path-collision-count path] inc)) 147 | 148 | (defmulti dev-trans first) 149 | 150 | (defmethod dev-trans :default [msg state] state) 151 | 152 | (defmethod dev-trans :register-card [[_ {:keys [path options func]}] state] 153 | (let [position (:position state) 154 | new-path (path-collision state path)] 155 | (-> state 156 | (update-in [:position] inc) 157 | (update-in (cons :cards new-path) 158 | (fn [dc] 159 | { :path new-path 160 | :func func 161 | :position position })) 162 | (register-collision path)))) 163 | 164 | (def devcard-initial-data { :current-path [] 165 | :position 0 166 | :cards {} 167 | :path-collision-count {} 168 | :base-card-options { :frame true 169 | :heading true 170 | :padding true 171 | :hidden false 172 | :inspect-data false 173 | :watch-atom true 174 | :history false } }) 175 | 176 | (defonce app-state (atom devcard-initial-data)) 177 | 178 | (defn valid-path? [state path] 179 | (or (= [] path) 180 | (get-in (:cards state) path))) 181 | 182 | (defn enforce-valid-path [state path] 183 | (vec (if (valid-path? state path) path []))) 184 | 185 | (defn add-to-current-path [{:keys [current-path] :as state} path] 186 | (assoc state 187 | :current-path 188 | (enforce-valid-path state (conj current-path (keyword path))))) 189 | 190 | (defn set-current-path [{:keys [current-path] :as state} path] 191 | (let [path (vec (map keyword path))] 192 | (if (not= current-path path) 193 | (-> state 194 | (assoc :current-path (enforce-valid-path state path)) 195 | #_add-navigate-effect) 196 | state))) 197 | 198 | (defn set-current-path! [state-atom path] 199 | (swap! state-atom set-current-path path) 200 | (hash-navigate path)) 201 | 202 | (defn current-page [data] 203 | (and (:current-path data) 204 | (:cards data) 205 | (get-in (:cards data) (:current-path data)))) 206 | 207 | (defn display-single-card? [state] 208 | (devcard? (current-page state))) 209 | 210 | (defn display-dir-paths [state] 211 | (let [cur (current-page state)] 212 | (filter (complement (comp devcard? second)) cur))) 213 | 214 | (defn display-cards [cur] 215 | (filter (comp #(and (not (:delete-card %)) 216 | (devcard? %)) second) cur)) 217 | 218 | (def ^:dynamic *devcard-data* nil) 219 | 220 | (defn card-template [state-atom {:keys [path options func] :as card}] 221 | (sab/html 222 | [:div.com-rigsomelight-devcard {:key (path->unique-card-id path)} 223 | (cljs.core/binding [*devcard-data* card] 224 | (func))])) 225 | 226 | (defn render-cards [cards state-atom] 227 | (map (comp (partial card-template state-atom) second) 228 | (sort-by (comp :position second) cards))) 229 | 230 | (defn main-cards-template [state-atom] 231 | (let [data @state-atom] 232 | (if (display-single-card? data) 233 | (card-template state-atom (current-page data)) 234 | (render-cards (display-cards (current-page data)) state-atom)))) 235 | 236 | (defn breadcrumbs [{:keys [current-path] :as state}] 237 | (let [cpath (map name (cons :devcards current-path)) 238 | crumbs 239 | (map (juxt last rest) 240 | (rest (map-indexed 241 | (fn [i v] (subvec v 0 i)) 242 | (take (inc (count cpath)) 243 | (repeat (vec cpath))))))] 244 | crumbs)) 245 | 246 | (declare cljs-logo) 247 | 248 | (defn breadcrumbs-templ [crumbs state-atom] 249 | (sab/html 250 | [:div.com-rigsomelight-devcards-card-base.com-rigsomelight-devcards-breadcrumbs.com-rigsomelight-devcards-typog 251 | (interpose 252 | (sab/html [:span.com-rigsomelight-devcards-breadcrumb-sep "/"]) 253 | (map (fn [[n path]] 254 | (sab/html 255 | [:span {:style {:display "inline-block" }} 256 | [:a.com-rigsomelight-devcards_set-current-path 257 | {:href "#" 258 | :onClick (prevent-> #(set-current-path! state-atom path))} 259 | (str n)]])) 260 | crumbs)) 261 | (cljs-logo)])) 262 | 263 | (defn navigate-to-path [key state-atom] 264 | (swap! state-atom 265 | (fn [s] 266 | (let [new-s (add-to-current-path s key)] 267 | (hash-navigate (:current-path new-s)) 268 | new-s)))) 269 | 270 | (defn dir-links [dirs state-atom] 271 | (when-not (empty? dirs) 272 | (sab/html 273 | [:div.com-rigsomelight-devcards-list-group.com-rigsomelight-devcards-typog 274 | (map (fn [[key child-tree]] 275 | (sab/html 276 | [:a.com-rigsomelight-devcards-list-group-item 277 | {:href "#" 278 | :onClick 279 | (prevent-> 280 | (fn [e] (navigate-to-path key state-atom))) 281 | #_:onTouchStart 282 | #_(prevent-> 283 | (fn [e] (navigate-to-path key state-atom)))} 284 | [:span.com-rigsomelight-devcards-badge 285 | {:style {:float "right"}} 286 | (count child-tree)] 287 | [:span " " (name key)]])) 288 | (sort-by (fn [[key _]] (name key)) dirs))]))) 289 | 290 | (defn main-template [state-atom] 291 | (let [data @state-atom] 292 | (sab/html 293 | [:div 294 | {:className 295 | (str "com-rigsomelight-devcards-base " 296 | (when-let [n (first (:current-path data))] 297 | (string/replace (name n) "." "-")))} 298 | #_[:div.com-rigsomelight-devcards-navbar 299 | [:div.com-rigsomelight-devcards-container 300 | [:span.com-rigsomelight-devcards-brand 301 | "(:devcards ClojureScript)"]]] 302 | [:div.com-rigsomelight-devcards-container 303 | (when-let [crumbs (breadcrumbs data)] 304 | (breadcrumbs-templ crumbs state-atom)) 305 | (when-not (display-single-card? data) 306 | (let [dir-paths (display-dir-paths data)] 307 | (dir-links dir-paths state-atom))) 308 | [:div 309 | (main-cards-template state-atom)]]]))) 310 | 311 | (defonce-react-class DevcardsRoot 312 | #js {:componentDidMount 313 | (fn [] 314 | (this-as this 315 | (add-watch app-state 316 | :renderer-watch 317 | (fn [_ _ _ _] 318 | (.forceUpdate this))))) 319 | :render (fn [] (main-template app-state)) } ) 320 | 321 | 322 | (defn renderer [state-atom] 323 | #_(prn "Rendering") 324 | (js/React.render 325 | (js/React.createElement DevcardsRoot) 326 | #_(sab/html [:div 327 | (main-template state-atom) 328 | #_(edn-rend/html-edn @state-atom)]) 329 | (devcards-app-node))) 330 | 331 | (comment 332 | 333 | 334 | a debug option :debug-card true 335 | 336 | when initial state changes we should reset the state 337 | 338 | an iterator to delinate a card in many states 339 | 340 | speed test pprint and hightlighting versus edn-renderer 341 | 342 | use a pure component for the edn renderer to memoize rerenders 343 | 344 | look at upndown.js and marked.js 345 | 346 | probably switch to marked for markdown parsing 347 | 348 | fix loading race 349 | 350 | move highlighting out and force folks to require hljs if they want it? 351 | 352 | generate blog posts from a namespace with devcards 353 | - can implement code modules 354 | - look at dev mode and prod mode for this 355 | - front matter in ns meta data 356 | 357 | fix style of history so that there is no margin under it 358 | when there is no data being inspected 359 | 360 | move documentation cards into more descriptive namespaces 361 | fill out details better 362 | 363 | look at being able to render cursors 364 | 365 | BACKBURNER 366 | make slider component 367 | consider web-components for hiding css styling!!! 368 | turn system into react component? 369 | 370 | ) 371 | 372 | (defn merge-in-new-data [state new-state] 373 | (assoc state 374 | :path-collision-count {} 375 | :position (:position new-state) 376 | :cards (merge 377 | (:cards state) 378 | (:cards new-state)))) 379 | 380 | ;; the only major potential problem here is that If we only register 381 | ;; some of the cards of a namespace then the other cards in the 382 | ;; namespace will dissapear. If one is doing calculations at the top 383 | ;; level that take more than the wait time this could be a problem 384 | (defn off-the-books 385 | "Run sequential messages off the books outside of the atom and 386 | then difference the result so we can only display the new cards 387 | that have arrived. This prevents multiple renders and allows us 388 | to delete cards live." 389 | [channel start-data first-message] 390 | (let [;timer (timeout 3000) 391 | initial-data (-> start-data 392 | (assoc :path-collision-count {}) 393 | (dissoc :cards))] 394 | #_(prn "off the books") 395 | (go-loop [data (dev-trans first-message initial-data)] 396 | #_(prn "here") 397 | (let [timer (timeout 500)] ;; needs to be longer for mobile think 398 | (when-let [[[msg-name payload] ch] (alts! [channel timer])] 399 | (cond 400 | (= ch timer) (merge-in-new-data start-data data) 401 | ;; this will function without jsreload. but allows us to 402 | ;; render a tick faster 403 | (= msg-name :jsreload) (merge-in-new-data start-data data) 404 | :else 405 | (do 406 | (recur (dev-trans [msg-name payload] data))))))))) 407 | 408 | (defn load-data-from-channel! [channel] 409 | (go (let [new-state ( 469 | 470 | ") 495 | 496 | (defn cljs-logo [] 497 | (.span (.-DOM js/React) 498 | (clj->js { :key "cljs-logo" 499 | :dangerouslySetInnerHTML 500 | { :__html 501 | cljs-logo-svg }}))) 502 | --------------------------------------------------------------------------------