create-extension-react 0.0.1

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +135 -0
  3. package/package.json +54 -0
  4. package/scripts/create.js +418 -0
  5. package/scripts/dev.js +26 -0
  6. package/src/background.ts +9 -0
  7. package/src/bookmarks/index.module.css +17 -0
  8. package/src/bookmarks/index.tsx +42 -0
  9. package/src/bookmarks.html +12 -0
  10. package/src/components/BookmarksContent/index.module.css +95 -0
  11. package/src/components/BookmarksContent/index.tsx +100 -0
  12. package/src/components/ConfigInfo/index.module.css +30 -0
  13. package/src/components/ConfigInfo/index.tsx +36 -0
  14. package/src/components/HistoryContent/index.module.css +99 -0
  15. package/src/components/HistoryContent/index.tsx +86 -0
  16. package/src/components/NewTabContent/index.module.css +86 -0
  17. package/src/components/NewTabContent/index.tsx +30 -0
  18. package/src/components/NewTabHeader/index.module.css +76 -0
  19. package/src/components/NewTabHeader/index.tsx +14 -0
  20. package/src/components/PopupContent/index.module.css +46 -0
  21. package/src/components/PopupContent/index.tsx +29 -0
  22. package/src/components/PopupHeader/index.module.css +15 -0
  23. package/src/components/PopupHeader/index.tsx +14 -0
  24. package/src/components/ThemeButton/index.module.css +202 -0
  25. package/src/components/ThemeButton/index.tsx +48 -0
  26. package/src/content.ts +10 -0
  27. package/src/history/index.module.css +17 -0
  28. package/src/history/index.tsx +42 -0
  29. package/src/history.html +12 -0
  30. package/src/hooks/useStorage.ts +50 -0
  31. package/src/hooks/useTabs.ts +37 -0
  32. package/src/hooks/useTheme.ts +27 -0
  33. package/src/manifest.json +26 -0
  34. package/src/newtab/index.module.css +17 -0
  35. package/src/newtab/index.tsx +43 -0
  36. package/src/newtab.html +12 -0
  37. package/src/popup/index.module.css +8 -0
  38. package/src/popup/index.tsx +36 -0
  39. package/src/popup.html +12 -0
  40. package/src/store/README.md +176 -0
  41. package/src/store/chromeStorage.ts +88 -0
  42. package/src/store/configSlice.ts +40 -0
  43. package/src/store/hooks.ts +6 -0
  44. package/src/store/initConfig.ts +78 -0
  45. package/src/store/storageMiddleware.ts +70 -0
  46. package/src/store/store.ts +49 -0
  47. package/src/style.css +107 -0
  48. package/src/utils/initTheme.ts +40 -0
  49. package/src/variables.css +146 -0
  50. package/src/vite-env.d.ts +6 -0
  51. package/tsconfig.json +22 -0
  52. package/tsconfig.node.json +10 -0
  53. package/vite-plugin-fix-extension.ts +81 -0
  54. package/vite.config.ts +50 -0
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { Provider } from 'react-redux';
4
+ import { PersistGate } from 'redux-persist/integration/react';
5
+ import '../style.css';
6
+ import { store, persistor } from '../store/store';
7
+ import { NewTabHeader } from '../components/NewTabHeader';
8
+ import ThemeButton from '../components/ThemeButton';
9
+ import { useTheme } from '../hooks/useTheme';
10
+ import { BookmarksContent } from '../components/BookmarksContent';
11
+ import styles from './index.module.css';
12
+
13
+ const BookmarksApp: React.FC = () => {
14
+ const { isDarkMode, toggleTheme } = useTheme();
15
+
16
+ return (
17
+ <div className={styles.container}>
18
+ <div className={styles.themeButtonWrapper}>
19
+ <ThemeButton isDarkMode={isDarkMode} onChange={toggleTheme} />
20
+ </div>
21
+ <NewTabHeader title="React Extension - Bookmarks" />
22
+ <BookmarksContent />
23
+ </div>
24
+ );
25
+ };
26
+
27
+ const BookmarksPage: React.FC = () => {
28
+ return <BookmarksApp />;
29
+ };
30
+
31
+ const container = document.getElementById('root');
32
+ if (container) {
33
+ const root = createRoot(container);
34
+
35
+ root.render(
36
+ <Provider store={store}>
37
+ <PersistGate loading={null} persistor={persistor}>
38
+ <BookmarksPage />
39
+ </PersistGate>
40
+ </Provider>
41
+ );
42
+ }
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>React Extension - Bookmarks</title>
7
+ </head>
8
+ <body class="bookmarks">
9
+ <div id="root"></div>
10
+ <script type="module" src="./bookmarks/index.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,95 @@
1
+ .content {
2
+ width: 100%;
3
+ max-width: 960px;
4
+ margin: 120px auto 40px;
5
+ padding: 0 24px;
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: 16px;
9
+ }
10
+
11
+ .card {
12
+ background: var(--bg-secondary);
13
+ border-radius: 16px;
14
+ padding: 24px;
15
+ box-shadow: 0 10px 30px var(--shadow-lg);
16
+ backdrop-filter: blur(8px);
17
+ }
18
+
19
+ .title {
20
+ font-size: 20px;
21
+ margin-bottom: 12px;
22
+ }
23
+
24
+ .summary {
25
+ font-size: 14px;
26
+ color: var(--text-secondary);
27
+ margin-bottom: 12px;
28
+ }
29
+
30
+ .list {
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: 10px;
34
+ }
35
+
36
+ .item {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 4px;
40
+ padding: 10px 12px;
41
+ border-radius: 12px;
42
+ background: rgba(255, 255, 255, 0.08);
43
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
44
+ }
45
+
46
+ .item:hover {
47
+ transform: translateY(-1px);
48
+ box-shadow: 0 6px 16px var(--shadow-md);
49
+ }
50
+
51
+ .itemTitle {
52
+ font-size: 15px;
53
+ font-weight: 600;
54
+ color: var(--text-primary);
55
+ word-break: break-word;
56
+ }
57
+
58
+ .itemUrl {
59
+ font-size: 13px;
60
+ color: var(--text-secondary);
61
+ word-break: break-all;
62
+ }
63
+
64
+ .folder {
65
+ font-size: 14px;
66
+ color: var(--text-secondary);
67
+ font-weight: 600;
68
+ }
69
+
70
+ .empty {
71
+ font-size: 14px;
72
+ color: var(--text-secondary);
73
+ }
74
+
75
+ .loading {
76
+ font-size: 14px;
77
+ color: var(--text-secondary);
78
+ }
79
+
80
+ .button {
81
+ margin-top: 8px;
82
+ padding: 8px 12px;
83
+ border-radius: 10px;
84
+ border: none;
85
+ cursor: pointer;
86
+ background: var(--color-primary);
87
+ color: #ffffff;
88
+ font-weight: 600;
89
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
90
+ }
91
+
92
+ .button:hover {
93
+ transform: translateY(-1px);
94
+ box-shadow: 0 6px 16px var(--shadow-md);
95
+ }
@@ -0,0 +1,100 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import styles from './index.module.css';
3
+
4
+ type FlatBookmark = {
5
+ id: string;
6
+ title: string;
7
+ url?: string;
8
+ depth: number;
9
+ isFolder: boolean;
10
+ };
11
+
12
+ export const BookmarksContent: React.FC = () => {
13
+ const [items, setItems] = useState<FlatBookmark[]>([]);
14
+ const [loading, setLoading] = useState(true);
15
+
16
+ useEffect(() => {
17
+ chrome.bookmarks.getTree((tree) => {
18
+ const rootChildren = tree[0]?.children ?? [];
19
+ const flat: FlatBookmark[] = [];
20
+
21
+ const walk = (nodes: chrome.bookmarks.BookmarkTreeNode[], depth: number) => {
22
+ nodes.forEach((node) => {
23
+ const isFolder = !node.url;
24
+ flat.push({
25
+ id: node.id,
26
+ title: node.title || (isFolder ? '未命名文件夹' : '未命名书签'),
27
+ url: node.url,
28
+ depth,
29
+ isFolder,
30
+ });
31
+ if (node.children && node.children.length > 0) {
32
+ walk(node.children, depth + 1);
33
+ }
34
+ });
35
+ };
36
+
37
+ walk(rootChildren, 0);
38
+ setItems(flat);
39
+ setLoading(false);
40
+ });
41
+ }, []);
42
+
43
+ const summary = useMemo(() => {
44
+ if (loading) {
45
+ return '正在读取书签...';
46
+ }
47
+ if (items.length === 0) {
48
+ return '暂无书签数据。';
49
+ }
50
+ return `当前共读取 ${items.length} 条书签与文件夹。`;
51
+ }, [items.length, loading]);
52
+
53
+ const openItem = (url?: string) => {
54
+ if (!url) {
55
+ return;
56
+ }
57
+ chrome.tabs.create({ url });
58
+ };
59
+
60
+ return (
61
+ <main className={styles.content}>
62
+ <section className={styles.card}>
63
+ <h2 className={styles.title}>书签概览</h2>
64
+ <p className={styles.summary}>{summary}</p>
65
+ {loading ? (
66
+ <div className={styles.loading}>加载中...</div>
67
+ ) : (
68
+ <div className={styles.list}>
69
+ {items.map((item) => (
70
+ <div
71
+ key={item.id}
72
+ className={styles.item}
73
+ style={{ marginLeft: item.depth * 12 }}
74
+ >
75
+ {item.isFolder ? (
76
+ <div className={styles.folder}>📁 {item.title}</div>
77
+ ) : (
78
+ <>
79
+ <div className={styles.itemTitle}>{item.title}</div>
80
+ {item.url && (
81
+ <div className={styles.itemUrl}>{item.url}</div>
82
+ )}
83
+ </>
84
+ )}
85
+ {!item.isFolder && item.url && (
86
+ <button
87
+ className={styles.button}
88
+ onClick={() => openItem(item.url)}
89
+ >
90
+ 打开书签
91
+ </button>
92
+ )}
93
+ </div>
94
+ ))}
95
+ </div>
96
+ )}
97
+ </section>
98
+ </main>
99
+ );
100
+ };
@@ -0,0 +1,30 @@
1
+ .container {
2
+ padding: var(--spacing-md);
3
+ background-color: var(--bg-secondary);
4
+ border-radius: var(--radius-md);
5
+ border: 1px solid var(--border-color);
6
+ }
7
+
8
+ .infoItem {
9
+ display: flex;
10
+ justify-content: space-between;
11
+ align-items: center;
12
+ padding: var(--spacing-sm) 0;
13
+ border-bottom: 1px solid var(--border-color-light);
14
+ }
15
+
16
+ .infoItem:last-child {
17
+ border-bottom: none;
18
+ }
19
+
20
+ .label {
21
+ font-weight: 500;
22
+ color: var(--text-secondary);
23
+ font-size: 14px;
24
+ }
25
+
26
+ .value {
27
+ color: var(--text-primary);
28
+ font-size: 14px;
29
+ font-family: 'Courier New', monospace;
30
+ }
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import { useAppSelector } from '../../store/hooks';
3
+ import dayjs from 'dayjs';
4
+ import styles from './index.module.css';
5
+
6
+ /**
7
+ * 配置信息组件 - 展示配置的最后修改时间
8
+ * 这是一个示例组件,展示如何使用 updatedAt 字段
9
+ */
10
+ export const ConfigInfo: React.FC = () => {
11
+ const updatedAt = useAppSelector((state) => state.config.updatedAt);
12
+ const isDarkMode = useAppSelector((state) => state.config.theme);
13
+
14
+ // 格式化日期显示
15
+ const formattedDate = dayjs(updatedAt).format('YYYY-MM-DD HH:mm:ss');
16
+ const relativeTime = dayjs(updatedAt).fromNow();
17
+
18
+ return (
19
+ <div className={styles.container}>
20
+ <div className={styles.infoItem}>
21
+ <span className={styles.label}>当前主题:</span>
22
+ <span className={styles.value}>{isDarkMode ? '暗色' : '亮色'}</span>
23
+ </div>
24
+ <div className={styles.infoItem}>
25
+ <span className={styles.label}>最后修改:</span>
26
+ <span className={styles.value} title={formattedDate}>
27
+ {relativeTime}
28
+ </span>
29
+ </div>
30
+ <div className={styles.infoItem}>
31
+ <span className={styles.label}>修改时间:</span>
32
+ <span className={styles.value}>{formattedDate}</span>
33
+ </div>
34
+ </div>
35
+ );
36
+ };
@@ -0,0 +1,99 @@
1
+ .content {
2
+ width: 100%;
3
+ max-width: 960px;
4
+ margin: 120px auto 40px;
5
+ padding: 0 24px;
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: 16px;
9
+ }
10
+
11
+ .card {
12
+ background: var(--bg-secondary);
13
+ border-radius: 16px;
14
+ padding: 24px;
15
+ box-shadow: 0 10px 30px var(--shadow-lg);
16
+ backdrop-filter: blur(8px);
17
+ }
18
+
19
+ .title {
20
+ font-size: 20px;
21
+ margin-bottom: 12px;
22
+ }
23
+
24
+ .summary {
25
+ font-size: 14px;
26
+ color: var(--text-secondary);
27
+ margin-bottom: 12px;
28
+ }
29
+
30
+ .list {
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: 12px;
34
+ }
35
+
36
+ .item {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 4px;
40
+ padding: 12px 14px;
41
+ border-radius: 12px;
42
+ background: rgba(255, 255, 255, 0.08);
43
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
44
+ }
45
+
46
+ .item:hover {
47
+ transform: translateY(-1px);
48
+ box-shadow: 0 6px 16px var(--shadow-md);
49
+ }
50
+
51
+ .itemTitle {
52
+ font-size: 15px;
53
+ font-weight: 600;
54
+ color: var(--text-primary);
55
+ word-break: break-word;
56
+ }
57
+
58
+ .itemUrl {
59
+ font-size: 13px;
60
+ color: var(--text-secondary);
61
+ word-break: break-all;
62
+ }
63
+
64
+ .itemMeta {
65
+ font-size: 12px;
66
+ color: var(--text-tertiary);
67
+ }
68
+
69
+ .empty {
70
+ font-size: 14px;
71
+ color: var(--text-secondary);
72
+ }
73
+
74
+ .loading {
75
+ font-size: 14px;
76
+ color: var(--text-secondary);
77
+ }
78
+
79
+ .action {
80
+ margin-top: 8px;
81
+ display: flex;
82
+ gap: 8px;
83
+ }
84
+
85
+ .button {
86
+ padding: 8px 12px;
87
+ border-radius: 10px;
88
+ border: none;
89
+ cursor: pointer;
90
+ background: var(--color-primary);
91
+ color: #ffffff;
92
+ font-weight: 600;
93
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
94
+ }
95
+
96
+ .button:hover {
97
+ transform: translateY(-1px);
98
+ box-shadow: 0 6px 16px var(--shadow-md);
99
+ }
@@ -0,0 +1,86 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import dayjs from 'dayjs';
3
+ import styles from './index.module.css';
4
+
5
+ const HISTORY_DAYS = 7;
6
+ const MAX_RESULTS = 20;
7
+
8
+ export const HistoryContent: React.FC = () => {
9
+ const [items, setItems] = useState<chrome.history.HistoryItem[]>([]);
10
+ const [loading, setLoading] = useState(true);
11
+
12
+ useEffect(() => {
13
+ const startTime = Date.now() - HISTORY_DAYS * 24 * 60 * 60 * 1000;
14
+ chrome.history.search(
15
+ {
16
+ text: '',
17
+ startTime,
18
+ maxResults: MAX_RESULTS,
19
+ },
20
+ (results) => {
21
+ const sorted = [...results].sort(
22
+ (a, b) => (b.lastVisitTime ?? 0) - (a.lastVisitTime ?? 0)
23
+ );
24
+ setItems(sorted);
25
+ setLoading(false);
26
+ }
27
+ );
28
+ }, []);
29
+
30
+ const summary = useMemo(() => {
31
+ if (loading) {
32
+ return '正在加载最近的浏览记录...';
33
+ }
34
+ if (items.length === 0) {
35
+ return `最近 ${HISTORY_DAYS} 天没有历史记录。`;
36
+ }
37
+ return `最近 ${HISTORY_DAYS} 天访问记录(最多 ${MAX_RESULTS} 条)。`;
38
+ }, [items.length, loading]);
39
+
40
+ const openItem = (url?: string) => {
41
+ if (!url) {
42
+ return;
43
+ }
44
+ chrome.tabs.create({ url });
45
+ };
46
+
47
+ return (
48
+ <main className={styles.content}>
49
+ <section className={styles.card}>
50
+ <h2 className={styles.title}>浏览历史</h2>
51
+ <p className={styles.summary}>{summary}</p>
52
+ {loading ? (
53
+ <div className={styles.loading}>加载中...</div>
54
+ ) : (
55
+ <div className={styles.list}>
56
+ {items.map((item) => (
57
+ <div key={item.id} className={styles.item}>
58
+ <div className={styles.itemTitle}>
59
+ {item.title || item.url || '未命名页面'}
60
+ </div>
61
+ {item.url && (
62
+ <div className={styles.itemUrl}>{item.url}</div>
63
+ )}
64
+ <div className={styles.itemMeta}>
65
+ {item.lastVisitTime
66
+ ? dayjs(item.lastVisitTime).format('YYYY-MM-DD HH:mm')
67
+ : '未知访问时间'}
68
+ </div>
69
+ {item.url && (
70
+ <div className={styles.action}>
71
+ <button
72
+ className={styles.button}
73
+ onClick={() => openItem(item.url)}
74
+ >
75
+ 打开页面
76
+ </button>
77
+ </div>
78
+ )}
79
+ </div>
80
+ ))}
81
+ </div>
82
+ )}
83
+ </section>
84
+ </main>
85
+ );
86
+ };
@@ -0,0 +1,86 @@
1
+ .content {
2
+ flex: 1;
3
+ display: flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ padding: var(--spacing-2xl) var(--spacing-lg);
7
+ }
8
+
9
+ .card {
10
+ background: var(--bg-primary);
11
+ border-radius: var(--radius-xl);
12
+ padding: var(--spacing-2xl);
13
+ max-width: 600px;
14
+ width: 100%;
15
+ box-shadow: 0 20px 60px var(--shadow-xl);
16
+ border: 1px solid var(--border-color-light);
17
+ transition: background-color 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
18
+ }
19
+
20
+ .card h2 {
21
+ font-size: 28px;
22
+ color: var(--text-primary);
23
+ margin-bottom: var(--spacing-md);
24
+ margin-top: 0;
25
+ transition: color 0.3s ease;
26
+ }
27
+
28
+ .card p {
29
+ font-size: 16px;
30
+ color: var(--text-secondary);
31
+ line-height: 1.6;
32
+ margin-bottom: var(--spacing-lg);
33
+ margin-top: 0;
34
+ transition: color 0.3s ease;
35
+ }
36
+
37
+ .counter {
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: var(--spacing-md);
41
+ align-items: center;
42
+ }
43
+
44
+ .counter p {
45
+ font-size: 18px;
46
+ font-weight: 500;
47
+ color: var(--color-primary);
48
+ margin: 0;
49
+ transition: color 0.3s ease;
50
+ }
51
+
52
+ [data-theme='dark'] .counter p {
53
+ color: var(--color-primary-light);
54
+ }
55
+
56
+ .button {
57
+ background: var(--button-bg-primary);
58
+ color: var(--text-inverse);
59
+ border: none;
60
+ padding: var(--spacing-md) var(--spacing-xl);
61
+ border-radius: var(--radius-md);
62
+ font-size: 16px;
63
+ font-weight: 500;
64
+ cursor: pointer;
65
+ transition: transform 0.2s, box-shadow 0.2s, opacity 0.3s, background 0.3s ease;
66
+ }
67
+
68
+ .button:hover {
69
+ transform: translateY(-2px);
70
+ box-shadow: 0 4px 12px var(--shadow-primary);
71
+ }
72
+
73
+ .button:active {
74
+ transform: translateY(0);
75
+ }
76
+
77
+ .loading {
78
+ text-align: center;
79
+ color: var(--text-inverse);
80
+ font-size: 18px;
81
+ transition: color 0.3s ease;
82
+ }
83
+
84
+ [data-theme='dark'] .loading {
85
+ color: var(--text-primary);
86
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useStorage } from '../../hooks/useStorage';
3
+ import styles from './index.module.css';
4
+
5
+ export const NewTabContent: React.FC = () => {
6
+ const [count, setCount, loading] = useStorage<number>('newtab_count', 0);
7
+
8
+ const handleIncrement = () => {
9
+ setCount(count + 1);
10
+ };
11
+
12
+ if (loading) {
13
+ return <div className={styles.loading}>加载中...</div>;
14
+ }
15
+
16
+ return (
17
+ <main className={styles.content}>
18
+ <div className={styles.card}>
19
+ <h2>欢迎使用新标签页</h2>
20
+ <p>这是一个使用 React + Vite 构建的新标签页</p>
21
+ <div className={styles.counter}>
22
+ <p>访问次数: {count}</p>
23
+ <button onClick={handleIncrement} className={styles.button}>
24
+ 增加计数
25
+ </button>
26
+ </div>
27
+ </div>
28
+ </main>
29
+ );
30
+ };
@@ -0,0 +1,76 @@
1
+ .header {
2
+ background: linear-gradient(
3
+ 135deg,
4
+ rgba(255, 255, 255, 0.5) 0%,
5
+ rgba(255, 255, 255, 0.3) 100%
6
+ );
7
+ backdrop-filter: blur(20px) saturate(180%);
8
+ padding: var(--spacing-xl);
9
+ text-align: center;
10
+ color: var(--text-inverse);
11
+ border-bottom: 2px solid rgba(255, 255, 255, 0.4);
12
+ transition: background 0.3s ease, border-color 0.3s ease;
13
+ box-shadow:
14
+ 0 4px 20px rgba(0, 0, 0, 0.25),
15
+ inset 0 1px 0 rgba(255, 255, 255, 0.2),
16
+ inset 0 -1px 0 rgba(0, 0, 0, 0.1);
17
+ position: relative;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .header::before {
22
+ content: '';
23
+ position: absolute;
24
+ top: 0;
25
+ left: 0;
26
+ right: 0;
27
+ bottom: 0;
28
+ background: linear-gradient(
29
+ to bottom,
30
+ rgba(255, 255, 255, 0.2) 0%,
31
+ transparent 50%,
32
+ rgba(0, 0, 0, 0.05) 100%
33
+ );
34
+ pointer-events: none;
35
+ }
36
+
37
+ [data-theme='dark'] .header {
38
+ background: linear-gradient(
39
+ 135deg,
40
+ rgba(0, 0, 0, 0.6) 0%,
41
+ rgba(0, 0, 0, 0.4) 100%
42
+ );
43
+ border-bottom-color: rgba(255, 255, 255, 0.2);
44
+ box-shadow:
45
+ 0 4px 20px rgba(0, 0, 0, 0.5),
46
+ inset 0 1px 0 rgba(255, 255, 255, 0.1),
47
+ inset 0 -1px 0 rgba(0, 0, 0, 0.2);
48
+ }
49
+
50
+ [data-theme='dark'] .header::before {
51
+ background: linear-gradient(
52
+ to bottom,
53
+ rgba(255, 255, 255, 0.08) 0%,
54
+ transparent 50%,
55
+ rgba(0, 0, 0, 0.1) 100%
56
+ );
57
+ }
58
+
59
+ .header h1 {
60
+ font-size: 32px;
61
+ font-weight: 700;
62
+ margin: 0;
63
+ color: #ffffff;
64
+ text-shadow: var(--text-shadow-header);
65
+ letter-spacing: 1.2px;
66
+ position: relative;
67
+ z-index: 1;
68
+ -webkit-font-smoothing: antialiased;
69
+ -moz-osx-font-smoothing: grayscale;
70
+ }
71
+
72
+ [data-theme='dark'] .header h1 {
73
+ text-shadow: var(--text-shadow-header-dark);
74
+ font-weight: 600;
75
+ color: #ffffff;
76
+ }