methanol 0.0.13 → 0.0.15

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.
Files changed (39) hide show
  1. package/index.js +1 -0
  2. package/package.json +1 -1
  3. package/src/build-system.js +1 -0
  4. package/src/config.js +33 -3
  5. package/src/dev-server.js +21 -13
  6. package/src/pages-index.js +42 -0
  7. package/src/pages.js +1 -0
  8. package/src/reframe.js +1 -1
  9. package/src/state.js +8 -0
  10. package/src/text-utils.js +60 -0
  11. package/src/vite-plugins.js +9 -0
  12. package/src/workers/build-pool.js +1 -0
  13. package/themes/blog/README.md +26 -0
  14. package/themes/blog/components/CategoryView.client.jsx +164 -0
  15. package/themes/blog/components/CategoryView.static.jsx +35 -0
  16. package/themes/blog/components/CollectionView.client.jsx +151 -0
  17. package/themes/blog/components/CollectionView.static.jsx +37 -0
  18. package/themes/blog/components/PostList.client.jsx +92 -0
  19. package/themes/blog/components/PostList.static.jsx +36 -0
  20. package/themes/blog/components/ThemeSearchBox.client.jsx +427 -0
  21. package/themes/blog/components/ThemeSearchBox.static.jsx +40 -0
  22. package/themes/blog/index.js +40 -0
  23. package/themes/blog/pages/404.mdx +12 -0
  24. package/themes/blog/pages/about.mdx +14 -0
  25. package/themes/blog/pages/categories.mdx +6 -0
  26. package/themes/blog/pages/collections.mdx +6 -0
  27. package/themes/blog/pages/index.mdx +16 -0
  28. package/themes/blog/pages/offline.mdx +11 -0
  29. package/themes/blog/sources/style.css +579 -0
  30. package/themes/blog/src/date-utils.js +28 -0
  31. package/themes/blog/src/heading.jsx +37 -0
  32. package/themes/blog/src/layout-categories.jsx +66 -0
  33. package/themes/blog/src/layout-collections.jsx +65 -0
  34. package/themes/blog/src/layout-home.jsx +66 -0
  35. package/themes/blog/src/layout-post.jsx +42 -0
  36. package/themes/blog/src/page.jsx +152 -0
  37. package/themes/blog/src/post-utils.js +83 -0
  38. package/themes/default/sources/theme-prepare.js +0 -1
  39. package/themes/default/src/page.jsx +1 -1
package/index.js CHANGED
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { HTMLRenderer } from './src/renderer.js'
22
+ export { extractExcerpt } from './src/text-utils.js'
22
23
 
23
24
  export { env } from './src/reframe.js'
24
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "methanol",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Static site generator powered by rEFui and MDX",
5
5
  "main": "./index.js",
6
6
  "type": "module",
@@ -147,6 +147,7 @@ export const buildHtmlEntries = async () => {
147
147
  }
148
148
  }
149
149
  pagesContext.refreshPagesTree?.()
150
+ state.PAGES_CONTEXT = pagesContext
150
151
 
151
152
  const titleSnapshot = pages.map((page) => page.title)
152
153
  await runWorkerStage({
package/src/config.js CHANGED
@@ -20,9 +20,10 @@
20
20
 
21
21
  import { readFile } from 'fs/promises'
22
22
  import { existsSync } from 'fs'
23
- import { resolve, isAbsolute, extname, basename } from 'path'
24
- import { pathToFileURL } from 'url'
23
+ import { resolve, isAbsolute, extname, basename, dirname } from 'path'
24
+ import { pathToFileURL, fileURLToPath } from 'url'
25
25
  import { mergeConfig } from 'vite'
26
+ import { projectRequire } from './node-loader.js'
26
27
  import { cli, state } from './state.js'
27
28
  import { logger } from './logger.js'
28
29
  import { HTMLRenderer } from './renderer.js'
@@ -239,6 +240,34 @@ const buildConfigContext = (mode) => ({
239
240
  HTMLRenderer
240
241
  })
241
242
 
243
+ const resolveTheme = async (themeValue, root) => {
244
+ if (typeof themeValue !== 'string') return themeValue
245
+
246
+ const load = async (p) => {
247
+ const mod = await import(pathToFileURL(p).href)
248
+ const fn = mod.default ?? mod
249
+ return typeof fn === 'function' ? fn() : fn
250
+ }
251
+
252
+ // 1. Check Methanol themes dir
253
+ const __dirname = dirname(fileURLToPath(import.meta.url))
254
+ const builtInPath = resolve(__dirname, '../themes', themeValue, 'index.js')
255
+ if (existsSync(builtInPath)) {
256
+ return load(builtInPath)
257
+ }
258
+
259
+ // 2. Resolve methanol-theme-<name> from user dir
260
+ try {
261
+ const pkgName = `methanol-theme-${themeValue}`
262
+ const pkgPath = projectRequire.resolve(pkgName)
263
+ return load(pkgPath)
264
+ } catch (e) {
265
+ // Ignore
266
+ }
267
+
268
+ throw new Error(`Theme not found: ${themeValue}`)
269
+ }
270
+
242
271
  export const loadUserConfig = async (mode, configPath = null) => {
243
272
  if (configPath) {
244
273
  const filePath = resolveConfigPath(configPath)
@@ -323,7 +352,8 @@ export const applyConfig = async (config, mode) => {
323
352
 
324
353
  state.VIRTUAL_HTML_OUTPUT_ROOT = state.PAGES_DIR
325
354
 
326
- state.USER_THEME = config.theme || await defaultTheme()
355
+ const themeValue = cli.CLI_THEME || config.theme
356
+ state.USER_THEME = themeValue ? await resolveTheme(themeValue, root) : await defaultTheme()
327
357
  if (!state.USER_THEME?.root && !config.theme?.root) {
328
358
  throw new Error('Theme root is required.')
329
359
  }
package/src/dev-server.js CHANGED
@@ -143,6 +143,7 @@ export const runViteDev = async () => {
143
143
  let pagesContextToken = 0
144
144
  const setPagesContext = (next) => {
145
145
  pagesContext = next
146
+ state.PAGES_CONTEXT = next
146
147
  pagesContextToken += 1
147
148
  }
148
149
  setPagesContext(await buildPagesContext({ compileAll: false }))
@@ -160,6 +161,23 @@ export const runViteDev = async () => {
160
161
  console.error(error?.stack || error)
161
162
  }
162
163
 
164
+ const _invalidate = (id) => {
165
+ const _module = server.moduleGraph.getModuleById(id)
166
+ if (_module) {
167
+ server.moduleGraph.invalidateModule(_module)
168
+ }
169
+ }
170
+ const invalidateRewindInject = () => {
171
+ _invalidate('\0/.methanol_virtual_module/registry.js')
172
+ _invalidate('\0methanol:registry')
173
+ _invalidate('\0/.methanol_virtual_module/inject.js')
174
+ _invalidate('\0methanol:inject')
175
+ }
176
+ const invalidatePagesIndex = () => {
177
+ _invalidate('\0/.methanol_virtual_module/pages.js')
178
+ _invalidate('\0methanol:pages')
179
+ }
180
+
163
181
  const refreshPagesContext = async () => {
164
182
  setPagesContext(await buildPagesContext({ compileAll: false }))
165
183
  }
@@ -214,6 +232,7 @@ export const runViteDev = async () => {
214
232
  }
215
233
  }
216
234
  pagesContext.refreshPagesTree?.()
235
+ invalidatePagesIndex()
217
236
  invalidateHtmlCache()
218
237
  const renderEpoch = htmlCacheEpoch
219
238
 
@@ -518,19 +537,6 @@ export const runViteDev = async () => {
518
537
  await server.listen()
519
538
  server.printUrls()
520
539
 
521
- const _invalidate = (id) => {
522
- const _module = server.moduleGraph.getModuleById(id)
523
- if (_module) {
524
- server.moduleGraph.invalidateModule(_module)
525
- }
526
- }
527
- const invalidateRewindInject = () => {
528
- _invalidate('\0/.methanol_virtual_module/registry.js')
529
- _invalidate('\0methanol:registry')
530
- _invalidate('\0/.methanol_virtual_module/inject.js')
531
- _invalidate('\0methanol:inject')
532
- }
533
-
534
540
  let queue = Promise.resolve()
535
541
  const enqueue = (task) => {
536
542
  queue = queue.then(task).catch((err) => {
@@ -547,6 +553,7 @@ export const runViteDev = async () => {
547
553
 
548
554
  const refreshPages = async () => {
549
555
  await refreshPagesContext()
556
+ invalidatePagesIndex()
550
557
  invalidateHtmlCache()
551
558
  reload()
552
559
  }
@@ -700,6 +707,7 @@ export const runViteDev = async () => {
700
707
  }
701
708
  const updated = await updatePageEntry(path, resolved)
702
709
  if (updated) {
710
+ invalidatePagesIndex()
703
711
  invalidateHtmlCache()
704
712
  reload()
705
713
  return
@@ -0,0 +1,42 @@
1
+ /* Copyright Yukino Song, SudoMaker Ltd.
2
+ *
3
+ * Licensed to the Apache Software Foundation (ASF) under one
4
+ * or more contributor license agreements. See the NOTICE file
5
+ * distributed with this work for additional information
6
+ * regarding copyright ownership. The ASF licenses this file
7
+ * to you under the Apache License, Version 2.0 (the
8
+ * "License"); you may not use this file except in compliance
9
+ * with the License. You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing,
14
+ * software distributed under the License is distributed on an
15
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ * KIND, either express or implied. See the License for the
17
+ * specific language governing permissions and limitations
18
+ * under the License.
19
+ */
20
+
21
+ import JSON5 from 'json5'
22
+ import { extractExcerpt } from './text-utils.js'
23
+
24
+ const OMIT_KEYS = new Set(['content', 'mdxComponent', 'mdxCtx', 'getSiblings'])
25
+
26
+ const sanitizePage = (page) => {
27
+ const result = {}
28
+ for (const [key, value] of Object.entries(page || {})) {
29
+ if (OMIT_KEYS.has(key)) continue
30
+ if (typeof value === 'function') continue
31
+ result[key] = value
32
+ }
33
+ result.excerpt = extractExcerpt(page)
34
+ return result
35
+ }
36
+
37
+ export const serializePagesIndex = (pages) => {
38
+ const list = Array.isArray(pages)
39
+ ? pages.filter((page) => !page?.hidden).map(sanitizePage)
40
+ : []
41
+ return JSON5.stringify(list)
42
+ }
package/src/pages.js CHANGED
@@ -51,6 +51,7 @@ const cliOverrides = {
51
51
  CLI_VERBOSE: cli.CLI_VERBOSE,
52
52
  CLI_BASE: cli.CLI_BASE,
53
53
  CLI_SEARCH: cli.CLI_SEARCH,
54
+ CLI_THEME: cli.CLI_THEME,
54
55
  CLI_PWA: cli.CLI_PWA
55
56
  }
56
57
 
package/src/reframe.js CHANGED
@@ -65,7 +65,7 @@ export function env(parentEnv) {
65
65
 
66
66
  const id = renderCount++
67
67
  const idStr = id.toString(16)
68
- const script = `$$rfrm(${JSON.stringify(key)},${id},${Object.keys(props).length ? JSON5.stringify(props) : '{}'})`
68
+ const script = `$$rfrm(${JSON.stringify(key)},${id},${Object.keys(props).length ? JSON5.stringify(props).replace(/<\/script/ig, '<\\/script') : '{}'})`
69
69
 
70
70
  return (R) => {
71
71
  return [
package/src/state.js CHANGED
@@ -130,6 +130,12 @@ const withCommonOptions = (y) =>
130
130
  type: 'boolean',
131
131
  default: undefined
132
132
  })
133
+ .option('theme', {
134
+ describe: 'Theme to use (name or path)',
135
+ type: 'string',
136
+ requiresArg: true,
137
+ nargs: 1
138
+ })
133
139
  .option('pwa', {
134
140
  describe: 'Enable or disable PWA support',
135
141
  type: 'boolean',
@@ -167,6 +173,7 @@ export const cli = {
167
173
  CLI_VERBOSE: Boolean(argv.verbose),
168
174
  CLI_BASE: argv.base || null,
169
175
  CLI_SEARCH: typeof argv.search === 'boolean' ? argv.search : undefined,
176
+ CLI_THEME: argv.theme || null,
170
177
  CLI_PWA: typeof argv.pwa === 'boolean' ? argv.pwa : undefined
171
178
  }
172
179
 
@@ -192,6 +199,7 @@ export const state = {
192
199
  USER_VITE_CONFIG: null,
193
200
  USER_MDX_CONFIG: null,
194
201
  USER_PUBLIC_OVERRIDE: false,
202
+ PAGES_CONTEXT: null,
195
203
  SOURCES: [],
196
204
  PAGEFIND_ENABLED: false,
197
205
  PAGEFIND_OPTIONS: null,
@@ -0,0 +1,60 @@
1
+ /* Copyright Yukino Song, SudoMaker Ltd.
2
+ *
3
+ * Licensed to the Apache Software Foundation (ASF) under one
4
+ * or more contributor license agreements. See the NOTICE file
5
+ * distributed with this work for additional information
6
+ * regarding copyright ownership. The ASF licenses this file
7
+ * to you under the Apache License, Version 2.0 (the
8
+ * "License"); you may not use this file except in compliance
9
+ * with the License. You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing,
14
+ * software distributed under the License is distributed on an
15
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ * KIND, either express or implied. See the License for the
17
+ * specific language governing permissions and limitations
18
+ * under the License.
19
+ */
20
+
21
+ const DEFAULT_EXCERPT_LENGTH = 200
22
+
23
+ const collapseWhitespace = (value) => value.replace(/\s+/g, ' ').trim()
24
+
25
+ const stripFirstHeading = (value) => value.replace(/^\s{0,3}#{1,6}\s+.*$/m, ' ')
26
+
27
+ const stripMarkdown = (value) => {
28
+ let text = value
29
+ text = text.replace(/```[\s\S]*?```/g, ' ')
30
+ text = text.replace(/`[^`]*`/g, ' ')
31
+ text = stripFirstHeading(text)
32
+ text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
33
+ text = text.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
34
+ text = text.replace(/<[^>]+>/g, ' ')
35
+ text = text.replace(/^\s{0,3}#{1,6}\s+/gm, '')
36
+ text = text.replace(/^\s{0,3}>\s?/gm, '')
37
+ text = text.replace(/^\s*[-*+]\s+/gm, '')
38
+ text = text.replace(/^\s*\d+\.\s+/gm, '')
39
+ return collapseWhitespace(text)
40
+ }
41
+
42
+ export const extractExcerpt = (page, options = {}) => {
43
+ if (!page) return ''
44
+ const length =
45
+ typeof options.length === 'number' && Number.isFinite(options.length)
46
+ ? options.length
47
+ : DEFAULT_EXCERPT_LENGTH
48
+ const raw =
49
+ page.excerpt ||
50
+ page.frontmatter?.excerpt ||
51
+ page.frontmatter?.description ||
52
+ page.content ||
53
+ ''
54
+ const sanitized = stripMarkdown(String(raw))
55
+ if (!sanitized) return ''
56
+ if (length > 0 && sanitized.length > length) {
57
+ return `${sanitized.slice(0, length).trim()}...`
58
+ }
59
+ return sanitized
60
+ }
@@ -26,6 +26,7 @@ import { normalizePath } from 'vite'
26
26
  import { state } from './state.js'
27
27
  import { resolveBasePrefix } from './config.js'
28
28
  import { genRegistryScript } from './components.js'
29
+ import { serializePagesIndex } from './pages-index.js'
29
30
  import { INJECT_SCRIPT, LOADER_SCRIPT, PAGEFIND_LOADER_SCRIPT, PWA_INJECT_SCRIPT } from './client/virtual-module/assets.js'
30
31
  import { projectRequire } from './node-loader.js'
31
32
 
@@ -144,6 +145,14 @@ const virtualModuleMap = {
144
145
  }
145
146
 
146
147
  return ''
148
+ },
149
+ get pages() {
150
+ const pages = state.PAGES_CONTEXT?.pages || []
151
+ return `export const pages = ${serializePagesIndex(pages)}\nexport default pages`
152
+ },
153
+ get 'pages.js'() {
154
+ const pages = state.PAGES_CONTEXT?.pages || []
155
+ return `export const pages = ${serializePagesIndex(pages)}\nexport default pages`
147
156
  }
148
157
  }
149
158
 
@@ -39,6 +39,7 @@ const cliOverrides = {
39
39
  CLI_VERBOSE: cli.CLI_VERBOSE,
40
40
  CLI_BASE: cli.CLI_BASE,
41
41
  CLI_SEARCH: cli.CLI_SEARCH,
42
+ CLI_THEME: cli.CLI_THEME,
42
43
  CLI_PWA: cli.CLI_PWA
43
44
  }
44
45
 
@@ -0,0 +1,26 @@
1
+ # Blog Theme
2
+
3
+ A simple, clean blog theme for Methanol.
4
+
5
+ ## Features
6
+ - Clean typography
7
+ - Post list on homepage
8
+ - Responsive design
9
+ - Dark mode support (via system preference)
10
+
11
+ ## Usage
12
+
13
+ To use this theme, configure your Methanol project to point to this directory.
14
+
15
+ ```js
16
+ // methanol.config.js
17
+ export default {
18
+ theme: './themes/blog',
19
+ // ...
20
+ }
21
+ ```
22
+
23
+ ## Structure
24
+ - `src/page.jsx`: Main layout template.
25
+ - `sources/style.css`: Stylesheet.
26
+ - `pages/`: Default pages (Home, 404, Offline).
@@ -0,0 +1,164 @@
1
+ /* Copyright Yukino Song, SudoMaker Ltd.
2
+ *
3
+ * Licensed to the Apache Software Foundation (ASF) under one
4
+ * or more contributor license agreements. See the NOTICE file
5
+ * distributed with this work for additional information
6
+ * regarding copyright ownership. The ASF licenses this file
7
+ * to you under the Apache License, Version 2.0 (the
8
+ * "License"); you may not use this file except in compliance
9
+ * with the License. You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing,
14
+ * software distributed under the License is distributed on an
15
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ * KIND, either express or implied. See the License for the
17
+ * specific language governing permissions and limitations
18
+ * under the License.
19
+ */
20
+
21
+ import { signal, $, For, If, onCondition } from 'refui'
22
+ import pages from 'methanol:pages'
23
+ import { formatDate } from '../src/date-utils.js'
24
+
25
+ const rawPages = Array.isArray(pages) ? pages : []
26
+ const isPostPage = (p) => {
27
+ if (!p?.routeHref) return false
28
+ if (p.routeHref === '/' || p.routeHref === '/404' || p.routeHref === '/offline') return false
29
+ if (p.routeHref.startsWith('/.methanol')) return false
30
+ return true
31
+ }
32
+ const resolvePosts = () => {
33
+ const list = rawPages.filter(isPostPage)
34
+ list.sort((a, b) => {
35
+ const dateA = new Date(a.frontmatter?.date || a.stats?.createdAt || 0)
36
+ const dateB = new Date(b.frontmatter?.date || b.stats?.createdAt || 0)
37
+ return dateB - dateA
38
+ })
39
+ return list.map((p) => ({
40
+ title: p.title,
41
+ routeHref: p.routeHref,
42
+ frontmatter: p.frontmatter || {},
43
+ stats: p.stats || {},
44
+ excerpt: p.excerpt || p.frontmatter?.excerpt || ''
45
+ }))
46
+ }
47
+ const allPosts = resolvePosts()
48
+ const defaultCategories = Array.from(
49
+ new Set(
50
+ allPosts.flatMap((p) => {
51
+ const c = p.frontmatter?.categories
52
+ return Array.isArray(c) ? c : c ? [c] : []
53
+ })
54
+ )
55
+ ).sort()
56
+
57
+ export default function ({ categories = null } = {}) {
58
+ const currentCategory = signal('')
59
+ const activeCategory = onCondition(currentCategory)
60
+ const visibleCount = signal(10)
61
+ const categoryList = Array.isArray(categories) && categories.length ? categories : defaultCategories
62
+
63
+ if (typeof window !== 'undefined') {
64
+ // Initial setup on client side
65
+ const params = new URLSearchParams(window.location.search)
66
+ const cat = params.get('category')
67
+ if (cat) currentCategory.value = cat
68
+
69
+ window.addEventListener('popstate', () => {
70
+ const p = new URLSearchParams(window.location.search)
71
+ currentCategory.value = p.get('category') || ''
72
+ })
73
+ }
74
+
75
+ const filteredPosts = $(() => {
76
+ const cat = currentCategory.value
77
+ if (!cat) return allPosts
78
+ return allPosts.filter((p) => {
79
+ const pCats = p.frontmatter?.categories || []
80
+ return Array.isArray(pCats) ? pCats.includes(cat) : pCats === cat
81
+ })
82
+ })
83
+
84
+ const visiblePosts = $(() => filteredPosts.value.slice(0, visibleCount.value))
85
+ const hasMore = $(() => visibleCount.value < filteredPosts.value.length)
86
+ const showEmpty = $(() => filteredPosts.value.length === 0)
87
+
88
+ const loadMore = () => {
89
+ visibleCount.value += 10
90
+ }
91
+
92
+ const setCategory = (cat) => {
93
+ currentCategory.value = cat
94
+ visibleCount.value = 10
95
+ const url = new URL(window.location)
96
+ if (cat) {
97
+ url.searchParams.set('category', cat)
98
+ } else {
99
+ url.searchParams.delete('category')
100
+ }
101
+ window.history.pushState({}, '', url)
102
+ }
103
+
104
+ return (
105
+ <div class="category-view">
106
+ <div class="category-list">
107
+ <button class="category-tag" class:active={activeCategory('')} on:click={() => setCategory('')}>
108
+ All
109
+ </button>
110
+ <For entries={categoryList}>
111
+ {({ item: cat }) => (
112
+ <button
113
+ class="category-tag"
114
+ class:active={activeCategory(cat.value)}
115
+ on:click={() => setCategory(cat.value)}
116
+ >
117
+ {cat}
118
+ </button>
119
+ )}
120
+ </For>
121
+ </div>
122
+
123
+ <div class="post-list">
124
+ <For entries={visiblePosts}>
125
+ {({ item: p }) => {
126
+ const dateStr = formatDate(p.frontmatter?.date || p.stats?.createdAt)
127
+
128
+ return (
129
+ <article class="post-item">
130
+ <div class="post-meta">
131
+ {dateStr && <span class="post-date">{dateStr}</span>}
132
+ {p.frontmatter.categories && (
133
+ <span class="post-categories">
134
+ &middot;{' '}
135
+ {Array.isArray(p.frontmatter.categories)
136
+ ? p.frontmatter.categories.join(', ')
137
+ : p.frontmatter.categories}
138
+ </span>
139
+ )}
140
+ </div>
141
+ <h2 class="post-item-title">
142
+ <a href={p.routeHref}>{p.title || 'Untitled'}</a>
143
+ </h2>
144
+ <div class="post-excerpt">{p.frontmatter.excerpt || p.excerpt || 'No excerpt available.'}</div>
145
+ </article>
146
+ )
147
+ }}
148
+ </For>
149
+ </div>
150
+
151
+ <If condition={showEmpty}>{() => <p>No posts found in this category.</p>}</If>
152
+
153
+ <If condition={hasMore}>
154
+ {() => (
155
+ <div class="pagination-container">
156
+ <button class="load-more-btn" on:click={loadMore}>
157
+ Load More
158
+ </button>
159
+ </div>
160
+ )}
161
+ </If>
162
+ </div>
163
+ )
164
+ }
@@ -0,0 +1,35 @@
1
+ /* Copyright Yukino Song, SudoMaker Ltd.
2
+ *
3
+ * Licensed to the Apache Software Foundation (ASF) under one
4
+ * or more contributor license agreements. See the NOTICE file
5
+ * distributed with this work for additional information
6
+ * regarding copyright ownership. The ASF licenses this file
7
+ * to you under the Apache License, Version 2.0 (the
8
+ * "License"); you may not use this file except in compliance
9
+ * with the License. You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing,
14
+ * software distributed under the License is distributed on an
15
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ * KIND, either express or implied. See the License for the
17
+ * specific language governing permissions and limitations
18
+ * under the License.
19
+ */
20
+
21
+ export default function ({ categories = [] } = {}, ...children) {
22
+ if (!children.length) return null
23
+ return (
24
+ <div class="category-view">
25
+ <div class="category-list">
26
+ <button class="category-tag active">All</button>
27
+ {categories.map((cat) => (
28
+ <button class="category-tag">{cat}</button>
29
+ ))}
30
+ </div>
31
+
32
+ <div class="post-list">{...children}</div>
33
+ </div>
34
+ )
35
+ }