@jant/core 0.3.27 → 0.3.28
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.
- package/dist/client/client.css +1 -0
- package/dist/client/client.js +31561 -0
- package/dist/index.js +15209 -15
- package/package.json +21 -15
- package/src/__tests__/helpers/app.ts +19 -3
- package/src/__tests__/helpers/db.ts +44 -0
- package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
- package/src/app.tsx +111 -174
- package/src/client.ts +13 -0
- package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
- package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
- package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
- package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
- package/src/db/schema.ts +24 -4
- package/src/i18n/locales/en.po +810 -385
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +733 -522
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +733 -522
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +7 -11
- package/src/index.ts +1 -1
- package/src/lib/__tests__/icons.test.ts +178 -0
- package/src/lib/__tests__/resolve-config.test.ts +184 -0
- package/src/lib/__tests__/schemas.test.ts +12 -6
- package/src/lib/__tests__/theme.test.ts +62 -0
- package/src/lib/__tests__/timezones.test.ts +1 -1
- package/src/lib/__tests__/url.test.ts +12 -0
- package/src/lib/__tests__/view.test.ts +1 -5
- package/src/lib/avatar-upload.ts +18 -10
- package/src/lib/collection-form-bridge.ts +52 -0
- package/src/lib/collections-reorder.ts +28 -0
- package/src/lib/compose-bridge.ts +251 -0
- package/src/lib/errors.ts +116 -0
- package/src/lib/excerpt.ts +1 -1
- package/src/lib/favicon.ts +3 -5
- package/src/lib/html.ts +22 -0
- package/src/lib/icon-catalog.ts +181 -0
- package/src/lib/icons.ts +202 -0
- package/src/lib/navigation.ts +18 -33
- package/src/lib/pagination.ts +3 -2
- package/src/lib/post-form-bridge.ts +136 -0
- package/src/lib/render.tsx +11 -4
- package/src/lib/resolve-config.ts +157 -0
- package/src/lib/schemas.ts +76 -12
- package/src/lib/settings-bridge.ts +139 -0
- package/src/lib/storage.ts +37 -16
- package/src/lib/theme.ts +5 -7
- package/src/lib/timeline.ts +4 -8
- package/src/lib/toast.ts +134 -0
- package/src/lib/upload.ts +71 -0
- package/src/lib/url.ts +9 -1
- package/src/lib/version.ts +16 -0
- package/src/lib/view.ts +9 -10
- package/src/middleware/__tests__/auth.test.ts +6 -28
- package/src/middleware/__tests__/onboarding.test.ts +1 -1
- package/src/middleware/auth.ts +6 -12
- package/src/middleware/config.ts +51 -0
- package/src/middleware/error-handler.ts +56 -0
- package/src/middleware/onboarding.ts +1 -1
- package/src/preset.css +6 -0
- package/src/routes/__tests__/compose.test.ts +104 -17
- package/src/routes/api/__tests__/collections.test.ts +93 -2
- package/src/routes/api/__tests__/posts.test.ts +2 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/collections.ts +64 -68
- package/src/routes/api/nav-items.ts +21 -59
- package/src/routes/api/pages.ts +18 -46
- package/src/routes/api/posts.ts +64 -86
- package/src/routes/api/search.ts +6 -4
- package/src/routes/api/settings.ts +8 -24
- package/src/routes/api/upload.ts +55 -53
- package/src/routes/auth/__tests__/setup.test.ts +118 -0
- package/src/routes/auth/reset.tsx +17 -66
- package/src/routes/auth/setup.tsx +67 -11
- package/src/routes/auth/signin.tsx +44 -8
- package/src/routes/compose.tsx +194 -0
- package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
- package/src/routes/dash/__tests__/pages.test.ts +2 -2
- package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
- package/src/routes/dash/appearance.tsx +173 -0
- package/src/routes/dash/collections.tsx +80 -14
- package/src/routes/dash/index.tsx +12 -14
- package/src/routes/dash/media.tsx +46 -49
- package/src/routes/dash/pages.tsx +85 -37
- package/src/routes/dash/posts.tsx +60 -23
- package/src/routes/dash/redirects.tsx +43 -33
- package/src/routes/dash/settings.tsx +234 -214
- package/src/routes/feed/__tests__/rss.test.ts +7 -3
- package/src/routes/feed/rss.ts +11 -16
- package/src/routes/feed/sitemap.ts +15 -9
- package/src/routes/pages/__tests__/collections.test.ts +9 -8
- package/src/routes/pages/archive.tsx +2 -2
- package/src/routes/pages/collection.tsx +76 -9
- package/src/routes/pages/collections.tsx +3 -1
- package/src/routes/pages/featured.tsx +2 -2
- package/src/routes/pages/home.tsx +3 -3
- package/src/routes/pages/latest.tsx +2 -2
- package/src/routes/pages/page.tsx +2 -2
- package/src/routes/pages/post.tsx +2 -2
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +324 -34
- package/src/services/__tests__/media.test.ts +1 -1
- package/src/services/__tests__/page.test.ts +116 -1
- package/src/services/auth.ts +88 -0
- package/src/services/collection.ts +169 -30
- package/src/services/index.ts +8 -3
- package/src/services/media.ts +39 -12
- package/src/services/navigation.ts +17 -5
- package/src/services/page.ts +24 -4
- package/src/services/post.ts +87 -19
- package/src/services/search.ts +0 -1
- package/src/services/settings.ts +21 -13
- package/src/style.css +3 -0
- package/src/styles/components.css +42 -1
- package/src/styles/tokens.css +4 -0
- package/src/styles/ui.css +902 -73
- package/src/types/app-context.ts +25 -0
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +60 -23
- package/src/types/entities.ts +12 -2
- package/src/types/lingui-react-macro.d.ts +3 -3
- package/src/types/operations.ts +2 -4
- package/src/types/views.ts +1 -3
- package/src/ui/__tests__/font-themes.test.ts +27 -8
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
- package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
- package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
- package/src/ui/components/collection-types.ts +45 -0
- package/src/ui/components/compose-types.ts +75 -0
- package/src/ui/components/jant-collection-form.ts +512 -0
- package/src/ui/components/jant-compose-dialog.ts +494 -0
- package/src/ui/components/jant-compose-editor.ts +799 -0
- package/src/ui/components/jant-post-form.ts +290 -0
- package/src/ui/components/jant-settings-avatar.ts +231 -0
- package/src/ui/components/jant-settings-general.ts +436 -0
- package/src/ui/components/post-form-template.ts +260 -0
- package/src/ui/components/post-form-types.ts +87 -0
- package/src/ui/components/settings-types.ts +62 -0
- package/src/ui/compose/ComposeDialog.tsx +141 -385
- package/src/ui/compose/ComposePrompt.tsx +3 -3
- package/src/ui/dash/PostList.tsx +55 -61
- package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
- package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
- package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
- package/src/ui/dash/collections/CollectionForm.tsx +130 -117
- package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
- package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
- package/src/ui/dash/index.ts +1 -1
- package/src/ui/dash/posts/PostForm.tsx +248 -0
- package/src/ui/dash/settings/AccountContent.tsx +69 -80
- package/src/ui/dash/settings/GeneralContent.tsx +159 -478
- package/src/ui/dash/settings/SettingsNav.tsx +4 -4
- package/src/ui/font-themes.ts +115 -32
- package/src/ui/layouts/BaseLayout.tsx +49 -19
- package/src/ui/layouts/DashLayout.tsx +14 -9
- package/src/ui/layouts/SiteLayout.tsx +38 -23
- package/src/ui/pages/CollectionPage.tsx +12 -2
- package/src/ui/pages/CollectionsPage.tsx +27 -27
- package/src/ui/pages/HomePage.tsx +15 -6
- package/src/ui/pages/SearchPage.tsx +1 -2
- package/src/ui/shared/CollectionsSidebar.tsx +59 -0
- package/src/ui/shared/Pagination.tsx +2 -2
- package/dist/app.js +0 -267
- package/dist/auth.js +0 -39
- package/dist/client.js +0 -13
- package/dist/db/index.js +0 -10
- package/dist/db/schema.js +0 -224
- package/dist/i18n/Trans.js +0 -24
- package/dist/i18n/context.js +0 -58
- package/dist/i18n/detect.js +0 -26
- package/dist/i18n/i18n.js +0 -49
- package/dist/i18n/index.js +0 -44
- package/dist/i18n/locales/en.js +0 -1
- package/dist/i18n/locales/zh-Hans.js +0 -1
- package/dist/i18n/locales/zh-Hant.js +0 -1
- package/dist/i18n/locales.js +0 -13
- package/dist/i18n/middleware.js +0 -30
- package/dist/lib/avatar-upload.js +0 -134
- package/dist/lib/config.js +0 -143
- package/dist/lib/constants.js +0 -50
- package/dist/lib/excerpt.js +0 -76
- package/dist/lib/favicon.js +0 -102
- package/dist/lib/feed.js +0 -123
- package/dist/lib/image-processor.js +0 -187
- package/dist/lib/image.js +0 -97
- package/dist/lib/index.js +0 -7
- package/dist/lib/markdown.js +0 -83
- package/dist/lib/media-helpers.js +0 -49
- package/dist/lib/media-upload.js +0 -104
- package/dist/lib/nav-reorder.js +0 -27
- package/dist/lib/navigation.js +0 -79
- package/dist/lib/pagination.js +0 -44
- package/dist/lib/render.js +0 -53
- package/dist/lib/schemas.js +0 -174
- package/dist/lib/sqid.js +0 -72
- package/dist/lib/sse.js +0 -218
- package/dist/lib/storage.js +0 -164
- package/dist/lib/theme.js +0 -65
- package/dist/lib/time.js +0 -159
- package/dist/lib/timeline.js +0 -95
- package/dist/lib/timezones.js +0 -388
- package/dist/lib/url.js +0 -89
- package/dist/lib/view.js +0 -217
- package/dist/middleware/auth.js +0 -52
- package/dist/middleware/onboarding.js +0 -41
- package/dist/routes/api/collections.js +0 -124
- package/dist/routes/api/nav-items.js +0 -104
- package/dist/routes/api/pages.js +0 -91
- package/dist/routes/api/posts.js +0 -218
- package/dist/routes/api/search.js +0 -48
- package/dist/routes/api/settings.js +0 -68
- package/dist/routes/api/upload.js +0 -246
- package/dist/routes/auth/reset.js +0 -221
- package/dist/routes/auth/setup.js +0 -194
- package/dist/routes/auth/signin.js +0 -176
- package/dist/routes/compose.js +0 -48
- package/dist/routes/dash/collections.js +0 -115
- package/dist/routes/dash/index.js +0 -118
- package/dist/routes/dash/media.js +0 -106
- package/dist/routes/dash/pages.js +0 -294
- package/dist/routes/dash/posts.js +0 -244
- package/dist/routes/dash/redirects.js +0 -257
- package/dist/routes/dash/settings.js +0 -379
- package/dist/routes/feed/rss.js +0 -62
- package/dist/routes/feed/sitemap.js +0 -49
- package/dist/routes/pages/archive.js +0 -62
- package/dist/routes/pages/collection.js +0 -34
- package/dist/routes/pages/collections.js +0 -28
- package/dist/routes/pages/featured.js +0 -36
- package/dist/routes/pages/home.js +0 -64
- package/dist/routes/pages/latest.js +0 -45
- package/dist/routes/pages/page.js +0 -68
- package/dist/routes/pages/post.js +0 -44
- package/dist/routes/pages/search.js +0 -54
- package/dist/services/collection.js +0 -109
- package/dist/services/index.js +0 -24
- package/dist/services/media.js +0 -117
- package/dist/services/navigation.js +0 -91
- package/dist/services/page.js +0 -84
- package/dist/services/post.js +0 -229
- package/dist/services/redirect.js +0 -48
- package/dist/services/search.js +0 -67
- package/dist/services/settings.js +0 -68
- package/dist/types/bindings.js +0 -3
- package/dist/types/config.js +0 -147
- package/dist/types/constants.js +0 -27
- package/dist/types/entities.js +0 -3
- package/dist/types/lingui-react-macro.d.js +0 -9
- package/dist/types/operations.js +0 -3
- package/dist/types/props.js +0 -3
- package/dist/types/sortablejs.d.js +0 -5
- package/dist/types/views.js +0 -5
- package/dist/types.js +0 -11
- package/dist/ui/color-themes.js +0 -268
- package/dist/ui/compose/ComposeDialog.js +0 -467
- package/dist/ui/compose/ComposePrompt.js +0 -55
- package/dist/ui/dash/ActionButtons.js +0 -46
- package/dist/ui/dash/CrudPageHeader.js +0 -22
- package/dist/ui/dash/DangerZone.js +0 -36
- package/dist/ui/dash/FormatBadge.js +0 -27
- package/dist/ui/dash/ListItemRow.js +0 -21
- package/dist/ui/dash/PageForm.js +0 -195
- package/dist/ui/dash/PostForm.js +0 -395
- package/dist/ui/dash/PostList.js +0 -83
- package/dist/ui/dash/StatusBadge.js +0 -46
- package/dist/ui/dash/collections/CollectionForm.js +0 -152
- package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
- package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
- package/dist/ui/dash/index.js +0 -10
- package/dist/ui/dash/media/MediaListContent.js +0 -166
- package/dist/ui/dash/media/ViewMediaContent.js +0 -212
- package/dist/ui/dash/pages/LinkFormContent.js +0 -130
- package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
- package/dist/ui/dash/settings/AccountContent.js +0 -209
- package/dist/ui/dash/settings/AppearanceContent.js +0 -259
- package/dist/ui/dash/settings/GeneralContent.js +0 -536
- package/dist/ui/dash/settings/SettingsNav.js +0 -41
- package/dist/ui/feed/LinkCard.js +0 -72
- package/dist/ui/feed/NoteCard.js +0 -58
- package/dist/ui/feed/QuoteCard.js +0 -63
- package/dist/ui/feed/ThreadPreview.js +0 -48
- package/dist/ui/feed/TimelineFeed.js +0 -41
- package/dist/ui/feed/TimelineItem.js +0 -27
- package/dist/ui/font-themes.js +0 -36
- package/dist/ui/layouts/BaseLayout.js +0 -153
- package/dist/ui/layouts/DashLayout.js +0 -141
- package/dist/ui/layouts/SiteLayout.js +0 -169
- package/dist/ui/pages/ArchivePage.js +0 -143
- package/dist/ui/pages/CollectionPage.js +0 -70
- package/dist/ui/pages/CollectionsPage.js +0 -76
- package/dist/ui/pages/FeaturedPage.js +0 -24
- package/dist/ui/pages/HomePage.js +0 -24
- package/dist/ui/pages/PostPage.js +0 -55
- package/dist/ui/pages/SearchPage.js +0 -122
- package/dist/ui/pages/SinglePage.js +0 -23
- package/dist/ui/shared/EmptyState.js +0 -27
- package/dist/ui/shared/MediaGallery.js +0 -35
- package/dist/ui/shared/Pagination.js +0 -195
- package/dist/ui/shared/ThreadView.js +0 -108
- package/dist/ui/shared/index.js +0 -5
- package/dist/vendor/datastar.js +0 -1606
- package/src/lib/__tests__/config.test.ts +0 -192
- package/src/lib/config.ts +0 -167
- package/src/routes/compose.ts +0 -63
- package/src/ui/dash/PostForm.tsx +0 -360
- package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon Catalog
|
|
3
|
+
*
|
|
4
|
+
* Curated subset of Lucide icons organized by category.
|
|
5
|
+
* Used by the dashboard icon picker to keep the response payload small.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Curated icon names (kebab-case) organized by category */
|
|
9
|
+
export const ICON_CATALOG: Record<string, string[]> = {
|
|
10
|
+
general: [
|
|
11
|
+
"library",
|
|
12
|
+
"bookmark",
|
|
13
|
+
"heart",
|
|
14
|
+
"star",
|
|
15
|
+
"flag",
|
|
16
|
+
"tag",
|
|
17
|
+
"hash",
|
|
18
|
+
"circle",
|
|
19
|
+
"square",
|
|
20
|
+
"triangle",
|
|
21
|
+
"diamond",
|
|
22
|
+
"award",
|
|
23
|
+
"trophy",
|
|
24
|
+
"medal",
|
|
25
|
+
"crown",
|
|
26
|
+
"gem",
|
|
27
|
+
"sparkles",
|
|
28
|
+
"zap",
|
|
29
|
+
"flame",
|
|
30
|
+
],
|
|
31
|
+
files: [
|
|
32
|
+
"file",
|
|
33
|
+
"file-text",
|
|
34
|
+
"folder",
|
|
35
|
+
"folder-open",
|
|
36
|
+
"archive",
|
|
37
|
+
"clipboard",
|
|
38
|
+
"notebook",
|
|
39
|
+
"book",
|
|
40
|
+
"book-open",
|
|
41
|
+
"book-marked",
|
|
42
|
+
"scroll",
|
|
43
|
+
"newspaper",
|
|
44
|
+
"sticky-note",
|
|
45
|
+
],
|
|
46
|
+
media: [
|
|
47
|
+
"image",
|
|
48
|
+
"camera",
|
|
49
|
+
"video",
|
|
50
|
+
"film",
|
|
51
|
+
"music",
|
|
52
|
+
"headphones",
|
|
53
|
+
"mic",
|
|
54
|
+
"radio",
|
|
55
|
+
"tv",
|
|
56
|
+
"monitor",
|
|
57
|
+
"podcast",
|
|
58
|
+
"palette",
|
|
59
|
+
"brush",
|
|
60
|
+
"pen-tool",
|
|
61
|
+
],
|
|
62
|
+
communication: [
|
|
63
|
+
"mail",
|
|
64
|
+
"message-circle",
|
|
65
|
+
"message-square",
|
|
66
|
+
"phone",
|
|
67
|
+
"at-sign",
|
|
68
|
+
"send",
|
|
69
|
+
"inbox",
|
|
70
|
+
"megaphone",
|
|
71
|
+
"bell",
|
|
72
|
+
"rss",
|
|
73
|
+
],
|
|
74
|
+
nature: [
|
|
75
|
+
"sun",
|
|
76
|
+
"moon",
|
|
77
|
+
"cloud",
|
|
78
|
+
"snowflake",
|
|
79
|
+
"droplets",
|
|
80
|
+
"leaf",
|
|
81
|
+
"flower-2",
|
|
82
|
+
"trees",
|
|
83
|
+
"mountain",
|
|
84
|
+
"waves",
|
|
85
|
+
"bird",
|
|
86
|
+
"bug",
|
|
87
|
+
"fish",
|
|
88
|
+
"paw-print",
|
|
89
|
+
],
|
|
90
|
+
tech: [
|
|
91
|
+
"code",
|
|
92
|
+
"terminal",
|
|
93
|
+
"cpu",
|
|
94
|
+
"database",
|
|
95
|
+
"server",
|
|
96
|
+
"hard-drive",
|
|
97
|
+
"wifi",
|
|
98
|
+
"globe",
|
|
99
|
+
"link",
|
|
100
|
+
"qr-code",
|
|
101
|
+
"smartphone",
|
|
102
|
+
"laptop",
|
|
103
|
+
"tablet",
|
|
104
|
+
"gamepad-2",
|
|
105
|
+
"bot",
|
|
106
|
+
],
|
|
107
|
+
travel: [
|
|
108
|
+
"map",
|
|
109
|
+
"map-pin",
|
|
110
|
+
"compass",
|
|
111
|
+
"navigation",
|
|
112
|
+
"plane",
|
|
113
|
+
"car",
|
|
114
|
+
"bike",
|
|
115
|
+
"ship",
|
|
116
|
+
"train-front",
|
|
117
|
+
"building-2",
|
|
118
|
+
"home",
|
|
119
|
+
"tent",
|
|
120
|
+
"landmark",
|
|
121
|
+
],
|
|
122
|
+
food: [
|
|
123
|
+
"coffee",
|
|
124
|
+
"wine",
|
|
125
|
+
"beer",
|
|
126
|
+
"utensils",
|
|
127
|
+
"pizza",
|
|
128
|
+
"cake",
|
|
129
|
+
"apple",
|
|
130
|
+
"cherry",
|
|
131
|
+
"grape",
|
|
132
|
+
"cookie",
|
|
133
|
+
],
|
|
134
|
+
people: [
|
|
135
|
+
"user",
|
|
136
|
+
"users",
|
|
137
|
+
"baby",
|
|
138
|
+
"smile",
|
|
139
|
+
"laugh",
|
|
140
|
+
"angry",
|
|
141
|
+
"hand-heart",
|
|
142
|
+
"brain",
|
|
143
|
+
"dumbbell",
|
|
144
|
+
"stethoscope",
|
|
145
|
+
"graduation-cap",
|
|
146
|
+
"briefcase",
|
|
147
|
+
],
|
|
148
|
+
objects: [
|
|
149
|
+
"key",
|
|
150
|
+
"lock",
|
|
151
|
+
"shield",
|
|
152
|
+
"clock",
|
|
153
|
+
"calendar",
|
|
154
|
+
"gift",
|
|
155
|
+
"shopping-bag",
|
|
156
|
+
"shopping-cart",
|
|
157
|
+
"wallet",
|
|
158
|
+
"scissors",
|
|
159
|
+
"wrench",
|
|
160
|
+
"hammer",
|
|
161
|
+
"lightbulb",
|
|
162
|
+
"rocket",
|
|
163
|
+
"umbrella",
|
|
164
|
+
"glasses",
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get all curated icon names as a flat array.
|
|
170
|
+
*
|
|
171
|
+
* @returns Array of kebab-case icon names
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```typescript
|
|
175
|
+
* const names = getAllCatalogIconNames();
|
|
176
|
+
* // ["library", "bookmark", "heart", ...]
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export function getAllCatalogIconNames(): string[] {
|
|
180
|
+
return Object.values(ICON_CATALOG).flat();
|
|
181
|
+
}
|
package/src/lib/icons.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection Icon Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles structured icon data (Lucide icons with color) stored as JSON in the DB.
|
|
5
|
+
* Backward-compatible with legacy emoji/text icon values.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as lucideIcons from "lucide-static";
|
|
9
|
+
|
|
10
|
+
/** Structured icon data stored as JSON in the DB `icon` column */
|
|
11
|
+
export interface CollectionIcon {
|
|
12
|
+
name: string;
|
|
13
|
+
svg: string;
|
|
14
|
+
color: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Curated color presets for the icon picker */
|
|
18
|
+
export const ICON_COLOR_PRESETS = [
|
|
19
|
+
{ name: "gray", value: "#6b7280" },
|
|
20
|
+
{ name: "red", value: "#ef4444" },
|
|
21
|
+
{ name: "orange", value: "#f97316" },
|
|
22
|
+
{ name: "amber", value: "#f59e0b" },
|
|
23
|
+
{ name: "green", value: "#22c55e" },
|
|
24
|
+
{ name: "teal", value: "#14b8a6" },
|
|
25
|
+
{ name: "blue", value: "#3b82f6" },
|
|
26
|
+
{ name: "indigo", value: "#6366f1" },
|
|
27
|
+
{ name: "purple", value: "#a855f7" },
|
|
28
|
+
{ name: "pink", value: "#ec4899" },
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_ICON_NAME = "library";
|
|
32
|
+
export const DEFAULT_ICON_COLOR = "#6b7280";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert a kebab-case icon name to PascalCase for lucide-static lookup.
|
|
36
|
+
*
|
|
37
|
+
* @param name - Kebab-case icon name (e.g. "book-open")
|
|
38
|
+
* @returns PascalCase string (e.g. "BookOpen")
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* toPascalCase("book-open") // "BookOpen"
|
|
43
|
+
* toPascalCase("library") // "Library"
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
function toPascalCase(name: string): string {
|
|
47
|
+
return name
|
|
48
|
+
.split("-")
|
|
49
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
50
|
+
.join("");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get SVG string for a Lucide icon by kebab-case name.
|
|
55
|
+
*
|
|
56
|
+
* @param name - Kebab-case icon name (e.g. "book-open", "library")
|
|
57
|
+
* @returns SVG string or null if icon not found
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const svg = getIconSvg("library");
|
|
62
|
+
* // '<svg class="lucide lucide-library" ...'
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function getIconSvg(name: string): string | null {
|
|
66
|
+
const pascalName = toPascalCase(name);
|
|
67
|
+
const svg = (lucideIcons as Record<string, string>)[pascalName];
|
|
68
|
+
return typeof svg === "string" ? svg : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse a collection icon value from the DB.
|
|
73
|
+
* Returns structured icon data or null for legacy emoji/text values or invalid JSON.
|
|
74
|
+
*
|
|
75
|
+
* @param icon - Raw icon string from the DB (JSON or legacy emoji/text)
|
|
76
|
+
* @returns Parsed CollectionIcon or null
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* parseCollectionIcon('{"name":"library","svg":"<svg...","color":"#6b7280"}')
|
|
81
|
+
* // { name: "library", svg: "<svg...", color: "#6b7280" }
|
|
82
|
+
*
|
|
83
|
+
* parseCollectionIcon("📚") // null (legacy emoji)
|
|
84
|
+
* parseCollectionIcon(null) // null
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function parseCollectionIcon(
|
|
88
|
+
icon: string | null,
|
|
89
|
+
): CollectionIcon | null {
|
|
90
|
+
if (!icon || !icon.startsWith("{")) return null;
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(icon) as Record<string, unknown>;
|
|
93
|
+
if (
|
|
94
|
+
typeof parsed.name === "string" &&
|
|
95
|
+
typeof parsed.svg === "string" &&
|
|
96
|
+
typeof parsed.color === "string"
|
|
97
|
+
) {
|
|
98
|
+
return parsed as unknown as CollectionIcon;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a JSON string for storing a structured icon in the DB.
|
|
108
|
+
*
|
|
109
|
+
* @param name - Kebab-case icon name
|
|
110
|
+
* @param svg - SVG string
|
|
111
|
+
* @param color - Hex color string
|
|
112
|
+
* @returns JSON string for DB storage
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* createIconValue("library", "<svg...", "#6b7280")
|
|
117
|
+
* // '{"name":"library","svg":"<svg...","color":"#6b7280"}'
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function createIconValue(
|
|
121
|
+
name: string,
|
|
122
|
+
svg: string,
|
|
123
|
+
color: string,
|
|
124
|
+
): string {
|
|
125
|
+
return JSON.stringify({ name, svg, color });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Render a collection icon as an HTML string.
|
|
130
|
+
*
|
|
131
|
+
* - Structured icon (JSON) -> colored SVG
|
|
132
|
+
* - Legacy emoji/text -> span with text
|
|
133
|
+
* - null + fallback -> default icon SVG
|
|
134
|
+
* - null without fallback -> empty string
|
|
135
|
+
*
|
|
136
|
+
* @param icon - Raw icon string from the DB
|
|
137
|
+
* @param opts - Rendering options
|
|
138
|
+
* @param opts.size - Icon size in pixels (default: 24)
|
|
139
|
+
* @param opts.fallback - Whether to render default icon when icon is null (default: false)
|
|
140
|
+
* @returns HTML string
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* renderCollectionIcon('{"name":"library","svg":"<svg...","color":"#3b82f6"}', { size: 16 })
|
|
145
|
+
* // '<svg ... style="color: #3b82f6" width="16" height="16">...</svg>'
|
|
146
|
+
*
|
|
147
|
+
* renderCollectionIcon("📚")
|
|
148
|
+
* // '<span>📚</span>'
|
|
149
|
+
*
|
|
150
|
+
* renderCollectionIcon(null, { fallback: true })
|
|
151
|
+
* // '<svg ... (default library icon)>'
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export function renderCollectionIcon(
|
|
155
|
+
icon: string | null,
|
|
156
|
+
opts?: { size?: number; fallback?: boolean },
|
|
157
|
+
): string {
|
|
158
|
+
const size = opts?.size ?? 24;
|
|
159
|
+
|
|
160
|
+
const parsed = parseCollectionIcon(icon);
|
|
161
|
+
if (parsed) {
|
|
162
|
+
return applyIconSize(parsed.svg, size, parsed.color);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Legacy emoji/text value
|
|
166
|
+
if (icon) {
|
|
167
|
+
return `<span>${escapeHtml(icon)}</span>`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Null — optionally show fallback
|
|
171
|
+
if (opts?.fallback) {
|
|
172
|
+
const defaultSvg = getIconSvg(DEFAULT_ICON_NAME);
|
|
173
|
+
if (defaultSvg) {
|
|
174
|
+
return applyIconSize(defaultSvg, size, DEFAULT_ICON_COLOR);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return "";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Apply size and color to an SVG string by replacing width/height attributes
|
|
183
|
+
* and adding a style attribute for color.
|
|
184
|
+
*/
|
|
185
|
+
function applyIconSize(svg: string, size: number, color?: string): string {
|
|
186
|
+
let result = svg
|
|
187
|
+
.replace(/width="24"/, `width="${size}"`)
|
|
188
|
+
.replace(/height="24"/, `height="${size}"`);
|
|
189
|
+
if (color) {
|
|
190
|
+
result = result.replace("<svg", `<svg style="color: ${color}"`);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Minimal HTML escaping for legacy emoji/text values */
|
|
196
|
+
function escapeHtml(str: string): string {
|
|
197
|
+
return str
|
|
198
|
+
.replace(/&/g, "&")
|
|
199
|
+
.replace(/</g, "<")
|
|
200
|
+
.replace(/>/g, ">")
|
|
201
|
+
.replace(/"/g, """);
|
|
202
|
+
}
|
package/src/lib/navigation.ts
CHANGED
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Context } from "hono";
|
|
8
|
-
import { getSiteName, getHomeDefaultView, getSiteFooter } from "./config.js";
|
|
9
8
|
import type { Collection, NavItemView } from "../types.js";
|
|
10
9
|
import { toNavItemViews } from "./view.js";
|
|
11
|
-
import { getMediaUrl, getPublicUrlForProvider } from "./image.js";
|
|
12
10
|
import { render as renderMarkdown } from "./markdown.js";
|
|
13
11
|
|
|
14
12
|
/**
|
|
@@ -49,31 +47,20 @@ export interface NavigationData {
|
|
|
49
47
|
export async function getNavigationData(c: Context): Promise<NavigationData> {
|
|
50
48
|
const items = await c.var.services.navItems.list();
|
|
51
49
|
const currentPath = new URL(c.req.url).pathname;
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
const appConfig = c.var.appConfig;
|
|
51
|
+
|
|
52
|
+
const siteName = appConfig.siteName;
|
|
53
|
+
const homeDefaultView = appConfig.homeDefaultView;
|
|
54
|
+
const siteFooter = appConfig.siteFooter;
|
|
57
55
|
|
|
58
56
|
// Only include description if explicitly set (DB or env), not the default
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
dbDescription || (typeof envDescription === "string" ? envDescription : "");
|
|
57
|
+
const siteDescription = appConfig.siteDescriptionExplicit
|
|
58
|
+
? appConfig.siteDescription
|
|
59
|
+
: "";
|
|
63
60
|
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
const showHeaderAvatar =
|
|
67
|
-
(await c.var.services.settings.get("SHOW_HEADER_AVATAR")) === "true";
|
|
68
|
-
let siteAvatarUrl: string | undefined;
|
|
69
|
-
if (avatarKey) {
|
|
70
|
-
const publicUrl = getPublicUrlForProvider(
|
|
71
|
-
c.env.STORAGE_DRIVER || "r2",
|
|
72
|
-
c.env.R2_PUBLIC_URL,
|
|
73
|
-
c.env.S3_PUBLIC_URL,
|
|
74
|
-
);
|
|
75
|
-
siteAvatarUrl = getMediaUrl(avatarKey, publicUrl);
|
|
76
|
-
}
|
|
61
|
+
// Avatar URL and display flag come from appConfig
|
|
62
|
+
const siteAvatarUrl = appConfig.siteAvatarUrl || undefined;
|
|
63
|
+
const showHeaderAvatar = appConfig.showHeaderAvatar;
|
|
77
64
|
|
|
78
65
|
// Render footer markdown
|
|
79
66
|
const siteFooterHtml = siteFooter ? renderMarkdown(siteFooter) : undefined;
|
|
@@ -83,15 +70,13 @@ export async function getNavigationData(c: Context): Promise<NavigationData> {
|
|
|
83
70
|
// Check auth status for compose button
|
|
84
71
|
let isAuthenticated = false;
|
|
85
72
|
let collections: Collection[] = [];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// Not authenticated
|
|
94
|
-
}
|
|
73
|
+
try {
|
|
74
|
+
const session = await c.var.auth.api.getSession({
|
|
75
|
+
headers: c.req.raw.headers,
|
|
76
|
+
});
|
|
77
|
+
isAuthenticated = !!session?.user;
|
|
78
|
+
} catch {
|
|
79
|
+
// Not authenticated
|
|
95
80
|
}
|
|
96
81
|
|
|
97
82
|
// Only load collections when authenticated (for compose dialog)
|
package/src/lib/pagination.ts
CHANGED
|
@@ -40,10 +40,11 @@ export function getPageNumbers(
|
|
|
40
40
|
// Insert 0 for gaps
|
|
41
41
|
const result: number[] = [];
|
|
42
42
|
for (let i = 0; i < sorted.length; i++) {
|
|
43
|
-
|
|
43
|
+
const current = sorted[i] as number;
|
|
44
|
+
if (i > 0 && current - (sorted[i - 1] as number) > 1) {
|
|
44
45
|
result.push(0); // ellipsis marker
|
|
45
46
|
}
|
|
46
|
-
result.push(
|
|
47
|
+
result.push(current);
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
return result;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post Form Bridge
|
|
3
|
+
*
|
|
4
|
+
* Connects <jant-post-form> to the server by handling:
|
|
5
|
+
* - `jant:post-submit` → POST JSON and redirect on success
|
|
6
|
+
* - `jant:post-load-media` → fetch media picker HTML and manage selections
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PostSubmitDetail } from "../ui/components/post-form-types.js";
|
|
10
|
+
import type { JantPostForm } from "../ui/components/jant-post-form.js";
|
|
11
|
+
import { showToast } from "./toast.js";
|
|
12
|
+
|
|
13
|
+
function findPostForm(
|
|
14
|
+
target: globalThis.EventTarget | null,
|
|
15
|
+
): JantPostForm | null {
|
|
16
|
+
if (target instanceof HTMLElement && target.tagName === "JANT-POST-FORM") {
|
|
17
|
+
return target as JantPostForm;
|
|
18
|
+
}
|
|
19
|
+
if (target instanceof HTMLElement) {
|
|
20
|
+
return target.closest("jant-post-form") as JantPostForm | null;
|
|
21
|
+
}
|
|
22
|
+
return document.querySelector("jant-post-form");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function applyMediaSelection(el: HTMLElement, selected: boolean) {
|
|
26
|
+
el.classList.toggle("ring-2", selected);
|
|
27
|
+
el.classList.toggle("ring-primary", selected);
|
|
28
|
+
el.classList.toggle("border-primary", selected);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function handlePostSubmit(event: Event) {
|
|
32
|
+
const customEvent = event as CustomEvent<PostSubmitDetail>;
|
|
33
|
+
const detail = customEvent.detail;
|
|
34
|
+
if (!detail) return;
|
|
35
|
+
|
|
36
|
+
const formEl = findPostForm(customEvent.target);
|
|
37
|
+
if (!formEl || !detail.endpoint) return;
|
|
38
|
+
|
|
39
|
+
formEl.loading = true;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(detail.endpoint, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: {
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
Accept: "application/json",
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify(detail.data),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
let message = detail.messages.error;
|
|
53
|
+
try {
|
|
54
|
+
const json = await res.json();
|
|
55
|
+
if (typeof json?.error === "string") message = json.error;
|
|
56
|
+
else if (typeof json?.message === "string") message = json.message;
|
|
57
|
+
} catch {
|
|
58
|
+
// Ignore JSON parse failure; keep fallback message.
|
|
59
|
+
}
|
|
60
|
+
throw new Error(message);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const json = await res.json();
|
|
64
|
+
|
|
65
|
+
if (json?.status === "redirect" && typeof json.url === "string") {
|
|
66
|
+
window.location.href = json.url;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
showToast(detail.messages.success);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const message =
|
|
73
|
+
err instanceof Error && err.message ? err.message : detail.messages.error;
|
|
74
|
+
showToast(message, "error");
|
|
75
|
+
} finally {
|
|
76
|
+
formEl.loading = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function handleMediaLoad(event: Event) {
|
|
81
|
+
const customEvent = event as CustomEvent<{
|
|
82
|
+
endpoint: string;
|
|
83
|
+
selectedIds: string[];
|
|
84
|
+
}>;
|
|
85
|
+
const detail = customEvent.detail;
|
|
86
|
+
if (!detail?.endpoint) return;
|
|
87
|
+
|
|
88
|
+
const grid = document.getElementById("post-media-grid");
|
|
89
|
+
const formEl = findPostForm(customEvent.target);
|
|
90
|
+
if (!grid || !formEl) return;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
grid.innerHTML =
|
|
94
|
+
'<p class="text-muted-foreground text-sm col-span-4">Loading...</p>';
|
|
95
|
+
|
|
96
|
+
const res = await fetch(detail.endpoint, {
|
|
97
|
+
headers: { Accept: "text/html" },
|
|
98
|
+
});
|
|
99
|
+
const html = await res.text();
|
|
100
|
+
grid.innerHTML = html;
|
|
101
|
+
} catch {
|
|
102
|
+
grid.innerHTML =
|
|
103
|
+
'<p class="text-red-500 text-sm col-span-4">Failed to load media.</p>';
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const selected = new Set(detail.selectedIds);
|
|
108
|
+
|
|
109
|
+
grid.querySelectorAll<HTMLElement>("[data-media-id]").forEach((el) => {
|
|
110
|
+
const id = el.dataset.mediaId;
|
|
111
|
+
if (!id) return;
|
|
112
|
+
applyMediaSelection(el, selected.has(id));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
grid.onclick = (e: Event) => {
|
|
116
|
+
const target = (e.target as HTMLElement).closest<HTMLElement>(
|
|
117
|
+
"[data-media-id]",
|
|
118
|
+
);
|
|
119
|
+
if (!target) return;
|
|
120
|
+
const id = target.dataset.mediaId;
|
|
121
|
+
if (!id) return;
|
|
122
|
+
|
|
123
|
+
const current = new Set(formEl.mediaIds);
|
|
124
|
+
if (current.has(id)) {
|
|
125
|
+
current.delete(id);
|
|
126
|
+
applyMediaSelection(target, false);
|
|
127
|
+
} else {
|
|
128
|
+
current.add(id);
|
|
129
|
+
applyMediaSelection(target, true);
|
|
130
|
+
}
|
|
131
|
+
formEl.mediaIds = [...current];
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
document.addEventListener("jant:post-submit", handlePostSubmit);
|
|
136
|
+
document.addEventListener("jant:post-load-media", handleMediaLoad);
|
package/src/lib/render.tsx
CHANGED
|
@@ -21,6 +21,8 @@ export interface RenderPublicPageOptions {
|
|
|
21
21
|
navData: NavigationData;
|
|
22
22
|
/** Page content JSX to render inside SiteLayout */
|
|
23
23
|
content: Child;
|
|
24
|
+
/** Optional sidebar content for sidebar layout */
|
|
25
|
+
sidebar?: Child;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/**
|
|
@@ -41,7 +43,7 @@ export interface RenderPublicPageOptions {
|
|
|
41
43
|
* ```
|
|
42
44
|
*/
|
|
43
45
|
export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
44
|
-
const { title, description, navData, content } = options;
|
|
46
|
+
const { title, description, navData, content, sidebar } = options;
|
|
45
47
|
|
|
46
48
|
const layoutProps: SiteLayoutProps = {
|
|
47
49
|
siteName: navData.siteName,
|
|
@@ -54,11 +56,14 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
54
56
|
siteAvatarUrl: navData.siteAvatarUrl,
|
|
55
57
|
showHeaderAvatar: navData.showHeaderAvatar,
|
|
56
58
|
siteFooterHtml: navData.siteFooterHtml,
|
|
59
|
+
sidebar,
|
|
57
60
|
};
|
|
58
61
|
|
|
59
|
-
// Read favicon and noindex from
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
+
// Read favicon, version, and noindex from appConfig
|
|
63
|
+
const appConfig = c.get("appConfig");
|
|
64
|
+
const faviconUrl = appConfig.siteAvatarUrl || undefined;
|
|
65
|
+
const faviconVersion = appConfig.faviconVersion || undefined;
|
|
66
|
+
const noindex = appConfig.noindex;
|
|
62
67
|
|
|
63
68
|
return c.html(
|
|
64
69
|
<BaseLayout
|
|
@@ -66,7 +71,9 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
66
71
|
description={description}
|
|
67
72
|
c={c}
|
|
68
73
|
faviconUrl={faviconUrl}
|
|
74
|
+
faviconVersion={faviconVersion}
|
|
69
75
|
noindex={noindex}
|
|
76
|
+
isAuthenticated={navData.isAuthenticated}
|
|
70
77
|
>
|
|
71
78
|
<SiteLayout {...layoutProps}>{content}</SiteLayout>
|
|
72
79
|
</BaseLayout>,
|