slidev-workspace 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Slidev Workspace
2
+
3
+ [![npm version](https://badge.fury.io/js/slidev-workspace.svg)](https://badge.fury.io/js/slidev-workspace)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Slidev Workspace is a specialized command-line tool designed to manage and showcase multiple [Slidev](https://sli.dev) presentations. It provides a unified web interface to browse, search, and access Slidev presentations distributed across different directories.
7
+
8
+ - **[Slidev Workspace Starter](https://github.com/leochiu-a/slidev-workspace-starter)** - Ready-to-use template
9
+ - **[Live Demo](https://leochiu-a.github.io/slidev-workspace-starter/)** - See it in action
10
+ - **[Documentation](https://leochiu-a.github.io/slidev-workspace/)**
11
+
12
+ ## Features
13
+
14
+ ✨ **Multi-presentation management** - Organize multiple Slidev presentations in one workspace
15
+ 📱 **Responsive interface** - Clean, modern UI for presentation management
16
+ 🔧 **Easy configuration** - Simple YAML-based configuration
17
+ 📦 **Build & Deploy** - Built-in commands for production builds
18
+ 🎨 **Thumbnail previews** - Visual presentation previews in the workspace
19
+
20
+ ## Usage
21
+
22
+ Slidev Workspace provides two flexible ways to work with your presentations:
23
+
24
+ ### Built-in Preview Interface
25
+
26
+ Use the command-line tool for an out-of-the-box solution:
27
+
28
+ ```bash
29
+ slidev-workspace preview
30
+ ```
31
+
32
+ This launches a responsive web interface with presentation management, search functionality, and thumbnail previews - perfect for users who want to get started quickly without building custom UI.
33
+
34
+ ### Content API
35
+
36
+ For developers who need custom UI integration, access slide data programmatically:
37
+
38
+ ```typescript
39
+ import { useSlides } from "slidev-workspace";
40
+
41
+ const { slides } = useSlides();
42
+ ```
43
+
44
+ The `useSlides` composable returns frontmatter data from all discovered presentations, enabling you to build entirely custom interfaces while leveraging Slidev Workspace's presentation discovery and parsing capabilities.
45
+
46
+ ## Quick Start
47
+
48
+ Get started in 5 minutes! See our [Quick Start Guide](https://leochiu-a.github.io/slidev-workspace/getting-started/quick-start.html).
49
+
50
+ ## Documentation
51
+
52
+ - 📚 [Quick Start Guide](https://leochiu-a.github.io/slidev-workspace/getting-started/quick-start.html) - Get up and running in 5 minutes
53
+ - 🚀 [Deployment Guide](https://leochiu-a.github.io/slidev-workspace/getting-started/deploy.html) - Deploy to GitHub Pages
54
+
55
+ ## Related Projects
56
+
57
+ - [Slidev](https://sli.dev) - Presentation slides for developers
58
+ - [Vue.js](https://vuejs.org) - The progressive JavaScript framework
59
+ - [Vite](https://vitejs.dev) - Next generation frontend tooling
package/dist/cli.js CHANGED
@@ -17,7 +17,11 @@ const DEFAULT_CONFIG = {
17
17
  slidesDir: ["./slides"],
18
18
  outputDir: "./dist",
19
19
  baseUrl: "/",
20
- exclude: ["node_modules", ".git"]
20
+ exclude: ["node_modules", ".git"],
21
+ hero: {
22
+ title: "Slide Deck",
23
+ description: "Browse all available slide decks and use the search function to quickly find what you need."
24
+ }
21
25
  };
22
26
  function loadConfig(workingDir) {
23
27
  const configPaths = [
@@ -240,7 +244,7 @@ function slidesPlugin() {
240
244
  });
241
245
  },
242
246
  resolveId(id) {
243
- if (id === "slidev:content") return id;
247
+ if (id === "slidev:content" || id === "slidev:config") return id;
244
248
  },
245
249
  load(id) {
246
250
  if (id === "slidev:content") try {
@@ -252,6 +256,16 @@ export default slidesData;`;
252
256
  return `export const slidesData = [];
253
257
  export default slidesData;`;
254
258
  }
259
+ if (id === "slidev:config") try {
260
+ const config = loadConfig();
261
+ const configData = { hero: config.hero };
262
+ return `export const configData = ${JSON.stringify(configData, null, 2)};
263
+ export default configData;`;
264
+ } catch (error) {
265
+ console.error("Error loading config:", error);
266
+ return `export const configData = { hero: {} };
267
+ export default configData;`;
268
+ }
255
269
  }
256
270
  };
257
271
  }
@@ -303,7 +317,9 @@ async function buildAllSlides() {
303
317
  console.log(`📦 Building slide: ${slideName}`);
304
318
  try {
305
319
  const baseUrl = config.baseUrl.endsWith("/") ? config.baseUrl : config.baseUrl + "/";
306
- const buildCmd = `pnpm --filter "./slides/${slideName}" run build --base ${baseUrl}${slideName}/`;
320
+ const subDir = slideDir.startsWith(workspaceCwd) ? slideDir.replace(workspaceCwd, "").replace(/^\//, "") : slideDir;
321
+ const buildCmd = `pnpm --filter "./${subDir}" run build --base ${baseUrl}${slideName}/`;
322
+ console.log(buildCmd);
307
323
  execSync(buildCmd, {
308
324
  cwd: workspaceCwd,
309
325
  stdio: "inherit"
package/dist/index.d.ts CHANGED
@@ -51,11 +51,16 @@ declare function useSlides(): {
51
51
  };
52
52
  //#endregion
53
53
  //#region src/types/config.d.ts
54
+ interface HeroConfig {
55
+ title: string;
56
+ description: string;
57
+ }
54
58
  interface SlidevWorkspaceConfig {
55
59
  slidesDir: string[];
56
60
  outputDir: string;
57
61
  baseUrl: string;
58
62
  exclude: string[];
63
+ hero: HeroConfig;
59
64
  }
60
65
  //#endregion
61
66
  export { type SlideData, type SlideFrontmatter, type SlideInfo, type SlidevWorkspaceConfig, useSlides };
@@ -8,7 +8,11 @@ const DEFAULT_CONFIG = {
8
8
  slidesDir: ["./slides"],
9
9
  outputDir: "./dist",
10
10
  baseUrl: "/",
11
- exclude: ["node_modules", ".git"]
11
+ exclude: ["node_modules", ".git"],
12
+ hero: {
13
+ title: "Slide Deck",
14
+ description: "Browse all available slide decks and use the search function to quickly find what you need."
15
+ }
12
16
  };
13
17
  function loadConfig(workingDir) {
14
18
  const configPaths = [
@@ -231,7 +235,7 @@ function slidesPlugin() {
231
235
  });
232
236
  },
233
237
  resolveId(id) {
234
- if (id === "slidev:content") return id;
238
+ if (id === "slidev:content" || id === "slidev:config") return id;
235
239
  },
236
240
  load(id) {
237
241
  if (id === "slidev:content") try {
@@ -243,6 +247,16 @@ export default slidesData;`;
243
247
  return `export const slidesData = [];
244
248
  export default slidesData;`;
245
249
  }
250
+ if (id === "slidev:config") try {
251
+ const config = loadConfig();
252
+ const configData = { hero: config.hero };
253
+ return `export const configData = ${JSON.stringify(configData, null, 2)};
254
+ export default configData;`;
255
+ } catch (error) {
256
+ console.error("Error loading config:", error);
257
+ return `export const configData = { hero: {} };
258
+ export default configData;`;
259
+ }
246
260
  }
247
261
  };
248
262
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidev-workspace",
3
- "version": "0.2.3",
3
+ "version": "0.4.0",
4
4
  "description": "A workspace tool for managing multiple Slidev presentations with API-based content management",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -4,113 +4,113 @@
4
4
  @custom-variant dark (&:is(.dark *));
5
5
 
6
6
  @theme inline {
7
- --color-background: var(--background);
8
- --color-foreground: var(--foreground);
9
- --color-card: var(--card);
10
- --color-card-foreground: var(--card-foreground);
11
- --color-popover: var(--popover);
12
- --color-popover-foreground: var(--popover-foreground);
13
- --color-primary: var(--primary);
14
- --color-primary-foreground: var(--primary-foreground);
15
- --color-secondary: var(--secondary);
16
- --color-secondary-foreground: var(--secondary-foreground);
17
- --color-muted: var(--muted);
18
- --color-muted-foreground: var(--muted-foreground);
19
- --color-accent: var(--accent);
20
- --color-accent-foreground: var(--accent-foreground);
21
- --color-destructive: var(--destructive);
22
- --color-destructive-foreground: var(--destructive-foreground);
23
- --color-border: var(--border);
24
- --color-input: var(--input);
25
- --color-ring: var(--ring);
26
- --color-chart-1: var(--chart-1);
27
- --color-chart-2: var(--chart-2);
28
- --color-chart-3: var(--chart-3);
29
- --color-chart-4: var(--chart-4);
30
- --color-chart-5: var(--chart-5);
7
+ --color-background: hsl(var(--background));
8
+ --color-foreground: hsl(var(--foreground));
9
+ --color-card: hsl(var(--card));
10
+ --color-card-foreground: hsl(var(--card-foreground));
11
+ --color-popover: hsl(var(--popover));
12
+ --color-popover-foreground: hsl(var(--popover-foreground));
13
+ --color-primary: hsl(var(--primary));
14
+ --color-primary-foreground: hsl(var(--primary-foreground));
15
+ --color-secondary: hsl(var(--secondary));
16
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
17
+ --color-muted: hsl(var(--muted));
18
+ --color-muted-foreground: hsl(var(--muted-foreground));
19
+ --color-accent: hsl(var(--accent));
20
+ --color-accent-foreground: hsl(var(--accent-foreground));
21
+ --color-destructive: hsl(var(--destructive));
22
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
23
+ --color-border: hsl(var(--border));
24
+ --color-input: hsl(var(--input));
25
+ --color-ring: hsl(var(--ring));
26
+ --color-chart-1: hsl(var(--chart-1));
27
+ --color-chart-2: hsl(var(--chart-2));
28
+ --color-chart-3: hsl(var(--chart-3));
29
+ --color-chart-4: hsl(var(--chart-4));
30
+ --color-chart-5: hsl(var(--chart-5));
31
31
  --radius-sm: calc(var(--radius) - 4px);
32
32
  --radius-md: calc(var(--radius) - 2px);
33
33
  --radius-lg: var(--radius);
34
34
  --radius-xl: calc(var(--radius) + 4px);
35
- --color-sidebar: var(--sidebar);
36
- --color-sidebar-foreground: var(--sidebar-foreground);
37
- --color-sidebar-primary: var(--sidebar-primary);
38
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
39
- --color-sidebar-accent: var(--sidebar-accent);
40
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
41
- --color-sidebar-border: var(--sidebar-border);
42
- --color-sidebar-ring: var(--sidebar-ring);
35
+ --color-sidebar: hsl(var(--sidebar));
36
+ --color-sidebar-foreground: hsl(var(--sidebar-foreground));
37
+ --color-sidebar-primary: hsl(var(--sidebar-primary));
38
+ --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
39
+ --color-sidebar-accent: hsl(var(--sidebar-accent));
40
+ --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
41
+ --color-sidebar-border: hsl(var(--sidebar-border));
42
+ --color-sidebar-ring: hsl(var(--sidebar-ring));
43
43
  }
44
44
 
45
45
  :root {
46
- --background: oklch(1 0 0);
47
- --foreground: oklch(0.145 0 0);
48
- --card: oklch(1 0 0);
49
- --card-foreground: oklch(0.145 0 0);
50
- --popover: oklch(1 0 0);
51
- --popover-foreground: oklch(0.145 0 0);
52
- --primary: oklch(0.205 0 0);
53
- --primary-foreground: oklch(0.985 0 0);
54
- --secondary: oklch(0.97 0 0);
55
- --secondary-foreground: oklch(0.205 0 0);
56
- --muted: oklch(0.97 0 0);
57
- --muted-foreground: oklch(0.556 0 0);
58
- --accent: oklch(0.97 0 0);
59
- --accent-foreground: oklch(0.205 0 0);
60
- --destructive: oklch(0.577 0.245 27.325);
61
- --destructive-foreground: oklch(0.577 0.245 27.325);
62
- --border: oklch(0.922 0 0);
63
- --input: oklch(0.922 0 0);
64
- --ring: oklch(0.708 0 0);
65
- --chart-1: oklch(0.646 0.222 41.116);
66
- --chart-2: oklch(0.6 0.118 184.704);
67
- --chart-3: oklch(0.398 0.07 227.392);
68
- --chart-4: oklch(0.828 0.189 84.429);
69
- --chart-5: oklch(0.769 0.188 70.08);
70
- --radius: 0.625rem;
71
- --sidebar: oklch(0.985 0 0);
72
- --sidebar-foreground: oklch(0.145 0 0);
73
- --sidebar-primary: oklch(0.205 0 0);
74
- --sidebar-primary-foreground: oklch(0.985 0 0);
75
- --sidebar-accent: oklch(0.97 0 0);
76
- --sidebar-accent-foreground: oklch(0.205 0 0);
77
- --sidebar-border: oklch(0.922 0 0);
78
- --sidebar-ring: oklch(0.708 0 0);
46
+ --background: 0 0% 100%;
47
+ --foreground: 0 0% 3.9%;
48
+ --card: 0 0% 100%;
49
+ --card-foreground: 0 0% 3.9%;
50
+ --popover: 0 0% 100%;
51
+ --popover-foreground: 0 0% 3.9%;
52
+ --primary: 0 0% 9%;
53
+ --primary-foreground: 0 0% 98%;
54
+ --secondary: 0 0% 96.1%;
55
+ --secondary-foreground: 0 0% 9%;
56
+ --muted: 0 0% 96.1%;
57
+ --muted-foreground: 0 0% 45.1%;
58
+ --accent: 0 0% 96.1%;
59
+ --accent-foreground: 0 0% 9%;
60
+ --destructive: 0 84.2% 60.2%;
61
+ --destructive-foreground: 0 0% 98%;
62
+ --border: 0 0% 89.8%;
63
+ --input: 0 0% 89.8%;
64
+ --ring: 0 0% 3.9%;
65
+ --chart-1: 12 76% 61%;
66
+ --chart-2: 173 58% 39%;
67
+ --chart-3: 197 37% 24%;
68
+ --chart-4: 43 74% 66%;
69
+ --chart-5: 27 87% 67%;
70
+ --radius: 0.5rem;
71
+ --sidebar: 0 0% 98%;
72
+ --sidebar-foreground: 0 0% 3.9%;
73
+ --sidebar-primary: 0 0% 9%;
74
+ --sidebar-primary-foreground: 0 0% 98%;
75
+ --sidebar-accent: 0 0% 96.1%;
76
+ --sidebar-accent-foreground: 0 0% 9%;
77
+ --sidebar-border: 0 0% 89.8%;
78
+ --sidebar-ring: 0 0% 3.9%;
79
79
  }
80
80
 
81
81
  .dark {
82
- --background: oklch(0.145 0 0);
83
- --foreground: oklch(0.985 0 0);
84
- --card: oklch(0.145 0 0);
85
- --card-foreground: oklch(0.985 0 0);
86
- --popover: oklch(0.145 0 0);
87
- --popover-foreground: oklch(0.985 0 0);
88
- --primary: oklch(0.985 0 0);
89
- --primary-foreground: oklch(0.205 0 0);
90
- --secondary: oklch(0.269 0 0);
91
- --secondary-foreground: oklch(0.985 0 0);
92
- --muted: oklch(0.269 0 0);
93
- --muted-foreground: oklch(0.708 0 0);
94
- --accent: oklch(0.269 0 0);
95
- --accent-foreground: oklch(0.985 0 0);
96
- --destructive: oklch(0.396 0.141 25.723);
97
- --destructive-foreground: oklch(0.637 0.237 25.331);
98
- --border: oklch(0.269 0 0);
99
- --input: oklch(0.269 0 0);
100
- --ring: oklch(0.439 0 0);
101
- --chart-1: oklch(0.488 0.243 264.376);
102
- --chart-2: oklch(0.696 0.17 162.48);
103
- --chart-3: oklch(0.769 0.188 70.08);
104
- --chart-4: oklch(0.627 0.265 303.9);
105
- --chart-5: oklch(0.645 0.246 16.439);
106
- --sidebar: oklch(0.205 0 0);
107
- --sidebar-foreground: oklch(0.985 0 0);
108
- --sidebar-primary: oklch(0.488 0.243 264.376);
109
- --sidebar-primary-foreground: oklch(0.985 0 0);
110
- --sidebar-accent: oklch(0.269 0 0);
111
- --sidebar-accent-foreground: oklch(0.985 0 0);
112
- --sidebar-border: oklch(0.269 0 0);
113
- --sidebar-ring: oklch(0.439 0 0);
82
+ --background: 224 71.4% 4.1%;
83
+ --foreground: 210 20% 98%;
84
+ --muted: 215 27.9% 16.9%;
85
+ --muted-foreground: 217.9 10.6% 64.9%;
86
+ --popover: 224 71.4% 4.1%;
87
+ --popover-foreground: 210 20% 98%;
88
+ --card: 224 71.4% 4.1%;
89
+ --card-foreground: 210 20% 98%;
90
+ --border: 215 27.9% 16.9%;
91
+ --input: 215 27.9% 16.9%;
92
+ --primary: 210 20% 98%;
93
+ --primary-foreground: 220.9 39.3% 11%;
94
+ --secondary: 215 27.9% 16.9%;
95
+ --secondary-foreground: 210 20% 98%;
96
+ --accent: 215 27.9% 16.9%;
97
+ --accent-foreground: 210 20% 98%;
98
+ --destructive: 0 62.8% 30.6%;
99
+ --destructive-foreground: 210 20% 98%;
100
+ --ring: 216 12.2% 83.9%;
101
+ --chart-1: 220 70% 50%;
102
+ --chart-2: 160 60% 45%;
103
+ --chart-3: 30 80% 55%;
104
+ --chart-4: 280 65% 60%;
105
+ --chart-5: 340 75% 55%;
106
+ --sidebar: 0 0% 9%;
107
+ --sidebar-foreground: 0 0% 98%;
108
+ --sidebar-primary: 220 70% 50%;
109
+ --sidebar-primary-foreground: 0 0% 98%;
110
+ --sidebar-accent: 0 0% 14.9%;
111
+ --sidebar-accent-foreground: 0 0% 98%;
112
+ --sidebar-border: 0 0% 14.9%;
113
+ --sidebar-ring: 0 0% 83.1%;
114
114
  }
115
115
 
116
116
  @layer base {
@@ -1,22 +1,38 @@
1
1
  <template>
2
2
  <div
3
- class="min-h-screen bg-gradient-to-br from-gray-100 via-white to-gray-200 py-16 px-4"
3
+ class="min-h-screen bg-gradient-to-br from-gray-100 via-white to-gray-200 dark:from-gray-900 dark:via-gray-950 dark:to-black py-16 px-4 transition-colors"
4
4
  >
5
5
  <div class="max-w-5xl mx-auto">
6
- <div class="mb-8">
7
- <h1 class="text-3xl font-bold mb-2">Slide Deck</h1>
8
- <p class="text-muted-foreground">
9
- Browse all available slide decks and use the search function to
10
- quickly find what you need.
11
- </p>
6
+ <div class="mb-8 flex justify-between items-start">
7
+ <div>
8
+ <h1 class="text-3xl font-bold mb-2">{{ hero.title }}</h1>
9
+ <p class="text-muted-foreground">
10
+ {{ hero.description }}
11
+ </p>
12
+ </div>
13
+ <button
14
+ @click="toggleDarkMode"
15
+ class="p-2 rounded-lg hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors cursor-pointer"
16
+ aria-label="Toggle dark mode"
17
+ type="button"
18
+ >
19
+ <Moon v-if="!isDark" class="size-6" />
20
+ <Sun v-else class="size-6" />
21
+ </button>
12
22
  </div>
13
23
 
14
24
  <div class="space-y-4 mb-8">
15
- <div class="relative">
25
+ <div class="relative w-full">
16
26
  <Input
27
+ class="pl-10"
17
28
  placeholder="Search by title, description, or author..."
18
29
  v-model="searchTerm"
19
30
  />
31
+ <span
32
+ class="absolute start-0 inset-y-0 flex items-center justify-center px-2"
33
+ >
34
+ <Search class="size-6 text-muted-foreground/30" />
35
+ </span>
20
36
  </div>
21
37
  </div>
22
38
 
@@ -51,12 +67,18 @@
51
67
 
52
68
  <script setup lang="ts">
53
69
  import { ref, computed } from "vue";
70
+ import { Search, Moon, Sun } from "lucide-vue-next";
71
+
54
72
  import { useSlides } from "../composables/useSlides";
73
+ import { useConfig } from "../composables/useConfig";
74
+ import { useDarkMode } from "../composables/useDarkMode";
55
75
  import { Input } from "../components/ui/input";
56
76
  import SlideCard from "./SlideCard.vue";
57
77
 
58
78
  const searchTerm = ref("");
59
79
  const { slides, slidesCount } = useSlides();
80
+ const { hero } = useConfig();
81
+ const { isDark, toggleDarkMode } = useDarkMode();
60
82
 
61
83
  const filteredSlides = computed(() => {
62
84
  if (!searchTerm.value) return slides.value;
@@ -12,7 +12,7 @@ const props = defineProps<{
12
12
  data-slot="card"
13
13
  :class="
14
14
  cn(
15
- 'bg-card text-card-foreground flex flex-col rounded-xl border shadow-sm',
15
+ 'bg-card text-card-foreground flex flex-col rounded-xl border shadow',
16
16
  props.class,
17
17
  )
18
18
  "
@@ -22,12 +22,9 @@ const modelValue = useVModel(props, "modelValue", emits, {
22
22
  <template>
23
23
  <input
24
24
  v-model="modelValue"
25
- data-slot="input"
26
25
  :class="
27
26
  cn(
28
- 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
29
- 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
30
- 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
27
+ 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
31
28
  props.class,
32
29
  )
33
30
  "
@@ -0,0 +1,15 @@
1
+ import type { HeroConfig } from "../../../types/config";
2
+
3
+ interface ConfigData {
4
+ hero: HeroConfig;
5
+ }
6
+
7
+ const mockConfigData: ConfigData = {
8
+ hero: {
9
+ title: "Slide Deck",
10
+ description:
11
+ "Browse all available slide decks and use the search function to quickly find what you need.",
12
+ },
13
+ };
14
+
15
+ export default mockConfigData;
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
+
3
+ describe("useConfig", () => {
4
+ beforeEach(() => {
5
+ vi.clearAllMocks();
6
+ });
7
+
8
+ afterEach(() => {
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ /**
13
+ * Helper function to setup useConfig and wait for data to load
14
+ */
15
+ async function setupUseConfig(configData?: {
16
+ hero: { title: string; description: string };
17
+ }) {
18
+ vi.resetModules();
19
+
20
+ // Mock the virtual module
21
+ vi.doMock("slidev:config", () => ({
22
+ default: configData || {
23
+ hero: {
24
+ title: "Slide Deck",
25
+ description:
26
+ "Browse all available slide decks and use the search function to quickly find what you need.",
27
+ },
28
+ },
29
+ }));
30
+
31
+ const { useConfig } = await import("./useConfig");
32
+ const result = useConfig();
33
+
34
+ // Wait for data to finish loading
35
+ await vi.waitFor(
36
+ () => {
37
+ // Check if config has loaded by comparing with expected values
38
+ if (configData) {
39
+ expect(result.hero.value.title).toBe(configData.hero.title);
40
+ } else {
41
+ expect(result.hero.value.title).toBe("Slide Deck");
42
+ }
43
+ },
44
+ { timeout: 1000, interval: 10 },
45
+ );
46
+
47
+ return result;
48
+ }
49
+
50
+ describe("hero config", () => {
51
+ it("should return hero config from slidev:config", async () => {
52
+ const result = await setupUseConfig({
53
+ hero: {
54
+ title: "Test Workspace",
55
+ description: "Test Description",
56
+ },
57
+ });
58
+
59
+ expect(result.hero.value.title).toBe("Test Workspace");
60
+ expect(result.hero.value.description).toBe("Test Description");
61
+ });
62
+
63
+ it("should provide reactive hero config", async () => {
64
+ const result = await setupUseConfig({
65
+ hero: {
66
+ title: "Initial Title",
67
+ description: "Initial Description",
68
+ },
69
+ });
70
+
71
+ // Initial values
72
+ expect(result.hero.value.title).toBe("Initial Title");
73
+ expect(result.hero.value.description).toBe("Initial Description");
74
+
75
+ // Hero should be reactive
76
+ expect(result.hero.value).toBeDefined();
77
+ expect(typeof result.hero.value).toBe("object");
78
+ });
79
+
80
+ it("should return default hero values when no config provided", async () => {
81
+ const result = await setupUseConfig();
82
+
83
+ expect(result.hero.value.title).toBe("Slide Deck");
84
+ expect(result.hero.value.description).toContain("Browse all available");
85
+ });
86
+ });
87
+
88
+ describe("HeroConfig properties", () => {
89
+ it("should have title and description properties", async () => {
90
+ const result = await setupUseConfig({
91
+ hero: {
92
+ title: "Test Title",
93
+ description: "Test Description",
94
+ },
95
+ });
96
+
97
+ const hero = result.hero.value;
98
+
99
+ expect("title" in hero).toBe(true);
100
+ expect("description" in hero).toBe(true);
101
+ expect(typeof hero.title).toBe("string");
102
+ expect(typeof hero.description).toBe("string");
103
+ });
104
+
105
+ it("should handle long descriptions", async () => {
106
+ const longDescription =
107
+ "This is a very long description that could contain multiple sentences and paragraphs to describe the workspace";
108
+
109
+ const result = await setupUseConfig({
110
+ hero: {
111
+ title: "My Workspace",
112
+ description: longDescription,
113
+ },
114
+ });
115
+
116
+ expect(result.hero.value.description).toBe(longDescription);
117
+ expect(result.hero.value.description.length).toBeGreaterThan(50);
118
+ });
119
+
120
+ it("should handle special characters in title and description", async () => {
121
+ const result = await setupUseConfig({
122
+ hero: {
123
+ title: "My Slides & Presentations",
124
+ description: "Browse <all> available slides @ our company",
125
+ },
126
+ });
127
+
128
+ expect(result.hero.value.title).toContain("&");
129
+ expect(result.hero.value.description).toContain("<");
130
+ expect(result.hero.value.description).toContain("@");
131
+ });
132
+ });
133
+
134
+ describe("error handling", () => {
135
+ it("should use default values when import fails", async () => {
136
+ vi.resetModules();
137
+
138
+ // Mock import to throw error
139
+ vi.doMock("slidev:config", () => {
140
+ throw new Error("Failed to load config");
141
+ });
142
+
143
+ const { useConfig } = await import("./useConfig");
144
+ const result = useConfig();
145
+
146
+ // Should fall back to default values
147
+ await vi.waitFor(
148
+ () => {
149
+ expect(result.hero.value.title).toBe("Slide Deck");
150
+ expect(result.hero.value.description).toContain(
151
+ "Browse all available",
152
+ );
153
+ },
154
+ { timeout: 1000, interval: 10 },
155
+ );
156
+ });
157
+ });
158
+ });
@@ -0,0 +1,31 @@
1
+ import { computed, ref } from "vue";
2
+ import type { HeroConfig } from "../../types/config.js";
3
+
4
+ const DEFAULT_CONFIG = {
5
+ hero: {
6
+ title: "Slide Deck",
7
+ description:
8
+ "Browse all available slide decks and use the search function to quickly find what you need.",
9
+ },
10
+ };
11
+
12
+ export function useConfig() {
13
+ const heroData = ref<HeroConfig>(DEFAULT_CONFIG.hero);
14
+
15
+ const loadConfigData = async () => {
16
+ try {
17
+ const module = await import("slidev:config");
18
+ heroData.value = module.default?.hero || heroData.value;
19
+ } catch (error) {
20
+ console.warn("Failed to load config data:", error);
21
+ }
22
+ };
23
+
24
+ loadConfigData();
25
+
26
+ const hero = computed(() => heroData.value);
27
+
28
+ return {
29
+ hero,
30
+ };
31
+ }
@@ -0,0 +1,54 @@
1
+ import { useDark } from "@vueuse/core";
2
+
3
+ export function useDarkMode() {
4
+ const isDark = useDark();
5
+
6
+ const toggleDarkMode = (event: MouseEvent) => {
7
+ if (!document.startViewTransition) {
8
+ isDark.value = !isDark.value;
9
+ return;
10
+ }
11
+
12
+ const x = event.clientX;
13
+ const y = event.clientY;
14
+ // Calculate percentage-based position for better accuracy
15
+ const xPercent = (x / window.innerWidth) * 100;
16
+ const yPercent = (y / window.innerHeight) * 100;
17
+
18
+ const endRadius = Math.hypot(
19
+ Math.max(x, window.innerWidth - x),
20
+ Math.max(y, window.innerHeight - y),
21
+ );
22
+
23
+ const wasDark = isDark.value;
24
+
25
+ const transition = document.startViewTransition(() => {
26
+ isDark.value = !wasDark;
27
+ });
28
+
29
+ transition.ready.then(() => {
30
+ const clipPath = [
31
+ `circle(0px at ${xPercent}% ${yPercent}%)`,
32
+ `circle(${endRadius}px at ${xPercent}% ${yPercent}%)`,
33
+ ];
34
+
35
+ document.documentElement.animate(
36
+ {
37
+ clipPath: isDark.value ? clipPath.reverse() : clipPath,
38
+ },
39
+ {
40
+ duration: 200,
41
+ easing: "ease-in",
42
+ pseudoElement: isDark.value
43
+ ? "::view-transition-old(root)"
44
+ : "::view-transition-new(root)",
45
+ },
46
+ );
47
+ });
48
+ };
49
+
50
+ return {
51
+ isDark,
52
+ toggleDarkMode,
53
+ };
54
+ }