@giancarlosgza/blog-layer 0.0.0-stage → 0.1.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.
- package/README.md +73 -2
- package/app/app.config.ts +42 -0
- package/app/assets/scss/abstracts/_tokens.scss +8 -0
- package/app/assets/scss/abstracts/_type-scale.scss +6 -0
- package/app/assets/scss/abstracts/_variables.scss +1 -0
- package/app/assets/scss/components/_icons.scss +15 -0
- package/app/assets/scss/components/_symbols.scss +20 -0
- package/app/assets/scss/main.scss +9 -0
- package/app/assets/scss/pages/_core.scss +84 -0
- package/app/assets/scss/pages/core/_article-layout.scss +260 -0
- package/app/assets/scss/pages/core/_code-layout.scss +42 -0
- package/app/assets/scss/pages/core/_layers.scss +1 -0
- package/app/assets/scss/pages/core/_post-article-layout.scss +198 -0
- package/app/assets/scss/pages/core/_post-layout.scss +286 -0
- package/app/assets/scss/pages/core/_shared-ui.scss +100 -0
- package/app/assets/scss/pages/core/_transitions-layout.scss +70 -0
- package/app/assets/scss/pages/core/_widgets.scss +17 -0
- package/app/blog-pages/archive.vue +136 -0
- package/app/blog-pages/author.vue +115 -0
- package/app/blog-pages/home.vue +178 -0
- package/app/blog-pages/post.vue +168 -0
- package/app/blog-pages/series.vue +127 -0
- package/app/blog-pages/tag.vue +96 -0
- package/app/components/OgImage/BlogPost.takumi.vue +117 -0
- package/app/components/blog/BlogCodeBlock.vue +144 -0
- package/app/components/blog/BlogPostArticle.vue +545 -0
- package/app/components/blog/BlogPostCard.vue +208 -0
- package/app/components/blog/BlogPostContent.vue +111 -0
- package/app/components/blog/BlogPostList.vue +143 -0
- package/app/components/layout/HubHeader.vue +62 -0
- package/app/composables/useBlogBrand.ts +13 -0
- package/app/composables/useBlogPath.ts +13 -0
- package/app/composables/useBlogPost.ts +12 -0
- package/app/composables/useBlogSettings.ts +15 -0
- package/app/composables/useBlogWidgets.ts +18 -0
- package/app/composables/useCachedResource.ts +72 -0
- package/app/composables/useDateValue.ts +27 -0
- package/app/composables/usePaginatedPosts.ts +57 -0
- package/app/composables/usePostFeed.ts +139 -0
- package/app/composables/useSeriesNames.ts +20 -0
- package/app/composables/useShikiHighlighter.ts +108 -0
- package/app/composables/useSiteSchemaLd.ts +44 -0
- package/app/composables/useSiteUrl.ts +4 -0
- package/app/utils/widget-props.ts +66 -0
- package/app/widgets/WidgetError.vue +13 -0
- package/blog-module.ts +71 -0
- package/nuxt.config.ts +84 -0
- package/package.json +44 -3
- package/scripts/subset-icons.mjs +167 -0
- package/server/api/blog/_sitemap-urls.ts +38 -0
- package/server/api/blog/authors/[slug].get.ts +42 -0
- package/server/api/blog/featured.get.ts +18 -0
- package/server/api/blog/posts/[slug].get.ts +116 -0
- package/server/api/blog/posts/index.get.ts +35 -0
- package/server/api/blog/series/[slug].get.ts +29 -0
- package/server/api/blog/series/index.get.ts +15 -0
- package/server/api/blog/settings.get.ts +18 -0
- package/server/api/blog/tags/[tag].get.ts +35 -0
- package/server/api/blog/tags/index.get.ts +17 -0
- package/server/handlers/rss.xml.ts +53 -0
- package/server/plugins/firebase.ts +20 -0
- package/server/utils/blog-data.ts +218 -0
- package/server/utils/blog-firestore.ts +80 -0
- package/server/utils/rss.ts +212 -0
- package/server/utils/sitemap.ts +80 -0
- package/shared/models/author.ts +19 -0
- package/shared/models/date.ts +11 -0
- package/shared/models/post.ts +114 -0
- package/shared/models/series.ts +14 -0
- package/shared/models/settings.ts +12 -0
- package/shared/models/tag.ts +15 -0
- package/shared/types/api.ts +78 -0
- package/shared/utils/blog-path.ts +23 -0
- package/shared/utils/blog.ts +45 -0
- package/shared/utils/date.ts +26 -0
- package/shared/utils/post-headings.ts +134 -0
- package/shared/utils/post-seo.ts +58 -0
- package/shared/utils/sanitize.ts +80 -0
- package/shared/widgets/types.ts +41 -0
package/README.md
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @giancarlosgza/blog-layer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Nuxt 4 layer that adds a Firestore-backed blog to a site under a configurable base path: home feed,
|
|
4
|
+
posts, tags, authors, series and paginated archive pages, cached read endpoints over the Firebase
|
|
5
|
+
Admin SDK, RSS, a sitemap source and per-page SEO (meta, JSON-LD, generated OG images), styled
|
|
6
|
+
with `@colorffy/ui` + `@colorffy/css`.
|
|
7
|
+
|
|
8
|
+
It only adds the blog. The site keeps its own app shell (app.vue, layout, header, footer) and must
|
|
9
|
+
load `@colorffy/css` itself. For a standalone blog site, install
|
|
10
|
+
[`@giancarlosgza/blog-site-layer`](../blog-site-layer) instead, which wraps this layer in a full shell.
|
|
11
|
+
|
|
12
|
+
The Firestore schema it reads is in [`docs/schema.md`](../../docs/schema.md); content is written by
|
|
13
|
+
the blog admin.
|
|
14
|
+
|
|
15
|
+
## Add a blog to a site
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @giancarlosgza/blog-layer
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// nuxt.config.ts
|
|
23
|
+
export default defineNuxtConfig({
|
|
24
|
+
extends: ['@giancarlosgza/blog-layer'],
|
|
25
|
+
blog: {
|
|
26
|
+
basePath: '/blog' // build time; '/' mounts the blog at the site root
|
|
27
|
+
},
|
|
28
|
+
css: ['@/assets/scss/main.scss', '@/assets/css/material-symbols.generated.css'],
|
|
29
|
+
site: { url: process.env.NUXT_SITE_URL, name: 'Acme' }
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
// package.json
|
|
35
|
+
"scripts": {
|
|
36
|
+
"prebuild": "blog-layer-subset-icons",
|
|
37
|
+
"predev": "blog-layer-subset-icons"
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then point `GOOGLE_APPLICATION_CREDENTIALS` at the Firebase project's service account. The account
|
|
42
|
+
alone selects the project.
|
|
43
|
+
|
|
44
|
+
## What it mounts
|
|
45
|
+
|
|
46
|
+
| Route | Name | Page |
|
|
47
|
+
| :--- | :--- | :--- |
|
|
48
|
+
| `<basePath>` | `blog` | Home feed (featured posts, tag filter, load more) |
|
|
49
|
+
| `<basePath>/:slug` | `blog-post` | Post |
|
|
50
|
+
| `<basePath>/page/:page` | `blog-archive` | Paginated archive (`/page/1` redirects home) |
|
|
51
|
+
| `<basePath>/tags/:tag` | `blog-tag` | Posts with a tag |
|
|
52
|
+
| `<basePath>/authors/:slug` | `blog-author` | Author and their posts |
|
|
53
|
+
| `<basePath>/series/:slug` | `blog-series` | Series parts in reading order |
|
|
54
|
+
| `<basePath>/rss.xml` | | RSS 2.0 feed |
|
|
55
|
+
| `/api/blog/*` | | Cached Firestore reads (always at this path) |
|
|
56
|
+
|
|
57
|
+
Link to the blog with route names (`{ name: 'blog' }`) or `useBlogPath().blogPath('/tags/css')`.
|
|
58
|
+
Blog pages use the site's `default` layout; set `blog.layout` to use another one.
|
|
59
|
+
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
- **`blog.basePath`** (`nuxt.config`, default `/`): where the blog mounts.
|
|
63
|
+
- **`blog.layout`** (`nuxt.config`, optional): layout for the blog pages.
|
|
64
|
+
- **Brand:** override `blog.*` in the site's `app/app.config.ts`. Every key is documented in this
|
|
65
|
+
package's `app/app.config.ts`. `settings/general` in Firestore overrides the name and description.
|
|
66
|
+
- **Widgets:** posts can embed `<div data-blog-widget="key" data-props='{...}'>`. A site registers
|
|
67
|
+
widgets by providing `blogWidgets` (`{ [key]: { descriptor, component } }`) from a plugin; without
|
|
68
|
+
one, placeholders render an "unavailable" fallback.
|
|
69
|
+
- **Icons:** `blog-layer-subset-icons` scans the site, its `layers/*` and the installed blog layers
|
|
70
|
+
for Material Symbols in use and writes `public/icon-fonts/*` plus
|
|
71
|
+
`app/assets/css/material-symbols.generated.css` in the site.
|
|
72
|
+
|
|
73
|
+
Inside the layer, files import each other through the `#blog/...` alias: `~` and `#shared` resolve
|
|
74
|
+
to the site.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog identity defaults. The site overrides any of these in its own `app.config.ts`;
|
|
3
|
+
* `settings/general` (blog name, description) overrides the name and description at runtime.
|
|
4
|
+
* Arrays merge by concatenation across layers, so list defaults stay empty.
|
|
5
|
+
*/
|
|
6
|
+
export default defineAppConfig({
|
|
7
|
+
blog: {
|
|
8
|
+
/** Formal name: `og:site_name`, title template, structured data, RSS and OG image fallbacks. */
|
|
9
|
+
name: 'Blog',
|
|
10
|
+
/** Short name: navbar/footer fallback and SEO copy ("Articles by X on Blog."). */
|
|
11
|
+
shortName: 'Blog',
|
|
12
|
+
description: '',
|
|
13
|
+
/** Byline when a post has no author name. */
|
|
14
|
+
defaultAuthor: '',
|
|
15
|
+
home: {
|
|
16
|
+
/** Small line above the home heading (hidden when empty). */
|
|
17
|
+
eyebrow: '',
|
|
18
|
+
/** Highlighted word after the blog name in the home heading (hidden when empty). */
|
|
19
|
+
headingAccent: '',
|
|
20
|
+
title: 'Articles',
|
|
21
|
+
ogDescription: ''
|
|
22
|
+
},
|
|
23
|
+
archive: {
|
|
24
|
+
/** Meta description of `/page/N`, followed by " — page N." (defaults to "More articles from {shortName}"). */
|
|
25
|
+
description: ''
|
|
26
|
+
},
|
|
27
|
+
ogImage: {
|
|
28
|
+
/** Small line above the big word in the accent band (hidden when empty). */
|
|
29
|
+
kicker: '',
|
|
30
|
+
title: 'Blog',
|
|
31
|
+
accentColor: '#382bf0',
|
|
32
|
+
/** Wordmark at the top of the accent band, a path in the site's `public/` (hidden when empty). */
|
|
33
|
+
logo: ''
|
|
34
|
+
},
|
|
35
|
+
/** Publisher in the `Organization` JSON-LD (falls back to the blog name and URL). */
|
|
36
|
+
organization: {
|
|
37
|
+
name: '',
|
|
38
|
+
url: '',
|
|
39
|
+
sameAs: [] as string[]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$easing-spring-elastic: linear(0, 0.002 0.3%, 0.007 0.6%, 0.029 1.3%, 0.065 2%, 0.119 2.8%, 0.237 4.2%, 0.659 8.7%, 0.778 10.2%, 0.871 11.6%, 0.95 13.1%, 1.009 14.6%, 1.033 15.4%, 1.052 16.2%, 1.066 17%, 1.078 17.9%, 1.085 18.8%, 1.088 19.7%, 1.088 20.7%, 1.085 21.7%, 1.074 23.6%, 1.032 28.7%, 1.014 31.4%, 1.006 33%, 1 34.6%, 0.993 38%, 0.992 41.9%, 0.999 51.4%, 1.001 57.6%, 1);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Material Symbols
|
|
2
|
+
@layer base.symbols {
|
|
3
|
+
.material-symbols-rounded {
|
|
4
|
+
font-family: "Material Symbols Rounded";
|
|
5
|
+
font-weight: normal;
|
|
6
|
+
font-style: normal;
|
|
7
|
+
font-size: 24px;
|
|
8
|
+
line-height: 1;
|
|
9
|
+
letter-spacing: normal;
|
|
10
|
+
text-transform: none;
|
|
11
|
+
display: inline-block;
|
|
12
|
+
white-space: nowrap;
|
|
13
|
+
word-wrap: normal;
|
|
14
|
+
direction: ltr;
|
|
15
|
+
-webkit-font-smoothing: antialiased;
|
|
16
|
+
-moz-osx-font-smoothing: grayscale;
|
|
17
|
+
text-rendering: optimizeLegibility;
|
|
18
|
+
font-feature-settings: "liga";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
@use './core/layers';
|
|
2
|
+
@use './core/shared-ui';
|
|
3
|
+
@use './core/article-layout';
|
|
4
|
+
@use './core/post-layout';
|
|
5
|
+
@use './core/post-article-layout';
|
|
6
|
+
@use './core/code-layout';
|
|
7
|
+
@use './core/transitions-layout';
|
|
8
|
+
@use './core/widgets';
|
|
9
|
+
|
|
10
|
+
@layer overrides {
|
|
11
|
+
code,
|
|
12
|
+
.font-code {
|
|
13
|
+
font-family: var(--cffy-font-code), monospace;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@layer pages {
|
|
18
|
+
.section-hero {
|
|
19
|
+
& h1 {
|
|
20
|
+
& span {
|
|
21
|
+
--_hero-color-start-from: var(--cffy-secondary-base-fixed);
|
|
22
|
+
--_hero-color-start-to: var(--cffy-secondary-a30);
|
|
23
|
+
--_hero-color-end-from: var(--cffy-accent-base-fixed);
|
|
24
|
+
--_hero-color-end-to: var(--cffy-accent-a30);
|
|
25
|
+
|
|
26
|
+
--hero-color-start: var(--_hero-color-start-from);
|
|
27
|
+
--hero-color-end: var(--_hero-color-end-from);
|
|
28
|
+
--_space: in srgb;
|
|
29
|
+
|
|
30
|
+
background: linear-gradient(90deg var(--_space),
|
|
31
|
+
var(--hero-color-start),
|
|
32
|
+
var(--hero-color-end));
|
|
33
|
+
background-clip: text;
|
|
34
|
+
color: transparent;
|
|
35
|
+
-webkit-background-clip: text;
|
|
36
|
+
-webkit-text-fill-color: transparent;
|
|
37
|
+
animation: hero-color-change 3s linear infinite alternate;
|
|
38
|
+
|
|
39
|
+
@supports (background: linear-gradient(in oklch, #fff, #fff)) {
|
|
40
|
+
--_space: in oklch;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@media (prefers-reduced-motion: reduce) {
|
|
44
|
+
animation: none;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@property --hero-color-start {
|
|
51
|
+
syntax: '<color>';
|
|
52
|
+
inherits: false;
|
|
53
|
+
initial-value: #382bf0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@property --hero-color-end {
|
|
57
|
+
syntax: '<color>';
|
|
58
|
+
inherits: false;
|
|
59
|
+
initial-value: #0075d6;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@keyframes hero-color-change {
|
|
63
|
+
to {
|
|
64
|
+
--hero-color-start: var(--_hero-color-start-to);
|
|
65
|
+
--hero-color-end: var(--_hero-color-end-to);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.gradient-blob {
|
|
70
|
+
--_color: var(--color, var(--cffy-secondary-base-fixed));
|
|
71
|
+
|
|
72
|
+
position: fixed;
|
|
73
|
+
top: 0;
|
|
74
|
+
right: 0;
|
|
75
|
+
width: 100vw;
|
|
76
|
+
aspect-ratio: 1 / 1;
|
|
77
|
+
border-radius: 50%;
|
|
78
|
+
pointer-events: none;
|
|
79
|
+
background: radial-gradient(circle at center, var(--_color) 0%, transparent 70%);
|
|
80
|
+
transform: translate(50%, -50%);
|
|
81
|
+
opacity: 0.45;
|
|
82
|
+
z-index: -1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
@use '@colorffy/css/scss/abstracts/mixins' as m;
|
|
2
|
+
@use '../../abstracts/type-scale' as t;
|
|
3
|
+
|
|
4
|
+
// Skips widget and Shiki subtrees
|
|
5
|
+
$prose-scope: ':not(:where([data-blog-widget], .blog-code-host, .code-container) *)';
|
|
6
|
+
|
|
7
|
+
@layer pages {
|
|
8
|
+
.blog-prose {
|
|
9
|
+
--_prose-space-xs: var(--cffy-space-8);
|
|
10
|
+
--_prose-space-sm: var(--cffy-space-24);
|
|
11
|
+
--_prose-space-md: var(--cffy-space-32);
|
|
12
|
+
--_prose-space-lg: var(--cffy-space-48);
|
|
13
|
+
--_prose-color: var(--cffy-muted-a10);
|
|
14
|
+
--_prose-heading-color: var(--cffy-on-background);
|
|
15
|
+
--_prose-border-color: var(--cffy-outline-surface);
|
|
16
|
+
--_prose-surface: var(--cffy-surface-container);
|
|
17
|
+
--_prose-anchor-offset: calc(var(--cffy-navbar-min-height, 4.25rem) + 1.5rem);
|
|
18
|
+
|
|
19
|
+
font-size: var(--cffy-fs-base);
|
|
20
|
+
line-height: 1.75;
|
|
21
|
+
color: var(--_prose-color);
|
|
22
|
+
|
|
23
|
+
@include m.media-query(768) {
|
|
24
|
+
font-size: .9375rem;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Rhythm
|
|
28
|
+
& > * {
|
|
29
|
+
margin-block: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
& > * + * {
|
|
33
|
+
margin-block-start: var(--_prose-space-sm);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
& > :is(h2, h3, h4, h5, h6) + * {
|
|
37
|
+
margin-block-start: var(--cffy-space-12);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
& > h2 {
|
|
41
|
+
margin-block-start: var(--_prose-space-lg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
& > :is(h3, h4, h5, h6) {
|
|
45
|
+
margin-block-start: var(--_prose-space-md);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
& > :is(figure, img, pre, table, hr, blockquote, .blog-code-host, [data-blog-widget]) {
|
|
49
|
+
margin-block: var(--_prose-space-md);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
& > :first-child {
|
|
53
|
+
margin-block-start: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
& > :last-child {
|
|
57
|
+
margin-block-end: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Headings
|
|
61
|
+
& :is(h2, h3, h4, h5, h6)#{$prose-scope} {
|
|
62
|
+
font-family: var(--cffy-font-primary);
|
|
63
|
+
font-weight: var(--cffy-fw-600);
|
|
64
|
+
color: var(--_prose-heading-color);
|
|
65
|
+
letter-spacing: t.$ls-ledger-title;
|
|
66
|
+
text-wrap: balance;
|
|
67
|
+
scroll-margin-top: var(--_prose-anchor-offset);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
& h2#{$prose-scope} {
|
|
71
|
+
font-size: t.$fs-landing-card-md;
|
|
72
|
+
line-height: 1.15;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
& h3#{$prose-scope} {
|
|
76
|
+
font-size: var(--cffy-fs-xl);
|
|
77
|
+
line-height: 1.3;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
& h4#{$prose-scope} {
|
|
81
|
+
font-size: var(--cffy-fs-lg);
|
|
82
|
+
line-height: 1.4;
|
|
83
|
+
letter-spacing: normal;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
& :is(h5, h6)#{$prose-scope} {
|
|
87
|
+
font-size: var(--cffy-fs-base);
|
|
88
|
+
line-height: 1.5;
|
|
89
|
+
letter-spacing: normal;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Text
|
|
93
|
+
& :is(p, li, dd, dt, td, th, figcaption, blockquote)#{$prose-scope} {
|
|
94
|
+
font-size: inherit;
|
|
95
|
+
line-height: inherit;
|
|
96
|
+
letter-spacing: normal;
|
|
97
|
+
overflow-wrap: break-word;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
& p#{$prose-scope} {
|
|
101
|
+
color: var(--_prose-color);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
& :is(strong, b)#{$prose-scope} {
|
|
105
|
+
font-weight: var(--cffy-fw-600);
|
|
106
|
+
color: var(--_prose-heading-color);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
& :is(em, i:not(.material-symbols-rounded))#{$prose-scope} {
|
|
110
|
+
font-style: italic;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
& a:not(.btn, .undecorated)#{$prose-scope} {
|
|
114
|
+
font-size: inherit;
|
|
115
|
+
font-weight: var(--cffy-fw-500);
|
|
116
|
+
line-height: inherit;
|
|
117
|
+
letter-spacing: inherit;
|
|
118
|
+
|
|
119
|
+
&:is(.text-link) {
|
|
120
|
+
border-bottom-color: var(--_link-underline-color);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
&:is(:hover, :focus-visible):is(.text-link) {
|
|
124
|
+
border-bottom-color: currentColor;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Lists
|
|
129
|
+
& :is(ul, ol)#{$prose-scope} {
|
|
130
|
+
margin-block-end: 0;
|
|
131
|
+
padding-inline-start: 1.5rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
& li#{$prose-scope} {
|
|
135
|
+
padding-inline-start: .25rem;
|
|
136
|
+
|
|
137
|
+
& + li {
|
|
138
|
+
margin-block-start: var(--_prose-space-xs);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
& > :is(ul, ol) {
|
|
142
|
+
margin-block-start: var(--_prose-space-xs);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
& > p {
|
|
146
|
+
margin-block: 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
&::marker {
|
|
150
|
+
color: var(--cffy-secondary-a10);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
& ol#{$prose-scope} > li::marker {
|
|
155
|
+
font-family: var(--cffy-font-code);
|
|
156
|
+
font-weight: var(--cffy-fw-500);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
& :is(ul, ol) ul#{$prose-scope} {
|
|
160
|
+
list-style-type: circle;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Blockquote
|
|
164
|
+
& blockquote#{$prose-scope} {
|
|
165
|
+
padding-block: calc(var(--_prose-space-sm) / 2);
|
|
166
|
+
padding-inline: var(--_prose-space-sm);
|
|
167
|
+
background-color: color-mix(in srgb, var(--cffy-muted-base-fixed) 10%, transparent);
|
|
168
|
+
border-inline-start: var(--cffy-border-width-md) solid var(--cffy-muted-a10);
|
|
169
|
+
border-radius: 0 var(--cffy-radius-lg) var(--cffy-radius-lg) 0;
|
|
170
|
+
|
|
171
|
+
& > * {
|
|
172
|
+
margin-block: 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
& > * + * {
|
|
176
|
+
margin-block-start: var(--_prose-space-xs);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Inline code
|
|
181
|
+
& :not(pre) > code#{$prose-scope} {
|
|
182
|
+
padding: .125em .375em;
|
|
183
|
+
font-family: var(--cffy-font-code), monospace;
|
|
184
|
+
font-size: .875em;
|
|
185
|
+
color: var(--_prose-heading-color);
|
|
186
|
+
background-color: var(--_prose-surface);
|
|
187
|
+
border: var(--cffy-border-width-sm) solid var(--_prose-border-color);
|
|
188
|
+
border-radius: var(--cffy-radius-sm);
|
|
189
|
+
overflow-wrap: anywhere;
|
|
190
|
+
cursor: text;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Raw blocks before Shiki hydrates
|
|
194
|
+
& > pre {
|
|
195
|
+
padding: .75rem 1rem;
|
|
196
|
+
font-family: var(--cffy-font-code), monospace;
|
|
197
|
+
font-size: .9em;
|
|
198
|
+
line-height: 1.6;
|
|
199
|
+
background-color: var(--_prose-surface);
|
|
200
|
+
border: var(--cffy-border-width-sm) solid var(--_prose-border-color);
|
|
201
|
+
border-radius: var(--cffy-radius-md);
|
|
202
|
+
overflow-x: auto;
|
|
203
|
+
|
|
204
|
+
& > code {
|
|
205
|
+
font-size: inherit;
|
|
206
|
+
color: inherit;
|
|
207
|
+
background: none;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
& hr#{$prose-scope} {
|
|
212
|
+
height: 0;
|
|
213
|
+
border: 0;
|
|
214
|
+
border-block-start: var(--cffy-border-width-sm) solid var(--_prose-border-color);
|
|
215
|
+
opacity: 1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Tables
|
|
219
|
+
& table#{$prose-scope} {
|
|
220
|
+
display: block;
|
|
221
|
+
inline-size: fit-content;
|
|
222
|
+
max-inline-size: 100%;
|
|
223
|
+
overflow-x: auto;
|
|
224
|
+
border-collapse: collapse;
|
|
225
|
+
font-size: var(--cffy-fs-sm);
|
|
226
|
+
|
|
227
|
+
& :is(th, td) {
|
|
228
|
+
padding: .5rem .75rem;
|
|
229
|
+
text-align: start;
|
|
230
|
+
vertical-align: top;
|
|
231
|
+
border: var(--cffy-border-width-sm) solid var(--_prose-border-color);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
& th {
|
|
235
|
+
font-weight: var(--cffy-fw-600);
|
|
236
|
+
color: var(--_prose-heading-color);
|
|
237
|
+
background-color: var(--_prose-surface);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Media
|
|
242
|
+
& img#{$prose-scope} {
|
|
243
|
+
display: block;
|
|
244
|
+
inline-size: 100%;
|
|
245
|
+
block-size: auto;
|
|
246
|
+
border-radius: var(--corner-radius-large);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
& figure#{$prose-scope} {
|
|
250
|
+
margin-inline: 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
& figcaption#{$prose-scope} {
|
|
254
|
+
margin-block-start: var(--cffy-space-12);
|
|
255
|
+
font-size: var(--cffy-fs-xs);
|
|
256
|
+
color: var(--cffy-muted-a10);
|
|
257
|
+
text-align: center;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
@layer pages {
|
|
2
|
+
.code-container {
|
|
3
|
+
--_code-container-border-color: var(--cffy-outline-surface);
|
|
4
|
+
--_code-container-bg-color: transparent;
|
|
5
|
+
|
|
6
|
+
position: relative;
|
|
7
|
+
background-color: var(--_code-container-bg-color);
|
|
8
|
+
border: var(--cffy-border-width-sm) solid var(--_code-container-border-color);
|
|
9
|
+
border-radius: var(--cffy-radius-md);
|
|
10
|
+
padding: 0;
|
|
11
|
+
margin: 0;
|
|
12
|
+
transition: background-color 200ms var(--cffy-ease-decelerate),
|
|
13
|
+
border 200ms var(--cffy-ease-decelerate);
|
|
14
|
+
|
|
15
|
+
&:is(:hover) {
|
|
16
|
+
--_code-container-border-color: var(--cffy-surface-a60);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
& pre {
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: .75rem 1rem;
|
|
22
|
+
border-radius: var(--cffy-radius-md);
|
|
23
|
+
overflow-x: auto;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
& code {
|
|
27
|
+
font-family: var(--cffy-font-code), monospace;
|
|
28
|
+
font-size: 0.9em;
|
|
29
|
+
line-height: 1.6;
|
|
30
|
+
background: transparent;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Raw source until Shiki loads
|
|
34
|
+
& .code-loading-placeholder code {
|
|
35
|
+
white-space: pre;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&.code-copied {
|
|
39
|
+
--_code-container-border-color: var(--cffy-surface-a60);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer overrides, pages;
|