starlight-theme-nova 0.2.1 → 0.3.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.
@@ -99,7 +99,8 @@
99
99
  .nova-pagination-link-icon-right{--nova-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m9 18l6-6l-6-6'/%3E%3C/svg%3E");-webkit-mask:var(--nova-icon) no-repeat;mask:var(--nova-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1em;height:1em;display:block;min-width:1.25rem;min-height:1.25rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
100
100
  .nova-theme-select-icon-dark{--nova-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M12 1.992a10 10 0 1 0 9.236 13.838c.341-.82-.476-1.644-1.298-1.31a6.5 6.5 0 0 1-6.864-10.787l.077-.08c.551-.63.113-1.653-.758-1.653h-.266l-.068-.006z'/%3E%3C/svg%3E");-webkit-mask:var(--nova-icon) no-repeat;mask:var(--nova-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1em;height:1em;}
101
101
  .nova-theme-select-icon-light{--nova-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M12 19a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0v-2a1 1 0 0 1 1-1m-4.95-2.05a1 1 0 0 1 0 1.414l-1.414 1.414a1 1 0 1 1-1.414-1.414l1.414-1.414a1 1 0 0 1 1.414 0m11.314 0l1.414 1.414a1 1 0 0 1-1.414 1.414l-1.414-1.414a1 1 0 0 1 1.414-1.414m-5.049-9.836a5 5 0 1 1-2.532 9.674a5 5 0 0 1 2.532-9.674M4 11a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2zm18 0a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2zM5.636 4.222L7.05 5.636A1 1 0 0 1 5.636 7.05L4.222 5.636a1 1 0 0 1 1.414-1.414m14.142 0a1 1 0 0 1 0 1.414L18.364 7.05a1 1 0 0 1-1.414-1.414l1.414-1.414a1 1 0 0 1 1.414 0M12 1a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0V2a1 1 0 0 1 1-1'/%3E%3C/svg%3E");-webkit-mask:var(--nova-icon) no-repeat;mask:var(--nova-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1em;height:1em;}
102
- .nova-code-container{position:relative;}
102
+ .nova-code-container{position:relative;overflow:hidden;border-width:1px;border-color:var(--sl-color-gray-5);border-radius:0.375rem;border-style:solid;}
103
+ .nova-code-container pre{position:relative;}
103
104
  .nova-code-copy-button{position:absolute;right:0.5rem;top:0.5rem;margin:0;width:1.5rem;height:1.5rem;border-width:1px;border-color:var(--sl-color-gray-5);border-radius:0.25rem;border-style:solid;background-color:rgb(243 244 246 / 0.3) /* #f3f4f6 */;padding:0.25rem;--nova-text-opacity:1;color:rgb(0 0 0 / var(--nova-text-opacity)) /* #000 */;--nova-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--nova-backdrop-blur) var(--nova-backdrop-brightness) var(--nova-backdrop-contrast) var(--nova-backdrop-grayscale) var(--nova-backdrop-hue-rotate) var(--nova-backdrop-invert) var(--nova-backdrop-opacity) var(--nova-backdrop-saturate) var(--nova-backdrop-sepia);backdrop-filter:var(--nova-backdrop-blur) var(--nova-backdrop-brightness) var(--nova-backdrop-contrast) var(--nova-backdrop-grayscale) var(--nova-backdrop-hue-rotate) var(--nova-backdrop-invert) var(--nova-backdrop-opacity) var(--nova-backdrop-saturate) var(--nova-backdrop-sepia);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
104
105
  .nova-link-card{position:relative;display:flex;flex-direction:column;gap:0.5rem;border-width:1px;border-color:var(--sl-color-gray-5);border-radius:0.75rem;border-style:solid;--nova-bg-opacity:1;background-color:rgb(255 255 255 / var(--nova-bg-opacity)) /* #fff */;padding-left:1.25rem;padding-right:1.25rem;padding-top:1rem;padding-bottom:1rem;--nova-text-opacity:1;color:rgb(55 65 81 / var(--nova-text-opacity)) /* #374151 */;--nova-shadow:var(--nova-shadow-inset) 0 1px 2px 0 var(--nova-shadow-color, rgb(0 0 0 / 0.05));box-shadow:var(--nova-ring-offset-shadow), var(--nova-ring-shadow), var(--nova-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;transition-duration:200ms;}
105
106
  .nova-page-frame-header{position:fixed;inset:0;z-index:var(--sl-z-index-navbar);box-sizing:border-box;width:100%;height:var(--sl-nav-height);border-width:0px;border-bottom-width:1px;border-color:var(--sl-color-hairline);border-style:solid;background-color:var(--sl-color-bg-nav) /* var(--sl-color-bg-nav) */;padding-top:var(--sl-nav-pad-y);padding-bottom:var(--sl-nav-pad-y);padding-left:var(--sl-nav-pad-x);padding-right:var(--sl-nav-pad-x);--nova-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--nova-backdrop-blur) var(--nova-backdrop-brightness) var(--nova-backdrop-contrast) var(--nova-backdrop-grayscale) var(--nova-backdrop-hue-rotate) var(--nova-backdrop-invert) var(--nova-backdrop-opacity) var(--nova-backdrop-saturate) var(--nova-backdrop-sepia);backdrop-filter:var(--nova-backdrop-blur) var(--nova-backdrop-brightness) var(--nova-backdrop-contrast) var(--nova-backdrop-grayscale) var(--nova-backdrop-hue-rotate) var(--nova-backdrop-invert) var(--nova-backdrop-opacity) var(--nova-backdrop-saturate) var(--nova-backdrop-sepia);}
@@ -108,6 +109,8 @@
108
109
  .nova-pagination-link{margin:0;display:flex;align-items:center;justify-content:flex-end;gap:0.5rem;border-radius:0.75rem;padding:0.5rem;color:var(--sl-color-gray-2) /* var(--sl-color-gray-2) */;font-weight:500;text-decoration:none;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;transition-duration:100ms;}
109
110
  .nova-link-button{margin-inline-end:0.5rem;margin-top:0.5rem;margin-bottom:0.5rem;display:inline-flex;align-items:center;justify-content:space-between;gap:0.5rem;border-width:1px;border-color:transparent;border-radius:0.75rem;border-style:solid;padding-left:1.5rem;padding-right:1.5rem;padding-top:0.75rem;padding-bottom:0.75rem;font-weight:500;text-decoration:none;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;transition-duration:200ms;}
110
111
  .nova-header{box-sizing:border-box;height:100%;display:flex;align-items:center;gap:0.5rem;}
112
+ div[data-nova-code-container][data-nova-code-title] .nova-code-title{display:block;}
113
+ .nova-code-title{display:none;border-bottom-width:1px;border-bottom-color:var(--sl-color-gray-5);border-bottom-style:solid;background-color:var(--sl-color-bg) /* var(--sl-color-bg) */;padding-left:1rem;padding-right:1rem;padding-top:0.5rem;padding-bottom:0.5rem;font-size:0.875rem;line-height:1.25rem;color:var(--sl-color-gray-3) /* var(--sl-color-gray-3) */;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;}
111
114
  .nova-header-actions{display:none;align-items:center;gap:0.5rem;}
112
115
  .nova-social-icons-link{width:2rem;height:2rem;display:flex;border-radius:0.375rem;padding:0.5rem;color:var(--sl-color-text) /* var(--sl-color-text) */;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
113
116
  .nova-theme-select{width:2rem;height:2rem;border-radius:0.375rem;padding:0.5rem;color:var(--sl-color-text) /* var(--sl-color-text) */;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
@@ -126,6 +129,7 @@
126
129
  .nova-pagination-link:active[data-side="left"]{--nova-translate-x:-0.25rem;transform:translateX(var(--nova-translate-x)) translateY(var(--nova-translate-y)) translateZ(var(--nova-translate-z)) rotate(var(--nova-rotate)) rotateX(var(--nova-rotate-x)) rotateY(var(--nova-rotate-y)) rotateZ(var(--nova-rotate-z)) skewX(var(--nova-skew-x)) skewY(var(--nova-skew-y)) scaleX(var(--nova-scale-x)) scaleY(var(--nova-scale-y)) scaleZ(var(--nova-scale-z));}
127
130
  .nova-pagination-link:active[data-side="right"]{--nova-translate-x:0.25rem;transform:translateX(var(--nova-translate-x)) translateY(var(--nova-translate-y)) translateZ(var(--nova-translate-z)) rotate(var(--nova-rotate)) rotateX(var(--nova-rotate-x)) rotateY(var(--nova-rotate-y)) rotateZ(var(--nova-rotate-z)) skewX(var(--nova-skew-x)) skewY(var(--nova-skew-y)) scaleX(var(--nova-scale-x)) scaleY(var(--nova-scale-y)) scaleZ(var(--nova-scale-z));}
128
131
  .nova-code-copy-button:active{--nova-scale-x:0.9;--nova-scale-y:0.9;transform:translateX(var(--nova-translate-x)) translateY(var(--nova-translate-y)) translateZ(var(--nova-translate-z)) rotate(var(--nova-rotate)) rotateX(var(--nova-rotate-x)) rotateY(var(--nova-rotate-y)) rotateZ(var(--nova-rotate-z)) skewX(var(--nova-skew-x)) skewY(var(--nova-skew-y)) scaleX(var(--nova-scale-x)) scaleY(var(--nova-scale-y)) scaleZ(var(--nova-scale-z));}
132
+ .sl-markdown-content .nova-code-container pre.astro-code{border-width:0px;border-color:transparent;border-radius:0;}
129
133
  .nova-page-frame-sidebar-pane{border-width:0px;border-color:var(--sl-color-hairline);border-style:solid;}
130
134
  .nova-link-button-secondary{border-color:var(--sl-color-gray-5);--nova-bg-opacity:1;background-color:rgb(255 255 255 / var(--nova-bg-opacity)) /* #fff */;--nova-text-opacity:1;color:rgb(55 65 81 / var(--nova-text-opacity)) /* #374151 */;--nova-shadow:var(--nova-shadow-inset) 0 1px 2px 0 var(--nova-shadow-color, rgb(0 0 0 / 0.05));box-shadow:var(--nova-ring-offset-shadow), var(--nova-ring-shadow), var(--nova-shadow);}
131
135
  .nova-mobile-table-of-contents summary{border-bottom-color:var(--sl-color-hairline);}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "starlight-theme-nova",
3
3
  "type": "module",
4
- "version": "0.2.1",
4
+ "version": "0.3.0",
5
5
  "description": "",
6
6
  "author": "ocavue <ocavue@gmail.com>",
7
7
  "license": "MIT",
@@ -29,8 +29,10 @@
29
29
  "@shikijs/transformers": "^3.2.1",
30
30
  "@shikijs/twoslash": "^3.2.1",
31
31
  "@shikijs/types": "^3.2.1",
32
+ "@types/hast": "^3.0.4",
32
33
  "astro-theme-toggle": "^0.6.0",
33
34
  "hast-util-is-element": "^3.0.0",
35
+ "rehype": "^13.0.2",
34
36
  "shiki-twoslash-renderer": "0.0.2"
35
37
  },
36
38
  "devDependencies": {
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import { Tabs } from '@astrojs/starlight/components'
3
+ import { processCodeTabs } from './rehype-code-tabs'
3
4
 
4
5
  interface Props {
5
6
  border?: boolean
@@ -7,11 +8,14 @@ interface Props {
7
8
  }
8
9
 
9
10
  const { border = true, syncKey } = Astro.props
11
+
12
+ const originalHtml = await Astro.slots.render('default')
13
+ const processedHtml = processCodeTabs(originalHtml)
10
14
  ---
11
15
 
12
16
  <div class:list={['code-tabs', border ? 'code-tabs-border' : undefined]}>
13
17
  <Tabs syncKey={syncKey}>
14
- <slot />
18
+ <Fragment set:html={processedHtml} />
15
19
  </Tabs>
16
20
  </div>
17
21
 
@@ -59,12 +63,14 @@ const { border = true, syncKey } = Astro.props
59
63
  margin: 0;
60
64
  }
61
65
 
62
- div[role='tabpanel'] pre {
63
- border-color: transparent;
66
+ .astro-code {
67
+ border-radius: 0;
64
68
  }
65
69
 
66
- .astro-code {
70
+ .nova-code-container {
67
71
  border-radius: 0;
72
+ border-width: 0;
73
+ border-color: transparent;
68
74
  }
69
75
  }
70
76
  </style>
@@ -0,0 +1,56 @@
1
+ import type { Element, ElementContent } from 'hast'
2
+ import { rehype } from 'rehype'
3
+
4
+ import { CODE_PROPERTY_CONTAINER, CODE_PROPERTY_TITLE } from '../constants'
5
+
6
+ // https://github.com/withastro/starlight/blob/bdb05e704a6cf06a029367f99b6bf2f575e5a5f3/packages/starlight/user-components/rehype-tabs.ts#L20
7
+ export const TabItemTagname = 'starlight-tab-item'
8
+
9
+ /**
10
+ * Rehype processor to extract tab panel data and turn each
11
+ * `<starlight-tab-item>` into a `<div>` with the necessary
12
+ * attributes.
13
+ */
14
+ const tabsProcessor = rehype()
15
+ .data('settings', { fragment: true })
16
+ .use(() => {
17
+ return (tree: Element) => {
18
+ return {
19
+ ...tree,
20
+ children: tree.children.map(replacePre),
21
+ }
22
+ }
23
+ })
24
+
25
+ function replacePre(node: ElementContent): ElementContent {
26
+ if (
27
+ node.type === 'element' &&
28
+ node.tagName === 'div' &&
29
+ node.properties[CODE_PROPERTY_CONTAINER] != null
30
+ ) {
31
+ const title = node.properties[CODE_PROPERTY_TITLE]
32
+
33
+ if (title) {
34
+ node.properties[CODE_PROPERTY_TITLE] = undefined
35
+ return {
36
+ type: 'element',
37
+ tagName: TabItemTagname,
38
+ properties: {
39
+ dataLabel: title,
40
+ },
41
+ children: [node],
42
+ }
43
+ }
44
+ }
45
+
46
+ return node
47
+ }
48
+
49
+ /**
50
+ * Processes the children of the `<CodeTabs>` component to transform
51
+ * any code blocks with "title" meta information into a <TabItem> component.
52
+ */
53
+ export function processCodeTabs(html: string): string {
54
+ const file = tabsProcessor.processSync({ value: html })
55
+ return file.toString()
56
+ }
package/src/constants.ts CHANGED
@@ -1 +1,5 @@
1
1
  export const PAGE_TITLE_ID = '_top'
2
+
3
+ export const CODE_PROPERTY_CONTAINER = 'dataNovaCodeContainer'
4
+
5
+ export const CODE_PROPERTY_TITLE = 'dataNovaCodeTitle'
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  transformerMetaHighlight,
3
3
  transformerMetaWordHighlight,
4
- transformerNotationDiff,
4
+ transformerNotationDiff,transformerRemoveNotationEscape,
5
5
  transformerNotationHighlight,
6
6
  transformerNotationWordHighlight,
7
7
  } from '@shikijs/transformers'
@@ -10,6 +10,7 @@ import type { ShikiTransformer } from '@shikijs/types'
10
10
  import type { ShikiConfig } from 'astro'
11
11
  import { createRenderer } from 'shiki-twoslash-renderer'
12
12
 
13
+ import { transformerContainer } from './shiki-transformer-container'
13
14
  import { transformerCopyButton } from './shiki-transformer-copy-button'
14
15
 
15
16
  export function createShikiConfig(options: { twoslash: boolean }): ShikiConfig {
@@ -19,8 +20,10 @@ export function createShikiConfig(options: { twoslash: boolean }): ShikiConfig {
19
20
  transformerNotationDiff(),
20
21
  transformerNotationHighlight(),
21
22
  transformerNotationWordHighlight(),
23
+ transformerRemoveNotationEscape(),
22
24
 
23
25
  transformerCopyButton(),
26
+ transformerContainer(),
24
27
  options.twoslash
25
28
  ? transformerTwoslash({
26
29
  renderer: createRenderer(),
@@ -0,0 +1,6 @@
1
+ export function parseTitleString(meta: string): string | null {
2
+ if (!meta) return null
3
+ const match = meta.match(/title="([^"]+)"/)
4
+ if (!match) return null
5
+ return match[1]
6
+ }
@@ -0,0 +1,70 @@
1
+ import type { ShikiTransformer } from '@shikijs/types'
2
+ import type { ElementContent, RootContent } from 'hast'
3
+ import { isElement } from 'hast-util-is-element'
4
+
5
+ import { CODE_PROPERTY_CONTAINER, CODE_PROPERTY_TITLE } from './constants'
6
+ import { parseTitleString } from './shiki-meta-title'
7
+
8
+ const name = 'starlight-theme-nova-shiki-transformer-container'
9
+
10
+ /**
11
+ * A transformer to wrap code blocks in a container.
12
+ */
13
+ export function transformerContainer(): ShikiTransformer {
14
+ return {
15
+ name: name,
16
+ root(node) {
17
+ if (node.children.length === 0) {
18
+ throw new Error(`[${name}] Expected at least one child`)
19
+ }
20
+
21
+ const pre = node.children.find((child) => isElement(child, 'pre'))
22
+
23
+ if (!pre) {
24
+ throw new Error(
25
+ `[${name}] Expected a <pre> element in the root node but got ${JSON.stringify(node)}`,
26
+ )
27
+ }
28
+
29
+ const title = parseTitleString(this.options.meta?.__raw || '')
30
+
31
+ const children: ElementContent[] = normalizeContent(node.children)
32
+
33
+ if (title) {
34
+ children.unshift({
35
+ type: 'element',
36
+ tagName: 'div',
37
+ properties: {
38
+ class: 'nova-code-title',
39
+ },
40
+ children: [
41
+ {
42
+ type: 'text',
43
+ value: title,
44
+ },
45
+ ],
46
+ })
47
+ }
48
+
49
+ node.children = [
50
+ {
51
+ type: 'element',
52
+ tagName: 'div',
53
+ properties: {
54
+ [CODE_PROPERTY_CONTAINER]: '',
55
+ [CODE_PROPERTY_TITLE]: title || undefined,
56
+ class: 'nova-code-container not-content',
57
+ },
58
+ children,
59
+ },
60
+ ]
61
+
62
+ return node
63
+ },
64
+ }
65
+ }
66
+
67
+ // A simple function to make typescript happy
68
+ function normalizeContent(children: RootContent[]): ElementContent[] {
69
+ return children.filter((child) => child.type !== 'doctype')
70
+ }
@@ -1,5 +1,4 @@
1
1
  import type { ShikiTransformer } from '@shikijs/types'
2
- import { isElement } from 'hast-util-is-element'
3
2
 
4
3
  const name = 'starlight-theme-nova-shiki-transformer-copy-button'
5
4
 
@@ -9,42 +8,25 @@ const name = 'starlight-theme-nova-shiki-transformer-copy-button'
9
8
  export function transformerCopyButton(): ShikiTransformer {
10
9
  return {
11
10
  name: name,
12
- root(node) {
13
- if (node.children.length !== 1) {
14
- throw new Error(`[${name}] Expected exactly one child`)
15
- }
16
-
17
- const pre = node.children[0]
18
- if (!isElement(pre, 'pre')) {
19
- throw new Error(
20
- `[${name}] Expected a <pre> element but got ${JSON.stringify({ ...pre, children: '...' })}`,
21
- )
22
- }
23
-
24
- node.children = [
25
- {
26
- type: 'element',
27
- tagName: 'div',
28
- properties: {
29
- class: 'nova-code-container not-content',
30
- },
31
- children: [
32
- pre,
33
- {
34
- type: 'element',
35
- tagName: 'nova-code-copy-button',
36
- properties: {
37
- type: 'button',
38
- 'data-code': this.source,
39
- title: 'Copy code',
40
- 'aria-label': 'Copy code',
41
- class: 'nova-code-copy-button',
42
- },
43
- children: [],
11
+ pre(node) {
12
+ return {
13
+ ...node,
14
+ children: [
15
+ ...node.children,
16
+ {
17
+ type: 'element',
18
+ tagName: 'nova-code-copy-button',
19
+ properties: {
20
+ type: 'button',
21
+ 'data-code': this.source,
22
+ title: 'Copy code',
23
+ 'aria-label': 'Copy code',
24
+ class: 'nova-code-copy-button',
44
25
  },
45
- ],
46
- },
47
- ]
26
+ children: [],
27
+ },
28
+ ],
29
+ }
48
30
  },
49
31
  }
50
32
  }