@kids-reporter/routing-ui 0.1.0-alpha.5 → 0.1.0-alpha.7

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 (42) hide show
  1. package/dist/components/input.d.ts +6 -2
  2. package/dist/components/input.d.ts.map +1 -1
  3. package/dist/components/input.js +30 -9
  4. package/dist/components/input.js.map +1 -1
  5. package/dist/header/shared-components.d.ts.map +1 -1
  6. package/dist/header/shared-components.js +2 -1
  7. package/dist/header/shared-components.js.map +1 -1
  8. package/dist/styles.css +11 -0
  9. package/package.json +6 -5
  10. package/.prettierignore +0 -17
  11. package/babel.config.cjs +0 -31
  12. package/eslint.config.mjs +0 -56
  13. package/prettier.config.mjs +0 -13
  14. package/scripts/build.sh +0 -18
  15. package/src/components/button.tsx +0 -108
  16. package/src/components/index.tsx +0 -2
  17. package/src/components/input.tsx +0 -171
  18. package/src/constants/default-values.tsx +0 -153
  19. package/src/footer.tsx +0 -149
  20. package/src/header/desktop-header.tsx +0 -132
  21. package/src/header/header-context.tsx +0 -82
  22. package/src/header/index.tsx +0 -104
  23. package/src/header/is-logged-in-setter.tsx +0 -27
  24. package/src/header/menu/header-menu-item-group.tsx +0 -37
  25. package/src/header/menu/header-menu-item.tsx +0 -132
  26. package/src/header/menu/index.tsx +0 -205
  27. package/src/header/mobile-back-button-href-setter.tsx +0 -22
  28. package/src/header/mobile-header.tsx +0 -77
  29. package/src/header/post-title-setter.tsx +0 -22
  30. package/src/header/shared-components.tsx +0 -325
  31. package/src/hooks/index.ts +0 -3
  32. package/src/hooks/use-is-at-top.ts +0 -23
  33. package/src/hooks/use-media-query.ts +0 -57
  34. package/src/hooks/use-scroll-level.ts +0 -52
  35. package/src/icons/index.tsx +0 -378
  36. package/src/index.ts +0 -11
  37. package/src/styles.css +0 -354
  38. package/src/types/index.ts +0 -10
  39. package/src/utils/cn.ts +0 -41
  40. package/src/utils/generate-social-media-config.ts +0 -75
  41. package/src/utils/index.ts +0 -2
  42. package/tsconfig.json +0 -33
@@ -1,325 +0,0 @@
1
- 'use client'
2
- import { cva } from 'class-variance-authority'
3
- import { useEffect, useRef, useState } from 'react'
4
-
5
- import { Button, Input } from '../components'
6
- import {
7
- ClearIcon,
8
- HamburgerIcon,
9
- HamburgerIconSmall,
10
- SearchIcon,
11
- SettingsIcon,
12
- } from '../icons'
13
- import type { MenuItem } from '../types'
14
- import { cn } from '../utils/cn'
15
-
16
- const searchFormVariants = cva(
17
- 'ease-in-out h-full transition-all duration-300',
18
- {
19
- variants: {
20
- mode: {
21
- inline: 'h-11 w-full',
22
- popover: 'top-0 -right-4 w-66 absolute overflow-hidden opacity-0',
23
- },
24
- isSearchOpen: {
25
- true: '',
26
- false: '',
27
- },
28
- },
29
- compoundVariants: [
30
- {
31
- mode: 'popover',
32
- isSearchOpen: true,
33
- class: 'w-66 pointer-events-auto opacity-100',
34
- },
35
- {
36
- mode: 'popover',
37
- isSearchOpen: false,
38
- class: 'pointer-events-none',
39
- },
40
- ],
41
- }
42
- )
43
-
44
- const searchDropdownVariants = cva(
45
- 'rounded-xl mt-2 w-66 ease-in-out h-0 p-0 z-50 bg-neutral-white opacity-0 transition-all duration-200',
46
- {
47
- variants: {
48
- mode: {
49
- inline: '',
50
- popover: 'top-12 -right-4 shadow-custom p-4 absolute',
51
- },
52
- isSearchOpen: {
53
- true: '',
54
- false: '',
55
- },
56
- isFocused: {
57
- true: '',
58
- false: '',
59
- },
60
- },
61
- compoundVariants: [
62
- {
63
- mode: 'popover',
64
- isSearchOpen: true,
65
- isFocused: true,
66
- class: 'p-4 h-min opacity-100',
67
- },
68
- {
69
- mode: 'popover',
70
- isSearchOpen: false,
71
- class: 'pointer-events-none',
72
- },
73
- {
74
- mode: 'inline',
75
- isFocused: true,
76
- class:
77
- 'translate-y-0 pt-6 mt-0 bg-neutral-transparent h-min w-full opacity-100',
78
- },
79
- {
80
- mode: 'inline',
81
- isFocused: false,
82
- class: '-translate-y-3 pointer-events-none w-full',
83
- },
84
- ],
85
- }
86
- )
87
-
88
- export function LogoLink({ compactMode = false }: { compactMode?: boolean }) {
89
- return (
90
- <a href="/" className="flex items-center" rel="home">
91
- <img
92
- src="/assets/images/brand-icon.svg"
93
- alt="少年報導者 The Reporter for Kids"
94
- loading="eager"
95
- className={cn(
96
- 'h-5 tablet:h-6 desktop:h-8 ease-in-out w-auto transition-all duration-500',
97
- compactMode && 'desktop:h-[26px]'
98
- )}
99
- width={293}
100
- height={32}
101
- />
102
- </a>
103
- )
104
- }
105
-
106
- type SearchInputSectionProps =
107
- | {
108
- mode: 'popover'
109
- isSearchOpen: boolean
110
- tags: string[]
111
- searchPlaceholder: string
112
- }
113
- | {
114
- mode: 'inline'
115
- tags: string[]
116
- searchPlaceholder: string
117
- }
118
-
119
- export function SearchInputSection(props: SearchInputSectionProps) {
120
- const ref = useRef<HTMLInputElement>(null)
121
- const [isFocused, setIsFocused] = useState(false)
122
- const [searchValue, setSearchValue] = useState('')
123
-
124
- const mode = props.mode
125
- const isSearchOpen = mode === 'popover' && props.isSearchOpen
126
- const tags = props.tags
127
- const searchPlaceholder = props.searchPlaceholder
128
-
129
- useEffect(() => {
130
- if (mode === 'inline') {
131
- return
132
- }
133
- if (isSearchOpen) {
134
- ref.current?.focus()
135
- setIsFocused(true)
136
- document.body.classList.add('no-scroll')
137
- return
138
- }
139
- setIsFocused(false)
140
- document.body.classList.remove('no-scroll')
141
- }, [mode, isSearchOpen])
142
-
143
- return (
144
- <div
145
- onFocus={() => setIsFocused(true)}
146
- onBlur={() => setIsFocused(false)}
147
- className={mode === 'inline' ? 'relative w-full' : 'h-11'}
148
- >
149
- <form
150
- role="search"
151
- method="get"
152
- action="/search"
153
- className={searchFormVariants({
154
- mode,
155
- isSearchOpen: mode === 'popover' ? isSearchOpen : undefined,
156
- })}
157
- >
158
- <Input
159
- placeholder={searchPlaceholder}
160
- name="q"
161
- title="Search for..."
162
- aria-label="Search for..."
163
- className="w-[99%]"
164
- inputRef={ref}
165
- onChange={setSearchValue}
166
- value={searchValue}
167
- />
168
- </form>
169
- <div
170
- className={searchDropdownVariants({
171
- mode,
172
- isSearchOpen: mode === 'popover' ? isSearchOpen : undefined,
173
- isFocused,
174
- })}
175
- >
176
- <h3 className="font-bold mb-3 prose-p3 text-neutral-700">熱門搜尋</h3>
177
- <div className="gap-2.5 flex flex-wrap">
178
- {tags.map((keyword) => (
179
- <a
180
- key={keyword}
181
- className="px-3 py-1 font-bold cursor-pointer rounded-full bg-neutral-200 prose-p2 text-neutral-900 transition-colors duration-200 hover:bg-red-500 hover:text-neutral-white"
182
- href={`/search?q=${encodeURIComponent(keyword)}`}
183
- >
184
- #{keyword}
185
- </a>
186
- ))}
187
- </div>
188
- </div>
189
- </div>
190
- )
191
- }
192
-
193
- export function ActionButtons({
194
- hideCtaButtons = false,
195
- tags,
196
- searchPlaceholder,
197
- subscribeUrl,
198
- }: {
199
- hideCtaButtons?: boolean
200
- tags: string[]
201
- searchPlaceholder: string
202
- subscribeUrl: string
203
- }) {
204
- const [isSearchOpen, setIsSearchOpen] = useState(false)
205
-
206
- const containerRef = useRef<HTMLDivElement>(null)
207
- const buttonRef = useRef<HTMLButtonElement>(null)
208
-
209
- useEffect(() => {
210
- const handleClickOutside = (event: MouseEvent) => {
211
- const containerElement = containerRef.current
212
- const buttonElement = buttonRef.current
213
- if (!containerElement || !buttonElement) return
214
- if (
215
- !containerElement.contains(event.target as Node) &&
216
- !buttonElement.contains(event.target as Node)
217
- ) {
218
- setIsSearchOpen(false)
219
- }
220
- }
221
-
222
- document.addEventListener('mousedown', handleClickOutside)
223
- return () => {
224
- document.removeEventListener('mousedown', handleClickOutside)
225
- }
226
- }, [])
227
-
228
- return (
229
- <div className="relative flex items-center">
230
- <div className="mr-6 relative flex items-center" ref={containerRef}>
231
- {/* CTA Buttons - Base layer */}
232
- {!hideCtaButtons && !isSearchOpen && (
233
- <div className="gap-4 flex items-center">
234
- <Button variant="secondary" size={32} asChild>
235
- <a href="/about#post">投稿</a>
236
- </Button>
237
- <Button variant="primary" size={32} asChild>
238
- <a href={subscribeUrl} target="_blank" rel="noopener noreferrer">
239
- 訂閱
240
- </a>
241
- </Button>
242
- </div>
243
- )}
244
-
245
- <SearchInputSection
246
- isSearchOpen={isSearchOpen}
247
- mode="popover"
248
- tags={tags}
249
- searchPlaceholder={searchPlaceholder}
250
- />
251
- </div>
252
-
253
- <button
254
- className="w-8 h-8 mr-4 flex cursor-pointer items-center justify-center rounded-full text-neutral-600 transition-all duration-200 hover:text-neutral-800"
255
- aria-label="搜尋"
256
- onClick={() => setIsSearchOpen(!isSearchOpen)}
257
- ref={buttonRef}
258
- >
259
- {isSearchOpen ? <ClearIcon /> : <SearchIcon />}
260
- </button>
261
- <button
262
- className="w-8 h-8 flex cursor-pointer items-center justify-center rounded-full text-neutral-600 transition-all duration-200 hover:text-neutral-800"
263
- aria-label="設定"
264
- >
265
- <SettingsIcon />
266
- </button>
267
- </div>
268
- )
269
- }
270
-
271
- export function BottomNavigation({
272
- onHamburgerOverlayOpen,
273
- menuItems,
274
- }: {
275
- onHamburgerOverlayOpen: () => void
276
- menuItems: MenuItem[]
277
- }) {
278
- return (
279
- <div className="py-2 px-4 flex w-full items-center justify-between border-y border-neutral-border">
280
- <HamburgerButton onHamburgerOverlayOpen={onHamburgerOverlayOpen} small />
281
-
282
- {menuItems.reduce((acc, item, index) => {
283
- return [
284
- ...acc,
285
- <div key={item.label} className="flex items-center">
286
- <a
287
- href={item.href}
288
- className="py-1 font-bold! h-6 flex items-center prose-p1 text-neutral-900 transition-colors hover:text-red-400"
289
- >
290
- {item.label}
291
- </a>
292
- </div>,
293
- ...(index < menuItems.length - 1
294
- ? [
295
- <div
296
- key={`separator-${index}`}
297
- className="h-4 mx-2 w-px bg-neutral-border"
298
- />,
299
- ]
300
- : []),
301
- ]
302
- }, [] as React.ReactNode[])}
303
- </div>
304
- )
305
- }
306
-
307
- export function HamburgerButton({
308
- onHamburgerOverlayOpen,
309
- small = false,
310
- }: {
311
- onHamburgerOverlayOpen: () => void
312
- small?: boolean
313
- }) {
314
- return (
315
- <button
316
- className={cn(
317
- 'rounded-sm ease-in-out flex cursor-pointer items-center justify-center transition-all duration-300 hover:[&>svg>path:nth-child(1)]:fill-blue-500 hover:[&>svg>path:nth-child(2)]:fill-red-500 hover:[&>svg>path:nth-child(3)]:fill-yellow-500 hover:[&>svg>rect:nth-child(1)]:fill-blue-500 hover:[&>svg>rect:nth-child(2)]:fill-red-500 hover:[&>svg>rect:nth-child(3)]:fill-yellow-500',
318
- small ? 'w-6 h-6' : 'w-8 h-8'
319
- )}
320
- onClick={onHamburgerOverlayOpen}
321
- >
322
- {small ? <HamburgerIconSmall /> : <HamburgerIcon />}
323
- </button>
324
- )
325
- }
@@ -1,3 +0,0 @@
1
- export { default as useIsAtTop } from './use-is-at-top'
2
- export { default as useMediaQuery } from './use-media-query'
3
- export { ScrollLevel, default as useScrollLevel } from './use-scroll-level'
@@ -1,23 +0,0 @@
1
- 'use client'
2
- import { useEffect, useState } from 'react'
3
-
4
- const useIsAtTop = (threshold = 35) => {
5
- const [isAtTop, setIsAtTop] = useState(true)
6
-
7
- useEffect(() => {
8
- const checkIsAtTop = () => {
9
- setIsAtTop(window.scrollY <= threshold)
10
- }
11
-
12
- checkIsAtTop()
13
- window.addEventListener('scroll', checkIsAtTop, { passive: true })
14
-
15
- return () => {
16
- window.removeEventListener('scroll', checkIsAtTop)
17
- }
18
- }, [threshold])
19
-
20
- return isAtTop
21
- }
22
-
23
- export default useIsAtTop
@@ -1,57 +0,0 @@
1
- 'use client'
2
- import { useEffect, useLayoutEffect, useState } from 'react'
3
-
4
- const useIsomorphicLayoutEffect =
5
- typeof window !== 'undefined' ? useLayoutEffect : useEffect
6
-
7
- type UseMediaQueryOptions = {
8
- defaultValue?: boolean
9
- initializeWithValue?: boolean
10
- }
11
-
12
- const IS_SERVER = typeof window === 'undefined'
13
-
14
- function useMediaQuery(
15
- query: string,
16
- {
17
- defaultValue = false,
18
- initializeWithValue = true,
19
- }: UseMediaQueryOptions = {}
20
- ): boolean {
21
- const getMatches = (query: string): boolean => {
22
- if (IS_SERVER) {
23
- return defaultValue
24
- }
25
- return window.matchMedia(query).matches
26
- }
27
-
28
- const [matches, setMatches] = useState<boolean>(() => {
29
- if (initializeWithValue && !IS_SERVER) {
30
- return getMatches(query)
31
- }
32
- return defaultValue
33
- })
34
-
35
- // Handles the change event of the media query.
36
- function handleChange() {
37
- setMatches(getMatches(query))
38
- }
39
-
40
- useIsomorphicLayoutEffect(() => {
41
- const matchMedia = window.matchMedia(query)
42
-
43
- // Triggered at the first client-side load and if query changes
44
- handleChange()
45
-
46
- // Use modern addEventListener/removeEventListener
47
- matchMedia.addEventListener('change', handleChange)
48
-
49
- return () => {
50
- matchMedia.removeEventListener('change', handleChange)
51
- }
52
- }, [query])
53
-
54
- return matches
55
- }
56
-
57
- export default useMediaQuery
@@ -1,52 +0,0 @@
1
- 'use client'
2
- import throttle from 'lodash/throttle'
3
- import { useEffect, useState } from 'react'
4
-
5
- export enum ScrollLevel {
6
- UP = 'up',
7
- DOWN_MINI = 'down-mini',
8
- DOWN_HIDDEN = 'down-hidden',
9
- }
10
-
11
- enum ScrollDirection {
12
- UP = 'up',
13
- DOWN = 'down',
14
- }
15
-
16
- const useScrollLevel = ({
17
- scrollDownDistance = 10,
18
- throttleThreshold = 200,
19
- } = {}) => {
20
- const [scrollLevel, setScrollLevel] = useState<ScrollLevel>(ScrollLevel.UP)
21
-
22
- useEffect(() => {
23
- let lastScrollY = window.scrollY
24
- const updateScrollLevel = throttle(() => {
25
- if (Math.abs(window.scrollY - lastScrollY) < scrollDownDistance) {
26
- return
27
- }
28
- const direction =
29
- window.scrollY > lastScrollY ? ScrollDirection.DOWN : ScrollDirection.UP
30
- let level = ScrollLevel.UP
31
- if (direction === ScrollDirection.DOWN) {
32
- level =
33
- scrollLevel === ScrollLevel.UP
34
- ? ScrollLevel.DOWN_MINI
35
- : ScrollLevel.DOWN_HIDDEN
36
- }
37
- if (level !== scrollLevel) {
38
- setScrollLevel(level)
39
- }
40
- lastScrollY = window.scrollY > 0 ? window.scrollY : 0
41
- }, throttleThreshold)
42
-
43
- window.addEventListener('scroll', updateScrollLevel, { passive: true })
44
- return () => {
45
- window.removeEventListener('scroll', updateScrollLevel)
46
- }
47
- }, [scrollLevel, scrollDownDistance, throttleThreshold])
48
-
49
- return scrollLevel
50
- }
51
-
52
- export default useScrollLevel