@master/internal-site 0.1.2 → 0.1.3

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 (183) hide show
  1. package/README.md +6 -0
  2. package/dist/astro.mjs +145 -10
  3. package/package.json +1 -1
  4. package/src/astro/index.ts +2 -0
  5. package/src/astro/routes.template.ts +1 -0
  6. package/src/astro/style-assets.ts +107 -0
  7. package/src/benchmarks/BenchmarkBarRows.astro +3 -2
  8. package/src/benchmarks/BenchmarkBars.astro +1 -1
  9. package/src/benchmarks/BenchmarkChart.astro +1 -0
  10. package/src/benchmarks/BenchmarkChartGroup.astro +1 -1
  11. package/src/benchmarks/BenchmarkDataTable.astro +1 -1
  12. package/src/benchmarks/BenchmarkFigure.astro +2 -1
  13. package/src/benchmarks/BenchmarkMetricTable.astro +1 -1
  14. package/src/benchmarks/BenchmarkMetrics.astro +1 -1
  15. package/src/benchmarks/BenchmarkSampleSummary.astro +1 -1
  16. package/src/benchmarks/BenchmarkScrollRegion.svelte +2 -1
  17. package/src/benchmarks/BenchmarkSource.astro +1 -1
  18. package/src/benchmarks/BenchmarkStackedBars.astro +2 -1
  19. package/src/benchmarks/types.ts +2 -0
  20. package/src/blog/article/BlogArticleContents.svelte +1 -1
  21. package/src/blog/article/BlogArticleMasthead.astro +12 -1
  22. package/src/css/NamespaceUtilityTable.astro +1 -1
  23. package/src/css/ThemeNumberVariableTable.astro +1 -1
  24. package/src/css/markdown.ts +14 -5
  25. package/src/demo/Demo.svelte +4 -0
  26. package/src/demo/DemoAsset.astro +1 -0
  27. package/src/demo/DemoCatalog.astro +1 -0
  28. package/src/demo/DemoComparison.svelte +3 -0
  29. package/src/demo/DemoContainer.svelte +3 -0
  30. package/src/demo/DemoControls.svelte +2 -0
  31. package/src/demo/DemoCopyButton.svelte +3 -0
  32. package/src/demo/DemoCopyGroup.svelte +2 -0
  33. package/src/demo/DemoFeatureSupport.svelte +2 -0
  34. package/src/demo/DemoGeneratedCSS.svelte +2 -0
  35. package/src/demo/DemoIndex.astro +1 -0
  36. package/src/demo/DemoMotion.svelte +1 -0
  37. package/src/demo/DemoScrollArea.svelte +2 -0
  38. package/src/demo/DemoStyleComparison.astro +1 -0
  39. package/src/demo/DemoSurface.svelte +2 -0
  40. package/src/demo/DemoThemeComparison.astro +1 -0
  41. package/src/demo/DemoTokenTable.astro +1 -1
  42. package/src/demo/DemoViewport.svelte +5 -0
  43. package/src/demo/DemoWaterfall.astro +1 -1
  44. package/src/demo/foundations/FoundationDialog.astro +1 -0
  45. package/src/demo/foundations/FoundationMedia.astro +1 -0
  46. package/src/demo/foundations/FoundationShrink.astro +1 -0
  47. package/src/demo/foundations/Palette.svelte +2 -0
  48. package/src/demo/illustration-materials.ts +1 -1
  49. package/src/demo/illustration.css +218 -0
  50. package/src/docs-shell/components/ArticleHeader.astro +2 -0
  51. package/src/docs-shell/components/Callout.astro +1 -0
  52. package/src/docs-shell/components/CodeCopyButton.astro +2 -0
  53. package/src/docs-shell/components/CodeTabBar.astro +2 -0
  54. package/src/docs-shell/components/CodeTabs.astro +2 -1
  55. package/src/docs-shell/components/CodeView.astro +1 -1
  56. package/src/docs-shell/components/DocHeader.svelte +10 -13
  57. package/src/docs-shell/components/DocMenuButton.svelte +126 -60
  58. package/src/docs-shell/components/DocSidebar.svelte +2 -0
  59. package/src/docs-shell/components/DocVersionSelect.svelte +2 -0
  60. package/src/docs-shell/components/DocumentAPIIndex.astro +1 -1
  61. package/src/docs-shell/components/DocumentChoices.astro +1 -1
  62. package/src/docs-shell/components/DocumentCodeExampleView.astro +3 -1
  63. package/src/docs-shell/components/DocumentCodeTable.astro +1 -1
  64. package/src/docs-shell/components/DocumentCopyButton.svelte +3 -0
  65. package/src/docs-shell/components/DocumentDisclosure.astro +2 -0
  66. package/src/docs-shell/components/DocumentFileTree.astro +3 -0
  67. package/src/docs-shell/components/DocumentFlow.astro +4 -1
  68. package/src/docs-shell/components/DocumentIllustration.astro +5 -11
  69. package/src/docs-shell/components/DocumentKeyList.astro +1 -1
  70. package/src/docs-shell/components/DocumentNamespaceTable.astro +1 -1
  71. package/src/docs-shell/components/DocumentOptionEntry.astro +0 -1
  72. package/src/docs-shell/components/DocumentOptionList.astro +1 -1
  73. package/src/docs-shell/components/DocumentOptions.astro +0 -1
  74. package/src/docs-shell/components/DocumentParameters.astro +1 -1
  75. package/src/docs-shell/components/DocumentPrompt.astro +2 -0
  76. package/src/docs-shell/components/DocumentValueList.astro +1 -1
  77. package/src/docs-shell/components/DocumentationIndex.svelte +2 -0
  78. package/src/docs-shell/components/ExpandContent.svelte +3 -0
  79. package/src/docs-shell/components/ExplorerView.astro +1 -0
  80. package/src/docs-shell/components/GeneratedCSS.astro +1 -0
  81. package/src/docs-shell/components/Header.svelte +1 -1
  82. package/src/docs-shell/components/InlineCode.astro +1 -0
  83. package/src/docs-shell/components/InteractingIndicator.astro +1 -0
  84. package/src/docs-shell/components/LanguageButton.svelte +1 -0
  85. package/src/docs-shell/components/LanguageSelect.svelte +2 -0
  86. package/src/docs-shell/components/NotFound.astro +3 -0
  87. package/src/docs-shell/components/PMAction.astro +1 -0
  88. package/src/docs-shell/components/PageContent.svelte +1 -1
  89. package/src/docs-shell/components/PageContentDisclosure.astro +2 -1
  90. package/src/docs-shell/components/PageContentList.svelte +2 -0
  91. package/src/docs-shell/components/PageNavs.astro +1 -0
  92. package/src/docs-shell/components/SearchButton.svelte +3 -0
  93. package/src/docs-shell/components/SearchDialog.svelte +6 -1
  94. package/src/docs-shell/components/Segments.svelte +2 -0
  95. package/src/docs-shell/components/StepSection.astro +1 -2
  96. package/src/docs-shell/components/Tabs.astro +1 -0
  97. package/src/docs-shell/components/ThemeSelect.svelte +2 -0
  98. package/src/docs-shell/components/markdown/Table.astro +1 -0
  99. package/src/docs-shell/components/markdown-components.ts +0 -1
  100. package/src/docs-shell/remark/markdown-components.js +28 -7
  101. package/src/docs-shell/scripts/document.ts +1 -1
  102. package/src/docs-shell/types/Metadata.ts +11 -0
  103. package/src/docs-shell/utils/generate-metadata.ts +3 -1
  104. package/src/docs-shell/utils/head.ts +5 -2
  105. package/src/examples/components/ArticleTransitionScene.svelte +4 -0
  106. package/src/examples/layout-system/page.astro +0 -1
  107. package/src/home/HomeBentoFrame.astro +1 -0
  108. package/src/home/HomeBentoGrid.astro +1 -0
  109. package/src/home/bento.css +167 -0
  110. package/src/home/design.css +1 -1
  111. package/src/home/home.css +4 -576
  112. package/src/home/segmented-control.css +94 -0
  113. package/src/layouts/Document.astro +3 -1
  114. package/src/public/icons/json.svg +1 -0
  115. package/src/scripts/atomic-class-authoring.ts +98 -0
  116. package/src/scripts/style-delivery.mjs +110 -0
  117. package/src/sections/blog.astro +1 -1
  118. package/src/sections/guide.astro +1 -2
  119. package/src/styles/blog-article.css +10 -7
  120. package/src/styles/blog-materials.css +13 -0
  121. package/src/styles/demo.css +0 -1
  122. package/src/styles/docs-shell/code.css +5 -61
  123. package/src/styles/docs-shell/documentation-search.css +1 -0
  124. package/src/styles/docs-shell/header.css +2 -73
  125. package/src/styles/docs-shell/menu.css +37 -17
  126. package/src/styles/docs-shell/page-content.css +2 -37
  127. package/src/styles/docs-shell/prose.css +18 -9
  128. package/src/styles/docs-shell/steps.css +2 -39
  129. package/src/styles/document-comparison.css +5 -2
  130. package/src/styles/documentation-illustration.css +1 -9
  131. package/src/styles/documentation-index.css +2 -1
  132. package/src/styles/documents.css +2 -4
  133. package/src/styles/interface.css +5 -3
  134. package/src/styles/system/DESIGN.md +34 -14
  135. package/src/styles/system/browser.css +21 -0
  136. package/src/styles/system/button-feedback.css +13 -0
  137. package/src/styles/system/callout.css +14 -0
  138. package/src/styles/system/chart.css +18 -0
  139. package/src/styles/system/checks.css +18 -0
  140. package/src/styles/system/code-panel.css +82 -0
  141. package/src/styles/system/code-tabs.css +40 -0
  142. package/src/styles/system/code.css +8 -85
  143. package/src/styles/system/compare.css +7 -0
  144. package/src/styles/system/controls.css +10 -109
  145. package/src/styles/system/copy.css +32 -0
  146. package/src/styles/system/data.css +10 -134
  147. package/src/styles/system/dialog.css +16 -0
  148. package/src/styles/system/disclosure.css +19 -0
  149. package/src/styles/system/expand.css +11 -0
  150. package/src/styles/system/fields.css +25 -0
  151. package/src/styles/system/header-core.css +101 -0
  152. package/src/styles/system/hint.css +5 -0
  153. package/src/styles/system/index.css +13 -0
  154. package/src/styles/system/inline-code.css +4 -0
  155. package/src/styles/system/kbd.css +4 -0
  156. package/src/styles/system/logos.css +10 -0
  157. package/src/styles/system/menu.css +14 -0
  158. package/src/styles/system/metrics.css +22 -0
  159. package/src/styles/system/pagehead.css +13 -0
  160. package/src/styles/system/pager.css +13 -0
  161. package/src/styles/system/parameters.css +17 -0
  162. package/src/styles/system/prompt.css +7 -0
  163. package/src/styles/system/prose.css +4 -75
  164. package/src/styles/system/range.css +12 -0
  165. package/src/styles/system/reading.css +44 -0
  166. package/src/styles/system/scroll.css +6 -0
  167. package/src/styles/system/search-trigger.css +10 -0
  168. package/src/styles/system/segmented.css +22 -0
  169. package/src/styles/system/sheet.css +8 -0
  170. package/src/styles/system/shell.css +8 -102
  171. package/src/styles/system/sidebar.css +14 -0
  172. package/src/styles/system/source.css +5 -0
  173. package/src/styles/system/specimens.css +3 -0
  174. package/src/styles/system/stage.css +18 -0
  175. package/src/styles/system/steps.css +51 -0
  176. package/src/styles/system/surfaces.css +8 -104
  177. package/src/styles/system/table.css +23 -0
  178. package/src/styles/system/toc.css +59 -0
  179. package/src/styles/system/tooltip.css +6 -0
  180. package/src/styles/system/tray-code.css +6 -0
  181. package/src/styles/system/tray.css +20 -0
  182. package/src/styles/system/tree.css +15 -0
  183. package/src/styles/system/waterfall.css +14 -0
@@ -52,6 +52,9 @@ if (['dark', 'light'].includes(value)) document.documentElement.style.colorSchem
52
52
  <link blocking="render" rel="preload" as="font" href="/fonts/IBMPlexMono-Regular.woff2" crossorigin="anonymous" />
53
53
  {(site.locale === 'tw' || basePathname === '/brand') && <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100..900&display=swap" rel="stylesheet" />}
54
54
  <style is:inline blocking="render" set:html={fontCSS}></style>
55
+ {/* client:load islands can hydrate while a streamed body is still parsing.
56
+ Their request-local context must already exist, even with cached CSS/JS. */}
57
+ <script is:inline type="application/json" id={siteContextElementId} set:html={siteContextJSON}></script>
55
58
  {assets.links.map((href) => <link rel="stylesheet" href={href} />)}
56
59
  {assets.styles.map((css) => <style is:inline set:html={css}></style>)}
57
60
  <title>{head.title}</title>
@@ -62,7 +65,6 @@ if (['dark', 'light'].includes(value)) document.documentElement.style.colorSchem
62
65
  <slot />
63
66
  {/* After the content, so persisted code tabs apply in the first pass, before paint. */}
64
67
  {!bare && <script is:inline set:html={`(${codeTabsBootstrap.toString()})();`}></script>}
65
- <script is:inline type="application/json" id={siteContextElementId} set:html={siteContextJSON}></script>
66
68
  <script>
67
69
  import '../docs-shell/scripts/document'
68
70
  import '../docs-shell/scripts/code'
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M5.759 3.975h1.783V5.76H5.759v4.458A1.783 1.783 0 0 1 3.975 12a1.783 1.783 0 0 1 1.784 1.783v4.459h1.783v1.783H5.759c-.954-.24-1.784-.803-1.784-1.783v-3.567a1.783 1.783 0 0 0-1.783-1.783H1.3v-1.783h.892a1.783 1.783 0 0 0 1.783-1.784V5.76A1.783 1.783 0 0 1 5.76 3.975m12.483 0a1.783 1.783 0 0 1 1.783 1.784v3.566a1.783 1.783 0 0 0 1.783 1.784h.892v1.783h-.892a1.783 1.783 0 0 0-1.783 1.783v3.567a1.783 1.783 0 0 1-1.783 1.783h-1.784v-1.783h1.784v-4.459A1.783 1.783 0 0 1 20.025 12a1.783 1.783 0 0 1-1.783-1.783V5.759h-1.784V3.975h1.784M12 14.675a.892.892 0 0 1 .892.892.892.892 0 0 1-.892.892.892.892 0 0 1-.891-.892.892.892 0 0 1 .891-.892m-3.566 0a.892.892 0 0 1 .891.892.892.892 0 0 1-.891.892.892.892 0 0 1-.892-.892.892.892 0 0 1 .892-.892m7.133 0a.892.892 0 0 1 .891.892.892.892 0 0 1-.891.892.892.892 0 0 1-.892-.892.892.892 0 0 1 .892-.892z" fill="#fbc02d"/></svg>
@@ -0,0 +1,98 @@
1
+ /** Repository authoring policy only. Native CSS and runtime parsing are unchanged. */
2
+ import { createToolingSessionSync } from '@master/css-tooling/node'
3
+ import type { MasterCSSManifest } from '@master/css-schema/manifest'
4
+ import preset from '@master/css-preset/default-manifest.json' with { type: 'json' }
5
+
6
+ export const compositeClassProperties = new Set(`all animation animation-range background border border-block border-block-end border-block-start border-bottom border-image border-inline border-inline-end border-inline-start border-left border-right border-top column-rule columns container flex flex-flow font font-synthesis font-variant grid grid-template line-clamp list-style mask mask-border offset outline scroll-timeline text-decoration text-emphasis text-wrap transition view-timeline white-space -webkit-text-stroke -webkit-mask -webkit-mask-box-image`.split(' '))
7
+ const axisProperties = new Set(`background-position background-repeat border-block-color border-block-style border-block-width border-color border-inline-color border-inline-style border-inline-width border-radius border-style border-width contain-intrinsic-size gap grid-area grid-column grid-row inset inset-block inset-inline margin margin-block margin-inline mask-position mask-repeat overflow overscroll-behavior padding padding-block padding-inline place-content place-items place-self scroll-margin scroll-margin-block scroll-margin-inline scroll-padding scroll-padding-block scroll-padding-inline -webkit-mask-position -webkit-mask-repeat`.split(' '))
8
+
9
+ export function atomicClassIssue(className: string): string | undefined {
10
+ const match = /^\(([a-z-]+):(.+)$/.exec(className)
11
+ if (!match) return
12
+ const [, property, tail] = match
13
+ if (!compositeClassProperties.has(property) && !axisProperties.has(property)) return
14
+ let depth = 0
15
+ let value = ''
16
+ let quote = ''
17
+ // The value ends at the closing parenthesis of the `(property:value)` head.
18
+ for (let index = 0; index < tail.length; index++) {
19
+ const char = tail[index]
20
+ if (char === '\\') { value += char + (tail[++index] ?? ''); continue }
21
+ if (quote) { if (char === quote) quote = ''; value += char; continue }
22
+ if (char === '"' || char === "'") { quote = char; value += char; continue }
23
+ if (char === '(') depth++
24
+ if (char === ')' && !depth--) break
25
+ value += !depth && (char === '|' || char === '/' || char === ',') ? '\0' : char
26
+ }
27
+ // A single grid line, named area, or span is one placement setting.
28
+ if (property.startsWith('grid-') && !tail.includes('/')) return
29
+ const parts = value.split('\0')
30
+ if (compositeClassProperties.has(property) && parts.length > 1) return `Express independent settings of ${property} separately`
31
+ if (new Set(parts).size > 1) return `Express different edges or axes of ${property} independently`
32
+ }
33
+
34
+ export function createAtomicClassAudit() {
35
+ const session = createToolingSessionSync({ manifest: preset as unknown as MasterCSSManifest })
36
+ return {
37
+ inspect(source: string, languageId: string) {
38
+ const positions = session.analyzeLintDocument(source, languageId).classPositions
39
+ const issues = positions.flatMap(position => {
40
+ const message = atomicClassIssue(position.token)
41
+ return message ? [{ ...position, message }] : []
42
+ })
43
+ const contexts = new Map<string, typeof positions[number][]>()
44
+ for (const position of positions) {
45
+ const key = `${position.contextRange.start}:${position.contextRange.end}`
46
+ const group = contexts.get(key) ?? []
47
+ group.push(position)
48
+ contexts.set(key, group)
49
+ }
50
+ for (const group of contexts.values()) {
51
+ for (const conflict of session.lintClassNames(group.map(item => item.token)).partialConflicts) {
52
+ const position = group.find(item => item.token === conflict.className)!
53
+ issues.push({ ...position, message: `Overlapping class ${conflict.conflict}; express each edge independently` })
54
+ }
55
+ }
56
+ return issues
57
+ },
58
+ dispose: () => session.dispose()
59
+ }
60
+ }
61
+
62
+ /** Inventory is explicit so adding a framework or relocating content cannot silently shrink coverage. */
63
+ export async function auditAuthoringRoots(roots: { name: string, directory: string, patterns?: string[] }[]) {
64
+ const { default: glob } = await import('fast-glob')
65
+ const { readFile } = await import('node:fs/promises')
66
+ const { resolve, extname } = await import('node:path')
67
+ const audit = createAtomicClassAudit()
68
+ const inventory: string[] = []
69
+ const failures: { file: string, line: number, token: string, message: string }[] = []
70
+ try {
71
+ for (const root of roots) {
72
+ const files = await glob(root.patterns ?? ['**/*.{astro,svelte,mdx,html,ts,tsx,js,jsx,vue}'], {
73
+ cwd: root.directory,
74
+ ignore: ['**/node_modules/**', '**/dist/**', '**/out/**', '**/.astro/**', '**/.cache/**', '**/*.test.*', '**/*.spec.*', '**/tests/**']
75
+ })
76
+ if (!files.length) throw new Error(`Empty authoring audit root: ${root.name}`)
77
+ for (const file of files.sort()) {
78
+ const name = `${root.name}/${file}`
79
+ inventory.push(name)
80
+ const source = await readFile(resolve(root.directory, file), 'utf8')
81
+ const extension = extname(file)
82
+ const language = ({ '.astro': 'astro', '.svelte': 'svelte', '.mdx': 'mdx', '.vue': 'vue' } as Record<string, string>)[extension] ?? (/\.[jt]sx?$/.test(file) ? 'typescriptreact' : 'html')
83
+ const add = (text: string, languageId: string, offset = 0) => {
84
+ for (const issue of audit.inspect(text, languageId)) {
85
+ failures.push({ file: name, line: source.slice(0, offset + issue.range.start).split('\n').length, token: issue.token, message: issue.message })
86
+ }
87
+ }
88
+ add(source, language)
89
+ // Preserve the existing policy's escaped HTML generator coverage.
90
+ if (language === 'typescriptreact') for (const match of source.matchAll(/"(?:\\.|[^"\\])*"/g)) {
91
+ if (!match[0].includes('class=\\"')) continue
92
+ add(JSON.parse(match[0]), 'html', match.index)
93
+ }
94
+ }
95
+ }
96
+ } finally { audit.dispose() }
97
+ return { inventory, failures }
98
+ }
@@ -0,0 +1,110 @@
1
+ /** Audit the built CSS graph. Bytes are asset sizes, not browser transfer estimates. */
2
+ import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises'
3
+ import { resolve, relative, dirname } from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
5
+ import { createHash } from 'node:crypto'
6
+ import { gzipSync, brotliCompressSync } from 'node:zlib'
7
+
8
+ export async function filesAt(root) {
9
+ const files = []
10
+ for (const entry of await readdir(root, { withFileTypes: true })) {
11
+ const file = resolve(root, entry.name)
12
+ if (entry.isDirectory()) files.push(...await filesAt(file))
13
+ else files.push(file)
14
+ }
15
+ return files.sort()
16
+ }
17
+ const hash = text => createHash('sha256').update(text).digest('hex')
18
+ const sizes = text => ({ bytes: Buffer.byteLength(text), gzip: gzipSync(text).length, brotli: brotliCompressSync(text).length })
19
+ const attributes = text => Object.fromEntries([...text.matchAll(/([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)].map(m => [m[1], m[2] ?? m[3]]))
20
+ export const importsOf = css => [...css.replace(/\/\*[\s\S]*?\*\//g, '').matchAll(/@import\s+(?:url\(\s*)?["']([^"']+)["']/g)].map(m => m[1])
21
+ const families = ['sidebar', 'toc', 'pager', 'tree', 'tray', 'switch', 'range', 'chart', 'code', 'table']
22
+
23
+ export async function auditStyles(directory) {
24
+ const root = resolve(directory), assets = {}, routes = {}
25
+ const origin = 'https://style-audit.invalid'
26
+ async function asset(href) {
27
+ if (assets[href]) return assets[href]
28
+ const file = resolve(root, '.' + decodeURIComponent(href))
29
+ if (!file.startsWith(root + '/')) throw new Error(`CSS escapes output: ${href}`)
30
+ const css = await readFile(file, 'utf8')
31
+ return assets[href] = { ...sizes(css), sha256: hash(css), imports: importsOf(css), css }
32
+ }
33
+ for (const file of await filesAt(root)) {
34
+ if (!file.endsWith('.html')) continue
35
+ const html = await readFile(file, 'utf8'), seen = new Set()
36
+ const pathname = '/' + relative(root, file).replaceAll('\\', '/')
37
+ const route = pathname === '/index.html' ? '/' : pathname.replace(/(?:\/index)?\.html$/, '')
38
+ let depth = 0
39
+ async function walk(href, parent, level, ancestors = []) {
40
+ const url = new URL(href, origin + parent)
41
+ if (url.origin !== origin || url.protocol !== 'https:') return
42
+ const key = url.pathname
43
+ if (ancestors.includes(key)) throw new Error(`Cyclic CSS import: ${[...ancestors, key].join(' -> ')}`)
44
+ depth = Math.max(depth, level)
45
+ const item = await asset(key)
46
+ seen.add(key)
47
+ for (const dep of item.imports) await walk(dep, key, level + 1, [...ancestors, key])
48
+ }
49
+ const links = [...html.matchAll(/<link\b([^>]+)>/g)].map(m => attributes(m[1])).filter(a => a.rel === 'stylesheet').map(a => a.href)
50
+ for (const href of links) await walk(href, pathname, 1)
51
+ const inline = [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/g)].map(m => m[1])
52
+ for (const css of inline) for (const href of importsOf(css)) await walk(href, pathname, 1)
53
+ const texts = [...seen].map(href => assets[href].css)
54
+ const delivered = [...texts, ...inline].join('\n')
55
+ const totals = [...seen].map(href => assets[href]).concat(inline.map(sizes))
56
+ routes[route] = {
57
+ links, assets: [...seen], files: seen.size, depth,
58
+ ...Object.fromEntries(['bytes', 'gzip', 'brotli'].map(k => [k, totals.reduce((n, x) => n + x[k], 0)])),
59
+ inlineBytes: inline.reduce((n, s) => n + Buffer.byteLength(s), 0),
60
+ families: Object.fromEntries(families.map(name => [name, new RegExp(`(?:^|[;{}])\\s*\\.ui-${name}\\s*\\{`).test(delivered)])),
61
+ foundationCount: [...delivered.matchAll(/body,\s*:host\s*\{\s*font-family:\s*var\(--font-family-sans\)/g)].length,
62
+ uncompiled: /(?:^|[;{}])\s*@(apply|compose|reference|prune|light|dark|variant|theme|mixin|utility|master|source|safelist|blocklist)\b/.test(delivered),
63
+ layerOrder: [...new Set([...delivered.matchAll(/@layer\s+([\w-]+(?:\s*,\s*[\w-]+)*)\s*[;{]/g)].flatMap(m => m[1].split(/\s*,\s*/)))],
64
+ }
65
+ }
66
+ return { version: 1, routes, assets: Object.fromEntries(Object.entries(assets).map(([href, { css, ...item }]) => [href, item])) }
67
+ }
68
+
69
+ /** Exact per-route ceilings. New routes require an explicit reviewed budget. */
70
+ export function assertStyleBudget(report, budget) {
71
+ const failures = []
72
+ if (!Object.keys(report.routes).length) throw new Error('Empty style delivery inventory')
73
+ for (const [route, item] of Object.entries(report.routes)) {
74
+ const cap = budget.routes[route]
75
+ if (!cap) { failures.push(`${route}: missing reviewed budget`); continue }
76
+ for (const key of ['files', 'depth', 'gzip', 'brotli', 'inlineBytes']) {
77
+ if (item[key] > cap[key]) failures.push(`${route}: ${key} ${item[key]} > ${cap[key]}`)
78
+ }
79
+ if (item.foundationCount !== 1) failures.push(`${route}: foundation count ${item.foundationCount}`)
80
+ if (item.layerOrder.join(',') !== 'theme,base,defaults,components,utilities') failures.push(`${route}: layer order`)
81
+ if (item.uncompiled) failures.push(`${route}: uncompiled directives`)
82
+ if (['/', '/404', '/tw', '/tw/404'].includes(route)) {
83
+ for (const name of ['sidebar', 'toc', 'pager', 'switch', 'range']) if (item.families[name]) failures.push(`${route}: unrelated ${name}`)
84
+ }
85
+ }
86
+ for (const route of Object.keys(budget.routes)) if (!report.routes[route]) failures.push(`${route}: missing route`)
87
+ if (failures.length) throw new Error(failures.join('\n'))
88
+ }
89
+
90
+ export function compareStyles(before, after) {
91
+ return Object.entries(after.routes).map(([route, b]) => {
92
+ const a = before.routes[route]
93
+ return { route, ...(a ? Object.fromEntries(['files', 'depth', 'bytes', 'gzip', 'brotli'].map(k => [k, b[k] - a[k]])) : { added: true }) }
94
+ })
95
+ }
96
+
97
+ export async function runStyleAudit(args) {
98
+ const [root, output, comparison, cap] = args
99
+ const baseline = comparison === '--budget' ? undefined : comparison
100
+ const budget = cap
101
+ if (!root || !output) throw new Error('Usage: node style-delivery.mjs <built-output> <report.json> [baseline.json]')
102
+ const report = await auditStyles(root)
103
+ await mkdir(dirname(resolve(output)), { recursive: true })
104
+ await writeFile(output, JSON.stringify(report, null, 2) + '\n')
105
+ if (budget) assertStyleBudget(report, JSON.parse(await readFile(budget, 'utf8')))
106
+ if (baseline) console.log(JSON.stringify(compareStyles(JSON.parse(await readFile(baseline, 'utf8')), report), null, 2))
107
+ else console.log(`${Object.keys(report.routes).length} routes, ${Object.keys(report.assets).length} CSS assets audited`)
108
+ }
109
+
110
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) await runStyleAudit(process.argv.slice(2))
@@ -2,7 +2,7 @@
2
2
  import DocumentShell from './document.astro'
3
3
  import DocHeader from '@master/internal-site/components/DocHeader.svelte'
4
4
 
5
- export const bodyClass = '(background-repeat:no-repeat) (background-image:linear-gradient(oklch(100%|0|none),var(--color-zinc-0)|100vh,var(--color-zinc-0))) (background-size:cover) (background-image:linear-gradient(var(--color-zinc-90),var(--color-zinc-95)|100vh,var(--color-zinc-95)))@apply(--site-dark)'
5
+ export const bodyClass = 'blog-page'
6
6
  ---
7
7
 
8
8
  <DocumentShell>
@@ -14,11 +14,10 @@ const sidebarCategories = pageCategories.map(({ name, pages }) => ({
14
14
  name,
15
15
  pages: pages.map(({ title, pathname, other }: { title: MetadataTitle, pathname: string, other?: { subject: string } }) => other ? { title, pathname, other } : { title, pathname })
16
16
  }))
17
- const menuCategories = sidebarCategories.map(category => ({ ...category, pages: category.pages.filter(page => page.pathname.split('/').length === 3) }))
18
17
  ---
19
18
 
20
19
  <DocumentShell>
21
- <DocHeader client:load contained documentNavigation={menuCategories} documentNavigationTitle="Guide" />
20
+ <DocHeader client:load contained />
22
21
  <DocWrapper>
23
22
  <DocSidebar client:load pageCategories={sidebarCategories} />
24
23
  <slot />
@@ -91,6 +91,16 @@
91
91
 
92
92
  /* ───────────────────────── Masthead ───────────────────────── */
93
93
 
94
+ .blog-article-cover {
95
+ display: block;
96
+ width: 100%;
97
+ height: auto;
98
+ margin-top: 2rem;
99
+ border-radius: var(--ui-radius-xl);
100
+ outline: 1px solid var(--ui-line-contour);
101
+ outline-offset: -1px;
102
+ }
103
+
94
104
  .blog-article-masthead {
95
105
  position: relative;
96
106
  min-width: 0;
@@ -650,13 +660,6 @@
650
660
  }
651
661
  }
652
662
 
653
- /* The page ground runs under the overscroll and behind the footer. */
654
- :root:has(.blog-article-page),
655
- :root:has(.blog-article-page) > body {
656
- background-color: light-dark(var(--color-zinc-0), var(--color-zinc-95));
657
- background-image: none;
658
- }
659
-
660
663
  /* Paper is white whatever the screen's scheme: every token resolves its light value. */
661
664
  @media print {
662
665
  :root:has(.blog-article-page) {
@@ -40,6 +40,19 @@
40
40
  * Accessibility: reduced motion drops transitions; more contrast lifts quiet text, contours and guides; forced
41
41
  * colors and print use Canvas / CanvasText and drop the decorative guides.
42
42
  */
43
+ /* The canvas continues through the footer and overscroll in either theme. */
44
+ :root:has(body.blog-page),
45
+ body.blog-page {
46
+ background-color: light-dark(var(--color-zinc-0), var(--color-zinc-95));
47
+ background-image: none;
48
+ }
49
+
50
+ @media print {
51
+ :root:has(body.blog-page) {
52
+ color-scheme: light;
53
+ }
54
+ }
55
+
43
56
  @layer components {
44
57
  .blog-materials {
45
58
  /* The page's own ground, the same the footer stands on, so the index never sits in a tray. */
@@ -7,7 +7,6 @@
7
7
  .ui-tray > .ui-disclosure > :not(summary) { margin-inline: calc(var(--ui-space-sm) * -1); }
8
8
  /* The panel's own padding is part of the summary's inset: its chevron starts where the code lines start. */
9
9
  .ui-tray > details.ui-disclosure > summary { padding-inline: calc(var(--ui-space-md) - var(--ui-space-sm)); }
10
- .ui-tray > .ui-disclosure .ui-code { border-radius: 0 0 var(--ui-radius-xl) var(--ui-radius-xl); background: transparent; box-shadow: inset 0 1px 0 var(--ui-line-hair); }
11
10
 
12
11
  /* A copy group's instruction is a caption over its swatches, not a button's status: one start-aligned sans line. */
13
12
  p.ui-copy > .ui-copy-status { max-width: none; color: var(--ui-text-muted); font: var(--ui-weight-regular) var(--ui-text-caption)/1.5 var(--font-family-sans); text-align: start; }
@@ -1,61 +1,5 @@
1
- /*
2
- * Code panels (`ui-code`, styles/system/code.css) as the site renders them: Shiki lines are `ui-code-line` parts, the
3
- * copy action sits in the bar or, without one, in the corner, and tab panels all show on paper and without scripts.
4
- */
5
- @layer components {
6
- /* The file's name keeps its width; a long result wraps in the room left beside the copy button. */
7
- .ui-code-bar > .ui-code-file { flex: 1 0 auto; }
8
- .ui-code-bar > .ui-copy { flex: 0 1 auto; min-width: 0; margin-left: auto; }
9
- /* Without a bar the copy takes the panel's corner (`--code-corner`, code.css). A long line scrolls under it: the panel's own face fades in behind the button and runs to the edge, so code never runs through it. */
10
- .ui-code > .ui-copy { position: absolute; z-index: 1; top: var(--code-corner); right: var(--code-corner); padding-left: var(--ui-space-sm); border-radius: var(--ui-radius-md); background: linear-gradient(90deg, transparent, var(--ui-surface-code) var(--ui-space-sm));
11
- /* The face runs on to the panel's contour (1px short of the edge), so a scrolled line never shows again past the button. */
12
- box-shadow: calc(var(--code-corner) - 1px) 0 0 var(--ui-surface-code); }
13
- .ui-code:has(> .ui-copy) .ui-code-line { padding-right: 3rem; }
14
- /* A shell command is read whole: it wraps under a hanging indent instead of hiding its flags behind the copy button. */
15
- :is(.ui-code, [role="tabpanel"])[data-lang="bash"] > pre, .ui-code[data-lang="sh"] > pre, .ui-code[data-lang="shell"] > pre { white-space: pre-wrap; overflow-wrap: anywhere; }
16
- :is(.ui-code, [role="tabpanel"])[data-lang="bash"] > pre .ui-code-line, .ui-code:is([data-lang="sh"], [data-lang="shell"]) > pre .ui-code-line { padding-left: calc(var(--ui-space-md) + 2ch); text-indent: -2ch; }
17
- /* Flags wrap whole: a token never breaks at its own hyphen, so `--yes` cannot split into `-` and `-yes`. */
18
- :is(.ui-code, [role="tabpanel"])[data-lang="bash"] > pre .ui-code-line > span, .ui-code:is([data-lang="sh"], [data-lang="shell"]) > pre .ui-code-line > span { display: inline-block; text-indent: 0; }
19
- /* Plain text, such as a prompt for an agent, is prose set in mono: it wraps at the panel like prose. */
20
- :is(.ui-code, [role="tabpanel"])[data-lang="text"] > pre { white-space: pre-wrap; overflow-wrap: break-word; }
21
- .ui-copy-status:empty { display: none; }
22
-
23
- /* A Generated CSS disclosure right after its source closes the same panel: one frame, the output under a hairline,
24
- as in a demo tray. Alone (after a table or a paragraph) it stays a quiet disclosure. */
25
- .ui-prose > .ui-code:has(+ .ui-disclosure[data-generated]) { margin-bottom: 0; border-end-start-radius: 0; border-end-end-radius: 0; box-shadow: inset 1px 0 0 var(--ui-line-contour), inset -1px 0 0 var(--ui-line-contour), inset 0 1px 0 var(--ui-line-contour); }
26
- .ui-prose > .ui-code + .ui-disclosure[data-generated] {
27
- max-width: none; margin-top: 0; margin-inline: calc(var(--bleed) * -1); padding-inline: var(--ui-space-md);
28
- border-radius: 0 0 var(--ui-radius-xl) var(--ui-radius-xl); background: var(--ui-surface-code);
29
- box-shadow: inset 0 0 0 1px var(--ui-line-contour), 0 1px 0 var(--ui-lip);
30
- }
31
- .ui-prose > .ui-code + .ui-disclosure[data-generated] > summary { margin-inline: calc(var(--ui-space-md) * -1); padding-inline: var(--ui-space-md); box-shadow: inset 0 1px 0 var(--ui-line-hair); }
32
- .ui-prose > .ui-code + .ui-disclosure[data-generated] > .ui-code { margin-inline: calc(var(--ui-space-md) * -1); border-radius: 0 0 var(--ui-radius-xl) var(--ui-radius-xl); background: transparent; box-shadow: inset 0 1px 0 var(--ui-line-hair); }
33
- .ui-copy > .ui-copy-status { min-width: 0; max-width: 16rem; text-align: end; line-height: 1.3; }
34
-
35
- /* @WARN and @ERROR marks: a wavy underline in the status tone instead of a wash. */
36
- .ui-code-line mark[data-tone] { padding: 0; background: none; box-shadow: none; text-decoration: underline wavy 1px; text-underline-offset: 3px; }
37
- .ui-code-line mark[data-tone="warning"] { text-decoration-color: var(--ui-status-warning); }
38
- .ui-code-line mark[data-tone="danger"] { text-decoration-color: var(--ui-status-danger); }
39
- /* [!code focus] blurs the other lines until the pointer reaches the code. */
40
- .ui-code pre.has-focused .ui-code-line { transition: filter var(--ui-duration-base) var(--ui-ease); }
41
- .ui-code pre.has-focused:not(:hover, :focus-visible) .ui-code-line:not(.focus) { filter: blur(3px); }
42
- .ui-code ::selection { background: color-mix(in oklab, var(--ui-code-function) 22%, transparent); }
43
-
44
- [data-code-tabs] > [role="tabpanel"] > .ui-code-file { display: none; }
45
- @media (scripting: none), print {
46
- [data-code-tabs] > .ui-code-bar { display: none; }
47
- [data-code-tabs] > [role="tabpanel"][hidden] { display: block; }
48
- [data-code-tabs] > [role="tabpanel"] > .ui-code-file { display: flex; padding: var(--ui-space-sm) var(--ui-space-md) 0; }
49
- .ui-copy { display: none; }
50
- }
51
- @media print {
52
- .ui-code { break-inside: avoid; }
53
- /* Paper cannot scroll: keep complete commands and long source lines readable. */
54
- .ui-code pre { overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; }
55
- .ui-code pre.has-focused .ui-code-line { filter: none; }
56
- }
57
- @media (prefers-reduced-motion: reduce) { .ui-code pre.has-focused .ui-code-line { transition: none; } }
58
- @media (forced-colors: active) { .ui-code { border: 1px solid CanvasText; } }
59
-
60
- .ui-tree img { flex: none; align-self: center; width: 14px; height: 14px; }
61
- }
1
+ /* Compatibility code entry; owners use these same leaves directly. */
2
+ @import '../system/code-panel.css';
3
+ @import '../system/code-tabs.css';
4
+ @import '../system/copy.css';
5
+ @import '../system/disclosure.css';
@@ -29,6 +29,7 @@
29
29
  .ui-results > li > a:focus-visible { @apply --ui-focus-ring; outline-offset: -2px; }
30
30
  .ui-dialog-foot { flex: none; }
31
31
  .ui-dialog-foot > span:last-child { margin-left: auto; }
32
+ .ui-search-shortcut { white-space: nowrap; }
32
33
  .ui-dialog-foot .ui-kbd svg { width: 12px; height: 12px; }
33
34
  @media (width < 48rem) {
34
35
  .ui-dialog[data-variant="search"] { width: 100%; max-width: 100%; height: 100dvh; max-height: 100dvh; margin: 0; border-radius: 0; }
@@ -1,73 +1,2 @@
1
- /*
2
- * The site header (`ui-header`, styles/system/shell.css) placed for the site: it sticks above documentation pages and
3
- * overlays the homepage and blog as a stickable header that slides away while reading down (`data-tucked`) and is
4
- * transparent at the top of the page (`data-background="transparent"`).
5
- */
6
- @layer components {
7
- .ui-header { padding-top: env(safe-area-inset-top); padding-inline: max(var(--ui-space-xl), env(safe-area-inset-left)) max(var(--ui-space-xl), env(safe-area-inset-right)); height: calc(3.75rem + env(safe-area-inset-top)); box-sizing: border-box; }
8
- .ui-header[data-variant="stickable"] { position: fixed; inset: 0 0 auto; width: 100%; transition: translate var(--ui-duration-base) var(--ui-ease), background-color var(--ui-duration-base), box-shadow var(--ui-duration-base); }
9
- .ui-header[data-tucked] { translate: 0 -100%; }
10
- /* A focused control stays on screen after the header has tucked away, revealed at once. */
11
- .ui-header:focus-within { translate: none; transition-duration: 0s; }
12
- .ui-header[data-background="transparent"] { background: transparent; box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; }
13
- /* Without scripts the header never leaves its transparent top state, so it scrolls away instead of covering content. */
14
- @media (scripting: none) { .ui-header[data-background="transparent"] { position: absolute; } }
15
- @media (prefers-reduced-motion: reduce) { .ui-header, .ui-header[data-variant="stickable"] { transition: none; } }
16
- @media print { .ui-header { display: none; } }
17
-
18
- .ui-header-brand { display: inline-flex; align-items: center; min-height: 2.75rem; border-radius: var(--ui-radius-sm); }
19
- .ui-header-brand:focus-visible { @apply --ui-focus-ring; outline-offset: 2px; }
20
- /* The version chip opens the native version list laid over it. */
21
- .ui-header .ui-version { position: relative; cursor: pointer; }
22
- .ui-header .ui-version > svg { width: 12px; height: 12px; color: var(--ui-text-muted); }
23
- .ui-header .ui-version:hover { color: var(--ui-text-strong); }
24
- .ui-header-nav > a > .ui-badge { margin-left: var(--ui-space-2xs); }
25
- /* External destinations carry a small raised arrow beside the label or icon. */
26
- .ui-header-nav > a:has(.ui-header-external) { position: relative; padding-right: 1.125rem; }
27
- .ui-header-nav > a > .ui-header-external, .ui-header-end > .ui-button > .ui-header-external { position: absolute; top: .375rem; right: .125rem; width: .625rem; height: .625rem; pointer-events: none; }
28
- .ui-header-end > .ui-button > .ui-header-external { top: .125rem; right: 0; }
29
- .ui-header-end > .ui-button[data-disabled] { color: var(--ui-text-disabled); pointer-events: none; }
30
- /* The community and preference icons are words in the bar, not buttons: ink under the pointer, no material. */
31
- .ui-header-end > .ui-button[data-shape="icon"] { width: 2.25rem; min-height: 2.25rem; }
32
- .ui-header-end > .ui-button > svg, .ui-header-menu > svg, .ui-dialog-bar > .ui-button > svg { width: 1.125rem; height: 1.125rem; }
33
- .ui-header-end > .ui-button[data-variant="quiet"]:where(:hover):not(:disabled) { background: transparent; box-shadow: none; }
34
-
35
- /* Icon-only controls name themselves with an ink tooltip under the pointer or keyboard focus. */
36
- .ui-header-end > :is(a, button, label) { position: relative; }
37
- .ui-header-end > * > .ui-tooltip { top: calc(100% + 8px); left: 50%; translate: -50% 0; opacity: 0; visibility: hidden; transition: opacity var(--ui-duration-quick), visibility 0s linear var(--ui-duration-quick); }
38
- .ui-header-end > :is(:hover, :focus-visible, :has(:focus-visible)) > .ui-tooltip { opacity: 1; visibility: visible; transition-delay: 400ms, 400ms; }
39
- .ui-header-end > :last-child > .ui-tooltip { left: auto; right: 0; translate: none; }
40
- .ui-header-end > :last-child > .ui-tooltip::before { left: auto; right: calc(1rem - 4px); translate: none; }
41
- @media (hover: none) { .ui-header-end > * > .ui-tooltip { display: none; } }
42
-
43
- /* Search: a quiet hairline pill that leads the end group — the glass and its shortcut, ink under the pointer. */
44
- /* The round end takes a little more room than the glass's side; the gap to the first icon matches the icons' own spacing. */
45
- .ui-header-search { appearance: none; display: inline-flex; align-items: center; gap: .4375rem; height: 1.875rem; margin-right: var(--ui-space-xs); padding: 0 .8125rem 0 .6875rem; border: 0; border-radius: var(--ui-radius-pill);
46
- background: transparent; color: var(--ui-text-muted); box-shadow: inset 0 0 0 1px var(--ui-line-contour); font: var(--ui-weight-regular) .75rem/1 var(--font-family-sans); cursor: pointer;
47
- transition: color var(--ui-duration-quick), box-shadow var(--ui-duration-quick), background-color var(--ui-duration-quick); }
48
- .ui-header-search:hover { color: var(--ui-text-strong); background: var(--ui-surface-chip); box-shadow: inset 0 0 0 1px var(--ui-line-strong); }
49
- .ui-header-search:focus-visible { @apply --ui-focus-ring; outline-offset: 2px; }
50
- .ui-header-search > svg { flex: none; width: 1rem; height: 1rem; }
51
- .ui-header-search > .ui-kbd { min-width: 0; height: auto; padding: 0; background: none; color: inherit; box-shadow: none; font: var(--ui-weight-regular) .6875rem/1 var(--font-family-sans); letter-spacing: .04em; }
52
-
53
- /* The compact header: the menu at the start, the brand centred, search at the end. */
54
- .ui-header-menu { display: none; }
55
- @media (width < 64rem) {
56
- .ui-header { gap: var(--ui-space-xs); padding-inline: max(var(--ui-space-xs), env(safe-area-inset-left)) max(var(--ui-space-xs), env(safe-area-inset-right)); }
57
- .ui-header-menu { display: inline-flex; }
58
- .ui-header-brand { margin-inline: auto; }
59
- .ui-header > :is(.ui-version, .ui-header-nav), .ui-header-end > :not(.ui-header-search) { display: none; }
60
- .ui-header-end { margin-left: 0; }
61
- /* On a phone search is a plain glass in a full target. */
62
- .ui-header-search { justify-content: center; width: 2.375rem; height: 2.375rem; margin: 0; padding: 0; border-radius: var(--ui-radius-md); box-shadow: none; }
63
- .ui-header-search:hover { background: transparent; box-shadow: none; }
64
- .ui-header-search > svg { width: 1.125rem; height: 1.125rem; }
65
- .ui-header-search > .ui-kbd { display: none; }
66
- /* The menu and search buttons keep the brand centred between equal ends. */
67
- .ui-header:not(:has(.ui-header-search)) .ui-header-end { width: 2.375rem; }
68
- }
69
- @media (width >= 64rem) {
70
- /* The homepage's wide header leaves search out. */
71
- .ui-header-search[data-compact] { display: none; }
72
- }
73
- }
1
+ /* Compatibility entry; the component owns base and adaptation together. */
2
+ @import '../system/header-core.css';
@@ -1,19 +1,39 @@
1
- /*
2
- * The compact header's menu: a `ui-dialog` that fills the viewport below 64rem (`data-variant="sheet"`), with a bar
3
- * (close, brand) and a reading-width body of navigation groups, the section's chapters and the preferences.
4
- */
1
+ /* Primary navigation fills the compact viewport; documents stay in their section indexes. */
5
2
  @layer components {
6
- html:has(.ui-dialog[data-variant="sheet"][open]) { overflow: hidden; scrollbar-gutter: stable; }
7
- .ui-dialog[data-variant="sheet"] { position: fixed; inset: 0; width: 100%; max-width: 100%; height: 100dvh; max-height: 100dvh; margin: 0; overflow-y: auto; overscroll-behavior: contain; border-radius: 0; background: var(--ui-surface-canvas); box-shadow: none; }
8
- .ui-dialog[data-variant="sheet"]::backdrop { background: transparent; }
9
- .ui-dialog-bar { position: sticky; top: 0; z-index: 1; display: grid; grid-template-columns: 2.375rem minmax(0, 1fr) 2.375rem; align-items: center; height: calc(3.75rem + env(safe-area-inset-top)); padding: env(safe-area-inset-top) max(var(--ui-space-xs), env(safe-area-inset-right)) 0 max(var(--ui-space-xs), env(safe-area-inset-left));
10
- background: color-mix(in oklab, var(--ui-surface-canvas) 82%, transparent); -webkit-backdrop-filter: blur(16px) saturate(1.3); backdrop-filter: blur(16px) saturate(1.3); box-shadow: inset 0 -1px 0 var(--ui-line-hair); }
11
- .ui-dialog-bar > .ui-header-brand { justify-self: center; }
12
- .ui-dialog[data-variant="sheet"] > .ui-dialog-body { display: grid; gap: var(--ui-space-xl); max-width: 36rem; margin-inline: auto; padding: var(--ui-space-lg) max(var(--ui-space-md), env(safe-area-inset-right)) max(var(--ui-space-2xl), env(safe-area-inset-bottom)) max(var(--ui-space-md), env(safe-area-inset-left)); }
13
- .ui-dialog[data-variant="sheet"] .ui-nav-link { gap: .625rem; min-height: 2.75rem; }
14
- .ui-dialog[data-variant="sheet"] .ui-nav-link > svg { flex: none; color: var(--ui-text-muted); }
15
- .ui-dialog[data-variant="sheet"] .ui-nav-link > svg:last-child:not(:first-child) { margin-left: auto; }
16
- .ui-dialog[data-variant="sheet"] .ui-disclosure > .ui-sidebar { padding: 0 0 var(--ui-space-md); }
17
- .ui-dialog-fields { display: grid; gap: var(--ui-space-md); }
18
- .ui-dialog-fields > h4 { margin: 0; padding-left: .625rem; color: var(--ui-text-muted); @apply --ui-label-type; }
3
+ html:has(.ui-mobile-menu[open]) { overflow: hidden; scrollbar-gutter: stable; }
4
+ /* Tabler's two paths morph into a small X inside the shared header target. */
5
+ .ui-menu-glyph > path { transform-box: fill-box; transform-origin: center; transition: transform 320ms cubic-bezier(.22, 1, .36, 1); }
6
+ .ui-menu-glyph[data-open="true"] > path { transform: translateY(4px) rotate(45deg) scaleX(1.06066); }
7
+ .ui-menu-glyph[data-open="true"] > path + path { transform: translateY(-4px) rotate(-45deg) scaleX(1.06066); }
8
+
9
+ .ui-dialog.ui-mobile-menu { position: fixed; inset: 0; width: 100%; max-width: 100%; height: 100dvh; max-height: 100dvh; margin: 0; padding: 0; overflow-y: auto; overscroll-behavior: contain; border-radius: 0; background: var(--ui-surface-canvas); box-shadow: none; opacity: 0; transition: opacity 180ms ease-out; }
10
+ .ui-dialog.ui-mobile-menu[open] { display: flex; flex-direction: column; }
11
+ .ui-dialog.ui-mobile-menu[data-open="true"] { opacity: 1; transition-duration: 320ms; }
12
+ .ui-dialog.ui-mobile-menu::backdrop { background: transparent; -webkit-backdrop-filter: none; backdrop-filter: none; }
13
+ .ui-mobile-menu-bar { position: sticky; top: 0; z-index: 1; flex: none; display: grid; grid-template-columns: var(--ui-header-target) minmax(0, 1fr) var(--ui-header-target); align-items: center; height: calc(3.75rem + env(safe-area-inset-top)); padding: env(safe-area-inset-top) max(var(--ui-space-xs), env(safe-area-inset-right)) 0 max(var(--ui-space-xs), env(safe-area-inset-left)); background: var(--ui-surface-canvas); }
14
+ .ui-mobile-menu-bar > .ui-header-brand { justify-self: center; margin: 0; }
15
+ .ui-mobile-menu-body { flex: 1; display: flex; flex-direction: column; gap: 4rem; width: 100%; max-width: 48rem; margin-inline: auto; padding: clamp(3rem, 10dvh, 6rem) max(clamp(1.5rem, 6vw, 3rem), env(safe-area-inset-right)) max(1.5rem, env(safe-area-inset-bottom)) max(clamp(1.5rem, 6vw, 3rem), env(safe-area-inset-left)); transform: translateY(-8px); transition: transform 180ms ease-out; }
16
+ .ui-mobile-menu[data-open="true"] .ui-mobile-menu-body { transform: translateY(0); transition: transform 320ms cubic-bezier(.22, 1, .36, 1); }
17
+ .ui-mobile-menu-nav { display: grid; gap: .5rem; }
18
+ .ui-mobile-menu-link { display: flex; align-items: baseline; gap: .5rem; width: fit-content; max-width: 100%; min-height: 2.75rem; padding-block: .375rem; border: 0; background: none; color: var(--ui-text-strong); font: 500 clamp(2.25rem, 11vw, 3.5rem)/1.3 var(--font-family-sans); letter-spacing: -.045em; text-decoration: none; overflow-wrap: anywhere; opacity: 0; transform: translateY(10px); transition: opacity 180ms ease-out, transform 180ms ease-out, color var(--ui-duration-quick); }
19
+ .ui-mobile-menu-link > span { min-width: 0; }
20
+ .ui-mobile-menu-link > svg { flex: none; align-self: flex-start; margin-top: .65em; color: var(--ui-text-muted); }
21
+ .ui-mobile-menu[data-open="true"] .ui-mobile-menu-link { opacity: 1; transform: translateY(0); transition: opacity 320ms ease-out, transform 320ms cubic-bezier(.22, 1, .36, 1), color var(--ui-duration-quick); transition-delay: calc(var(--menu-order) * 40ms), calc(var(--menu-order) * 40ms), 0s; }
22
+ .ui-mobile-menu-link[aria-current] > span { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: .2em; text-decoration-color: var(--ui-line-mark); }
23
+ .ui-mobile-menu-link:focus-visible, .ui-mobile-menu-community > a:focus-visible { @apply --ui-focus-ring; outline-offset: 5px; border-radius: var(--ui-radius-sm); }
24
+ .ui-mobile-menu-footer { display: grid; gap: 1rem; margin-top: auto; }
25
+ .ui-mobile-menu-community { display: flex; flex-wrap: wrap; gap: .5rem; margin-left: -.75rem; }
26
+ .ui-mobile-menu-community > a { position: relative; display: inline-flex; align-items: center; justify-content: center; min-width: 2.75rem; min-height: 2.75rem; padding: .5rem .75rem; color: var(--ui-text-muted); text-decoration: none; transition: color var(--ui-duration-quick); }
27
+ .ui-mobile-menu-community > a > .ui-menu-external { position: absolute; top: calc(50% - .875rem); right: calc(50% - 1rem); width: .625rem; height: .625rem; pointer-events: none; }
28
+ .ui-mobile-menu-preferences { display: flex; flex-wrap: wrap; align-items: center; gap: .25rem .75rem; padding-top: 1rem; border-top: 1px solid var(--ui-line-hair); }
29
+ .ui-mobile-menu-preferences > .ui-select { width: auto; max-width: 100%; min-height: 2.75rem; padding-left: 0; padding-right: 1.5rem; background-color: transparent; background-position: right .125rem center; border-radius: var(--ui-radius-sm); box-shadow: none; color: var(--ui-text-muted); font-size: .8125rem; }
30
+ .ui-mobile-menu-preferences > .ui-select:focus-visible { @apply --ui-focus-ring; outline-offset: 3px; }
31
+ @media (hover: hover) {
32
+ .ui-mobile-menu-link:hover, .ui-mobile-menu-community > a:hover { color: var(--ui-text-body); }
33
+ }
34
+ @media (prefers-reduced-motion: reduce) {
35
+ .ui-dialog.ui-mobile-menu[data-open], .ui-mobile-menu-body, .ui-mobile-menu-link, .ui-menu-glyph[data-open] > path,
36
+ .ui-mobile-menu[data-open="true"] .ui-mobile-menu-body, .ui-mobile-menu[data-open="true"] .ui-mobile-menu-link { transition: none; }
37
+ .ui-mobile-menu-body, .ui-mobile-menu-link { transform: none; }
38
+ }
19
39
  }
@@ -1,37 +1,2 @@
1
- /*
2
- * The page's contents (`ui-toc`, styles/system/shell.css) as the site uses them: the documentation rail (docs.css
3
- * places it), the narrow-screen disclosure under the page head, and a blog post's margin contents. Sections nest
4
- * their subsections; the entry being read carries `aria-current="location"`.
5
- */
6
- @layer components {
7
- .ui-toc a { overflow-wrap: anywhere; text-wrap: pretty; }
8
- .ui-toc a code { font-family: var(--font-family-mono); font-size: .9em; }
9
- /* A section whose subsection is being read keeps a step of ink. */
10
- .ui-toc li:has(> ol [aria-current="location"]) > a { color: var(--ui-text-body); }
11
- /* A long list folds: every section stays listed, only the one being read opens its subsections. */
12
- .ui-toc ol[data-fold] > li:not(:has([aria-current="location"])) > ol { display: none; }
13
- .ui-toc-actions > a:focus-visible { @apply --ui-focus-ring; outline-offset: 2px; border-radius: var(--ui-radius-xs); }
14
- .ui-toc-actions > a { text-decoration: none; transition: color var(--ui-duration-quick); }
15
- .ui-toc-actions > a:hover { color: var(--ui-text-strong); }
16
- /* The rail scrolls and clips, so its rings sit inside the entry. */
17
- .ui-toc[data-rail] a:focus-visible { outline-offset: -2px; }
18
- @media (pointer: coarse) {
19
- .ui-toc { --toc-row-padding: .8125rem; }
20
- .ui-toc a { min-height: 2.75rem; }
21
- .ui-toc-actions > a { min-height: 2.75rem; }
22
- }
23
- @media (forced-colors: active) { .ui-toc a[aria-current="location"] { text-decoration: underline; } }
24
- @media print { .ui-toc { display: none; } }
25
-
26
- /* Narrow screens: the same contents, closed between hairlines under the page head. */
27
- [data-contents-disclosure] { margin-block: 0 var(--ui-space-xl); }
28
- [data-contents-disclosure] > .ui-toc { padding: 0 0 var(--ui-space-md); }
29
- @media (width >= 80rem) { [data-contents-disclosure] { display: none; } }
30
- /* Hidden beside the rail, the disclosure no longer closes the text before the next section, so that heading takes its own hairline again. The page head
31
- already parts the page from its first section, and the blog switches its disclosure by its own width. */
32
- @media (width >= 80rem) {
33
- .ui-docs-article > [data-contents-disclosure]:not(:first-child, .ui-pagehead + *) + h2, .ui-docs-article > [data-contents-disclosure]:not(:first-child, .ui-pagehead + *) + a[id]:empty + h2,
34
- .ui-docs-article > [data-contents-disclosure]:not(:first-child, .ui-pagehead + *) + hr + h2, .ui-docs-article > [data-contents-disclosure]:not(:first-child, .ui-pagehead + *) + hr + a[id]:empty + h2 { --section-rule: block; }
35
- }
36
- @media print { [data-contents-disclosure] { display: none; } }
37
- }
1
+ /* Compatibility entry; the component owns base and adaptation together. */
2
+ @import '../system/toc.css';