@giancarlosgza/blog-layer 0.0.0-stage → 0.1.0

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 (79) hide show
  1. package/README.md +73 -2
  2. package/app/app.config.ts +42 -0
  3. package/app/assets/scss/abstracts/_tokens.scss +8 -0
  4. package/app/assets/scss/abstracts/_type-scale.scss +6 -0
  5. package/app/assets/scss/abstracts/_variables.scss +1 -0
  6. package/app/assets/scss/components/_icons.scss +15 -0
  7. package/app/assets/scss/components/_symbols.scss +20 -0
  8. package/app/assets/scss/main.scss +9 -0
  9. package/app/assets/scss/pages/_core.scss +84 -0
  10. package/app/assets/scss/pages/core/_article-layout.scss +260 -0
  11. package/app/assets/scss/pages/core/_code-layout.scss +42 -0
  12. package/app/assets/scss/pages/core/_layers.scss +1 -0
  13. package/app/assets/scss/pages/core/_post-article-layout.scss +198 -0
  14. package/app/assets/scss/pages/core/_post-layout.scss +286 -0
  15. package/app/assets/scss/pages/core/_shared-ui.scss +100 -0
  16. package/app/assets/scss/pages/core/_transitions-layout.scss +70 -0
  17. package/app/assets/scss/pages/core/_widgets.scss +17 -0
  18. package/app/blog-pages/archive.vue +136 -0
  19. package/app/blog-pages/author.vue +115 -0
  20. package/app/blog-pages/home.vue +178 -0
  21. package/app/blog-pages/post.vue +168 -0
  22. package/app/blog-pages/series.vue +127 -0
  23. package/app/blog-pages/tag.vue +96 -0
  24. package/app/components/OgImage/BlogPost.takumi.vue +117 -0
  25. package/app/components/blog/BlogCodeBlock.vue +144 -0
  26. package/app/components/blog/BlogPostArticle.vue +545 -0
  27. package/app/components/blog/BlogPostCard.vue +208 -0
  28. package/app/components/blog/BlogPostContent.vue +111 -0
  29. package/app/components/blog/BlogPostList.vue +143 -0
  30. package/app/components/layout/HubHeader.vue +62 -0
  31. package/app/composables/useBlogBrand.ts +13 -0
  32. package/app/composables/useBlogPath.ts +13 -0
  33. package/app/composables/useBlogPost.ts +12 -0
  34. package/app/composables/useBlogSettings.ts +15 -0
  35. package/app/composables/useBlogWidgets.ts +18 -0
  36. package/app/composables/useCachedResource.ts +72 -0
  37. package/app/composables/useDateValue.ts +27 -0
  38. package/app/composables/usePaginatedPosts.ts +57 -0
  39. package/app/composables/usePostFeed.ts +139 -0
  40. package/app/composables/useSeriesNames.ts +20 -0
  41. package/app/composables/useShikiHighlighter.ts +108 -0
  42. package/app/composables/useSiteSchemaLd.ts +44 -0
  43. package/app/composables/useSiteUrl.ts +4 -0
  44. package/app/utils/widget-props.ts +66 -0
  45. package/app/widgets/WidgetError.vue +13 -0
  46. package/blog-module.ts +71 -0
  47. package/nuxt.config.ts +80 -0
  48. package/package.json +44 -3
  49. package/scripts/subset-icons.mjs +167 -0
  50. package/server/api/blog/_sitemap-urls.ts +38 -0
  51. package/server/api/blog/authors/[slug].get.ts +42 -0
  52. package/server/api/blog/featured.get.ts +18 -0
  53. package/server/api/blog/posts/[slug].get.ts +116 -0
  54. package/server/api/blog/posts/index.get.ts +35 -0
  55. package/server/api/blog/series/[slug].get.ts +29 -0
  56. package/server/api/blog/series/index.get.ts +15 -0
  57. package/server/api/blog/settings.get.ts +18 -0
  58. package/server/api/blog/tags/[tag].get.ts +35 -0
  59. package/server/api/blog/tags/index.get.ts +17 -0
  60. package/server/handlers/rss.xml.ts +53 -0
  61. package/server/plugins/firebase.ts +20 -0
  62. package/server/utils/blog-data.ts +218 -0
  63. package/server/utils/blog-firestore.ts +80 -0
  64. package/server/utils/rss.ts +212 -0
  65. package/server/utils/sitemap.ts +80 -0
  66. package/shared/models/author.ts +19 -0
  67. package/shared/models/date.ts +11 -0
  68. package/shared/models/post.ts +114 -0
  69. package/shared/models/series.ts +14 -0
  70. package/shared/models/settings.ts +12 -0
  71. package/shared/models/tag.ts +15 -0
  72. package/shared/types/api.ts +78 -0
  73. package/shared/utils/blog-path.ts +23 -0
  74. package/shared/utils/blog.ts +45 -0
  75. package/shared/utils/date.ts +26 -0
  76. package/shared/utils/post-headings.ts +134 -0
  77. package/shared/utils/post-seo.ts +58 -0
  78. package/shared/utils/sanitize.ts +80 -0
  79. package/shared/widgets/types.ts +41 -0
@@ -0,0 +1,198 @@
1
+ @use '@colorffy/css/scss/abstracts/mixins' as m;
2
+
3
+ @layer pages {
4
+ // Centered reading column, side rails from xl
5
+ .post-article-grid {
6
+ --_post-column: 42rem;
7
+
8
+ display: grid;
9
+ grid-template-columns: minmax(0, 1fr);
10
+
11
+ @include m.media-breakpoint-up(lg) {
12
+ grid-template-columns: minmax(0, 1fr) minmax(0, var(--_post-column)) minmax(0, 1fr);
13
+ column-gap: 2.5rem;
14
+
15
+ & > * {
16
+ grid-column: 2;
17
+ grid-row: 1;
18
+ }
19
+ }
20
+
21
+ @include m.media-breakpoint-up(xxl) {
22
+ --_post-column: 45rem;
23
+
24
+ column-gap: 4rem;
25
+ }
26
+ }
27
+
28
+ // Shrinkable track for wide code and widgets
29
+ .post-article-main {
30
+ grid-template-columns: minmax(0, 1fr);
31
+ }
32
+
33
+ .post-article-share-inline {
34
+ @include m.media-breakpoint-up(xl) {
35
+ display: none !important;
36
+ }
37
+ }
38
+
39
+ .post-article-rail {
40
+ display: none;
41
+
42
+ @include m.media-breakpoint-up(xl) {
43
+ position: sticky;
44
+ top: calc(var(--cffy-navbar-min-height, 4.25rem) + 1.5rem);
45
+ align-self: start;
46
+ display: flex;
47
+ flex-direction: column;
48
+ }
49
+ }
50
+
51
+ .post-article-rail-start {
52
+ @include m.media-breakpoint-up(xl) {
53
+ grid-column: 1;
54
+ justify-self: end;
55
+ align-items: center;
56
+ gap: var(--cffy-space-8);
57
+ }
58
+ }
59
+
60
+ .post-article-rail-divider {
61
+ inline-size: 1px;
62
+ block-size: 1.5rem;
63
+ background: var(--cffy-outline-surface);
64
+ }
65
+
66
+ .post-article-rail-end {
67
+ @include m.media-breakpoint-up(xl) {
68
+ grid-column: 3;
69
+ justify-self: start;
70
+ inline-size: 100%;
71
+ max-inline-size: 14rem;
72
+ gap: var(--cffy-space-24);
73
+ }
74
+ }
75
+
76
+ .post-article-rail-group {
77
+ display: flex;
78
+ flex-direction: column;
79
+ }
80
+
81
+ .post-article-rail-label {
82
+ padding-block-end: var(--cffy-space-8);
83
+ font-family: var(--cffy-font-code);
84
+ font-size: var(--cffy-fs-4xs);
85
+ letter-spacing: .05em;
86
+ text-transform: uppercase;
87
+ color: var(--cffy-muted-a10);
88
+ }
89
+
90
+ .post-article-rail-title {
91
+ padding-block-end: var(--cffy-space-8);
92
+ font-size: var(--cffy-fs-xs);
93
+ font-weight: var(--cffy-fw-600);
94
+ color: var(--cffy-on-background);
95
+
96
+ &:is(:hover, :focus-visible) {
97
+ text-decoration: underline;
98
+ }
99
+ }
100
+
101
+ // Compact links, a dot marks the current section
102
+ .post-article-rail-link {
103
+ display: flex;
104
+ align-items: flex-start;
105
+ gap: var(--cffy-space-8);
106
+ padding-block: var(--cffy-space-6);
107
+ font-size: var(--cffy-fs-xs);
108
+ line-height: 1.4;
109
+ text-align: start;
110
+ color: var(--cffy-muted-a10);
111
+ transition: color 200ms var(--cffy-ease-decelerate);
112
+
113
+ &.has-dot::before {
114
+ content: '';
115
+ flex-shrink: 0;
116
+ inline-size: 6px;
117
+ block-size: 6px;
118
+ margin-block-start: .5em;
119
+ border-radius: calc(var(--cffy-radius-xl) * 2);
120
+ background: transparent;
121
+ transition: background-color 200ms var(--cffy-ease-decelerate);
122
+ }
123
+
124
+ &.is-sub {
125
+ padding-inline-start: var(--cffy-space-12);
126
+ }
127
+
128
+ &:is(:hover, :focus-visible),
129
+ &.is-active {
130
+ color: var(--cffy-on-background);
131
+ }
132
+
133
+ &.is-active {
134
+ font-weight: var(--cffy-fw-500);
135
+
136
+ &::before {
137
+ background: var(--cffy-secondary-a50);
138
+ }
139
+ }
140
+ }
141
+
142
+ span.post-article-rail-link {
143
+ cursor: default;
144
+ }
145
+
146
+ .post-article-rail-number {
147
+ flex-shrink: 0;
148
+ min-inline-size: 1rem;
149
+ font-family: var(--cffy-font-code);
150
+ font-size: var(--cffy-fs-3xs);
151
+ line-height: 1.8;
152
+ }
153
+
154
+ .post-article-mark {
155
+ --_mark-size: 1.5rem;
156
+ }
157
+
158
+ .post-article-lead {
159
+ font-size: var(--cffy-fs-lg);
160
+ line-height: 1.5;
161
+ max-inline-size: 60ch;
162
+ }
163
+
164
+ // Wider than the reading column, centered on it
165
+ .post-article-cover {
166
+ display: block;
167
+ inline-size: min(100%, 60rem);
168
+ margin-inline: auto;
169
+ block-size: auto;
170
+ aspect-ratio: 1200 / 630;
171
+ object-fit: cover;
172
+ border-radius: var(--corner-radius-card-increased);
173
+ }
174
+
175
+ .post-article-toc-mobile {
176
+ @include m.media-breakpoint-up(xl) {
177
+ display: none;
178
+ }
179
+ }
180
+
181
+ // Parts in the xl rail, summary inline
182
+ .post-article-series-parts {
183
+ @include m.media-breakpoint-up(xl) {
184
+ display: none;
185
+ }
186
+ }
187
+
188
+ // Part number in the icon slot
189
+ .post-article-series-number {
190
+ min-inline-size: 1.75rem;
191
+ text-align: center;
192
+ color: var(--_list-icon-color);
193
+ }
194
+
195
+ .post-article-toc-sub {
196
+ padding-inline-start: 1.25rem;
197
+ }
198
+ }
@@ -0,0 +1,286 @@
1
+ @use '../../abstracts/variables' as lv;
2
+ @use '../../abstracts/type-scale' as t;
3
+
4
+ @layer pages {
5
+ // Home hero lead under the display title
6
+ .section-hero-lead {
7
+ max-inline-size: 56ch;
8
+ margin-inline: auto;
9
+ }
10
+
11
+ // Tag filter: wraps on desktop, scrolls on narrow screens
12
+ .post-filter-chips {
13
+ @media (width < 768px) {
14
+ flex-wrap: nowrap;
15
+ max-inline-size: 100%;
16
+ overflow-x: auto;
17
+ scrollbar-width: none;
18
+ padding-block: .25rem;
19
+
20
+ & > * {
21
+ flex-shrink: 0;
22
+ }
23
+ }
24
+ }
25
+
26
+ // Post card
27
+ .card-post {
28
+ --_card-bg-color: transparent;
29
+ --cffy-card-padding-inline: 0;
30
+ --_card-post-radius: var(--corner-radius-card-increased);
31
+ --_card-post-easing-in: #{lv.$easing-spring-elastic};
32
+ --_card-post-easing-duration: 900ms;
33
+
34
+ height: 100%;
35
+ border-radius: 0;
36
+
37
+ &:is(:hover) {
38
+ --_card-post-radius: var(--corner-radius-card);
39
+ }
40
+
41
+ & .card-header {
42
+ padding: 0;
43
+ border-radius: var(--_card-post-radius);
44
+ transition: border-radius var(--_card-post-easing-duration) var(--_card-post-easing-in);
45
+ }
46
+
47
+ & .card-post-cover {
48
+ height: 12rem;
49
+ border-radius: var(--_card-post-radius);
50
+ background: var(--cffy-surface-body-pane);
51
+ overflow: clip;
52
+ transition: border-radius var(--_card-post-easing-duration) var(--_card-post-easing-in);
53
+
54
+ & img {
55
+ display: block;
56
+ width: 100%;
57
+ height: 100%;
58
+ object-fit: cover;
59
+ transition: scale 400ms var(--cffy-ease-overshoot-soft);
60
+ }
61
+ }
62
+
63
+ & .card-body {
64
+ padding-block: var(--cffy-space-20) 0;
65
+ }
66
+
67
+ & .card-post-details {
68
+ display: grid;
69
+ align-content: start;
70
+ row-gap: var(--cffy-space-12);
71
+ }
72
+
73
+ & .card-post-meta {
74
+ display: flex;
75
+ flex-wrap: wrap;
76
+ align-items: center;
77
+ gap: var(--cffy-space-8) var(--cffy-space-12);
78
+
79
+ & .badge {
80
+ margin-inline: 0;
81
+ }
82
+ }
83
+
84
+ & .card-post-title {
85
+ text-wrap: balance;
86
+ }
87
+
88
+ & .card-post-excerpt {
89
+ line-clamp: 2;
90
+ -webkit-line-clamp: 2;
91
+ display: -webkit-box;
92
+ -webkit-box-orient: vertical;
93
+ overflow: hidden;
94
+ text-overflow: ellipsis;
95
+ }
96
+
97
+ & .card-post-tags {
98
+ display: flex;
99
+ flex-wrap: wrap;
100
+ align-items: flex-start;
101
+ gap: var(--cffy-space-8);
102
+ }
103
+
104
+ // Links above the stretched title link
105
+ & :is(.card-post-chip, .card-post-author) {
106
+ position: relative;
107
+ z-index: 2;
108
+ }
109
+
110
+ & .card-post-chip {
111
+ --_badge-border-color: transparent;
112
+
113
+ margin-inline: 0;
114
+ transition: background-color 200ms var(--cffy-ease-decelerate);
115
+ }
116
+
117
+ & .card-post-author:is(:hover, :focus-visible) {
118
+ text-decoration: underline;
119
+ }
120
+
121
+ // Title link stretched over the card
122
+ & .card-post-link {
123
+ position: static;
124
+ color: inherit;
125
+ font: inherit;
126
+ letter-spacing: inherit;
127
+
128
+ &::after {
129
+ content: '';
130
+ position: absolute;
131
+ inset: 0;
132
+ z-index: 1;
133
+ width: auto;
134
+ height: auto;
135
+ background: none;
136
+ border-radius: var(--_card-post-radius);
137
+ scale: none;
138
+ transition: none;
139
+ }
140
+
141
+ &:focus-visible {
142
+ outline: none;
143
+ }
144
+
145
+ & > span {
146
+ text-decoration: underline;
147
+ text-decoration-color: transparent;
148
+ text-decoration-thickness: 2px;
149
+ text-underline-offset: .2em;
150
+ transition: text-decoration-color 200ms var(--cffy-ease-decelerate);
151
+ }
152
+ }
153
+
154
+ &:is(:hover) {
155
+ & .card-post-cover img {
156
+ scale: 1.05;
157
+ }
158
+
159
+ & .card-post-link > span {
160
+ text-decoration-color: currentColor;
161
+ }
162
+ }
163
+
164
+ &:has(.card-post-link:focus-visible) {
165
+ outline: var(--cffy-border-width-md) solid var(--cffy-primary-a10);
166
+ outline-offset: 4px;
167
+ border-radius: var(--_card-post-radius);
168
+ }
169
+
170
+ &:is(.card-post-skeleton) {
171
+ pointer-events: none;
172
+ user-select: none;
173
+
174
+ & .card-post-cover {
175
+ background: rgb(from var(--cffy-muted-base-fixed) r g b / 0.5);
176
+ animation: skeleton-loading 2500ms both alternate ease-in-out infinite;
177
+ }
178
+ }
179
+
180
+ // Featured: cover left, details right (stacked below md)
181
+ &:is(.wide-post) {
182
+ width: 100%;
183
+
184
+ & .card-post-cover {
185
+ height: auto;
186
+ aspect-ratio: 1200 / 630;
187
+ }
188
+
189
+ & .card-post-title {
190
+ font-size: t.$fs-hub-title-long;
191
+ line-height: 1.05;
192
+ }
193
+
194
+ @media (min-width: 768px) {
195
+ display: grid;
196
+ grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
197
+ align-items: center;
198
+ gap: var(--cffy-space-32);
199
+
200
+ & > .card-body {
201
+ padding: 0;
202
+ }
203
+
204
+ & .card-post-details {
205
+ row-gap: var(--cffy-space-16);
206
+ }
207
+
208
+ & .card-post-excerpt {
209
+ line-clamp: 3;
210
+ -webkit-line-clamp: 3;
211
+ }
212
+ }
213
+
214
+ @media (min-width: 1200px) {
215
+ gap: var(--cffy-space-48);
216
+ }
217
+ }
218
+ }
219
+
220
+ // Subgrid aligns cover and detail rows across the grid
221
+ .blog-post-grid,
222
+ .blog-post-skeleton-grid {
223
+ grid-auto-rows: auto;
224
+ position: relative;
225
+
226
+ & > article {
227
+ grid-row: span 5;
228
+ display: grid;
229
+ grid-template-rows: subgrid;
230
+ row-gap: 0;
231
+
232
+ & > .card-post {
233
+ grid-row: span 5;
234
+ display: grid;
235
+ grid-template-rows: subgrid;
236
+ row-gap: 0;
237
+
238
+ & > .card-header {
239
+ grid-row: span 1;
240
+ }
241
+
242
+ & > .card-body {
243
+ grid-row: span 4;
244
+ display: grid;
245
+ grid-template-rows: subgrid;
246
+ row-gap: 0;
247
+
248
+ & > .card-post-details {
249
+ grid-row: span 4;
250
+ row-gap: var(--cffy-space-12);
251
+
252
+ & > * {
253
+ grid-row: span 1;
254
+ }
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ // Filter reflow: kept cards slide, removed/added fade
262
+ .post-card-move,
263
+ .post-card-enter-active,
264
+ .post-card-leave-active {
265
+ transition: transform 800ms var(--cffy-ease-overshoot-soft),
266
+ opacity 400ms var(--cffy-ease-overshoot-soft);
267
+ }
268
+
269
+ .post-card-enter-from,
270
+ .post-card-leave-to {
271
+ opacity: 0;
272
+ transform: scale(.95);
273
+ }
274
+
275
+ .post-card-leave-active {
276
+ position: absolute;
277
+ }
278
+
279
+ @media (prefers-reduced-motion: reduce) {
280
+ .post-card-move,
281
+ .post-card-enter-active,
282
+ .post-card-leave-active {
283
+ transition: none;
284
+ }
285
+ }
286
+ }
@@ -0,0 +1,100 @@
1
+ @use '@colorffy/css/scss/abstracts/mixins' as m;
2
+ @use '../../abstracts/type-scale' as t;
3
+
4
+ @layer pages {
5
+ .display-1,
6
+ .display-3 {
7
+ font-family: var(--cffy-font-primary);
8
+ line-height: 1;
9
+ letter-spacing: -3%;
10
+ }
11
+
12
+ .display-1 {
13
+ font-size: t.$fs-landing-h1;
14
+ font-weight: var(--cffy-fw-800);
15
+ }
16
+
17
+ .display-3 {
18
+ font-size: t.$fs-landing-card-md;
19
+ font-weight: var(--cffy-fw-500);
20
+ letter-spacing: -0.64px;
21
+ }
22
+
23
+ .color-mark {
24
+ --_mark-size: 1.75rem;
25
+ --_mark-radius: var(--cffy-radius-md);
26
+ --_mark-border-color: var(--cffy-outline-surface);
27
+
28
+ display: flex;
29
+ flex: none;
30
+ overflow: hidden;
31
+ inline-size: var(--_mark-size);
32
+ block-size: var(--_mark-size);
33
+ border-radius: var(--_mark-radius);
34
+ border: var(--cffy-border-width-sm) solid var(--_mark-border-color);
35
+ background: var(--_mark-bg, transparent);
36
+ }
37
+
38
+ .facet-hub-header {
39
+ display: grid;
40
+ gap: 1.5rem;
41
+ align-items: end;
42
+ padding-block-end: 1.75rem;
43
+
44
+ @include m.media-breakpoint-up(md) {
45
+ grid-template-columns: 1.2fr 1fr;
46
+ gap: 3rem;
47
+ }
48
+
49
+ & .facet-hub-title {
50
+ margin-block-end: 0;
51
+ }
52
+
53
+ & .facet-hub-meta {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 1rem;
57
+ padding-block-end: .25rem;
58
+
59
+ & p {
60
+ max-inline-size: 52ch;
61
+ }
62
+ }
63
+ }
64
+
65
+ .glass-border {
66
+ --_glass-border-width: var(--cffy-border-width-sm);
67
+ --_glass-border-start: light-dark(rgb(0 0 0 / .2), rgb(255 255 255 / .35));
68
+ --_glass-border-end: light-dark(rgb(0 0 0 / .15), rgb(255 255 255 / .35));
69
+ --_glass-border-shift: .075rem;
70
+ --_glass-border-nudge: .0125rem;
71
+ --_glass-border-ring-blur: .3px;
72
+
73
+ position: relative;
74
+ overflow: clip;
75
+
76
+ &::before,
77
+ &::after {
78
+ content: '';
79
+ position: absolute;
80
+ padding: var(--_glass-border-width);
81
+ border-radius: inherit;
82
+ filter: blur(var(--_glass-border-ring-blur));
83
+ pointer-events: none;
84
+ mask-clip: border-box, content-box, border-box;
85
+ mask-composite: intersect, exclude;
86
+ }
87
+
88
+ &::before {
89
+ inset: 0 var(--_glass-border-nudge) calc(var(--_glass-border-shift) * -1) calc(var(--_glass-border-nudge) * -1);
90
+ background: linear-gradient(var(--_glass-border-start), transparent);
91
+ mask-image: linear-gradient(45deg, #000, rgb(0 0 0 / .5) 50%, transparent 80%), linear-gradient(#000 0 0), linear-gradient(#000 0 0);
92
+ }
93
+
94
+ &::after {
95
+ inset: calc(var(--_glass-border-shift) * -1) calc(var(--_glass-border-nudge) * -1) 0 var(--_glass-border-nudge);
96
+ background: linear-gradient(-45deg, var(--_glass-border-end), transparent);
97
+ mask-image: linear-gradient(-45deg, #000, rgb(0 0 0 / .5) 50%, transparent 80%), linear-gradient(#000 0 0), linear-gradient(#000 0 0);
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,70 @@
1
+ @layer pages {
2
+ .vt-post-image {
3
+ view-transition-name: selected-post-image;
4
+ contain: paint;
5
+ }
6
+
7
+ .vt-post-header {
8
+ view-transition-name: selected-post-header;
9
+ contain: paint;
10
+ padding-block-end: .15em;
11
+ margin-block-end: -.15em;
12
+ }
13
+
14
+ ::view-transition-old(root),
15
+ ::view-transition-new(root) {
16
+ animation-duration: 200ms;
17
+ }
18
+
19
+ ::view-transition-group(selected-post-image),
20
+ ::view-transition-group(selected-post-header) {
21
+ animation-duration: 350ms;
22
+ animation-timing-function: var(--cffy-ease-emphasized);
23
+ }
24
+
25
+ ::view-transition-old(selected-post-image),
26
+ ::view-transition-new(selected-post-image) {
27
+ height: 100%;
28
+ object-fit: cover;
29
+ }
30
+
31
+ // Color mode circle reveal
32
+ html.theme-transition {
33
+ &::view-transition-group(root) {
34
+ animation-timing-function: var(--cffy-ease-emphasized);
35
+ }
36
+
37
+ &::view-transition-new(root) {
38
+ mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="0" cy="0" r="18" fill="white" filter="url(%23blur)"/></svg>') top left / 0 no-repeat;
39
+ mask-origin: content-box;
40
+ transform-origin: top left;
41
+ animation: theme-reveal 1s both;
42
+ }
43
+
44
+ // Old theme stays beneath the mask
45
+ &::view-transition-old(root) {
46
+ transform-origin: top left;
47
+ animation: theme-reveal 1s both;
48
+ z-index: -1;
49
+ }
50
+ }
51
+
52
+ @keyframes theme-reveal {
53
+ to {
54
+ mask-size: 350vmax;
55
+ }
56
+ }
57
+
58
+ @media (prefers-reduced-motion: reduce) {
59
+ .vt-post-image,
60
+ .vt-post-header {
61
+ view-transition-name: none;
62
+ }
63
+
64
+ ::view-transition-group(*),
65
+ ::view-transition-old(*),
66
+ ::view-transition-new(*) {
67
+ animation: none !important;
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,17 @@
1
+ @layer pages {
2
+ // Fallback for unknown or unregistered widgets
3
+ .widget-error {
4
+ display: flex;
5
+ align-items: center;
6
+ gap: 0.6rem;
7
+ padding: 1rem 1.25rem;
8
+ border-radius: var(--corner-radius-large);
9
+ background: var(--cffy-surface-body-pane);
10
+ font-size: var(--cffy-fs-sm);
11
+
12
+ & .material-symbols-rounded {
13
+ font-size: 20px;
14
+ opacity: 0.7;
15
+ }
16
+ }
17
+ }