Skip to content

Commit 40b767a

Browse files
add repl sidebar example
1 parent dd958e7 commit 40b767a

File tree

2 files changed

+341
-0
lines changed

2 files changed

+341
-0
lines changed

src/civitas/repl-menu.webp

101 KB
Loading

src/civitas/repl.clj

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
^:kindly/hide-code
2+
^{:clay {:title "I'll take a side of REPL with that"
3+
:quarto {:author [:timothypratley]
4+
:description "Selective interaction with a persistent REPL sidebar inside your Clay documents"
5+
:category :clojure
6+
:type :post
7+
:date "2025-08-05"
8+
:tags [:repl :clay]
9+
:page-layout :full}}}
10+
(ns civitas.repl
11+
(:require [scicloj.kindly.v4.kind :as kind]))
12+
13+
;; > "Would you like to make that interactive?"
14+
;; >
15+
;; > -- The eternal question facing documentation writers.
16+
17+
;; Most code documentation has either static code with fixed results
18+
;; or dynamic editor blocks with evaluated results.
19+
;; This post explores a third option, a sidebar REPL that coexists with static examples,
20+
;; letting you copy interesting snippets and build on them.
21+
22+
;; ## Persistent Workspace Benefits
23+
24+
;; The sidebar REPL mirrors the intuitive "one environment, one REPL" model,
25+
;; keeping state across interactions.
26+
27+
(kind/hiccup
28+
'((require '[reagent.core :as r])
29+
(defonce state (r/atom {:zaniness "moderate"
30+
:mood "curious"}))))
31+
32+
;; We start with some initial mood and zaniness.
33+
;; Here's a suggestion to modify it:
34+
35+
;; ```clojure
36+
;; (swap! state assoc :zaniness "maximum")
37+
;; ```
38+
39+
;; ::: {.callout-tip}
40+
;; Click the "copy" icon in the code example to transfer it to the REPL window.
41+
;; :::
42+
43+
;; And now we'll add to the post a mini-app for monitoring the state.
44+
45+
(kind/hiccup
46+
[:div.card {:style {:margin "20px 0"
47+
:max-width "600px"}}
48+
[:div.card-header.bg-success.text-white
49+
[:h5.mb-0 "🔍 State Monitor"]]
50+
[:div.card-body
51+
[:dl.row.mb-0
52+
[:dt.col-sm-3.text-end "Current state:"]
53+
[:dd.col-sm-9
54+
[:div.p-2.bg-light.border.rounded.font-monospace.small
55+
['(fn [] (pr-str @state))]]]
56+
[:dt.col-sm-3.text-end "Instructions:"]
57+
[:dd.col-sm-9.text-muted.small
58+
"Try: " [:div.sourceCode [:code "(swap! state assoc :key \"value\")"]] " in the REPL"]]]])
59+
60+
;; The state you create in the REPL affects page components.
61+
62+
;; ## Curated and Open Exploration
63+
64+
;; Authors can provide focused examples for specific learning goals,
65+
;; while readers can pursue tangents.
66+
;; Here's an example showcasing Clojure's `frequencies` function with some delicious data:
67+
68+
(frequencies ["apple"
69+
"banana"
70+
"apple"
71+
"cherry"
72+
"banana"
73+
"apple"])
74+
75+
;; Try it with different data, maybe the letters in your name.
76+
77+
;; ```clojure
78+
;; (frequencies "supercalifragilisticexpialidocious")
79+
;; ```
80+
81+
;; Or simulate random selections:
82+
83+
;; ```clojure
84+
;; (frequencies (repeatedly 1000
85+
;; #(rand-nth [:a :b :c :d :e :f])))
86+
;; ```
87+
88+
;; ## Visual Playground
89+
90+
;; The REPL can affect components on the page.
91+
;; This interactive canvas demonstrates how data can drive visual elements.
92+
93+
(kind/hiccup
94+
[:div.card {:style {:margin "20px 0"}}
95+
[:div.card-header.bg-primary.text-white
96+
[:h5.mb-0 "🎨 Interactive Canvas"]]
97+
[:div.card-body
98+
[:p.mb-3 "Shapes controlled by " [:code "@state"] " — use the REPL to create and modify them."]
99+
['(fn []
100+
[:svg {:xmlns "http://www.w3.org/2000/svg"
101+
:width "100%"
102+
:height "250px"
103+
:viewBox "0 0 500 250"
104+
:style {:border "2px solid #dee2e6" :border-radius "4px" :background "#f8f9fa"}}
105+
;; Grid pattern
106+
[:defs
107+
[:pattern {:id "grid" :width "50" :height "50" :patternUnits "userSpaceOnUse"}
108+
[:path {:d "M 50 0 L 0 0 0 50" :fill "none" :stroke "#e9ecef" :stroke-width "1"}]]]
109+
[:rect {:width "500" :height "250" :fill "url(#grid)"}]
110+
;; Dynamic shapes from state
111+
(for [{:keys [type x y size color]} (:shapes @state)]
112+
(case type
113+
:circle [:circle {:cx x :cy y :r size :fill color :stroke "#333" :stroke-width 1}]
114+
:square [:rect {:x (- x size) :y (- y size)
115+
:width (* 2 size) :height (* 2 size)
116+
:fill color :stroke "#333" :stroke-width 1}]
117+
:triangle [:polygon {:points (str x "," (- y size) " "
118+
(- x size) "," (+ y size) " "
119+
(+ x size) "," (+ y size))
120+
:fill color :stroke "#333" :stroke-width 1}]
121+
nil))])]]])
122+
123+
;; Add a single shape (maybe a tiny orange dot? 🟠):
124+
125+
;; ```clojure
126+
;; (swap! state update :shapes conj
127+
;; {:type :circle :x 250 :y 200 :size 15 :color "#f39c12"})
128+
;; ```
129+
130+
;; ::: {.callout-tip}
131+
;; Click the "copy" icon in the code example to transfer it to the REPL window, then press CTRL+Enter to eval.
132+
;; :::
133+
134+
;; Add more shapes by modifying the `:shapes` vector:
135+
136+
;; ```clojure
137+
;; (swap! state update :shapes into
138+
;; [{:type :circle :x 100 :y 100 :size 30 :color "#e74c3c"}
139+
;; {:type :square :x 200 :y 150 :size 25 :color "#3498db"}
140+
;; {:type :triangle :x 350 :y 100 :size 40 :color "#2ecc71"}])
141+
;; ```
142+
143+
;; Generate a ✨random constellation✨:
144+
145+
;; ```clojure
146+
;; (swap! state assoc :shapes
147+
;; (repeatedly 15
148+
;; #(hash-map :type (rand-nth [:circle :square :triangle])
149+
;; :x (rand-int 500)
150+
;; :y (rand-int 250)
151+
;; :size (+ 8 (rand-int 25))
152+
;; :color (rand-nth ["#e74c3c" "#3498db" "#2ecc71"
153+
;; "#f39c12" "#9b59b6" "#1abc9c"]))))
154+
;; ```
155+
156+
;; ## The Full Development Experience
157+
158+
;; While the sidebar REPL provides convenient experimentation for simple examples,
159+
;; the real power of [Clay](https://github.com/scicloj/clay) is in working with the source code.
160+
;; Clone the [repository](https://github.com/clojurecivitas/clojurecivitas.github.io)
161+
;; and open it in your editor to get the full interactive notebook experience.
162+
;; You can modify examples, create new namespaces, and contribute ideas back to the community.
163+
;; Add your own namespace under `src/` and it is published as a new post.
164+
165+
;; ## Sidebar REPL Usage
166+
167+
;; ClojureCivitas authors can require this namespace and enable the REPL sidebar:
168+
169+
;; ```clojure
170+
;; (require '[civitas.repl :as repl])
171+
;; (repl/scittle-sidebar)
172+
;; ```
173+
174+
;; Set `:page-layout :full` in the `:quarto` metadata to make best use of the available page space.
175+
176+
;; ![I'll have the static examples with a side of REPL, thanks](repl-menu.webp)
177+
178+
;; ## Wrapping Up
179+
180+
;; I hope you enjoyed exploring this sidebar REPL approach.
181+
;; The sidebar REPL complements Clay's strength of showing examples and results.
182+
;; Authors can intersperse static examples with outputs and interactive prompts for experimentation.
183+
;; The real magic happens when you clone this repository and work with the source code directly.
184+
185+
;; **Ready to contribute?** Add your own namespace under `src/` and it becomes a new post automatically.
186+
187+
;; **Join the conversation:** The best place to discuss this feature, share improvements, or ask questions is the [Zulip: clay-dev channel](https://clojurians.zulipchat.com/#narrow/channel/422115-clay-dev)
188+
189+
;; Give it a try and let us know what you think! 🚀
190+
191+
^:kindly/hide-code
192+
(defn scittle-sidebar
193+
"A sidebar component that integrates a REPL with textarea editor and output display."
194+
[]
195+
(kind/hiccup
196+
'((require '[reagent.core :as r])
197+
[(fn []
198+
(let [output-ref (atom nil)
199+
editor-ref (atom nil)
200+
show-editor? (r/atom true)
201+
layout (r/atom :right)
202+
repl-output (r/atom [])
203+
scroll-to-bottom! #(when @output-ref
204+
(js/setTimeout
205+
(fn [] (set! (.-scrollTop @output-ref) (.-scrollHeight @output-ref)))
206+
10))
207+
eval-code! (fn [code]
208+
(let [captured-output (atom [])
209+
result (binding [*print-fn* #(swap! captured-output conj %)
210+
*print-err-fn* #(swap! captured-output conj (str "ERR: " %))]
211+
(try
212+
(pr-str (js/scittle.core.eval_string code))
213+
(catch :default e
214+
(str "Error: " (.-message e)))))
215+
output-str (when (seq @captured-output)
216+
(clojure.string/join "" @captured-output))]
217+
(swap! repl-output into (cond-> [(str "> " code)]
218+
output-str (conj output-str)
219+
:always (conj result)))
220+
(scroll-to-bottom!)))]
221+
(.addEventListener js/document "click"
222+
(fn [e]
223+
(when @editor-ref
224+
(when-let [code-container (.. e -target (closest ".sourceCode"))]
225+
(when-let [code-element (.querySelector code-container "code")]
226+
(let [code-text (.-textContent code-element)]
227+
(set! (.-value @editor-ref) code-text)
228+
(.focus @editor-ref)))))))
229+
(fn []
230+
(let [collapsed? (not @show-editor?)
231+
is-bottom? (= @layout :bottom)
232+
toggle! #(swap! show-editor? not)
233+
switch-layout! #(reset! layout (if (= @layout :right) :bottom :right))
234+
collapsed-size "40px"
235+
full-size "350px"]
236+
[:<>
237+
;; Editor container
238+
[:div#scittle-sidebar.card.shadow-sm
239+
{:style (merge
240+
{:position "fixed"
241+
:z-index "1200"
242+
:display "flex"
243+
:flex-direction "column"}
244+
(if is-bottom?
245+
{:left "0"
246+
:right "0"
247+
:bottom "0"
248+
:height (if collapsed? collapsed-size full-size)}
249+
{:top "0"
250+
:right "0"
251+
:width (if collapsed? collapsed-size full-size)
252+
:height "100vh"
253+
:padding-top "77px"}))}
254+
255+
;; Button bar
256+
(when (not collapsed?)
257+
[:div.d-flex.justify-content-between.align-items-center
258+
{:style {:flex "0 0 auto"
259+
:order "0"}}
260+
[:div.btn-group
261+
[:button.btn.btn-outline-primary.btn-sm
262+
{:on-click toggle!}
263+
(if is-bottom?
264+
[:i.bi.bi-chevron-bar-down]
265+
[:i.bi.bi-chevron-bar-right])
266+
" Hide"]
267+
[:button.btn.btn-outline-secondary.btn-sm
268+
{:on-click switch-layout!}
269+
"↕ Switch Layout"]]
270+
[:small.text-muted "Ctrl+Enter to eval"]])
271+
272+
;; Restore button
273+
(when collapsed?
274+
[:button.btn.btn-outline-primary.w-100.h-100
275+
{:on-click toggle!}
276+
(if is-bottom?
277+
[:i.bi.bi-chevron-bar-up]
278+
[:i.bi.bi-chevron-bar-left])])
279+
280+
;; Editor and Output container - flex direction changes based on layout
281+
(when (not collapsed?)
282+
[:div.card-body.p-0
283+
{:style {:flex "1 1 auto"
284+
:order "1"
285+
:display "flex"
286+
:flex-direction (if is-bottom? "row" "column")}}
287+
288+
;; Editor
289+
[:div.position-relative
290+
{:style {:flex "1 1 0"
291+
:overflow "hidden"}}
292+
[:textarea.form-control.border-0
293+
{:ref #(reset! editor-ref %)
294+
:on-key-down (fn [e]
295+
(when (and (.-ctrlKey e) (= (.-key e) "Enter"))
296+
(eval-code! (.. e -target -value))))
297+
:placeholder "Type Clojure code here...\nCtrl+Enter to evaluate\nClick copy buttons in code blocks to paste here"
298+
:style {:resize "none"
299+
:width "100%"
300+
:height "100%"
301+
:box-sizing "border-box"
302+
:font-family "var(--bs-font-monospace)"
303+
:font-size "0.875rem"
304+
:border-radius "0"
305+
:overflow-y "auto"}}]]
306+
307+
;; REPL Output
308+
[:div.position-relative
309+
{:style {:flex "1 1 0"
310+
:overflow "hidden"}}
311+
312+
;; Floating label in top-right
313+
[:div.position-absolute.top-0.end-0.badge.bg-secondary.m-2
314+
{:style {:font-size "0.65rem"
315+
:z-index "10"}}
316+
"Output"]
317+
318+
[:div#repl-output.sourceCode.border-start
319+
{:ref #(reset! output-ref %)
320+
:class (if is-bottom? "border-start" "border-top")
321+
:style {:height "100%"
322+
:padding "0.75rem"
323+
:overflow-y "auto"
324+
:font-family "var(--bs-font-monospace)"
325+
:font-size "0.8125rem"
326+
:line-height "1.4"}}
327+
328+
;; Render output lines without rebuilding entire structure
329+
(for [[i output] (map-indexed vector @repl-output)]
330+
[:div.mb-1 {:key i} output])]]])
331+
332+
[:style
333+
(str
334+
;; Dynamic content adjustments based on editor state
335+
(when @show-editor?
336+
(if is-bottom?
337+
(str ".content { margin-bottom: " full-size " !important; }")
338+
(str ".content { margin-right: " full-size " !important; }"))))]]]))))])))
339+
340+
^:kindly/hide-code
341+
(scittle-sidebar)

0 commit comments

Comments
 (0)