@janga/norna 0.7.23 → 0.7.25

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 (82) hide show
  1. package/README.md +14 -18
  2. package/astro.config.mjs +14 -0
  3. package/bin/norna-cli.mjs +6 -0
  4. package/package.json +25 -2
  5. package/schemas/category.schema.json +2 -2
  6. package/schemas/config.schema.json +80 -15
  7. package/schemas/content-frontmatter.schema.json +7 -7
  8. package/schemas/page-theme.schema.json +26 -26
  9. package/schemas/sitewide-content.schema.json +18 -18
  10. package/schemas/theme.schema.json +246 -246
  11. package/scripts/build-site.mjs +1 -0
  12. package/scripts/check-config.mjs +16 -1
  13. package/scripts/dev-local.mjs +106 -11
  14. package/scripts/generate-search-index.mjs +57 -0
  15. package/scripts/init-site.mjs +3 -0
  16. package/scripts/lib/code-fence-metadata.mjs +228 -0
  17. package/scripts/lib/edit-source-link.mjs +102 -0
  18. package/scripts/lib/editor-language-service.mjs +32 -12
  19. package/scripts/lib/image-presentation.mjs +4 -0
  20. package/scripts/lib/navigation-model.mjs +34 -13
  21. package/scripts/lib/navigation-review.mjs +396 -0
  22. package/scripts/lib/norna-markdown-blocks.mjs +164 -26
  23. package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
  24. package/scripts/lib/page-aliases.mjs +21 -1
  25. package/scripts/lib/page-markdown.mjs +32 -1
  26. package/scripts/lib/page-move-plan.mjs +659 -0
  27. package/scripts/lib/presentation-palette-metadata.mjs +1 -1
  28. package/scripts/lib/presentation.mjs +1 -10
  29. package/scripts/lib/project-config.mjs +141 -5
  30. package/scripts/lib/public-asset-conventions.mjs +36 -2
  31. package/scripts/lib/schema-definitions.mjs +30 -5
  32. package/scripts/lib/schema-editor-metadata.mjs +36 -4
  33. package/scripts/lib/schema-value-definitions.mjs +4 -1
  34. package/scripts/lib/semantic-callouts.mjs +128 -0
  35. package/scripts/lib/site-content.mjs +2 -1
  36. package/scripts/lib/site-link-graph.mjs +33 -2
  37. package/scripts/lib/site-navigation-tree.mjs +85 -0
  38. package/scripts/lib/site-paths.mjs +14 -2
  39. package/scripts/lib/social-image-assets.mjs +30 -0
  40. package/scripts/lib/table-render-plugin.mjs +33 -0
  41. package/scripts/lib/theme-presets.mjs +2 -2
  42. package/scripts/lib/theme-profiles.mjs +0 -4
  43. package/scripts/move-site-page.mjs +256 -0
  44. package/scripts/review-navigation.mjs +32 -0
  45. package/scripts/sync-content-sections.mjs +67 -4
  46. package/scripts/sync-site-public.mjs +24 -5
  47. package/src/components/CardList.astro +1 -0
  48. package/src/components/CodeBlockCopyScript.astro +11 -5
  49. package/src/components/EditSourceLink.astro +17 -0
  50. package/src/components/ImageCarousel.astro +9 -7
  51. package/src/components/ImageStack.astro +34 -9
  52. package/src/components/ImageStackEnhancement.astro +331 -0
  53. package/src/components/NavigationPageTree.astro +66 -55
  54. package/src/components/NavigationTreeControls.astro +77 -0
  55. package/src/components/PageAliasRedirect.astro +1 -1
  56. package/src/components/PageContentsNavigation.astro +2 -3
  57. package/src/components/PageList.astro +33 -0
  58. package/src/components/PageSequenceNavigation.astro +37 -0
  59. package/src/components/SearchPage.astro +151 -0
  60. package/src/components/SectionNavigationScript.astro +45 -7
  61. package/src/components/SiteNavigation.astro +102 -51
  62. package/src/components/SitePage.astro +107 -39
  63. package/src/components/SiteSection.astro +11 -3
  64. package/src/components/SiteTreeNavigation.astro +11 -1
  65. package/src/components/TableOverflowScript.astro +251 -0
  66. package/src/components/TreeNavigationScript.astro +271 -34
  67. package/src/layouts/BaseLayout.astro +83 -2
  68. package/src/lib/generatedImages.ts +18 -0
  69. package/src/lib/sectionContent.ts +36 -30
  70. package/src/lib/siteNavigation.ts +31 -52
  71. package/src/lib/sitePages.ts +6 -3
  72. package/src/lib/sitePublicAssets.ts +1 -0
  73. package/src/pages/404.astro +110 -0
  74. package/src/pages/[...slug].astro +15 -5
  75. package/src/styles/content.css +538 -39
  76. package/src/styles/media.css +187 -8
  77. package/src/styles/navigation.css +199 -3
  78. package/src/styles/page-layout.css +481 -100
  79. package/src/styles/responsive.css +144 -28
  80. package/starters/basic/README.md +6 -19
  81. package/starters/basic/package.json +1 -0
  82. package/starters/basic/site/pages/000-home/content.md +13 -50
@@ -0,0 +1,128 @@
1
+ export const semanticCalloutTypes = Object.freeze([
2
+ 'NOTE',
3
+ 'TIP',
4
+ 'IMPORTANT',
5
+ 'WARNING',
6
+ 'CAUTION',
7
+ 'DANGER',
8
+ ]);
9
+
10
+ const semanticCalloutTypeSet = new Set(semanticCalloutTypes);
11
+ const markerPattern = /^\[!([A-Za-z][A-Za-z0-9-]*)\]([^\n]*)(?:\n|$)/;
12
+
13
+ const hasMeaningfulContent = (node) => {
14
+ if (!node || typeof node !== 'object') return false;
15
+ if ('value' in node && typeof node.value === 'string') return node.value.trim().length > 0;
16
+ if (node.type === 'image') return Boolean(node.alt);
17
+ return Array.isArray(node.children) && node.children.some(hasMeaningfulContent);
18
+ };
19
+
20
+ export const getSemanticCalloutMarker = (node) => {
21
+ if (node?.type !== 'blockquote') return null;
22
+ const firstParagraph = node.children?.[0];
23
+ const firstText = firstParagraph?.type === 'paragraph' ? firstParagraph.children?.[0] : null;
24
+ if (firstText?.type !== 'text') return null;
25
+
26
+ const match = firstText.value.match(markerPattern);
27
+ if (!match) {
28
+ return firstText.value.startsWith('[!')
29
+ ? { malformed: true, firstParagraph, firstText }
30
+ : null;
31
+ }
32
+
33
+ const rawType = match[1] ?? '';
34
+ const type = rawType.toUpperCase();
35
+ const markerSource = match[0];
36
+ const title = (match[2] ?? '').trim();
37
+ const firstTextRemainder = firstText.value.slice(markerSource.length);
38
+ const firstParagraphRemainder = [
39
+ { ...firstText, value: firstTextRemainder },
40
+ ...(firstParagraph.children?.slice(1) ?? []),
41
+ ];
42
+ const hasBody = firstParagraphRemainder.some(hasMeaningfulContent)
43
+ || (node.children?.slice(1) ?? []).some(hasMeaningfulContent);
44
+
45
+ return {
46
+ firstParagraph,
47
+ firstText,
48
+ firstTextRemainder,
49
+ hasBody,
50
+ malformed: false,
51
+ markerSource,
52
+ rawType,
53
+ supported: semanticCalloutTypeSet.has(type),
54
+ title,
55
+ type,
56
+ };
57
+ };
58
+
59
+ const formatDiagnostic = ({ code, fix, label, line, message, offset }) => ({
60
+ code,
61
+ fix,
62
+ line,
63
+ message: `${label} line ${line}: ${message}`,
64
+ offset,
65
+ });
66
+
67
+ export const getSemanticCalloutDiagnostics = (tree, options = {}) => {
68
+ const diagnostics = [];
69
+ const label = options.label ?? 'Markdown';
70
+ const lineOffset = options.lineOffset ?? 0;
71
+
72
+ const visit = (node, blockquoteDepth = 0) => {
73
+ if (!node || typeof node !== 'object') return;
74
+ const nextBlockquoteDepth = blockquoteDepth + (node.type === 'blockquote' ? 1 : 0);
75
+ const marker = getSemanticCalloutMarker(node);
76
+
77
+ if (marker) {
78
+ const line = lineOffset + (node.position?.start.line ?? 1);
79
+ const offset = node.position?.start.offset ?? 0;
80
+ let issue = null;
81
+
82
+ if (blockquoteDepth > 0) {
83
+ issue = {
84
+ code: 'nested-semantic-callout',
85
+ message: 'Semantic callouts cannot be nested inside another blockquote or callout.',
86
+ fix: 'Move the inner callout after the surrounding blockquote.',
87
+ };
88
+ } else if (marker.malformed) {
89
+ issue = {
90
+ code: 'invalid-semantic-callout-marker',
91
+ message: 'This blockquote starts like a semantic callout but its marker is incomplete.',
92
+ fix: 'Write the marker on its own first line, for example "> [!WARNING]".',
93
+ };
94
+ } else if (marker.rawType !== marker.type) {
95
+ issue = {
96
+ code: 'invalid-semantic-callout-type-case',
97
+ message: `Semantic callout type "${marker.rawType}" must use uppercase ASCII letters.`,
98
+ fix: `Write "> [!${marker.type}]".`,
99
+ };
100
+ } else if (!marker.supported) {
101
+ issue = {
102
+ code: 'unknown-semantic-callout-type',
103
+ message: `Unknown semantic callout type "${marker.rawType}".`,
104
+ fix: `Use one of: ${semanticCalloutTypes.join(', ')}.`,
105
+ };
106
+ } else if (marker.title) {
107
+ issue = {
108
+ code: 'unsupported-semantic-callout-title',
109
+ message: `Semantic callout markers cannot contain a custom title ("${marker.title}").`,
110
+ fix: `Keep "> [!${marker.type}]" on its own line. Begin the callout body with bold text when local emphasis is useful.`,
111
+ };
112
+ } else if (!marker.hasBody) {
113
+ issue = {
114
+ code: 'empty-semantic-callout',
115
+ message: `${marker.type} callout has no content.`,
116
+ fix: 'Add at least one quoted content line after the marker.',
117
+ };
118
+ }
119
+
120
+ if (issue) diagnostics.push(formatDiagnostic({ ...issue, label, line, offset }));
121
+ }
122
+
123
+ for (const child of node.children ?? []) visit(child, nextBlockquoteDepth);
124
+ };
125
+
126
+ visit(tree);
127
+ return diagnostics;
128
+ };
@@ -30,6 +30,7 @@ const knownNestedFrontmatterKeys = new Set([
30
30
  'align',
31
31
  'aliases',
32
32
  'alt',
33
+ 'baseUrl',
33
34
  'body',
34
35
  'blockGap',
35
36
  'caption',
@@ -152,7 +153,7 @@ export const validateFrontmatterStructure = (frontmatter, addIssue, {
152
153
  } else if (fileKind === 'content' && key === 'description') {
153
154
  fix = 'Indent "description:" under "page:". The optional meta description belongs in the page object.';
154
155
  } else if (fileKind === 'content' && key === 'images') {
155
- fix = 'Put local image references in norna-image-stack or norna-image-carousel blocks in the Markdown body.';
156
+ fix = 'Put local image references in image-stack or image-carousel blocks in the Markdown body.';
156
157
  } else if (fileKind === 'content' && knownThemeTopLevelFrontmatterKeys.has(key)) {
157
158
  fix = `Move "${key}:" to theme.yaml. Visual settings do not belong in content frontmatter.`;
158
159
  } else if ((fileKind === 'theme' || fileKind === 'page theme') && key === 'navigation') {
@@ -2,6 +2,7 @@ import { readdir, readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import {
4
4
  createPageAliasModel,
5
+ generatedSiteRoutes,
5
6
  } from './page-aliases.mjs';
6
7
  import { parsePageMarkdownSource } from './page-markdown.mjs';
7
8
  import {
@@ -117,6 +118,9 @@ const createPageRecord = ({ contentFile, data = {}, document }) => {
117
118
  anchors,
118
119
  contentFile,
119
120
  document,
121
+ navigation: {
122
+ listed: data.navigation?.listed ?? true,
123
+ },
120
124
  pathname,
121
125
  title: document.pageTitle?.title ?? contentFile.pageId,
122
126
  };
@@ -140,7 +144,13 @@ const looksLikePublicFile = (pathname) => {
140
144
  return (pathname.split('/').at(-1) ?? '').includes('.');
141
145
  };
142
146
 
143
- export const createSiteLinkGraph = ({ siteStructure, pageDocuments, publicFiles = [] }) => {
147
+ export const createSiteLinkGraph = ({
148
+ siteStructure,
149
+ pageDocuments,
150
+ publicFiles = [],
151
+ generatedRoutes,
152
+ }) => {
153
+ const activeGeneratedRoutes = generatedRoutes ?? generatedSiteRoutes;
144
154
  const documentsByDirectory = new Map(pageDocuments.map(({ contentFile, data, document }) => [
145
155
  contentFile.pageDirectory,
146
156
  { contentFile, data, document },
@@ -167,7 +177,11 @@ export const createSiteLinkGraph = ({ siteStructure, pageDocuments, publicFiles
167
177
  categories: siteStructure.categories,
168
178
  pages,
169
179
  publicFiles,
180
+ generatedRoutes: activeGeneratedRoutes,
170
181
  });
182
+ const generatedRoutesByPathname = new Map(
183
+ activeGeneratedRoutes.map((route) => [route.pathname, route]),
184
+ );
171
185
 
172
186
  const references = [];
173
187
  const referencesByTarget = new Map();
@@ -249,6 +263,18 @@ export const createSiteLinkGraph = ({ siteStructure, pageDocuments, publicFiles
249
263
  continue;
250
264
  }
251
265
 
266
+ const generatedRoute = generatedRoutesByPathname.get(
267
+ target.pageLookupPathname ?? target.pathname,
268
+ );
269
+ if (generatedRoute && !target.fragment) {
270
+ reference.resolution = {
271
+ kind: 'generated-route',
272
+ pathname: generatedRoute.pathname,
273
+ route: generatedRoute,
274
+ };
275
+ continue;
276
+ }
277
+
252
278
  const targetCategory = target.pageLookupPathname
253
279
  ? categoriesByPathname.get(target.pageLookupPathname)
254
280
  : null;
@@ -320,7 +346,12 @@ export const getSiteLinkGraph = async (options = {}) => {
320
346
  readPublicFiles(options.publicDir ?? sitePublicDir),
321
347
  ]);
322
348
 
323
- return createSiteLinkGraph({ pageDocuments, publicFiles, siteStructure });
349
+ return createSiteLinkGraph({
350
+ pageDocuments,
351
+ publicFiles,
352
+ siteStructure,
353
+ generatedRoutes: options.generatedRoutes,
354
+ });
324
355
  };
325
356
 
326
357
  export const getSitePublicFiles = async (publicDir = sitePublicDir) => readPublicFiles(publicDir);
@@ -0,0 +1,85 @@
1
+ export const getSiteNavigationTree = (entries) => {
2
+ const nodes = new Map();
3
+ const roots = [];
4
+
5
+ for (const entry of entries) {
6
+ nodes.set(entry.node.pagePath, { ...entry, children: [] });
7
+ }
8
+
9
+ for (const entry of entries) {
10
+ const node = nodes.get(entry.node.pagePath);
11
+ if (!node) continue;
12
+
13
+ if (entry.node.isHome || entry.node.parentPagePath === null) {
14
+ roots.push(node);
15
+ continue;
16
+ }
17
+
18
+ const parent = nodes.get(entry.node.parentPagePath);
19
+ if (!parent) {
20
+ throw new Error(`Site node "${entry.node.pagePath}" has no parent node "${entry.node.parentPagePath}".`);
21
+ }
22
+
23
+ parent.children.push(node);
24
+ }
25
+
26
+ return roots;
27
+ };
28
+
29
+ export const getListedSiteNavigationTree = (entries) => {
30
+ const filterListed = (nodes) => nodes.flatMap((node) => {
31
+ if (!node.node.isHome && !node.node.navigation.listed) return [];
32
+ const children = filterListed(node.children);
33
+ if (node.node.kind === 'category' && children.length === 0) return [];
34
+ return [{ ...node, children }];
35
+ });
36
+
37
+ return filterListed(getSiteNavigationTree(entries));
38
+ };
39
+
40
+ export const getFirstPageInNavigationNode = (node) => {
41
+ if (node.node.kind === 'page') return node.node;
42
+
43
+ for (const child of node.children) {
44
+ const page = getFirstPageInNavigationNode(child);
45
+ if (page) return page;
46
+ }
47
+
48
+ return null;
49
+ };
50
+
51
+ export const flattenSiteNavigationTree = (nodes) => nodes.flatMap((node) => [
52
+ node,
53
+ ...flattenSiteNavigationTree(node.children),
54
+ ]);
55
+
56
+ export const getSequentialPageNavigation = (nodes, currentPagePath) => {
57
+ const activeRoot = nodes.find((root) => (
58
+ flattenSiteNavigationTree([root]).some(({ node }) => node.pagePath === currentPagePath)
59
+ ));
60
+ if (!activeRoot) return { previous: null, next: null };
61
+
62
+ const pages = flattenSiteNavigationTree([activeRoot])
63
+ .filter(({ node }) => node.kind === 'page')
64
+ .map(({ node }) => node);
65
+ const currentIndex = pages.findIndex(({ pagePath }) => pagePath === currentPagePath);
66
+ if (currentIndex < 0) return { previous: null, next: null };
67
+
68
+ return {
69
+ previous: pages[currentIndex - 1] ?? null,
70
+ next: pages[currentIndex + 1] ?? null,
71
+ };
72
+ };
73
+
74
+ export const getDirectChildPages = (nodes, currentPagePath) => {
75
+ const current = flattenSiteNavigationTree(nodes)
76
+ .find(({ node }) => node.pagePath === currentPagePath);
77
+ if (!current) return [];
78
+ const children = current.node.isHome
79
+ ? nodes.filter(({ node }) => !node.isHome)
80
+ : current.children;
81
+
82
+ return children
83
+ .filter(({ node }) => node.kind === 'page')
84
+ .map(({ node }) => node);
85
+ };
@@ -5,6 +5,7 @@ import { homePageDirectory } from './site-conventions.mjs';
5
5
 
6
6
  const siteDirectoryEnvName = 'NORNA_SITE_DIR';
7
7
  const invocationRootEnvName = 'NORNA_INVOCATION_ROOT';
8
+ const stateDirectoryEnvName = 'NORNA_INTERNAL_STATE_DIR';
8
9
  const defaultSiteDirectory = 'site';
9
10
  export { homePageDirectory };
10
11
 
@@ -16,12 +17,19 @@ export const siteDirectoryEnv = siteDirectoryEnvName;
16
17
 
17
18
  const normalizeSiteDirectory = (value) => String(value ?? '').trim();
18
19
  const normalizeInvocationRoot = (value) => String(value ?? '').trim();
20
+ const normalizeStateDirectory = (value) => String(value ?? '').trim();
19
21
 
20
22
  const configuredInvocationRoot = normalizeInvocationRoot(process.env[invocationRootEnvName]);
21
23
  export const invocationRoot = configuredInvocationRoot
22
24
  ? path.resolve(configuredInvocationRoot)
23
25
  : process.cwd();
24
26
 
27
+ const hasStateDirectoryEnv = Object.hasOwn(process.env, stateDirectoryEnvName);
28
+ const configuredStateDirectory = normalizeStateDirectory(process.env[stateDirectoryEnvName]);
29
+ if (hasStateDirectoryEnv && !configuredStateDirectory) {
30
+ throw new Error(`${stateDirectoryEnvName} must not be empty.`);
31
+ }
32
+
25
33
  const hasSiteFilesInDirectory = (siteDir) => {
26
34
  return (
27
35
  existsSync(path.join(siteDir, 'config.yaml'))
@@ -126,9 +134,13 @@ export const siteHomePageDir = path.join(sitePagesDir, homePageDirectory);
126
134
  export const siteContentPath = path.join(siteHomePageDir, 'content.md');
127
135
  export const siteImagesDir = path.join(siteHomePageDir, 'images');
128
136
  export const sitePublicDir = path.join(siteDir, 'public');
129
- export const siteStateDir = path.join(siteDir, '.norna');
137
+ export const siteStateDir = configuredStateDirectory
138
+ ? path.resolve(configuredStateDirectory)
139
+ : path.join(siteDir, '.norna');
130
140
  export const astroPublicDir = path.join(siteStateDir, 'public');
131
- export const astroDistDir = path.join(siteProjectRoot, 'dist');
141
+ export const astroDistDir = configuredStateDirectory
142
+ ? path.join(siteStateDir, 'dist')
143
+ : path.join(siteProjectRoot, 'dist');
132
144
  export const astroRootDir = siteStateDir;
133
145
  export const astroCacheDir = path.join(astroRootDir, '.astro');
134
146
  export const generatedImagesDir = path.join(astroPublicDir, 'images', 'generated');
@@ -0,0 +1,30 @@
1
+ import { getPublicAssetInspection } from './logo-assets.mjs';
2
+ import {
3
+ socialImageAssetDefinitions,
4
+ socialImageAssetFilenames,
5
+ } from './public-asset-conventions.mjs';
6
+ import { sitePublicLabel } from './site-paths.mjs';
7
+
8
+ const definitionsByFilename = new Map(
9
+ socialImageAssetDefinitions.map((definition) => [definition.filename, definition]),
10
+ );
11
+
12
+ export const getSocialImageAssets = () => getPublicAssetInspection().socialImages
13
+ .map((filename) => ({
14
+ ...definitionsByFilename.get(filename),
15
+ filename,
16
+ href: `/${filename}`,
17
+ }));
18
+
19
+ export const getSocialImageAsset = () => {
20
+ const assets = getSocialImageAssets();
21
+
22
+ if (assets.length > 1) {
23
+ throw new Error([
24
+ `Found multiple social sharing images in ${sitePublicLabel}. Keep exactly one of ${socialImageAssetFilenames.join(', ')}.`,
25
+ ...assets.map(({ filename }) => `- ${sitePublicLabel}/${filename}`),
26
+ ].join('\n'));
27
+ }
28
+
29
+ return assets[0] ?? null;
30
+ };
@@ -0,0 +1,33 @@
1
+ import { defineHastPlugin } from 'satteri';
2
+
3
+ export const nornaTableRenderPlugin = defineHastPlugin({
4
+ name: 'norna-table-render',
5
+ element: {
6
+ filter: ['table'],
7
+ visit(node, context) {
8
+ context.replaceNode(node, {
9
+ type: 'element',
10
+ tagName: 'div',
11
+ properties: {
12
+ className: ['norna-table-frame', 'content-block-note-lane-boundary'],
13
+ 'data-table-frame': '',
14
+ },
15
+ children: [{
16
+ type: 'element',
17
+ tagName: 'div',
18
+ properties: {
19
+ className: ['norna-table-scroll'],
20
+ 'data-table-scroll': '',
21
+ tabIndex: 0,
22
+ },
23
+ children: [{
24
+ type: 'element',
25
+ tagName: node.tagName,
26
+ properties: { ...(node.properties ?? {}) },
27
+ children: [...(node.children ?? [])],
28
+ }],
29
+ }],
30
+ });
31
+ },
32
+ },
33
+ });
@@ -4,7 +4,7 @@ import { resolveThemeProfileRecipe } from './theme-profiles.mjs';
4
4
 
5
5
  export const themePresetRecipes = Object.freeze({
6
6
  portfolio: Object.freeze({
7
- color: 'near-monochrome-dark',
7
+ color: 'near-monochrome-adaptive',
8
8
  typography: 'restrained-sans',
9
9
  rhythm: 'balanced',
10
10
  geometry: 'image-led',
@@ -231,7 +231,7 @@ export const renderThemePresetReference = (presetName, sourceLabel = 'theme.yaml
231
231
  'blocks:',
232
232
  ' cardList:',
233
233
  ' # Alternatives: text, narrow, normal, wide.',
234
- ' # A width written in a norna-card-list block overrides this default.',
234
+ ' # A width written in a card-list block overrides this default.',
235
235
  ` width: ${blocks.cardList.width}`,
236
236
  '',
237
237
  'typography:',
@@ -2,10 +2,6 @@ import { freezeDeep, mergeDeep } from './object.mjs';
2
2
 
3
3
  export const themeProfileDefinitions = freezeDeep({
4
4
  color: {
5
- 'near-monochrome-dark': {
6
- appearance: { default: 'dark' },
7
- palette: 'near-monochrome',
8
- },
9
5
  'near-monochrome-adaptive': {
10
6
  appearance: { default: 'system' },
11
7
  palette: 'near-monochrome',