sekkei-preview 0.1.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/dist/cli.d.ts +2 -0
- package/dist/cli.js +116 -0
- package/dist/cli.js.map +1 -0
- package/dist/generate-config.d.ts +22 -0
- package/dist/generate-config.js +138 -0
- package/dist/generate-config.js.map +1 -0
- package/dist/generate-index.d.ts +5 -0
- package/dist/generate-index.js +74 -0
- package/dist/generate-index.js.map +1 -0
- package/dist/resolve-docs-dir.d.ts +8 -0
- package/dist/resolve-docs-dir.js +46 -0
- package/dist/resolve-docs-dir.js.map +1 -0
- package/package.json +44 -0
- package/plugins/file-api-plugin.ts +198 -0
- package/plugins/frontmatter-utils.ts +22 -0
- package/plugins/safe-path.ts +35 -0
- package/theme/components/EditButton.vue +68 -0
- package/theme/components/EditorToolbar.vue +131 -0
- package/theme/components/Layout.vue +139 -0
- package/theme/components/MilkdownEditor.vue +78 -0
- package/theme/components/MilkdownWrapper.vue +94 -0
- package/theme/composables/use-edit-mode.ts +55 -0
- package/theme/composables/use-file-api.ts +46 -0
- package/theme/index.ts +16 -0
- package/theme/styles/custom.css +85 -0
- package/theme/styles/editor.css +31 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted } from 'vue'
|
|
3
|
+
import MilkdownEditor from './MilkdownEditor.vue'
|
|
4
|
+
import { useFileApi } from '../composables/use-file-api'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
filePath: string
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const emit = defineEmits<{
|
|
11
|
+
change: []
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const content = ref('')
|
|
15
|
+
const loading = ref(true)
|
|
16
|
+
const error = ref('')
|
|
17
|
+
const editorRef = ref<InstanceType<typeof MilkdownEditor> | null>(null)
|
|
18
|
+
const currentMarkdown = ref('')
|
|
19
|
+
|
|
20
|
+
const { read, save } = useFileApi()
|
|
21
|
+
|
|
22
|
+
onMounted(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const result = await read(props.filePath)
|
|
25
|
+
content.value = result.content
|
|
26
|
+
currentMarkdown.value = result.content
|
|
27
|
+
} catch (err) {
|
|
28
|
+
error.value = (err as Error).message
|
|
29
|
+
} finally {
|
|
30
|
+
loading.value = false
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function onEditorChange(markdown: string) {
|
|
35
|
+
currentMarkdown.value = markdown
|
|
36
|
+
emit('change')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function saveContent(): Promise<void> {
|
|
40
|
+
const markdown = editorRef.value?.getMarkdown() ?? currentMarkdown.value
|
|
41
|
+
await save(props.filePath, markdown)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getMarkdown(): string {
|
|
45
|
+
return editorRef.value?.getMarkdown() ?? currentMarkdown.value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
defineExpose({ save: saveContent, getMarkdown })
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<div class="sekkei-editor-wrapper">
|
|
53
|
+
<div v-if="loading" class="editor-loading">
|
|
54
|
+
<span>Loading editor...</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div v-else-if="error" class="editor-error">
|
|
57
|
+
<span>Failed to load: {{ error }}</span>
|
|
58
|
+
</div>
|
|
59
|
+
<ClientOnly v-else>
|
|
60
|
+
<template #fallback>
|
|
61
|
+
<div class="editor-loading">Loading editor...</div>
|
|
62
|
+
</template>
|
|
63
|
+
<MilkdownEditor
|
|
64
|
+
ref="editorRef"
|
|
65
|
+
:initial-value="content"
|
|
66
|
+
@change="onEditorChange"
|
|
67
|
+
/>
|
|
68
|
+
</ClientOnly>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<style scoped>
|
|
73
|
+
.sekkei-editor-wrapper {
|
|
74
|
+
min-height: 400px;
|
|
75
|
+
background: var(--vp-c-bg, #fff);
|
|
76
|
+
border: 1px solid var(--vp-c-divider, #e2e8f0);
|
|
77
|
+
border-radius: 8px;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.editor-loading,
|
|
82
|
+
.editor-error {
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: center;
|
|
86
|
+
min-height: 400px;
|
|
87
|
+
color: var(--vp-c-text-2, #666);
|
|
88
|
+
font-size: 0.9rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.editor-error {
|
|
92
|
+
color: var(--vp-c-danger-1, #e53e3e);
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ref, computed, provide, inject, type Ref, type InjectionKey } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type EditState = 'view' | 'loading' | 'editing' | 'saving'
|
|
4
|
+
|
|
5
|
+
interface EditModeContext {
|
|
6
|
+
state: Ref<EditState>
|
|
7
|
+
isDirty: Ref<boolean>
|
|
8
|
+
isEditing: Ref<boolean>
|
|
9
|
+
startEdit: () => void
|
|
10
|
+
cancelEdit: () => void
|
|
11
|
+
setSaving: () => void
|
|
12
|
+
setView: () => void
|
|
13
|
+
setEditing: () => void
|
|
14
|
+
markDirty: () => void
|
|
15
|
+
clearDirty: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const EDIT_MODE_KEY: InjectionKey<EditModeContext> = Symbol('sekkei-edit-mode')
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Provide edit mode state in Layout component.
|
|
22
|
+
*/
|
|
23
|
+
export function provideEditMode(): EditModeContext {
|
|
24
|
+
const state = ref<EditState>('view')
|
|
25
|
+
const isDirty = ref(false)
|
|
26
|
+
|
|
27
|
+
const isEditing = computed(() =>
|
|
28
|
+
state.value === 'editing' || state.value === 'loading' || state.value === 'saving'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const ctx: EditModeContext = {
|
|
32
|
+
state,
|
|
33
|
+
isDirty,
|
|
34
|
+
isEditing,
|
|
35
|
+
startEdit: () => { state.value = 'loading' },
|
|
36
|
+
cancelEdit: () => { state.value = 'view'; isDirty.value = false },
|
|
37
|
+
setSaving: () => { state.value = 'saving' },
|
|
38
|
+
setView: () => { state.value = 'view'; isDirty.value = false },
|
|
39
|
+
setEditing: () => { state.value = 'editing' },
|
|
40
|
+
markDirty: () => { isDirty.value = true },
|
|
41
|
+
clearDirty: () => { isDirty.value = false },
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
provide(EDIT_MODE_KEY, ctx)
|
|
45
|
+
return ctx
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Inject edit mode state in child components.
|
|
50
|
+
*/
|
|
51
|
+
export function useEditMode(): EditModeContext {
|
|
52
|
+
const ctx = inject(EDIT_MODE_KEY)
|
|
53
|
+
if (!ctx) throw new Error('useEditMode() called outside of provideEditMode()')
|
|
54
|
+
return ctx
|
|
55
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
interface ReadResult {
|
|
2
|
+
content: string
|
|
3
|
+
frontmatter: string
|
|
4
|
+
path: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface ListResult {
|
|
8
|
+
files: Array<{ path: string; title: string }>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Composable for interacting with the sekkei file API endpoints.
|
|
13
|
+
*/
|
|
14
|
+
export function useFileApi() {
|
|
15
|
+
async function read(path: string): Promise<ReadResult> {
|
|
16
|
+
const res = await fetch(`/__api/read?path=${encodeURIComponent(path)}`)
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
|
19
|
+
throw new Error(err.error ?? `Failed to read: ${res.status}`)
|
|
20
|
+
}
|
|
21
|
+
return res.json()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function save(path: string, content: string): Promise<void> {
|
|
25
|
+
const res = await fetch('/__api/save', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify({ path, content }),
|
|
29
|
+
})
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
|
32
|
+
throw new Error(err.error ?? `Failed to save: ${res.status}`)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function list(): Promise<ListResult> {
|
|
37
|
+
const res = await fetch('/__api/list')
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
|
40
|
+
throw new Error(err.error ?? `Failed to list: ${res.status}`)
|
|
41
|
+
}
|
|
42
|
+
return res.json()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { read, save, list }
|
|
46
|
+
}
|
package/theme/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import DefaultTheme from 'vitepress/theme'
|
|
2
|
+
import type { Theme } from 'vitepress'
|
|
3
|
+
import Layout from './components/Layout.vue'
|
|
4
|
+
import MilkdownWrapper from './components/MilkdownWrapper.vue'
|
|
5
|
+
import './styles/custom.css'
|
|
6
|
+
import './styles/editor.css'
|
|
7
|
+
|
|
8
|
+
const theme: Theme = {
|
|
9
|
+
extends: DefaultTheme,
|
|
10
|
+
Layout,
|
|
11
|
+
enhanceApp({ app }) {
|
|
12
|
+
app.component('MilkdownWrapper', MilkdownWrapper)
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default theme
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/* Sekkei Docs — Japanese typography + spec table styling */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--vp-font-family-base: 'Noto Sans JP', 'Hiragino Kaku Gothic ProN', 'Yu Gothic',
|
|
5
|
+
'Meiryo', sans-serif;
|
|
6
|
+
--vp-font-family-mono: 'JetBrains Mono', 'Source Code Pro', 'Menlo', monospace;
|
|
7
|
+
--vp-layout-max-width: 1440px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* JP-friendly line height */
|
|
11
|
+
.vp-doc {
|
|
12
|
+
line-height: 1.8;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.vp-doc p,
|
|
16
|
+
.vp-doc li {
|
|
17
|
+
line-height: 1.8;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Spec tables — visible borders, comfortable padding */
|
|
21
|
+
.vp-doc table {
|
|
22
|
+
width: 100%;
|
|
23
|
+
border-collapse: collapse;
|
|
24
|
+
margin: 1rem 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.vp-doc th,
|
|
28
|
+
.vp-doc td {
|
|
29
|
+
border: 1px solid var(--vp-c-divider);
|
|
30
|
+
padding: 0.6rem 0.8rem;
|
|
31
|
+
text-align: left;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.vp-doc th {
|
|
35
|
+
background-color: var(--vp-c-bg-soft);
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.vp-doc tr:hover td {
|
|
40
|
+
background-color: var(--vp-c-bg-soft);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Wider content area for tables */
|
|
44
|
+
.VPDoc .content-container {
|
|
45
|
+
max-width: 960px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@media (min-width: 1440px) {
|
|
49
|
+
.VPDoc .content-container {
|
|
50
|
+
max-width: 1100px;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Headings — slightly tighter for JP */
|
|
55
|
+
.vp-doc h1 {
|
|
56
|
+
font-size: 1.8rem;
|
|
57
|
+
font-weight: 700;
|
|
58
|
+
letter-spacing: -0.01em;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.vp-doc h2 {
|
|
62
|
+
font-size: 1.4rem;
|
|
63
|
+
font-weight: 600;
|
|
64
|
+
border-top: 1px solid var(--vp-c-divider);
|
|
65
|
+
padding-top: 1.2rem;
|
|
66
|
+
margin-top: 2rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.vp-doc h3 {
|
|
70
|
+
font-size: 1.15rem;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Code blocks */
|
|
75
|
+
.vp-doc div[class*='language-'] {
|
|
76
|
+
margin: 1rem 0;
|
|
77
|
+
border-radius: 6px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Frontmatter ID badges (REQ-xxx, F-xxx, etc.) */
|
|
81
|
+
.vp-doc code {
|
|
82
|
+
font-size: 0.85em;
|
|
83
|
+
padding: 0.15em 0.35em;
|
|
84
|
+
border-radius: 4px;
|
|
85
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* Editor mode styles */
|
|
2
|
+
|
|
3
|
+
/* Smooth transition for editor mount */
|
|
4
|
+
.sekkei-editor-container {
|
|
5
|
+
animation: sekkei-fade-in 0.2s ease-in;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@keyframes sekkei-fade-in {
|
|
9
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
10
|
+
to { opacity: 1; transform: translateY(0); }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Ensure editor takes full width */
|
|
14
|
+
.sekkei-editor-wrapper {
|
|
15
|
+
width: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Milkdown editor theme adjustments for VitePress */
|
|
19
|
+
.sekkei-editor-wrapper .milkdown {
|
|
20
|
+
background: var(--vp-c-bg, #fff);
|
|
21
|
+
color: var(--vp-c-text-1);
|
|
22
|
+
font-family: var(--vp-font-family-base);
|
|
23
|
+
line-height: 1.8;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Editor focus indicator */
|
|
27
|
+
.sekkei-editor-wrapper .milkdown:focus-within {
|
|
28
|
+
outline: 2px solid var(--vp-c-brand-1, #3451b2);
|
|
29
|
+
outline-offset: -2px;
|
|
30
|
+
border-radius: 8px;
|
|
31
|
+
}
|