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 +59 -0
- package/dist/cli.js +19 -3
- package/dist/index.d.ts +5 -0
- package/dist/plugin-slides.js +16 -2
- package/package.json +1 -1
- package/src/preview/assets/main.css +97 -97
- package/src/preview/components/SlideDeck.vue +30 -8
- package/src/preview/components/ui/card/Card.vue +1 -1
- package/src/preview/components/ui/input/Input.vue +1 -4
- package/src/preview/composables/__mocks__/slidev-config.ts +15 -0
- package/src/preview/composables/useConfig.test.ts +158 -0
- package/src/preview/composables/useConfig.ts +31 -0
- package/src/preview/composables/useDarkMode.ts +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Slidev Workspace
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/slidev-workspace)
|
|
4
|
+
[](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
|
|
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 };
|
package/dist/plugin-slides.js
CHANGED
|
@@ -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
|
@@ -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:
|
|
47
|
-
--foreground:
|
|
48
|
-
--card:
|
|
49
|
-
--card-foreground:
|
|
50
|
-
--popover:
|
|
51
|
-
--popover-foreground:
|
|
52
|
-
--primary:
|
|
53
|
-
--primary-foreground:
|
|
54
|
-
--secondary:
|
|
55
|
-
--secondary-foreground:
|
|
56
|
-
--muted:
|
|
57
|
-
--muted-foreground:
|
|
58
|
-
--accent:
|
|
59
|
-
--accent-foreground:
|
|
60
|
-
--destructive:
|
|
61
|
-
--destructive-foreground:
|
|
62
|
-
--border:
|
|
63
|
-
--input:
|
|
64
|
-
--ring:
|
|
65
|
-
--chart-1:
|
|
66
|
-
--chart-2:
|
|
67
|
-
--chart-3:
|
|
68
|
-
--chart-4:
|
|
69
|
-
--chart-5:
|
|
70
|
-
--radius: 0.
|
|
71
|
-
--sidebar:
|
|
72
|
-
--sidebar-foreground:
|
|
73
|
-
--sidebar-primary:
|
|
74
|
-
--sidebar-primary-foreground:
|
|
75
|
-
--sidebar-accent:
|
|
76
|
-
--sidebar-accent-foreground:
|
|
77
|
-
--sidebar-border:
|
|
78
|
-
--sidebar-ring:
|
|
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:
|
|
83
|
-
--foreground:
|
|
84
|
-
--
|
|
85
|
-
--
|
|
86
|
-
--popover:
|
|
87
|
-
--popover-foreground:
|
|
88
|
-
--
|
|
89
|
-
--
|
|
90
|
-
--
|
|
91
|
-
--
|
|
92
|
-
--
|
|
93
|
-
--
|
|
94
|
-
--
|
|
95
|
-
--
|
|
96
|
-
--
|
|
97
|
-
--
|
|
98
|
-
--
|
|
99
|
-
--
|
|
100
|
-
--ring:
|
|
101
|
-
--chart-1:
|
|
102
|
-
--chart-2:
|
|
103
|
-
--chart-3:
|
|
104
|
-
--chart-4:
|
|
105
|
-
--chart-5:
|
|
106
|
-
--sidebar:
|
|
107
|
-
--sidebar-foreground:
|
|
108
|
-
--sidebar-primary:
|
|
109
|
-
--sidebar-primary-foreground:
|
|
110
|
-
--sidebar-accent:
|
|
111
|
-
--sidebar-accent-foreground:
|
|
112
|
-
--sidebar-border:
|
|
113
|
-
--sidebar-ring:
|
|
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
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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;
|
|
@@ -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
|
-
'
|
|
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
|
+
}
|