@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,104 +0,0 @@
1
- 'use client'
2
- import {
3
- ADDITIONAL_MENU_ITEMS,
4
- DONATE_URL,
5
- MENU_ITEMS,
6
- SEARCH_PLACEHOLDER,
7
- SOCIAL_MEDIA_ITEMS,
8
- SUBSCRIBE_URL,
9
- } from '../constants/default-values'
10
- import {
11
- ScrollLevel,
12
- useIsAtTop,
13
- useMediaQuery,
14
- useScrollLevel,
15
- } from '../hooks'
16
- import type { MenuItem, SocialMediaHrefs } from '../types'
17
- import { DesktopHeader } from './desktop-header'
18
- import { useHeaderContext } from './header-context'
19
- import Menu from './menu'
20
- import { MobileHeader } from './mobile-header'
21
-
22
- type HeaderProps = {
23
- menuItems?: MenuItem[]
24
- additionalMenuItems?: MenuItem[]
25
- socialMediaHrefs?: SocialMediaHrefs
26
- searchPlaceholder?: string
27
- subscribeUrl?: string
28
- donateUrl?: string
29
- }
30
-
31
- function Header({
32
- menuItems = MENU_ITEMS,
33
- additionalMenuItems = ADDITIONAL_MENU_ITEMS,
34
- socialMediaHrefs = SOCIAL_MEDIA_ITEMS.map((item) => item.href),
35
- searchPlaceholder = SEARCH_PLACEHOLDER,
36
- subscribeUrl = SUBSCRIBE_URL,
37
- donateUrl = DONATE_URL,
38
- }: HeaderProps) {
39
- const context = useHeaderContext()
40
- const postTitle = context?.postTitle
41
- const isLoggedIn = context?.isLoggedIn || false
42
- const loginUrl = context?.loginUrl || '/login'
43
- const isMenuOpen = context?.isMenuOpen || false
44
- const openMenu = context?.openMenu
45
- const closeMenu = context?.closeMenu
46
- const keywords = context?.keywords || []
47
- const onHamburgerOverlayOpen = () => {
48
- openMenu?.()
49
- }
50
- const mobileBackButtonHref = context?.mobileBackButtonHref
51
-
52
- const onCloseMenu = () => {
53
- closeMenu?.()
54
- }
55
-
56
- const isMobile = useMediaQuery('(max-width: 768px)')
57
-
58
- const isAtTop = useIsAtTop()
59
- const scrollingLevel = useScrollLevel({
60
- scrollDownDistance: 150,
61
- throttleThreshold: 50,
62
- })
63
-
64
- const isScrollingDown = scrollingLevel === ScrollLevel.DOWN_HIDDEN
65
-
66
- return (
67
- <>
68
- <DesktopHeader
69
- onHamburgerOverlayOpen={onHamburgerOverlayOpen}
70
- keywords={keywords}
71
- hide={isScrollingDown}
72
- compactMode={!isAtTop}
73
- postTitle={isAtTop ? undefined : postTitle}
74
- searchPlaceholder={searchPlaceholder}
75
- subscribeUrl={subscribeUrl}
76
- menuItems={menuItems}
77
- isLoggedIn={isLoggedIn}
78
- loginUrl={loginUrl}
79
- />
80
- <MobileHeader
81
- onCloseMenu={onCloseMenu}
82
- showCloseButtonWhenMenuOpen={isMobile}
83
- onHamburgerOverlayOpen={onHamburgerOverlayOpen}
84
- isMenuOpen={isMenuOpen}
85
- isLoggedIn={isLoggedIn}
86
- loginUrl={loginUrl}
87
- mobileBackButtonHref={mobileBackButtonHref}
88
- />
89
- <Menu
90
- isOpen={isMenuOpen}
91
- onClose={closeMenu || (() => undefined)}
92
- keywords={keywords}
93
- menuItems={menuItems}
94
- additionalMenuItems={additionalMenuItems}
95
- socialMediaHrefs={socialMediaHrefs}
96
- donateUrl={donateUrl}
97
- subscribeUrl={subscribeUrl}
98
- searchPlaceholder={searchPlaceholder}
99
- />
100
- </>
101
- )
102
- }
103
-
104
- export default Header
@@ -1,27 +0,0 @@
1
- import { useEffect } from 'react'
2
-
3
- import { useHeaderContext } from './header-context'
4
-
5
- type IsLoggedInSetterProps = {
6
- isLoggedIn: boolean
7
- loginUrl: string
8
- }
9
-
10
- function IsLoggedInSetter({ isLoggedIn, loginUrl }: IsLoggedInSetterProps) {
11
- const context = useHeaderContext()
12
- const setIsLoggedIn = context?.setIsLoggedIn
13
- const setLoginUrl = context?.setLoginUrl
14
-
15
- useEffect(() => {
16
- setIsLoggedIn?.(isLoggedIn)
17
- setLoginUrl?.(loginUrl)
18
- return () => {
19
- setIsLoggedIn?.(false)
20
- setLoginUrl?.(undefined)
21
- }
22
- }, [isLoggedIn, setIsLoggedIn, loginUrl, setLoginUrl])
23
-
24
- return null
25
- }
26
-
27
- export default IsLoggedInSetter
@@ -1,37 +0,0 @@
1
- import { useEffect, useState } from 'react'
2
-
3
- import { MenuItem } from '../../types'
4
- import HeaderMenuItem from './header-menu-item'
5
-
6
- function MenuItemGroup({
7
- isMenuOpen,
8
- menuItems,
9
- }: {
10
- isMenuOpen: boolean
11
- menuItems: MenuItem[]
12
- }) {
13
- const [currentExpandedItem, setCurrentExpandedItem] = useState<string | null>(
14
- null
15
- )
16
-
17
- const handleClick = (label: string | null) => {
18
- setCurrentExpandedItem(label)
19
- }
20
-
21
- useEffect(() => {
22
- if (!isMenuOpen) {
23
- setCurrentExpandedItem(null)
24
- }
25
- }, [isMenuOpen])
26
-
27
- return menuItems.map((item) => (
28
- <HeaderMenuItem
29
- key={item.label}
30
- {...item}
31
- isExpanded={currentExpandedItem === item.label}
32
- onExpand={handleClick}
33
- />
34
- ))
35
- }
36
-
37
- export default MenuItemGroup
@@ -1,132 +0,0 @@
1
- import { MenuItem } from '../../types'
2
- import { cn } from '../../utils/cn'
3
- import { useHeaderContext } from '../header-context'
4
-
5
- type HeaderMenuItemProps = {
6
- contentClassName?: string
7
- isExpanded?: boolean
8
- onExpand?: (label: string | null) => void
9
- } & MenuItem
10
-
11
- function HeaderMenuItem({
12
- label,
13
- href,
14
- subItems,
15
- external,
16
- showIcon,
17
- icon,
18
- isExpanded,
19
- contentClassName,
20
- onExpand,
21
- }: HeaderMenuItemProps) {
22
- const hasSubItems = subItems && subItems.length > 0
23
- const context = useHeaderContext()
24
- const closeMenu = context?.closeMenu
25
-
26
- const handleExpand = () => {
27
- if (hasSubItems) {
28
- onExpand?.(isExpanded ? null : label)
29
- }
30
- }
31
-
32
- const content = (
33
- <div
34
- className={cn(
35
- 'group flex w-full items-center justify-between text-neutral-900 transition-colors duration-100 group-hover:text-neutral-900',
36
- contentClassName
37
- )}
38
- >
39
- <div className="gap-2 flex items-center">
40
- {showIcon && icon && (
41
- <div className="w-4 h-4 flex items-center justify-center">{icon}</div>
42
- )}
43
- <span
44
- className={cn(
45
- 'prose-p1-bold',
46
- isExpanded && 'text-red-400 group-hover:text-red-400'
47
- )}
48
- >
49
- {label}
50
- </span>
51
- </div>
52
-
53
- {hasSubItems && (
54
- <div
55
- className={cn(
56
- 'w-6 h-6 flex items-center justify-center transition-transform',
57
- isExpanded && 'rotate-180 text-red-400 group-hover:text-red-400'
58
- )}
59
- >
60
- <svg width="14" height="7" viewBox="0 0 14 7" fill="none">
61
- <path
62
- d="M1 1L7 6L13 1"
63
- stroke="currentColor"
64
- strokeWidth="2.5"
65
- strokeLinecap="round"
66
- strokeLinejoin="round"
67
- />
68
- </svg>
69
- </div>
70
- )}
71
- </div>
72
- )
73
-
74
- if (hasSubItems) {
75
- return (
76
- <div className="w-full">
77
- <button
78
- onClick={handleExpand}
79
- className="px-6 tablet:px-8 py-2 flex w-full cursor-pointer items-center justify-between transition-colors duration-200 hover:bg-neutral-black/5 active:bg-neutral-black/10"
80
- >
81
- {content}
82
- </button>
83
- <div
84
- className={cn(
85
- 'ease-in-out overflow-hidden transition-all duration-300',
86
- isExpanded ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
87
- )}
88
- >
89
- {subItems.map((subItem, index) => (
90
- <a
91
- key={index}
92
- href={subItem.href}
93
- className="px-6 tablet:px-12 py-2 pl-12 font-medium block prose-p2 transition-colors duration-200 hover:bg-neutral-black/5 hover:text-neutral-900 active:bg-neutral-black/10"
94
- onClick={closeMenu}
95
- >
96
- {subItem.label}
97
- </a>
98
- ))}
99
- </div>
100
- </div>
101
- )
102
- }
103
-
104
- if (external) {
105
- return (
106
- <a
107
- href={href}
108
- target="_blank"
109
- rel="noopener noreferrer"
110
- className={
111
- 'px-6 tablet:px-8 py-2 block transition-colors duration-200 hover:bg-neutral-black/5 active:bg-neutral-black/10'
112
- }
113
- >
114
- {content}
115
- </a>
116
- )
117
- }
118
-
119
- return (
120
- <a
121
- href={href}
122
- className={
123
- 'px-6 tablet:px-8 py-2 block transition-colors duration-200 hover:bg-neutral-black/5 active:bg-neutral-black/10'
124
- }
125
- onClick={closeMenu}
126
- >
127
- {content}
128
- </a>
129
- )
130
- }
131
-
132
- export default HeaderMenuItem
@@ -1,205 +0,0 @@
1
- 'use client'
2
-
3
- import { useEffect } from 'react'
4
-
5
- import Button from '../../components/button'
6
- import { ClearIcon } from '../../icons'
7
- import type { MenuItem, SocialMediaHrefs } from '../../types'
8
- import { cn } from '../../utils/cn'
9
- import { generateSocialMediaConfig } from '../../utils/generate-social-media-config'
10
- import { SearchInputSection } from '../shared-components'
11
- import HeaderMenuItem from './header-menu-item'
12
- import HeaderMenuItemGroup from './header-menu-item-group'
13
-
14
- type MenuProps = {
15
- isOpen: boolean
16
- onClose: () => void
17
- keywords: string[]
18
- menuItems: MenuItem[]
19
- additionalMenuItems: MenuItem[]
20
- socialMediaHrefs: SocialMediaHrefs
21
- donateUrl: string
22
- subscribeUrl: string
23
- searchPlaceholder: string
24
- }
25
-
26
- function Divider() {
27
- return (
28
- <div className="px-6 tablet:px-8 py-4 w-full">
29
- <div className="h-px w-full bg-neutral-300"></div>
30
- </div>
31
- )
32
- }
33
-
34
- function Menu({
35
- isOpen,
36
- onClose,
37
- keywords,
38
- menuItems,
39
- additionalMenuItems,
40
- socialMediaHrefs,
41
- donateUrl,
42
- subscribeUrl,
43
- searchPlaceholder,
44
- }: MenuProps) {
45
- const socialMediaConfig = generateSocialMediaConfig(socialMediaHrefs)
46
-
47
- useEffect(() => {
48
- if (isOpen) {
49
- document.body.classList.add('no-scroll')
50
- } else {
51
- document.body.classList.remove('no-scroll')
52
- }
53
-
54
- return () => {
55
- document.body.classList.remove('no-scroll')
56
- }
57
- }, [isOpen])
58
-
59
- useEffect(() => {
60
- const handleEscape = (e: KeyboardEvent) => {
61
- if (e.key === 'Escape') {
62
- onClose()
63
- }
64
- }
65
-
66
- if (isOpen) {
67
- document.addEventListener('keydown', handleEscape)
68
- }
69
-
70
- return () => {
71
- document.removeEventListener('keydown', handleEscape)
72
- }
73
- }, [isOpen, onClose])
74
-
75
- return (
76
- <>
77
- {/* Overlay */}
78
- <div
79
- className={cn(
80
- 'inset-0 fixed z-1001 bg-neutral-500/50 transition-opacity duration-300',
81
- isOpen ? 'opacity-100' : 'pointer-events-none opacity-0'
82
- )}
83
- onClick={onClose}
84
- />
85
-
86
- {/* Menu */}
87
- <div
88
- className={cn(
89
- 'top-0 left-0 tablet:w-80 bg-white shadow-2xl ease-in-out tablet:pt-0 fixed z-1001 h-full scrollbar-thin w-full transform pt-(--mobile-header-height) transition-transform duration-300',
90
- isOpen ? 'translate-x-0' : '-translate-x-full'
91
- )}
92
- >
93
- <div className="flex h-full flex-col overflow-y-auto">
94
- <div className="px-6 tablet:px-8 py-4 mt-4 hidden items-center justify-between tablet:flex">
95
- <div className="flex items-center">
96
- <a href="/">
97
- <img
98
- src="/assets/images/brand-icon.svg"
99
- alt="少年報導者 The Reporter for Kids"
100
- className="h-5"
101
- height={20}
102
- width={183}
103
- loading="eager"
104
- />
105
- </a>
106
- </div>
107
- <button
108
- onClick={onClose}
109
- className="w-8 h-8 flex cursor-pointer items-center justify-center rounded-full text-neutral-600 transition-colors duration-200 hover:text-neutral-800"
110
- aria-label="關閉選單"
111
- >
112
- <ClearIcon />
113
- </button>
114
- </div>
115
-
116
- <div className="px-6 tablet:px-8 pt-4 desktop:hidden">
117
- <SearchInputSection
118
- mode="inline"
119
- tags={keywords}
120
- searchPlaceholder={searchPlaceholder}
121
- />
122
- </div>
123
-
124
- <div className="py-4 flex-1">
125
- <HeaderMenuItem {...menuItems?.[0]} />
126
-
127
- <Divider />
128
-
129
- {/* Categories */}
130
- <div className="py-2">
131
- <HeaderMenuItemGroup isMenuOpen={isOpen} menuItems={menuItems} />
132
- </div>
133
-
134
- <Divider />
135
-
136
- {/* Reading Settings */}
137
- <HeaderMenuItem
138
- {...additionalMenuItems?.[0]}
139
- contentClassName="text-neutral-600 [&_span]:[font-family:var(--font-family-noto)] [&_span]:[font-size:var(--font-size-p2)] [&_span]:[font-weight:500] [&_span]:[line-height:var(--line-height-normal)] [&_span]:[letter-spacing:var(--letter-spacing-wide)] hover:text-neutral-900"
140
- />
141
-
142
- <Divider />
143
-
144
- {/* About Us Section */}
145
- <div className="py-2">
146
- {additionalMenuItems.slice(1).map((item, index) => (
147
- <HeaderMenuItem
148
- key={index}
149
- label={item.label}
150
- href={item.href}
151
- external={item.external}
152
- contentClassName="text-neutral-600 [&_span]:[font-family:var(--font-family-noto)] [&_span]:[font-size:var(--font-size-p2)] [&_span]:[font-weight:500] [&_span]:[line-height:var(--line-height-normal)] [&_span]:[letter-spacing:var(--letter-spacing-wide)] hover:text-neutral-900"
153
- />
154
- ))}
155
- </div>
156
-
157
- <Divider />
158
-
159
- {/* Social Media */}
160
- <div className="px-6 tablet:px-8">
161
- <div className="gap-4 tablet:gap-0 px-4 flex items-center justify-center tablet:justify-between">
162
- {socialMediaConfig.map((item) => (
163
- <a
164
- key={item.label}
165
- href={item.href}
166
- className="text-neutral-900 transition-colors duration-200 hover:text-red-500"
167
- target="_blank"
168
- rel="noopener noreferrer"
169
- aria-label={item.label}
170
- >
171
- <div className="w-6 h-6 flex items-center justify-center">
172
- {item.icon && <item.icon />}
173
- </div>
174
- </a>
175
- ))}
176
- </div>
177
- </div>
178
- </div>
179
-
180
- {/* Action Buttons */}
181
- <div className="px-6 tablet:px-8 py-6 tablet:pt-6 tablet:pb-8">
182
- <div className="gap-4 flex flex-col">
183
- <Button variant="secondary" size={44} asChild className="w-full">
184
- <a
185
- href={subscribeUrl}
186
- target="_blank"
187
- rel="noopener noreferrer"
188
- >
189
- 訂閱電子報
190
- </a>
191
- </Button>
192
- <Button variant="primary" size={44} asChild className="w-full">
193
- <a href={donateUrl} target="_blank" rel="noopener noreferrer">
194
- 贊助我們
195
- </a>
196
- </Button>
197
- </div>
198
- </div>
199
- </div>
200
- </div>
201
- </>
202
- )
203
- }
204
-
205
- export default Menu
@@ -1,22 +0,0 @@
1
- import { useEffect } from 'react'
2
-
3
- import { useHeaderContext } from './header-context'
4
-
5
- type MobileBackButtonHrefSetterProps = {
6
- href?: string
7
- }
8
-
9
- function MobileBackButtonHrefSetter({ href }: MobileBackButtonHrefSetterProps) {
10
- const context = useHeaderContext()
11
- const setMobileBackButtonHref = context?.setMobileBackButtonHref
12
-
13
- useEffect(() => {
14
- setMobileBackButtonHref?.(href)
15
-
16
- return () => setMobileBackButtonHref?.(undefined)
17
- }, [href, setMobileBackButtonHref])
18
-
19
- return null
20
- }
21
-
22
- export default MobileBackButtonHrefSetter
@@ -1,77 +0,0 @@
1
- 'use client'
2
-
3
- import { ArrowIcon, ClearIcon, LoginIcon } from '../icons'
4
- import { cn } from '../utils/cn'
5
- import { HamburgerButton, LogoLink } from './shared-components'
6
-
7
- type MobileHeaderProps = {
8
- onHamburgerOverlayOpen: () => void
9
- onCloseMenu: () => void
10
- showCloseButtonWhenMenuOpen: boolean
11
- isMenuOpen: boolean
12
- isLoggedIn?: boolean
13
- mobileBackButtonHref?: string
14
- loginUrl?: string
15
- }
16
-
17
- export function MobileHeader({
18
- onHamburgerOverlayOpen,
19
- onCloseMenu,
20
- showCloseButtonWhenMenuOpen,
21
- isMenuOpen,
22
- isLoggedIn,
23
- loginUrl,
24
- mobileBackButtonHref,
25
- }: MobileHeaderProps) {
26
- const showCloseButton = showCloseButtonWhenMenuOpen && isMenuOpen
27
-
28
- return (
29
- <>
30
- <div className="h-(--mobile-header-height) desktop:hidden"></div>
31
- <div
32
- className={cn(
33
- 'px-6 tablet:px-8 ease-in-out top-0 translate-y-0 pointer-events-auto fixed z-1002 w-full bg-neutral-white opacity-100 transition-all duration-300 tablet:z-1000 desktop:hidden'
34
- )}
35
- >
36
- <div className="py-4 flex items-center justify-between">
37
- <div className="flex items-center">
38
- {mobileBackButtonHref && (
39
- <a
40
- href={mobileBackButtonHref}
41
- className="size-8 mr-2 flex cursor-pointer items-center justify-center text-neutral-600 tablet:hidden"
42
- >
43
- <ArrowIcon />
44
- </a>
45
- )}
46
- <LogoLink />
47
- </div>
48
-
49
- <div className="gap-4 flex items-center">
50
- {!showCloseButton && (
51
- <a
52
- href={isLoggedIn ? '/member' : (loginUrl ?? '/login')}
53
- className="w-8 h-8 flex items-center justify-center rounded-full text-red-400 transition-colors duration-200 hover:text-red-500"
54
- aria-label="登入"
55
- >
56
- <LoginIcon />
57
- </a>
58
- )}
59
- {showCloseButton ? (
60
- <button
61
- onClick={onCloseMenu}
62
- className="w-8 h-8 flex cursor-pointer items-center justify-center rounded-full text-neutral-600 transition-colors duration-200 hover:text-neutral-800"
63
- aria-label="關閉選單"
64
- >
65
- <ClearIcon />
66
- </button>
67
- ) : (
68
- <HamburgerButton
69
- onHamburgerOverlayOpen={onHamburgerOverlayOpen}
70
- />
71
- )}
72
- </div>
73
- </div>
74
- </div>
75
- </>
76
- )
77
- }
@@ -1,22 +0,0 @@
1
- 'use client'
2
-
3
- import { useEffect } from 'react'
4
-
5
- import { useHeaderContext } from './header-context'
6
-
7
- type PostTitleSetterProps = {
8
- postTitle?: string
9
- }
10
-
11
- function PostTitleSetter({ postTitle }: PostTitleSetterProps) {
12
- const context = useHeaderContext()
13
- const setPostTitle = context?.setPostTitle
14
- useEffect(() => {
15
- setPostTitle?.(postTitle)
16
- return () => setPostTitle?.(undefined)
17
- }, [postTitle, setPostTitle])
18
-
19
- return null
20
- }
21
-
22
- export default PostTitleSetter