├── 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 | [](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+=""+t(e)+">"}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:/,e:/>;/,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:/,r:0,c:[e,{cN:"attribute",b:t,r:0},{b:"=",r:0,c:[{cN:"value",c:[e],v:[{b:/"/,e:/"/},{b:/'/,e:/'/},{b:/[^\s\/>]+/}]}]}]};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:"",rE:!0,sL:"css"}},{cN:"tag",b:"",rE:!0,sL:"javascript"}},e,{cN:"pi",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"?",e:"/?>",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 | [](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 | [](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 |
--------------------------------------------------------------------------------