@sarj/docs-ui 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,11 +13,13 @@ Import `@sarj/docs-ui/starlight.css` once, then compose the typed Astro componen
13
13
  Public exports:
14
14
 
15
15
  - `@sarj/docs-ui/Breadcrumbs.astro`
16
+ - `@sarj/docs-ui/CodeComparison.astro`
16
17
  - `@sarj/docs-ui/PageAnchor.astro`
17
18
  - `@sarj/docs-ui/ReferencePage.astro`
18
19
  - `@sarj/docs-ui/RulePager.astro`
19
20
  - `@sarj/docs-ui/catalog`
20
21
  - `@sarj/docs-ui/contracts`
22
+ - `@sarj/docs-ui/line-diff`
21
23
  - `@sarj/docs-ui/starlight.css`
22
24
  - `@sarj/docs-ui/styles.css`
23
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/docs-ui",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "packageManager": "npm@12.0.2",
5
5
  "description": "Shared Astro and Starlight reference UI for Sarj standards sites",
6
6
  "type": "module",
@@ -12,6 +12,8 @@
12
12
  "./starlight.css": "./src/styles/starlight.css",
13
13
  "./catalog": "./src/catalog.ts",
14
14
  "./contracts": "./src/contracts.ts",
15
+ "./line-diff": "./src/line-diff.ts",
16
+ "./CodeComparison.astro": "./src/components/CodeComparison.astro",
15
17
  "./PageAnchor.astro": "./src/components/PageAnchor.astro",
16
18
  "./ReferencePage.astro": "./src/components/ReferencePage.astro",
17
19
  "./RulePager.astro": "./src/components/RulePager.astro",
@@ -40,7 +42,8 @@
40
42
  },
41
43
  "license": "MIT",
42
44
  "dependencies": {
43
- "@fontsource-variable/nunito": "5.3.0"
45
+ "@fontsource-variable/nunito": "5.3.0",
46
+ "diff": "8.0.4"
44
47
  },
45
48
  "peerDependencies": {
46
49
  "@astrojs/starlight": ">=0.41.7 <0.42",
package/src/catalog.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BreadcrumbsProps, ReferencePageProps, RulePagerProps } from './contracts';
1
+ import type { BreadcrumbsProps, CodeComparisonProps, ReferencePageProps, RulePagerProps } from './contracts';
2
2
 
3
3
  export interface ComponentDefinition<Props extends object = object> {
4
4
  exportPath: string;
@@ -22,6 +22,16 @@ export const componentCatalog = Object.freeze({
22
22
  current: 'Current page label, announced with aria-current="page".',
23
23
  },
24
24
  } satisfies ComponentDefinition<BreadcrumbsProps>,
25
+ CodeComparison: {
26
+ exportPath: '@sarj/docs-ui/CodeComparison.astro',
27
+ purpose: 'Compare independently readable before and after source with accessible change cues.',
28
+ properties: {
29
+ id: 'Stable identifier used to connect the comparison and its heading.',
30
+ title: 'Optional scenario heading shared by both sides.',
31
+ before: 'Rejected or prior source snapshot and its presentation metadata.',
32
+ after: 'Preferred or resulting source snapshot and its presentation metadata.',
33
+ },
34
+ } satisfies ComponentDefinition<CodeComparisonProps>,
25
35
  PageAnchor: {
26
36
  exportPath: '@sarj/docs-ui/PageAnchor.astro',
27
37
  purpose: 'Provide the focusable top anchor used by Starlight reference pages.',
@@ -0,0 +1,44 @@
1
+ ---
2
+ import { Code } from '@astrojs/starlight/components';
3
+
4
+ import type { CodeComparisonFile, CodeComparisonProps as Props, CodeComparisonSide } from '../contracts';
5
+
6
+ const { id, title, before, after } = Astro.props;
7
+ const headingId = `${id}-title`;
8
+
9
+ function files(side: CodeComparisonSide): readonly CodeComparisonFile[] {
10
+ return side.files.filter((file) => file.source.trim().length > 0);
11
+ }
12
+ ---
13
+
14
+ <section
15
+ class="sarj-code-comparison"
16
+ aria-label={title === undefined ? 'Before and after code comparison' : undefined}
17
+ aria-labelledby={title === undefined ? undefined : headingId}
18
+ data-code-comparison={id}
19
+ >
20
+ {title !== undefined && <h3 id={headingId} class="sarj-code-comparison__title">{title}</h3>}
21
+ <div class="sarj-code-comparison__grid">
22
+ {[{ side: before, kind: 'before' }, { side: after, kind: 'after' }].map(({ side, kind }) => (
23
+ <figure class:list={['sarj-code-comparison__side', `sarj-code-comparison__side--${kind}`]}>
24
+ <figcaption>
25
+ <span>{side.label}</span>
26
+ {side.title !== undefined && <strong>{side.title}</strong>}
27
+ </figcaption>
28
+ <div class="sarj-code-comparison__files">
29
+ {files(side).map((file) => (
30
+ <Code
31
+ code={file.source}
32
+ lang={file.language}
33
+ title={file.title}
34
+ frame={file.title === undefined ? 'none' : 'code'}
35
+ del={kind === 'before' ? file.marks : undefined}
36
+ ins={kind === 'after' ? file.marks : undefined}
37
+ wrap
38
+ />
39
+ ))}
40
+ </div>
41
+ </figure>
42
+ ))}
43
+ </div>
44
+ </section>
package/src/contracts.ts CHANGED
@@ -12,6 +12,34 @@ export interface BreadcrumbsProps {
12
12
  current: string;
13
13
  }
14
14
 
15
+ /** A one-based inclusive range understood by Expressive Code text markers. */
16
+ export interface CodeLineMark {
17
+ range: string;
18
+ }
19
+
20
+ /** One source file rendered inside a comparison side. */
21
+ export interface CodeComparisonFile {
22
+ source: string;
23
+ language: string;
24
+ title?: string;
25
+ marks?: CodeLineMark;
26
+ }
27
+
28
+ /** One independently readable state in a code comparison. */
29
+ export interface CodeComparisonSide {
30
+ label: string;
31
+ title?: string;
32
+ files: readonly CodeComparisonFile[];
33
+ }
34
+
35
+ /** Public properties accepted by {@link CodeComparison}. */
36
+ export interface CodeComparisonProps {
37
+ id: string;
38
+ title?: string;
39
+ before: CodeComparisonSide;
40
+ after: CodeComparisonSide;
41
+ }
42
+
15
43
  export interface RulePagerLink {
16
44
  href: string;
17
45
  label: string;
@@ -0,0 +1,55 @@
1
+ import { diffLines } from 'diff';
2
+
3
+ import type { CodeLineMark } from './contracts';
4
+
5
+ export interface ChangedLineMarks {
6
+ before: CodeLineMark | undefined;
7
+ after: CodeLineMark | undefined;
8
+ }
9
+
10
+ /** Return presentation-only line markers for two explicitly related sources. */
11
+ export function changedLineMarks(before: string, after: string): ChangedLineMarks {
12
+ const beforeLines: number[] = [];
13
+ const afterLines: number[] = [];
14
+ let beforeLine = 1;
15
+ let afterLine = 1;
16
+
17
+ for (const change of diffLines(before, after)) {
18
+ const count = change.count;
19
+ if (change.removed) {
20
+ addLines(beforeLines, beforeLine, count);
21
+ beforeLine += count;
22
+ } else if (change.added) {
23
+ addLines(afterLines, afterLine, count);
24
+ afterLine += count;
25
+ } else {
26
+ beforeLine += count;
27
+ afterLine += count;
28
+ }
29
+ }
30
+
31
+ return { before: lineMark(beforeLines), after: lineMark(afterLines) };
32
+ }
33
+
34
+ function addLines(output: number[], first: number, count: number): void {
35
+ for (let offset = 0; offset < count; offset += 1) output.push(first + offset);
36
+ }
37
+
38
+ function lineMark(lines: readonly number[]): CodeLineMark | undefined {
39
+ if (lines.length === 0) return undefined;
40
+ const ranges: string[] = [];
41
+ let first = lines[0] ?? 1;
42
+ let last = first;
43
+
44
+ for (const line of lines.slice(1)) {
45
+ if (line === last + 1) {
46
+ last = line;
47
+ continue;
48
+ }
49
+ ranges.push(first === last ? String(first) : `${String(first)}-${String(last)}`);
50
+ first = line;
51
+ last = line;
52
+ }
53
+ ranges.push(first === last ? String(first) : `${String(first)}-${String(last)}`);
54
+ return { range: ranges.join(',') };
55
+ }
@@ -1,5 +1,34 @@
1
1
  @import './theme.css';
2
2
 
3
+ html {
4
+ scroll-behavior: smooth;
5
+ }
6
+
7
+ body,
8
+ .content-panel,
9
+ .main-pane,
10
+ .right-sidebar-panel,
11
+ .sidebar-pane {
12
+ background-color: var(--sl-color-black);
13
+ }
14
+
15
+ body {
16
+ font-weight: 450;
17
+ }
18
+
19
+ .sarj-visually-hidden {
20
+ position: absolute;
21
+ width: 1px;
22
+ height: 1px;
23
+ padding: 0;
24
+ margin: -1px;
25
+ overflow: hidden;
26
+ clip: rect(0 0 0 0);
27
+ clip-path: inset(50%);
28
+ white-space: nowrap;
29
+ border: 0;
30
+ }
31
+
3
32
  .main-pane,
4
33
  .main-frame,
5
34
  .sl-container {
@@ -7,6 +36,7 @@
7
36
  }
8
37
 
9
38
  .main-pane {
39
+ --sl-content-width: 100%;
10
40
  width: 100%;
11
41
  }
12
42
 
@@ -14,22 +44,211 @@
14
44
  display: none;
15
45
  }
16
46
 
17
- header.header {
47
+ .page > header.header {
18
48
  border-bottom: 1px solid var(--sarj-color-rule);
19
49
  box-shadow: none;
20
50
  }
21
51
 
22
- .sidebar-pane a,
52
+ .page > header.header > .header {
53
+ border-bottom: 0;
54
+ }
55
+
56
+ .sidebar-content a,
57
+ .sidebar-content summary,
23
58
  .social-icons a,
59
+ .header button,
24
60
  starlight-menu-button button {
25
61
  min-width: 44px;
26
62
  min-height: 44px;
27
63
  }
28
64
 
29
- .sidebar-content a[aria-current='page'] {
65
+ .sidebar-content a,
66
+ .sidebar-content summary {
67
+ display: flex;
68
+ align-items: center;
69
+ }
70
+
71
+ .sidebar-content a[aria-current='page'],
72
+ .sidebar-content a[aria-current='page']:hover,
73
+ .sidebar-content a[aria-current='page']:focus {
74
+ background: var(--sl-color-accent-low);
30
75
  color: var(--sl-color-accent-high);
31
76
  }
32
77
 
78
+ :where(.sl-markdown-content, main) :is(h1, h2, h3) {
79
+ color: var(--sl-color-white);
80
+ font-weight: 700;
81
+ letter-spacing: -0.035em;
82
+ text-wrap: balance;
83
+ }
84
+
85
+ :where(.sl-markdown-content, main) h1 {
86
+ font-size: clamp(1.8rem, 3.5vw, 2.35rem);
87
+ line-height: 1.08;
88
+ }
89
+
90
+ .sarj-about-page {
91
+ width: min(100%, 48rem);
92
+ padding-block: 1.25rem 3rem;
93
+ }
94
+
95
+ .sarj-about-page > h1 {
96
+ margin-block: 0 0.75rem;
97
+ }
98
+
99
+ .sarj-about-page > p {
100
+ max-width: 42rem;
101
+ color: var(--sl-color-gray-2);
102
+ font-size: 1.05rem;
103
+ line-height: 1.65;
104
+ }
105
+
106
+ .sarj-related-standards {
107
+ margin-top: 3rem;
108
+ padding-top: 1.5rem;
109
+ border-top: 1px solid var(--sarj-color-rule);
110
+ }
111
+
112
+ .sarj-related-standards h2 {
113
+ margin: 0 0 0.5rem;
114
+ font-size: 1rem;
115
+ }
116
+
117
+ .sarj-breadcrumbs {
118
+ margin-block: 0 1.25rem;
119
+ }
120
+
121
+ .sarj-breadcrumbs ol {
122
+ display: flex;
123
+ align-items: center;
124
+ flex-wrap: wrap;
125
+ gap: 0.45rem;
126
+ margin: 0;
127
+ padding: 0;
128
+ color: var(--sl-color-gray-2);
129
+ font-size: 0.85rem;
130
+ list-style: none;
131
+ }
132
+
133
+ .sarj-breadcrumbs li + li::before {
134
+ margin-inline-end: 0.45rem;
135
+ color: var(--sl-color-gray-4);
136
+ content: '/';
137
+ }
138
+
139
+ .sarj-breadcrumbs a {
140
+ display: inline-flex;
141
+ min-height: 44px;
142
+ align-items: center;
143
+ color: inherit;
144
+ text-decoration: underline;
145
+ text-underline-offset: 0.2em;
146
+ }
147
+
148
+ .sarj-breadcrumbs [aria-current='page'] {
149
+ color: var(--sl-color-white);
150
+ font-weight: 650;
151
+ }
152
+
153
+ .sarj-code-comparison {
154
+ min-width: 0;
155
+ max-width: 100%;
156
+ container-type: inline-size;
157
+ }
158
+
159
+ .sarj-code-comparison__title {
160
+ margin: 0 0 0.5rem;
161
+ font-size: 0.95rem;
162
+ line-height: 1.35;
163
+ }
164
+
165
+ .sarj-code-comparison__grid {
166
+ display: grid;
167
+ min-width: 0;
168
+ gap: 1rem;
169
+ }
170
+
171
+ .sarj-code-comparison__side {
172
+ display: flex;
173
+ min-width: 0;
174
+ max-width: 100%;
175
+ margin: 0;
176
+ flex-direction: column;
177
+ border: 1px solid var(--sarj-color-rule);
178
+ border-radius: 0.55rem;
179
+ background: var(--sl-color-black);
180
+ }
181
+
182
+ .sarj-code-comparison__side--before {
183
+ border-top-color: var(--sarj-color-report);
184
+ }
185
+
186
+ .sarj-code-comparison__side--after {
187
+ border-top-color: var(--sarj-color-pass);
188
+ }
189
+
190
+ .sarj-code-comparison__side figcaption {
191
+ display: grid;
192
+ gap: 0.15rem;
193
+ padding: 0.75rem 0.9rem;
194
+ border-bottom: 1px solid var(--sarj-color-rule);
195
+ }
196
+
197
+ .sarj-code-comparison__side figcaption span {
198
+ color: var(--sl-color-gray-2);
199
+ font: 700 0.68rem/1.4 var(--sl-font-mono);
200
+ letter-spacing: 0.08em;
201
+ text-transform: uppercase;
202
+ }
203
+
204
+ .sarj-code-comparison__side figcaption strong {
205
+ font-size: 1rem;
206
+ line-height: 1.35;
207
+ }
208
+
209
+ .sarj-code-comparison__files,
210
+ .sarj-code-comparison__files > .expressive-code {
211
+ min-width: 0;
212
+ max-width: 100%;
213
+ }
214
+
215
+ .sarj-code-comparison__files {
216
+ display: flex;
217
+ flex: 1;
218
+ flex-direction: column;
219
+ }
220
+
221
+ .sarj-code-comparison__files > .expressive-code {
222
+ margin: 0;
223
+ }
224
+
225
+ .sarj-code-comparison__files .expressive-code figure {
226
+ border: 0;
227
+ border-top: 1px solid var(--sarj-color-rule);
228
+ border-radius: 0;
229
+ }
230
+
231
+ .sarj-code-comparison__files .expressive-code figcaption.header:empty {
232
+ display: none;
233
+ }
234
+
235
+ .sarj-code-comparison__files pre {
236
+ max-width: 100%;
237
+ overflow: auto;
238
+ }
239
+
240
+ *:focus-visible {
241
+ outline: 3px solid var(--sl-color-accent);
242
+ outline-offset: 3px;
243
+ }
244
+
245
+ @container (min-width: 52rem) {
246
+ .sarj-code-comparison__grid {
247
+ grid-template-columns: repeat(2, minmax(0, 1fr));
248
+ align-items: stretch;
249
+ }
250
+ }
251
+
33
252
  .sarj-rule-pager {
34
253
  display: flex;
35
254
  flex: 0 0 auto;
@@ -71,3 +290,10 @@ starlight-menu-button button {
71
290
  @media (prefers-reduced-motion: reduce) {
72
291
  :root { scroll-behavior: auto; }
73
292
  }
293
+
294
+ @media (forced-colors: active) {
295
+ .sarj-code-comparison__side--before,
296
+ .sarj-code-comparison__side--after {
297
+ border-top-width: 3px;
298
+ }
299
+ }