create-rue 0.0.14 → 0.0.16

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.
@@ -2,7 +2,7 @@ import { computed, type FC, ref, useEffect, watch } from '@rue-js/rue'
2
2
  import { RouterLink, useRoute } from '@rue-js/router'
3
3
 
4
4
  const themeStorageKey = 'rue.base.theme'
5
- const reportHeaderStorageKey = 'rue.base.report.hideHeader'
5
+ const headerToggleRoutes = ['/', '/report', '/todo']
6
6
 
7
7
  const navItems = [
8
8
  { to: '/', label: '首页' },
@@ -80,9 +80,20 @@ const themeLabels: Record<string, string> = {
80
80
  sunset: '日落',
81
81
  }
82
82
 
83
- const getStoredTheme = () => localStorage.getItem(themeStorageKey) || 'light'
83
+ const getStoredTheme = () => {
84
+ const storedTheme = localStorage.getItem(themeStorageKey)
85
+ return storedTheme && themes.includes(storedTheme) ? storedTheme : 'light'
86
+ }
87
+
88
+ const getHeaderStorageKey = (path: string) => {
89
+ if (path === '/') {
90
+ return 'rue.base.header.hide.home'
91
+ }
92
+
93
+ return `rue.base.header.hide.${path.replace(/^\//, '')}`
94
+ }
84
95
 
85
- const getStoredHeaderHidden = () => localStorage.getItem(reportHeaderStorageKey) === '1'
96
+ const getStoredHeaderHidden = (path: string) => localStorage.getItem(getHeaderStorageKey(path)) === '1'
86
97
 
87
98
  const applyTheme = (theme: string) => {
88
99
  document.documentElement.setAttribute('data-theme', theme)
@@ -91,20 +102,48 @@ const applyTheme = (theme: string) => {
91
102
  const ThemePicker: FC = () => {
92
103
  const theme = ref(getStoredTheme())
93
104
 
105
+ const applyThemeState = () => {
106
+ const currentTheme = theme.value
107
+ const themePicker = document.querySelector<HTMLSelectElement>('[data-base-theme-picker="true"]')
108
+
109
+ applyTheme(currentTheme)
110
+
111
+ if (themePicker && themePicker.value !== currentTheme) {
112
+ themePicker.value = currentTheme
113
+ }
114
+ }
115
+
116
+ const syncStoredTheme = () => {
117
+ theme.value = getStoredTheme()
118
+ requestAnimationFrame(applyThemeState)
119
+ }
120
+
94
121
  useEffect(() => {
95
- applyTheme(theme.value)
96
- })
122
+ localStorage.setItem(themeStorageKey, theme.value)
123
+ requestAnimationFrame(applyThemeState)
124
+
125
+ window.addEventListener('storage', syncStoredTheme)
126
+ window.addEventListener('focus', syncStoredTheme)
127
+ document.addEventListener('visibilitychange', syncStoredTheme)
128
+
129
+ return () => {
130
+ window.removeEventListener('storage', syncStoredTheme)
131
+ window.removeEventListener('focus', syncStoredTheme)
132
+ document.removeEventListener('visibilitychange', syncStoredTheme)
133
+ }
134
+ }, [])
97
135
 
98
136
  watch(
99
137
  () => theme.value,
100
138
  () => {
101
- applyTheme(theme.value)
102
139
  localStorage.setItem(themeStorageKey, theme.value)
140
+ requestAnimationFrame(applyThemeState)
103
141
  },
104
142
  )
105
143
 
106
144
  return (
107
145
  <select
146
+ data-base-theme-picker="true"
108
147
  aria-label="切换主题"
109
148
  className="select select-bordered select-sm w-40"
110
149
  onChange={(event: Event) => {
@@ -112,7 +151,7 @@ const ThemePicker: FC = () => {
112
151
  }}
113
152
  >
114
153
  {themes.map((name) => (
115
- <option key={name} value={name} selected={theme.value === name}>
154
+ <option key={name} value={name}>
116
155
  {themeLabels[name] || name}
117
156
  </option>
118
157
  ))}
@@ -123,11 +162,34 @@ const ThemePicker: FC = () => {
123
162
  const Header: FC = () => {
124
163
  const route = useRoute()
125
164
  const currentPath = computed(() => route.get()?.path || '/')
165
+ const navClass = (path: string) =>
166
+ `btn btn-sm ${currentPath.get() === path ? 'btn-primary' : 'btn-ghost'}`.trim()
167
+
168
+ const applyHeaderNavState = () => {
169
+ const path = currentPath.get()
170
+ const navLinks = document.querySelectorAll<HTMLElement>('[data-base-nav-path]')
171
+
172
+ navLinks.forEach((link) => {
173
+ const targetPath = link.dataset.baseNavPath || ''
174
+ link.className = `btn btn-sm ${path === targetPath ? 'btn-primary' : 'btn-ghost'}`.trim()
175
+ })
176
+ }
177
+
178
+ watch(
179
+ () => currentPath.get(),
180
+ () => {
181
+ requestAnimationFrame(applyHeaderNavState)
182
+ },
183
+ )
184
+
185
+ useEffect(() => {
186
+ requestAnimationFrame(applyHeaderNavState)
187
+ }, [])
126
188
 
127
189
  return (
128
- <header className="sticky top-0 z-40 border-b border-base-300/70 bg-base-100/85 backdrop-blur">
129
- <div className="content-width navbar min-h-0 gap-4 py-3">
130
- <div className="navbar-start min-w-0">
190
+ <header className="sticky top-0 z-40 border-b border-base-300/70 bg-base-100/90 backdrop-blur">
191
+ <div className="content-width flex flex-wrap items-center gap-3 py-3">
192
+ <div className="min-w-0 flex-1 md:flex-none">
131
193
  <RouterLink to="/" className="flex min-w-0 items-center gap-3">
132
194
  <div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-primary text-primary-content shadow-lg shadow-primary/20">
133
195
  R
@@ -137,18 +199,19 @@ const Header: FC = () => {
137
199
  </div>
138
200
  </RouterLink>
139
201
  </div>
140
- <nav className="navbar-center hidden flex-1 justify-center gap-2 md:flex">
202
+ <nav className="order-3 flex w-full flex-wrap gap-2 md:order-none md:w-auto md:flex-1 md:justify-center">
141
203
  {navItems.map((item) => (
142
204
  <RouterLink
143
205
  key={item.to}
144
206
  to={item.to}
145
- className={`btn btn-sm ${currentPath.get() === item.to ? 'btn-primary' : 'btn-ghost'}`}
207
+ data-base-nav-path={item.to}
208
+ className={navClass(item.to)}
146
209
  >
147
- {item.label}
210
+ <span>{item.label}</span>
148
211
  </RouterLink>
149
212
  ))}
150
213
  </nav>
151
- <div className="navbar-end flex items-center gap-2">
214
+ <div className="flex items-center gap-2 md:ml-auto">
152
215
  <ThemePicker />
153
216
  </div>
154
217
  </div>
@@ -174,26 +237,69 @@ const Footer: FC = () => (
174
237
  const HeaderToggleButton: FC<{ hidden: boolean; onToggle: () => void }> = (props) => (
175
238
  <button
176
239
  type="button"
177
- className="btn btn-circle btn-sm fixed left-3 top-3 z-50 border-base-300 bg-base-100/90 shadow-lg backdrop-blur"
178
- aria-label={props.hidden ? '显示顶部导航' : '隐藏顶部导航'}
179
- title={props.hidden ? '显示顶部导航' : '隐藏顶部导航'}
240
+ className="btn btn-circle btn-sm fixed left-3 top-3 z-50 border-base-300 bg-base-100/95 text-base-content shadow-lg backdrop-blur"
241
+ aria-label={props.hidden ? '显示头部和底部' : '隐藏头部和底部'}
242
+ title={props.hidden ? '显示头部和底部' : '隐藏头部和底部'}
180
243
  onClick={props.onToggle}
181
244
  >
182
- {props.hidden ? '显' : '隐'}
245
+ {props.hidden ? (
246
+ <svg
247
+ viewBox="0 0 24 24"
248
+ width="16"
249
+ height="16"
250
+ aria-hidden="true"
251
+ fill="none"
252
+ stroke="currentColor"
253
+ stroke-width="2"
254
+ stroke-linecap="round"
255
+ stroke-linejoin="round"
256
+ >
257
+ <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
258
+ <circle cx="12" cy="12" r="3" />
259
+ </svg>
260
+ ) : (
261
+ <svg
262
+ viewBox="0 0 24 24"
263
+ width="16"
264
+ height="16"
265
+ aria-hidden="true"
266
+ fill="none"
267
+ stroke="currentColor"
268
+ stroke-width="2"
269
+ stroke-linecap="round"
270
+ stroke-linejoin="round"
271
+ >
272
+ <path d="M3 3l18 18" />
273
+ <path d="M10.6 10.7a3 3 0 0 0 4.2 4.2" />
274
+ <path d="M9.9 5.1A10.9 10.9 0 0 1 12 5c6.5 0 10 7 10 7a21.7 21.7 0 0 1-4 5.2" />
275
+ <path d="M6.6 6.7C4.2 8.3 2.7 10.9 2 12c0 0 3.5 7 10 7 1.8 0 3.4-.5 4.8-1.3" />
276
+ </svg>
277
+ )}
183
278
  </button>
184
279
  )
185
280
 
186
281
  const SiteLayout: FC = (props) => {
187
282
  const route = useRoute()
188
- const reportHeaderHidden = ref(getStoredHeaderHidden())
189
283
  const currentPath = computed(() => route.get()?.path || '/')
190
- const isReportRoute = computed(() => currentPath.get() === '/report')
191
- const hideHeader = computed(() => isReportRoute.get() && reportHeaderHidden.value)
284
+ const allowHeaderToggle = computed(() => headerToggleRoutes.includes(currentPath.get()))
285
+ const reportHeaderHidden = ref(getStoredHeaderHidden(currentPath.get()))
286
+ const hideHeader = computed(() => allowHeaderToggle.get() && reportHeaderHidden.value)
287
+
288
+ watch(
289
+ () => currentPath.get(),
290
+ (path) => {
291
+ reportHeaderHidden.value = allowHeaderToggle.get() ? getStoredHeaderHidden(path) : false
292
+ },
293
+ )
192
294
 
193
295
  watch(
194
296
  () => reportHeaderHidden.value,
195
297
  () => {
196
- localStorage.setItem(reportHeaderStorageKey, reportHeaderHidden.value ? '1' : '0')
298
+ if (!allowHeaderToggle.get()) {
299
+ return
300
+ }
301
+
302
+ localStorage.setItem(getHeaderStorageKey(currentPath.get()), reportHeaderHidden.value ? '1' : '0')
197
303
  },
198
304
  )
199
305
 
@@ -201,7 +307,7 @@ const SiteLayout: FC = (props) => {
201
307
  <div className="app-shell">
202
308
  <div className="app-surface">
203
309
  {!hideHeader.get() && <Header />}
204
- {isReportRoute.get() && (
310
+ {allowHeaderToggle.get() && (
205
311
  <HeaderToggleButton
206
312
  hidden={hideHeader.get()}
207
313
  onToggle={() => {
@@ -209,12 +315,10 @@ const SiteLayout: FC = (props) => {
209
315
  }}
210
316
  />
211
317
  )}
212
- <main
213
- className={`content-width pb-10 ${hideHeader.get() ? 'pt-6 sm:pt-8' : 'pt-6 sm:pt-8'}`}
214
- >
318
+ <main className="content-width pb-10 pt-6 sm:pt-8">
215
319
  {props.children}
216
320
  </main>
217
- <Footer />
321
+ {!hideHeader.get() && <Footer />}
218
322
  </div>
219
323
  </div>
220
324
  )