Skip to content

Commit ba38716

Browse files
committed
try out CodeStepper
1 parent 99f2e6b commit ba38716

5 files changed

Lines changed: 200 additions & 1 deletion

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<script setup>
2+
import { ShikiMagicMove } from 'shiki-magic-move/vue'
3+
import 'shiki-magic-move/dist/style.css'
4+
import { createHighlighter } from 'shiki'
5+
import { ref, onMounted } from 'vue'
6+
7+
const props = defineProps({
8+
steps: { type: Array, required: true },
9+
lang: { type: String, default: 'r' },
10+
theme: { type: String, default: 'github-light' }
11+
})
12+
13+
const currentStep = ref(0)
14+
const highlighter = ref(null)
15+
const ready = ref(false)
16+
17+
onMounted(async () => {
18+
highlighter.value = await createHighlighter({
19+
themes: [props.theme],
20+
langs: [props.lang]
21+
})
22+
ready.value = true
23+
})
24+
25+
function prev() {
26+
if (currentStep.value > 0) currentStep.value--
27+
}
28+
function next() {
29+
if (currentStep.value < props.steps.length - 1) currentStep.value++
30+
}
31+
</script>
32+
33+
<template>
34+
<div class="code-stepper" v-if="ready">
35+
<div class="code-stepper-header">
36+
<span class="code-stepper-title">
37+
<strong>{{ steps[currentStep].label }}</strong>
38+
</span>
39+
<div class="code-stepper-controls">
40+
<button @click="prev" :disabled="currentStep === 0">&larr; Prev</button>
41+
<span class="code-stepper-indicator">Step {{ currentStep + 1 }} / {{ steps.length }}</span>
42+
<button @click="next" :disabled="currentStep === steps.length - 1">Next &rarr;</button>
43+
</div>
44+
</div>
45+
<div class="code-stepper-body">
46+
<ShikiMagicMove
47+
:highlighter="highlighter"
48+
:code="steps[currentStep].code"
49+
:lang="lang"
50+
:theme="theme"
51+
:options="{ duration: 500, stagger: 2 }"
52+
/>
53+
</div>
54+
</div>
55+
<div v-else class="code-stepper-loading">Loading code animation...</div>
56+
</template>
57+
58+
<style scoped>
59+
.code-stepper {
60+
border: 1px solid var(--vp-c-divider);
61+
border-radius: 8px;
62+
overflow: hidden;
63+
margin: 16px 0;
64+
}
65+
66+
.code-stepper-header {
67+
display: flex;
68+
align-items: center;
69+
justify-content: space-between;
70+
padding: 8px 16px;
71+
background: var(--vp-c-bg-soft);
72+
border-bottom: 1px solid var(--vp-c-divider);
73+
font-size: 0.85rem;
74+
}
75+
76+
.code-stepper-controls {
77+
display: flex;
78+
align-items: center;
79+
gap: 8px;
80+
}
81+
82+
.code-stepper-controls button {
83+
padding: 4px 12px;
84+
border: 1px solid var(--vp-c-divider);
85+
border-radius: 4px;
86+
background: var(--vp-c-bg);
87+
cursor: pointer;
88+
font-size: 0.8rem;
89+
transition: background 0.2s, border-color 0.2s;
90+
}
91+
92+
.code-stepper-controls button:hover:not(:disabled) {
93+
background: var(--vp-c-bg-soft);
94+
border-color: var(--vp-c-brand);
95+
}
96+
97+
.code-stepper-controls button:disabled {
98+
opacity: 0.4;
99+
cursor: default;
100+
}
101+
102+
.code-stepper-indicator {
103+
font-weight: 600;
104+
font-variant-numeric: tabular-nums;
105+
color: var(--vp-c-text-2);
106+
}
107+
108+
.code-stepper-body {
109+
padding: 16px 20px;
110+
overflow-x: auto;
111+
font-size: 0.875rem;
112+
line-height: 1.6;
113+
}
114+
115+
.code-stepper-loading {
116+
padding: 24px;
117+
text-align: center;
118+
color: var(--vp-c-text-3);
119+
font-size: 0.85rem;
120+
}
121+
</style>

.vitepress/theme/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import DefaultTheme from 'vitepress/theme'
22
import './custom.css'
33
import VideoEmbed from './components/VideoEmbed.vue'
4+
import CodeStepper from './components/CodeStepper.vue'
45

56
export default {
67
extends: DefaultTheme,
78
enhanceApp({ app }) {
89
app.component('VideoEmbed', VideoEmbed)
10+
app.component('CodeStepper', CodeStepper)
911
}
1012
}

docs/dev/create-block.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
<script setup>
2+
const blockSteps = [
3+
{
4+
label: 'Skeleton',
5+
code: 'new_my_block <- function(column = character(), threshold = 0) {\n new_block(\n ui = ...,\n expr_server = ...,\n state = list(column = column, threshold = threshold),\n class = c("my_block", "transform_block")\n )\n}'
6+
},
7+
{
8+
label: 'Add UI function',
9+
code: 'my_block_ui <- function(id) {\n ns <- shiny::NS(id)\n shiny::tagList(\n shiny::selectInput(ns("column"), "Column", choices = NULL),\n shiny::numericInput(ns("threshold"), "Threshold", value = 0)\n )\n}\n\nnew_my_block <- function(column = character(), threshold = 0) {\n new_block(\n ui = my_block_ui,\n expr_server = ...,\n state = list(column = column, threshold = threshold),\n class = c("my_block", "transform_block")\n )\n}'
10+
},
11+
{
12+
label: 'Add server function',
13+
code: 'my_block_ui <- function(id) {\n ns <- shiny::NS(id)\n shiny::tagList(\n shiny::selectInput(ns("column"), "Column", choices = NULL),\n shiny::numericInput(ns("threshold"), "Threshold", value = 0)\n )\n}\n\nmy_block_server <- function(id, data) {\n shiny::moduleServer(id, function(input, output, session) {\n shiny::observe({\n shiny::updateSelectInput(session, "column",\n choices = names(data()))\n })\n\n list(\n expr = shiny::reactive({\n rlang::expr(\n dplyr::filter(data, !!rlang::sym(input$column) > !!input$threshold)\n )\n }),\n state = shiny::reactive(list(\n column = input$column,\n threshold = input$threshold\n ))\n )\n })\n}\n\nnew_my_block <- function(column = character(), threshold = 0) {\n new_block(\n ui = my_block_ui,\n expr_server = my_block_server,\n state = list(column = column, threshold = threshold),\n class = c("my_block", "transform_block")\n )\n}'
14+
},
15+
{
16+
label: 'Add registration',
17+
code: 'my_block_ui <- function(id) {\n ns <- shiny::NS(id)\n shiny::tagList(\n shiny::selectInput(ns("column"), "Column", choices = NULL),\n shiny::numericInput(ns("threshold"), "Threshold", value = 0)\n )\n}\n\nmy_block_server <- function(id, data) {\n shiny::moduleServer(id, function(input, output, session) {\n shiny::observe({\n shiny::updateSelectInput(session, "column",\n choices = names(data()))\n })\n\n list(\n expr = shiny::reactive({\n rlang::expr(\n dplyr::filter(data, !!rlang::sym(input$column) > !!input$threshold)\n )\n }),\n state = shiny::reactive(list(\n column = input$column,\n threshold = input$threshold\n ))\n )\n })\n}\n\nnew_my_block <- function(column = character(), threshold = 0) {\n new_block(\n ui = my_block_ui,\n expr_server = my_block_server,\n state = list(column = column, threshold = threshold),\n class = c("my_block", "transform_block")\n )\n}\n\n.onLoad <- function(libname, pkgname) {\n blockr.core::register_block(\n constructor = new_my_block,\n name = "My custom filter",\n description = "Filter rows by a numeric threshold",\n category = "transform",\n package = pkgname\n )\n}'
18+
}
19+
]
20+
</script>
21+
122
# Create a block
223

324
<VideoEmbed id="-PdixmAscQI" title="Creating blocks in blockr" />
@@ -8,7 +29,11 @@ Blocks should live in an R package so they can be registered, shared, and tested
829

930
## Block anatomy
1031

11-
Every block has three parts:
32+
Every block has three parts: a **UI function**, a **server function**, and a **constructor**. Step through the animation below to see how they come together:
33+
34+
<CodeStepper :steps="blockSteps" />
35+
36+
The sections below break down each part in detail.
1237

1338
```
1439
Block

package-lock.json

Lines changed: 50 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"license": "ISC",
1313
"dependencies": {
1414
"@vue/devtools-kit": "^8.1.0",
15+
"shiki-magic-move": "^1.3.0",
1516
"vitepress": "^1.6.4",
1617
"vue": "^3.5.30"
1718
}

0 commit comments

Comments
 (0)