@dirsigler/astro-techradar 0.4.0 → 0.6.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.
- package/README.md +75 -8
- package/config.ts +47 -13
- package/integration.ts +6 -0
- package/package.json +1 -1
- package/schemas.ts +17 -0
- package/src/components/Radar.astro +77 -15
- package/src/components/RadarLegend.astro +11 -1
- package/src/layouts/Base.astro +32 -2
- package/src/pages/feed.xml.ts +64 -0
- package/src/pages/index.astro +92 -12
- package/src/pages/technology/[...slug].astro +144 -3
- package/src/themes/default.css +16 -16
package/README.md
CHANGED
|
@@ -27,6 +27,12 @@ An [Astro](https://astro.build) integration that adds a complete, interactive te
|
|
|
27
27
|
- **Fully Static & Fast** — Builds to plain HTML/CSS/JS. Deploy anywhere
|
|
28
28
|
- **Movement Indicators** — Mark technologies as moved in/out to highlight recent changes
|
|
29
29
|
- **SEO Ready** — Open Graph, Twitter Cards, canonical URLs, and custom 404 page
|
|
30
|
+
- **RSS Feed** — Auto-generated feed at `{basePath}/feed.xml` for subscribers
|
|
31
|
+
- **Owner Attribution** — Assign teams or individuals to technologies
|
|
32
|
+
- **External Links** — Link technologies to docs, repos, communities with typed icons
|
|
33
|
+
- **Tags & Filtering** — Cross-cutting tags for discovery, filterable on the main radar page
|
|
34
|
+
- **Related Technologies** — Manual and auto-suggested related technologies on detail pages
|
|
35
|
+
- **Keyboard Navigation** — Arrow keys to cycle radar dots, focus indicators, screen reader friendly
|
|
30
36
|
|
|
31
37
|
---
|
|
32
38
|
|
|
@@ -109,6 +115,25 @@ Each technology file:
|
|
|
109
115
|
title: Kubernetes
|
|
110
116
|
ring: adopt
|
|
111
117
|
moved: 0
|
|
118
|
+
owner:
|
|
119
|
+
name: Platform Engineering
|
|
120
|
+
url: https://github.com/orgs/example/teams/platform
|
|
121
|
+
tags:
|
|
122
|
+
- cloud-native
|
|
123
|
+
- containers
|
|
124
|
+
related:
|
|
125
|
+
- cloud/helm
|
|
126
|
+
- cloud/argocd
|
|
127
|
+
links:
|
|
128
|
+
- label: Documentation
|
|
129
|
+
url: https://kubernetes.io/docs/
|
|
130
|
+
type: docs
|
|
131
|
+
- label: GitHub
|
|
132
|
+
url: https://github.com/kubernetes/kubernetes
|
|
133
|
+
type: repo
|
|
134
|
+
- label: Kubernetes Slack
|
|
135
|
+
url: https://kubernetes.slack.com
|
|
136
|
+
type: community
|
|
112
137
|
history:
|
|
113
138
|
- date: "2025-03"
|
|
114
139
|
ring: adopt
|
|
@@ -124,7 +149,36 @@ Your description in Markdown. Explain why this technology is in this ring
|
|
|
124
149
|
and what your experience has been.
|
|
125
150
|
```
|
|
126
151
|
|
|
127
|
-
|
|
152
|
+
#### `owner` (optional)
|
|
153
|
+
|
|
154
|
+
Displays a team or person responsible for this technology on the detail page.
|
|
155
|
+
|
|
156
|
+
| Field | Required | Description |
|
|
157
|
+
| ----- | -------- | ----------- |
|
|
158
|
+
| `name` | yes | Owner name (e.g. team, guild, or person) |
|
|
159
|
+
| `url` | no | Link to an external resource (GitHub team, Slack channel, wiki page) |
|
|
160
|
+
|
|
161
|
+
#### `links` (optional)
|
|
162
|
+
|
|
163
|
+
External resource links displayed inline on the detail page. Links are automatically sorted by type in a consistent order: docs, repo, website, community.
|
|
164
|
+
|
|
165
|
+
| Field | Required | Description |
|
|
166
|
+
| ----- | -------- | ----------- |
|
|
167
|
+
| `label` | yes | Display text for the link |
|
|
168
|
+
| `url` | yes | Full URL |
|
|
169
|
+
| `type` | no | Link type — determines the icon. One of `docs`, `repo`, `website`, `community`. Defaults to `website` |
|
|
170
|
+
|
|
171
|
+
#### `tags` (optional)
|
|
172
|
+
|
|
173
|
+
Free-text tags for cross-cutting concerns (e.g. `cloud-native`, `security`, `iac`). Tags appear as `#tag` pills on the detail page and enable tag-based filtering on the main radar page. Clicking a tag pill navigates to the radar filtered by that tag.
|
|
174
|
+
|
|
175
|
+
#### `related` (optional)
|
|
176
|
+
|
|
177
|
+
A list of technology slugs (matching entry IDs like `cloud/helm`) to display as related technologies on the detail page. In addition to these manual entries, the radar automatically suggests technologies that share 2 or more tags with the current entry.
|
|
178
|
+
|
|
179
|
+
#### `history` (optional)
|
|
180
|
+
|
|
181
|
+
When present, a timeline is rendered on the technology detail page showing how its ring classification changed over time.
|
|
128
182
|
|
|
129
183
|
| Field | Required | Description |
|
|
130
184
|
| ----- | -------- | ----------- |
|
|
@@ -152,13 +206,21 @@ techradar({
|
|
|
152
206
|
// Optional
|
|
153
207
|
basePath: "/techradar", // Mount under a sub-path (e.g. acme.com/techradar/)
|
|
154
208
|
logo: "/logo.svg", // Path to logo in public/
|
|
155
|
-
|
|
156
|
-
|
|
209
|
+
footer: "Built by the Platform Team", // Supports HTML
|
|
210
|
+
editing: {
|
|
211
|
+
enabled: true, // Show "Edit this page" links (default: true)
|
|
212
|
+
baseUrl: "https://github.com/your-org/your-radar/edit/main/segments",
|
|
213
|
+
},
|
|
157
214
|
theme: "default", // 'default' | 'catppuccin-mocha' | path to custom CSS
|
|
158
215
|
color: {
|
|
159
216
|
toggle: true, // Show the light/dark mode toggle (default: true)
|
|
160
217
|
mode: "system", // 'light' | 'dark' | 'system' (default: 'system')
|
|
161
218
|
},
|
|
219
|
+
search: {
|
|
220
|
+
enabled: true, // Show the search input in the navbar (default: true)
|
|
221
|
+
placeholder: "Search technologies...", // Placeholder text
|
|
222
|
+
},
|
|
223
|
+
feed: true, // Enable RSS feed at {basePath}/feed.xml (default: true)
|
|
162
224
|
socialLinks: [
|
|
163
225
|
{
|
|
164
226
|
label: "GitHub",
|
|
@@ -185,11 +247,16 @@ socialLinks: [
|
|
|
185
247
|
|
|
186
248
|
### Technology Frontmatter
|
|
187
249
|
|
|
188
|
-
| Field
|
|
189
|
-
|
|
|
190
|
-
| `title` | `string`
|
|
191
|
-
| `ring`
|
|
192
|
-
| `moved` | `-1 \| 0 \| 1`
|
|
250
|
+
| Field | Type | Description |
|
|
251
|
+
| :-- | :-- | :-- |
|
|
252
|
+
| `title` | `string` | Display name |
|
|
253
|
+
| `ring` | `'adopt' \| 'trial' \| 'assess' \| 'hold'` | Which ring the technology belongs to |
|
|
254
|
+
| `moved` | `-1 \| 0 \| 1` | Movement indicator (-1 = out, 0 = none, 1 = in) |
|
|
255
|
+
| `owner` | `{ name, url? }` | Optional team/person responsible (see [owner](#owner-optional)) |
|
|
256
|
+
| `links` | `Array<{ label, url, type? }>` | Optional external links (see [links](#links-optional)) |
|
|
257
|
+
| `tags` | `string[]` | Optional free-text tags for filtering (see [tags](#tags-optional)) |
|
|
258
|
+
| `related` | `string[]` | Optional technology slugs for related (see [related](#related-optional)) |
|
|
259
|
+
| `history` | `Array<{ date, ring, description? }>` | Optional ring change timeline (see [history](#history-optional)) |
|
|
193
260
|
|
|
194
261
|
### Segment Frontmatter
|
|
195
262
|
|
package/config.ts
CHANGED
|
@@ -5,6 +5,30 @@ export interface SocialLink {
|
|
|
5
5
|
icon?: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface EditConfig {
|
|
9
|
+
/** Show "Edit this page" links on technology pages. Default: true */
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
/** Base URL for edit links (e.g. "https://github.com/org/repo/edit/main/segments"). */
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ResolvedEdit {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SearchConfig {
|
|
21
|
+
/** Enable the search input in the navbar. Default: true */
|
|
22
|
+
enabled?: boolean;
|
|
23
|
+
/** Placeholder text for the search input. Default: 'Search technologies...' */
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ResolvedSearch {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
placeholder: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
8
32
|
export interface ColorModeConfig {
|
|
9
33
|
/** Show the light/dark mode toggle in the header. Default: true */
|
|
10
34
|
toggle?: boolean;
|
|
@@ -38,13 +62,10 @@ export interface TechRadarUserConfig {
|
|
|
38
62
|
logo?: string;
|
|
39
63
|
|
|
40
64
|
/** Footer text. Supports simple HTML. */
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/** Show "Edit this page" links on technology pages. Default: true */
|
|
44
|
-
allowEditing?: boolean;
|
|
65
|
+
footer?: string;
|
|
45
66
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
67
|
+
/** Edit page configuration. */
|
|
68
|
+
editing?: EditConfig;
|
|
48
69
|
|
|
49
70
|
/** Social links shown in the footer. */
|
|
50
71
|
socialLinks?: SocialLink[];
|
|
@@ -54,6 +75,12 @@ export interface TechRadarUserConfig {
|
|
|
54
75
|
|
|
55
76
|
/** Color mode configuration. */
|
|
56
77
|
color?: ColorModeConfig;
|
|
78
|
+
|
|
79
|
+
/** Enable RSS feed at {basePath}/feed.xml. Default: true */
|
|
80
|
+
feed?: boolean;
|
|
81
|
+
|
|
82
|
+
/** Search configuration. */
|
|
83
|
+
search?: SearchConfig;
|
|
57
84
|
}
|
|
58
85
|
|
|
59
86
|
export interface ResolvedConfig {
|
|
@@ -63,11 +90,13 @@ export interface ResolvedConfig {
|
|
|
63
90
|
/** Normalized base path with leading slash, no trailing slash. Empty string when mounted at root. */
|
|
64
91
|
basePath: string;
|
|
65
92
|
logo?: string;
|
|
66
|
-
|
|
67
|
-
|
|
93
|
+
footer: string;
|
|
94
|
+
editing: ResolvedEdit;
|
|
68
95
|
socialLinks: SocialLink[];
|
|
69
96
|
theme: string;
|
|
70
97
|
color: ResolvedColorMode;
|
|
98
|
+
feed: boolean;
|
|
99
|
+
search: ResolvedSearch;
|
|
71
100
|
}
|
|
72
101
|
|
|
73
102
|
/** Normalize a user-supplied path: ensure leading slash, strip trailing slash. Returns "" for root. */
|
|
@@ -85,16 +114,21 @@ export function resolveConfig(user: TechRadarUserConfig): ResolvedConfig {
|
|
|
85
114
|
subtitle: user.subtitle,
|
|
86
115
|
basePath: normalizeBasePath(user.basePath),
|
|
87
116
|
logo: user.logo,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
117
|
+
footer: user.footer ?? '',
|
|
118
|
+
editing: {
|
|
119
|
+
enabled: user.editing?.enabled ?? true,
|
|
120
|
+
baseUrl: (user.editing?.enabled ?? true) ? user.editing?.baseUrl : undefined,
|
|
121
|
+
},
|
|
93
122
|
socialLinks: user.socialLinks ?? [],
|
|
94
123
|
theme: user.theme ?? 'default',
|
|
95
124
|
color: {
|
|
96
125
|
toggle: user.color?.toggle ?? true,
|
|
97
126
|
mode: user.color?.mode ?? 'system',
|
|
98
127
|
},
|
|
128
|
+
feed: user.feed ?? true,
|
|
129
|
+
search: {
|
|
130
|
+
enabled: user.search?.enabled ?? true,
|
|
131
|
+
placeholder: user.search?.placeholder ?? 'Search technologies...',
|
|
132
|
+
},
|
|
99
133
|
};
|
|
100
134
|
}
|
package/integration.ts
CHANGED
|
@@ -40,6 +40,12 @@ export function createIntegration(
|
|
|
40
40
|
'src/pages/technology/[...slug].astro',
|
|
41
41
|
),
|
|
42
42
|
});
|
|
43
|
+
if (config.feed) {
|
|
44
|
+
injectRoute({
|
|
45
|
+
pattern: `${bp}/feed.xml`,
|
|
46
|
+
entrypoint: path.join(PKG_DIR, 'src/pages/feed.xml.ts'),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
43
49
|
// Resolve theme CSS
|
|
44
50
|
let themeCSS = '';
|
|
45
51
|
const builtinPath = path.join(
|
package/package.json
CHANGED
package/schemas.ts
CHANGED
|
@@ -22,4 +22,21 @@ export const technologySchema = z.object({
|
|
|
22
22
|
}),
|
|
23
23
|
)
|
|
24
24
|
.optional(),
|
|
25
|
+
owner: z
|
|
26
|
+
.object({
|
|
27
|
+
name: z.string(),
|
|
28
|
+
url: z.string().url().optional(),
|
|
29
|
+
})
|
|
30
|
+
.optional(),
|
|
31
|
+
links: z
|
|
32
|
+
.array(
|
|
33
|
+
z.object({
|
|
34
|
+
label: z.string(),
|
|
35
|
+
url: z.string().url(),
|
|
36
|
+
type: z.enum(['docs', 'repo', 'website', 'community']).default('website'),
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
.optional(),
|
|
40
|
+
tags: z.array(z.string()).optional(),
|
|
41
|
+
related: z.array(z.string()).optional(),
|
|
25
42
|
});
|
|
@@ -9,6 +9,7 @@ interface Technology {
|
|
|
9
9
|
segment: string;
|
|
10
10
|
color: string;
|
|
11
11
|
order: number;
|
|
12
|
+
tags: string[];
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
interface Segment {
|
|
@@ -172,6 +173,7 @@ const segmentLabels = segments.map((seg) => {
|
|
|
172
173
|
data-title={dot.title}
|
|
173
174
|
data-ring={dot.ring}
|
|
174
175
|
data-segment={dot.segment}
|
|
176
|
+
data-tags={technologies.find((t) => t.slug === dot.slug)?.tags.join(',') ?? ''}
|
|
175
177
|
/>
|
|
176
178
|
{dot.moved === 1 && (
|
|
177
179
|
<polygon
|
|
@@ -197,26 +199,83 @@ const segmentLabels = segments.map((seg) => {
|
|
|
197
199
|
const svg = document.querySelector('.radar-container svg');
|
|
198
200
|
const tooltip = document.getElementById('radar-tooltip');
|
|
199
201
|
if (svg && tooltip) {
|
|
202
|
+
const container = svg.closest('.radar-container') as HTMLElement;
|
|
203
|
+
|
|
204
|
+
function showTooltip(dot: SVGCircleElement) {
|
|
205
|
+
const title = dot.getAttribute('data-title') ?? '';
|
|
206
|
+
const ring = dot.getAttribute('data-ring') ?? '';
|
|
207
|
+
tooltip!.textContent = `${title} (${ring})`;
|
|
208
|
+
tooltip!.classList.remove('hidden');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function positionTooltipAtDot(dot: SVGCircleElement) {
|
|
212
|
+
const containerRect = container.getBoundingClientRect();
|
|
213
|
+
const dotRect = dot.getBoundingClientRect();
|
|
214
|
+
tooltip!.style.left = `${dotRect.left - containerRect.left + dotRect.width / 2 + 12}px`;
|
|
215
|
+
tooltip!.style.top = `${dotRect.top - containerRect.top - 10}px`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function hideTooltip() {
|
|
219
|
+
tooltip!.classList.add('hidden');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Collect all radar links for arrow key navigation
|
|
223
|
+
const allLinks = Array.from(svg.querySelectorAll<SVGAElement>('.radar-link'));
|
|
224
|
+
|
|
225
|
+
function getVisibleLinks(): SVGAElement[] {
|
|
226
|
+
return allLinks.filter((link) => link.style.opacity !== '0.1');
|
|
227
|
+
}
|
|
228
|
+
|
|
200
229
|
svg.querySelectorAll('.radar-dot').forEach((dot) => {
|
|
201
|
-
dot.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const ring = target.getAttribute('data-ring') ?? '';
|
|
205
|
-
tooltip.textContent = `${title} (${ring})`;
|
|
206
|
-
tooltip.classList.remove('hidden');
|
|
207
|
-
});
|
|
230
|
+
const link = dot.closest('.radar-link') as SVGAElement | null;
|
|
231
|
+
|
|
232
|
+
dot.addEventListener('mouseenter', () => showTooltip(dot as SVGCircleElement));
|
|
208
233
|
|
|
209
234
|
dot.addEventListener('mousemove', (e) => {
|
|
210
|
-
const container = svg.closest('.radar-container') as HTMLElement;
|
|
211
235
|
const rect = container.getBoundingClientRect();
|
|
212
236
|
const me = e as MouseEvent;
|
|
213
|
-
tooltip
|
|
214
|
-
tooltip
|
|
237
|
+
tooltip!.style.left = `${me.clientX - rect.left + 12}px`;
|
|
238
|
+
tooltip!.style.top = `${me.clientY - rect.top - 10}px`;
|
|
215
239
|
});
|
|
216
240
|
|
|
217
|
-
dot.addEventListener('mouseleave',
|
|
218
|
-
|
|
219
|
-
|
|
241
|
+
dot.addEventListener('mouseleave', hideTooltip);
|
|
242
|
+
|
|
243
|
+
// Show tooltip on keyboard focus
|
|
244
|
+
if (link) {
|
|
245
|
+
link.addEventListener('focus', () => {
|
|
246
|
+
showTooltip(dot as SVGCircleElement);
|
|
247
|
+
positionTooltipAtDot(dot as SVGCircleElement);
|
|
248
|
+
});
|
|
249
|
+
link.addEventListener('blur', hideTooltip);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Arrow key navigation within the radar
|
|
254
|
+
svg.addEventListener('keydown', (e) => {
|
|
255
|
+
const ke = e as KeyboardEvent;
|
|
256
|
+
if (ke.key === 'Escape') {
|
|
257
|
+
(document.activeElement as HTMLElement)?.blur();
|
|
258
|
+
hideTooltip();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (ke.key !== 'ArrowLeft' && ke.key !== 'ArrowRight') return;
|
|
263
|
+
|
|
264
|
+
const visible = getVisibleLinks();
|
|
265
|
+
if (visible.length === 0) return;
|
|
266
|
+
|
|
267
|
+
const focused = document.activeElement?.closest('.radar-link') as SVGAElement | null;
|
|
268
|
+
const currentIndex = focused ? visible.indexOf(focused) : -1;
|
|
269
|
+
|
|
270
|
+
let nextIndex: number;
|
|
271
|
+
if (ke.key === 'ArrowRight') {
|
|
272
|
+
nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % visible.length;
|
|
273
|
+
} else {
|
|
274
|
+
nextIndex = currentIndex < 0 ? visible.length - 1 : (currentIndex - 1 + visible.length) % visible.length;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
ke.preventDefault();
|
|
278
|
+
visible[nextIndex].focus();
|
|
220
279
|
});
|
|
221
280
|
}
|
|
222
281
|
</script>
|
|
@@ -250,14 +309,17 @@ const segmentLabels = segments.map((seg) => {
|
|
|
250
309
|
.radar-link:active {
|
|
251
310
|
color: inherit;
|
|
252
311
|
}
|
|
253
|
-
.radar-link:focus
|
|
254
|
-
.radar-link:focus-visible {
|
|
312
|
+
.radar-link:focus {
|
|
255
313
|
outline: none;
|
|
256
314
|
}
|
|
257
315
|
.radar-link:hover .radar-dot,
|
|
258
316
|
.radar-link:focus .radar-dot {
|
|
259
317
|
r: 13;
|
|
260
318
|
}
|
|
319
|
+
.radar-link:focus-visible .radar-dot {
|
|
320
|
+
r: 13;
|
|
321
|
+
filter: brightness(1.3) drop-shadow(0 0 10px rgba(205, 214, 244, 0.6));
|
|
322
|
+
}
|
|
261
323
|
|
|
262
324
|
.radar-dot {
|
|
263
325
|
cursor: pointer;
|
|
@@ -10,6 +10,7 @@ interface Technology {
|
|
|
10
10
|
href: string;
|
|
11
11
|
segment: string;
|
|
12
12
|
color: string;
|
|
13
|
+
tags: string[];
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
interface Segment {
|
|
@@ -45,6 +46,7 @@ const { technologies, segments } = Astro.props;
|
|
|
45
46
|
class="legend-row"
|
|
46
47
|
data-segment={tech.segment}
|
|
47
48
|
data-ring={tech.ring}
|
|
49
|
+
data-tags={tech.tags.join(',')}
|
|
48
50
|
>
|
|
49
51
|
<span class="legend-indicator"><MovedIndicator moved={tech.moved} /></span>
|
|
50
52
|
<span class="legend-title">{tech.title}</span>
|
|
@@ -97,10 +99,18 @@ const { technologies, segments } = Astro.props;
|
|
|
97
99
|
transition: background-color 0.15s, color 0.15s, opacity 0.3s ease;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
.legend-row:hover
|
|
102
|
+
.legend-row:hover,
|
|
103
|
+
.legend-row:focus-visible {
|
|
101
104
|
background-color: var(--radar-hover-bg);
|
|
102
105
|
color: var(--radar-text);
|
|
103
106
|
}
|
|
107
|
+
.legend-row:focus-visible {
|
|
108
|
+
outline: 2px solid var(--radar-link);
|
|
109
|
+
outline-offset: -2px;
|
|
110
|
+
}
|
|
111
|
+
.legend-row:focus:not(:focus-visible) {
|
|
112
|
+
outline: none;
|
|
113
|
+
}
|
|
104
114
|
|
|
105
115
|
.legend-indicator {
|
|
106
116
|
display: flex;
|
package/src/layouts/Base.astro
CHANGED
|
@@ -34,6 +34,14 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
|
|
34
34
|
<meta name="twitter:card" content="summary_large_image" />
|
|
35
35
|
<meta name="twitter:title" content={title} />
|
|
36
36
|
<meta name="twitter:description" content={description} />
|
|
37
|
+
{config.feed && (
|
|
38
|
+
<link
|
|
39
|
+
rel="alternate"
|
|
40
|
+
type="application/rss+xml"
|
|
41
|
+
title={config.title}
|
|
42
|
+
href={`${base}feed.xml`}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
37
45
|
<title>{title}</title>
|
|
38
46
|
<style set:html={themeCSS}></style>
|
|
39
47
|
<script is:inline define:vars={{ toggle: config.color.toggle, colorMode: config.color.mode }}>
|
|
@@ -67,7 +75,10 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
|
|
67
75
|
)}
|
|
68
76
|
{config.name}
|
|
69
77
|
</a>
|
|
70
|
-
|
|
78
|
+
<div class="flex items-center gap-3">
|
|
79
|
+
<slot name="nav-end" />
|
|
80
|
+
{config.color.toggle && <ThemeToggle />}
|
|
81
|
+
</div>
|
|
71
82
|
</nav>
|
|
72
83
|
</header>
|
|
73
84
|
|
|
@@ -82,7 +93,7 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
|
|
82
93
|
© <a href="https://github.com/dirsigler/techradar" target="_blank" rel="noopener noreferrer" class="footer-link">Dennis Irsigler</a>
|
|
83
94
|
·
|
|
84
95
|
<a href="https://github.com/dirsigler/techradar/blob/main/LICENSE" target="_blank" rel="noopener noreferrer" class="footer-link">MIT License</a>
|
|
85
|
-
{config.
|
|
96
|
+
{config.footer && <Fragment> · <span set:html={config.footer} /></Fragment>}
|
|
86
97
|
</span>
|
|
87
98
|
|
|
88
99
|
{config.socialLinks && config.socialLinks.length > 0 && (
|
|
@@ -146,4 +157,23 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
|
|
|
146
157
|
.social-link:hover {
|
|
147
158
|
color: var(--radar-text);
|
|
148
159
|
}
|
|
160
|
+
|
|
161
|
+
:global(.search-input) {
|
|
162
|
+
width: 12rem;
|
|
163
|
+
height: calc(1.25rem + 1rem + 2px); /* match toggle: icon 1.25rem + padding 0.5rem*2 + border 1px*2 */
|
|
164
|
+
padding: 0 0.625rem;
|
|
165
|
+
border-radius: 0.5rem;
|
|
166
|
+
border: 1px solid var(--radar-border);
|
|
167
|
+
background-color: transparent;
|
|
168
|
+
color: var(--radar-text);
|
|
169
|
+
font-size: 0.8125rem;
|
|
170
|
+
outline: none;
|
|
171
|
+
transition: color 0.15s, border-color 0.15s;
|
|
172
|
+
}
|
|
173
|
+
:global(.search-input)::placeholder {
|
|
174
|
+
color: var(--radar-text-faint);
|
|
175
|
+
}
|
|
176
|
+
:global(.search-input):focus {
|
|
177
|
+
border-color: var(--radar-active-bg);
|
|
178
|
+
}
|
|
149
179
|
</style>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import config from 'virtual:techradar/config';
|
|
4
|
+
|
|
5
|
+
function escapeXml(str: string): string {
|
|
6
|
+
return str
|
|
7
|
+
.replace(/&/g, '&')
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/"/g, '"')
|
|
11
|
+
.replace(/'/g, ''');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function GET(context: APIContext) {
|
|
15
|
+
const site = context.site?.toString().replace(/\/+$/, '') ?? 'https://example.com';
|
|
16
|
+
const technologies = await getCollection('technologies');
|
|
17
|
+
|
|
18
|
+
const items = technologies.map((entry) => {
|
|
19
|
+
const ring = entry.data.ring.charAt(0).toUpperCase() + entry.data.ring.slice(1);
|
|
20
|
+
|
|
21
|
+
let pubDate = '';
|
|
22
|
+
if (entry.data.history && entry.data.history.length > 0) {
|
|
23
|
+
const sorted = [...entry.data.history].sort((a, b) =>
|
|
24
|
+
b.date.localeCompare(a.date),
|
|
25
|
+
);
|
|
26
|
+
const parsed = new Date(sorted[0].date);
|
|
27
|
+
if (!isNaN(parsed.getTime())) {
|
|
28
|
+
pubDate = `<pubDate>${parsed.toUTCString()}</pubDate>`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const link = `${site}${config.basePath}/technology/${entry.id}/`;
|
|
33
|
+
|
|
34
|
+
return ` <item>
|
|
35
|
+
<title>${escapeXml(`${entry.data.title} — ${ring}`)}</title>
|
|
36
|
+
<link>${escapeXml(link)}</link>
|
|
37
|
+
<guid>${escapeXml(link)}</guid>
|
|
38
|
+
<description>${escapeXml(`${entry.data.title} is in the ${ring} ring of the technology radar.`)}</description>
|
|
39
|
+
${pubDate}
|
|
40
|
+
</item>`;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const channelTitle = escapeXml(config.title);
|
|
44
|
+
const channelDescription = escapeXml(
|
|
45
|
+
config.subtitle ?? `${config.name} — tracking technology adoption`,
|
|
46
|
+
);
|
|
47
|
+
const channelLink = `${site}${config.basePath}/`;
|
|
48
|
+
|
|
49
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
50
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
51
|
+
<channel>
|
|
52
|
+
<title>${channelTitle}</title>
|
|
53
|
+
<description>${channelDescription}</description>
|
|
54
|
+
<link>${escapeXml(channelLink)}</link>
|
|
55
|
+
<atom:link href="${escapeXml(`${channelLink}feed.xml`)}" rel="self" type="application/rss+xml" />
|
|
56
|
+
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
|
|
57
|
+
${items.join('\n')}
|
|
58
|
+
</channel>
|
|
59
|
+
</rss>`;
|
|
60
|
+
|
|
61
|
+
return new Response(xml, {
|
|
62
|
+
headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' },
|
|
63
|
+
});
|
|
64
|
+
}
|
package/src/pages/index.astro
CHANGED
|
@@ -40,6 +40,7 @@ const technologies = technologyEntries
|
|
|
40
40
|
color: seg?.color ?? '#6b7280',
|
|
41
41
|
order: seg?.order ?? 1,
|
|
42
42
|
href: `${base}technology/${entry.id}/`,
|
|
43
|
+
tags: (entry.data.tags ?? []) as string[],
|
|
43
44
|
};
|
|
44
45
|
})
|
|
45
46
|
.sort((a, b) => a.title.localeCompare(b.title));
|
|
@@ -51,6 +52,7 @@ const legendSegments = segments.map((s) => ({
|
|
|
51
52
|
}));
|
|
52
53
|
|
|
53
54
|
const ringEntries = Object.entries(RING_LABELS) as [string, string][];
|
|
55
|
+
|
|
54
56
|
---
|
|
55
57
|
|
|
56
58
|
<Base title={config.name}>
|
|
@@ -63,6 +65,17 @@ const ringEntries = Object.entries(RING_LABELS) as [string, string][];
|
|
|
63
65
|
)}
|
|
64
66
|
</div>
|
|
65
67
|
|
|
68
|
+
{config.search.enabled && (
|
|
69
|
+
<input
|
|
70
|
+
type="search"
|
|
71
|
+
id="radar-search"
|
|
72
|
+
placeholder={config.search.placeholder}
|
|
73
|
+
autocomplete="off"
|
|
74
|
+
class="search-input"
|
|
75
|
+
slot="nav-end"
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
|
|
66
79
|
<!-- Filter bar -->
|
|
67
80
|
<div id="filter-bar" class="mb-8 flex flex-col items-center gap-3">
|
|
68
81
|
<div class="flex flex-wrap items-center justify-center gap-2">
|
|
@@ -116,7 +129,7 @@ const ringEntries = Object.entries(RING_LABELS) as [string, string][];
|
|
|
116
129
|
border-color: var(--radar-border-subtle);
|
|
117
130
|
color: var(--radar-text);
|
|
118
131
|
}
|
|
119
|
-
|
|
132
|
+
.filter-btn.active {
|
|
120
133
|
background-color: var(--radar-active-bg);
|
|
121
134
|
border-color: var(--radar-active-bg);
|
|
122
135
|
color: var(--radar-active-text);
|
|
@@ -164,44 +177,111 @@ const ringEntries = Object.entries(RING_LABELS) as [string, string][];
|
|
|
164
177
|
const dots = document.querySelectorAll<SVGCircleElement>('.radar-dot');
|
|
165
178
|
const legendRows = document.querySelectorAll<HTMLElement>('.legend-row');
|
|
166
179
|
|
|
180
|
+
const searchInput = document.getElementById('radar-search') as HTMLInputElement | null;
|
|
181
|
+
|
|
182
|
+
function filterDot(dot: SVGCircleElement, visible: boolean) {
|
|
183
|
+
const link = dot.closest('a') as SVGAElement | null;
|
|
184
|
+
if (!link) return;
|
|
185
|
+
link.style.opacity = visible ? '1' : '0.1';
|
|
186
|
+
link.style.pointerEvents = visible ? 'auto' : 'none';
|
|
187
|
+
link.setAttribute('tabindex', visible ? '0' : '-1');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function filterRow(row: HTMLElement, visible: boolean) {
|
|
191
|
+
row.style.opacity = visible ? '1' : '0.15';
|
|
192
|
+
row.style.pointerEvents = visible ? 'auto' : 'none';
|
|
193
|
+
row.setAttribute('tabindex', visible ? '0' : '-1');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function applySearch(query: string) {
|
|
197
|
+
const q = query.toLowerCase().trim();
|
|
198
|
+
|
|
199
|
+
buttons.forEach((b) => b.classList.remove('active'));
|
|
200
|
+
document.querySelector('[data-filter="all"]')?.classList.add('active');
|
|
201
|
+
|
|
202
|
+
dots.forEach((dot) => {
|
|
203
|
+
const title = (dot.getAttribute('data-title') ?? '').toLowerCase();
|
|
204
|
+
const segment = (dot.getAttribute('data-segment') ?? '').toLowerCase();
|
|
205
|
+
const ring = (dot.getAttribute('data-ring') ?? '').toLowerCase();
|
|
206
|
+
const tags = (dot.getAttribute('data-tags') ?? '').toLowerCase();
|
|
207
|
+
filterDot(dot, !q || title.includes(q) || segment.includes(q) || ring.includes(q) || tags.includes(q));
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
legendRows.forEach((row) => {
|
|
211
|
+
const title = (row.querySelector('.legend-title')?.textContent ?? '').toLowerCase();
|
|
212
|
+
const segment = (row.dataset.segment ?? '').toLowerCase();
|
|
213
|
+
const ring = (row.dataset.ring ?? '').toLowerCase();
|
|
214
|
+
const tags = (row.dataset.tags ?? '').toLowerCase();
|
|
215
|
+
filterRow(row, !q || title.includes(q) || segment.includes(q) || ring.includes(q) || tags.includes(q));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
searchInput?.addEventListener('input', () => applySearch(searchInput!.value));
|
|
220
|
+
|
|
221
|
+
const allBtn = document.querySelector<HTMLButtonElement>('[data-filter="all"]')!;
|
|
222
|
+
|
|
223
|
+
function resetAll() {
|
|
224
|
+
buttons.forEach((b) => b.classList.remove('active'));
|
|
225
|
+
allBtn.classList.add('active');
|
|
226
|
+
dots.forEach((dot) => filterDot(dot, true));
|
|
227
|
+
legendRows.forEach((row) => filterRow(row, true));
|
|
228
|
+
}
|
|
229
|
+
|
|
167
230
|
buttons.forEach((btn) => {
|
|
168
231
|
btn.addEventListener('click', () => {
|
|
169
|
-
|
|
232
|
+
if (searchInput) searchInput.value = '';
|
|
233
|
+
|
|
234
|
+
// Toggle off if already active (unless it's the "All" button)
|
|
235
|
+
if (btn.classList.contains('active') && btn.dataset.filter !== 'all') {
|
|
236
|
+
resetAll();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
170
240
|
buttons.forEach((b) => b.classList.remove('active'));
|
|
171
241
|
btn.classList.add('active');
|
|
172
242
|
|
|
173
243
|
const filter = btn.dataset.filter!;
|
|
174
244
|
const type = btn.dataset.type!;
|
|
175
245
|
|
|
176
|
-
// Filter radar dots
|
|
177
246
|
dots.forEach((dot) => {
|
|
178
|
-
const link = dot.closest('a') as SVGAElement | null;
|
|
179
|
-
if (!link) return;
|
|
180
|
-
|
|
181
247
|
const segment = dot.getAttribute('data-segment') ?? '';
|
|
182
248
|
const ring = dot.getAttribute('data-ring') ?? '';
|
|
249
|
+
const tags = (dot.getAttribute('data-tags') ?? '').split(',').filter(Boolean);
|
|
183
250
|
|
|
184
251
|
let visible = true;
|
|
185
252
|
if (type === 'segment') visible = segment === filter;
|
|
186
253
|
else if (type === 'ring') visible = ring === filter;
|
|
254
|
+
else if (type === 'tag') visible = tags.includes(filter);
|
|
187
255
|
|
|
188
|
-
|
|
189
|
-
link.style.opacity = visible ? '1' : '0.1';
|
|
190
|
-
link.style.pointerEvents = visible ? 'auto' : 'none';
|
|
256
|
+
filterDot(dot, visible);
|
|
191
257
|
});
|
|
192
258
|
|
|
193
|
-
// Filter legend rows
|
|
194
259
|
legendRows.forEach((row) => {
|
|
195
260
|
const segment = row.dataset.segment ?? '';
|
|
196
261
|
const ring = row.dataset.ring ?? '';
|
|
262
|
+
const tags = (row.dataset.tags ?? '').split(',').filter(Boolean);
|
|
197
263
|
|
|
198
264
|
let visible = true;
|
|
199
265
|
if (type === 'segment') visible = segment === filter;
|
|
200
266
|
else if (type === 'ring') visible = ring === filter;
|
|
267
|
+
else if (type === 'tag') visible = tags.includes(filter);
|
|
201
268
|
|
|
202
|
-
row
|
|
203
|
-
row.style.pointerEvents = visible ? 'auto' : 'none';
|
|
269
|
+
filterRow(row, visible);
|
|
204
270
|
});
|
|
205
271
|
});
|
|
206
272
|
});
|
|
273
|
+
|
|
274
|
+
// Activate filter from URL query parameters
|
|
275
|
+
const params = new URLSearchParams(window.location.search);
|
|
276
|
+
const urlQuery = params.get('q');
|
|
277
|
+
const urlTag = params.get('tag');
|
|
278
|
+
if (urlQuery && searchInput) {
|
|
279
|
+
searchInput.value = urlQuery;
|
|
280
|
+
applySearch(urlQuery);
|
|
281
|
+
} else if (urlTag) {
|
|
282
|
+
const tagBtn = Array.from(buttons).find(
|
|
283
|
+
(b) => b.dataset.type === 'tag' && b.dataset.filter === urlTag,
|
|
284
|
+
);
|
|
285
|
+
if (tagBtn) tagBtn.click();
|
|
286
|
+
}
|
|
207
287
|
</script>
|
|
@@ -3,6 +3,7 @@ import { getCollection, render, type CollectionEntry } from 'astro:content';
|
|
|
3
3
|
import Base from '../../layouts/Base.astro';
|
|
4
4
|
import RingBadge from '../../components/RingBadge.astro';
|
|
5
5
|
import MovedIndicator from '../../components/MovedIndicator.astro';
|
|
6
|
+
import { Icon } from 'astro-icon/components';
|
|
6
7
|
import config from 'virtual:techradar/config';
|
|
7
8
|
import type { GetStaticPaths } from 'astro';
|
|
8
9
|
|
|
@@ -35,9 +36,48 @@ const segmentColor = segmentEntry?.data.color ?? '#6c7086';
|
|
|
35
36
|
|
|
36
37
|
// Build "Edit on GitHub" URL from the entry's file path
|
|
37
38
|
const techFileName = entry.id.split('/').pop();
|
|
38
|
-
const editUrl = config.
|
|
39
|
-
? `${config.
|
|
39
|
+
const editUrl = config.editing.enabled && config.editing.baseUrl
|
|
40
|
+
? `${config.editing.baseUrl}/${segmentSlug}/${techFileName}.md`
|
|
40
41
|
: null;
|
|
42
|
+
|
|
43
|
+
const linkIconMap: Record<string, string> = {
|
|
44
|
+
docs: 'lucide:book-open',
|
|
45
|
+
repo: 'lucide:git-fork',
|
|
46
|
+
website: 'lucide:globe',
|
|
47
|
+
community: 'lucide:messages-square',
|
|
48
|
+
};
|
|
49
|
+
const linkOrder: Record<string, number> = { docs: 0, repo: 1, website: 2, community: 3 };
|
|
50
|
+
const links = (entry.data.links ?? [])
|
|
51
|
+
.map((link: { label: string; url: string; type: string }) => ({
|
|
52
|
+
...link,
|
|
53
|
+
icon: linkIconMap[link.type] ?? 'lucide:globe',
|
|
54
|
+
order: linkOrder[link.type] ?? 99,
|
|
55
|
+
}))
|
|
56
|
+
.sort((a, b) => a.order - b.order);
|
|
57
|
+
|
|
58
|
+
const tags = (entry.data.tags ?? []) as string[];
|
|
59
|
+
const manualRelatedSlugs = (entry.data.related ?? []) as string[];
|
|
60
|
+
|
|
61
|
+
// Build related technologies: manual + auto (2+ shared tags)
|
|
62
|
+
const allTechnologies = await getCollection('technologies');
|
|
63
|
+
const manualRelated = manualRelatedSlugs
|
|
64
|
+
.map((slug) => allTechnologies.find((t) => t.id === slug))
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
|
|
67
|
+
const manualIds = new Set(manualRelatedSlugs);
|
|
68
|
+
manualIds.add(entry.id); // exclude self
|
|
69
|
+
|
|
70
|
+
let autoRelated: typeof manualRelated = [];
|
|
71
|
+
if (tags.length > 0) {
|
|
72
|
+
autoRelated = allTechnologies.filter((t) => {
|
|
73
|
+
if (manualIds.has(t.id)) return false;
|
|
74
|
+
const otherTags = (t.data.tags ?? []) as string[];
|
|
75
|
+
const shared = tags.filter((tag) => otherTags.includes(tag));
|
|
76
|
+
return shared.length >= 2;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const relatedTechnologies = [...manualRelated, ...autoRelated];
|
|
41
81
|
---
|
|
42
82
|
|
|
43
83
|
<Base title={`${entry.data.title} — Tech Radar`}>
|
|
@@ -57,13 +97,50 @@ const editUrl = config.editBaseUrl
|
|
|
57
97
|
{entry.data.title}
|
|
58
98
|
<MovedIndicator moved={entry.data.moved} />
|
|
59
99
|
</h1>
|
|
60
|
-
<div class="mt-3 flex items-center gap-3">
|
|
100
|
+
<div class="mt-3 flex items-center gap-3 flex-wrap">
|
|
61
101
|
<RingBadge ring={entry.data.ring} />
|
|
62
102
|
<span class="flex items-center gap-1.5 text-sm" style="color: var(--radar-text-muted);">
|
|
63
103
|
<span class="inline-block h-2.5 w-2.5 rounded-full" style={`background-color: ${segmentColor}`}></span>
|
|
64
104
|
{segmentTitle}
|
|
65
105
|
</span>
|
|
106
|
+
{entry.data.owner && (
|
|
107
|
+
<span class="flex items-center gap-1.5 text-sm" style="color: var(--radar-text-muted);">
|
|
108
|
+
<Icon name="lucide:users-round" class="h-3.5 w-3.5" />
|
|
109
|
+
{entry.data.owner.url ? (
|
|
110
|
+
<a href={entry.data.owner.url} target="_blank" rel="noopener noreferrer" class="owner-link">
|
|
111
|
+
{entry.data.owner.name}
|
|
112
|
+
</a>
|
|
113
|
+
) : (
|
|
114
|
+
entry.data.owner.name
|
|
115
|
+
)}
|
|
116
|
+
</span>
|
|
117
|
+
)}
|
|
118
|
+
{links.length > 0 && (
|
|
119
|
+
<>
|
|
120
|
+
<span class="meta-divider" />
|
|
121
|
+
{links.map((link) => (
|
|
122
|
+
<a
|
|
123
|
+
href={link.url}
|
|
124
|
+
target="_blank"
|
|
125
|
+
rel="noopener noreferrer"
|
|
126
|
+
class="tech-link"
|
|
127
|
+
>
|
|
128
|
+
<Icon name={link.icon} class="h-3.5 w-3.5" />
|
|
129
|
+
{link.label}
|
|
130
|
+
</a>
|
|
131
|
+
))}
|
|
132
|
+
</>
|
|
133
|
+
)}
|
|
66
134
|
</div>
|
|
135
|
+
{tags.length > 0 && (
|
|
136
|
+
<div class="mt-3 flex items-center gap-2 flex-wrap">
|
|
137
|
+
{tags.map((tag) => (
|
|
138
|
+
<a href={`${base}?tag=${encodeURIComponent(tag)}`} class="tag-pill">
|
|
139
|
+
#{tag}
|
|
140
|
+
</a>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
67
144
|
</header>
|
|
68
145
|
|
|
69
146
|
<div class="prose max-w-none">
|
|
@@ -95,6 +172,18 @@ const editUrl = config.editBaseUrl
|
|
|
95
172
|
</section>
|
|
96
173
|
)}
|
|
97
174
|
|
|
175
|
+
{relatedTechnologies.length > 0 && (
|
|
176
|
+
<div class="mt-8 text-sm" style="color: var(--radar-text-muted);">
|
|
177
|
+
<span class="font-medium" style="color: var(--radar-text-secondary);">Related</span>
|
|
178
|
+
{relatedTechnologies.map((tech, i) => (
|
|
179
|
+
<>
|
|
180
|
+
<span class="mx-1.5">{i > 0 ? '·' : ''}</span>
|
|
181
|
+
<a href={`${base}technology/${tech.id}/`} class="related-link">{tech.data.title}</a>
|
|
182
|
+
</>
|
|
183
|
+
))}
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
|
|
98
187
|
{editUrl && (
|
|
99
188
|
<div class="mt-8 pt-6" style="border-top: 1px solid var(--radar-border);">
|
|
100
189
|
<a
|
|
@@ -136,6 +225,58 @@ const editUrl = config.editBaseUrl
|
|
|
136
225
|
color: var(--radar-link);
|
|
137
226
|
}
|
|
138
227
|
|
|
228
|
+
.owner-link {
|
|
229
|
+
color: var(--radar-text-muted);
|
|
230
|
+
text-decoration: none;
|
|
231
|
+
transition: color 0.15s;
|
|
232
|
+
}
|
|
233
|
+
.owner-link:hover {
|
|
234
|
+
color: var(--radar-link);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.meta-divider {
|
|
238
|
+
width: 1px;
|
|
239
|
+
height: 1rem;
|
|
240
|
+
background-color: var(--radar-border);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.tech-link {
|
|
244
|
+
display: inline-flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
gap: 0.375rem;
|
|
247
|
+
font-size: 0.875rem;
|
|
248
|
+
color: var(--radar-text-muted);
|
|
249
|
+
text-decoration: none;
|
|
250
|
+
transition: color 0.15s;
|
|
251
|
+
}
|
|
252
|
+
.tech-link:hover {
|
|
253
|
+
color: var(--radar-link);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.tag-pill {
|
|
257
|
+
display: inline-block;
|
|
258
|
+
padding: 0.125rem 0.5rem;
|
|
259
|
+
border-radius: 0.25rem;
|
|
260
|
+
border: 1px solid var(--radar-border-subtle);
|
|
261
|
+
font-size: 0.75rem;
|
|
262
|
+
color: var(--radar-text-muted);
|
|
263
|
+
text-decoration: none;
|
|
264
|
+
transition: color 0.15s, border-color 0.15s;
|
|
265
|
+
}
|
|
266
|
+
.tag-pill:hover {
|
|
267
|
+
color: var(--radar-link);
|
|
268
|
+
border-color: var(--radar-link);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.related-link {
|
|
272
|
+
color: var(--radar-text-muted);
|
|
273
|
+
text-decoration: none;
|
|
274
|
+
transition: color 0.15s;
|
|
275
|
+
}
|
|
276
|
+
.related-link:hover {
|
|
277
|
+
color: var(--radar-link);
|
|
278
|
+
}
|
|
279
|
+
|
|
139
280
|
.history-timeline {
|
|
140
281
|
list-style: none;
|
|
141
282
|
padding: 0;
|
package/src/themes/default.css
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
--radar-bg-secondary: #f8fafc;
|
|
8
8
|
--radar-text: #0f172a;
|
|
9
9
|
--radar-text-secondary: #334155;
|
|
10
|
-
--radar-text-muted: #
|
|
11
|
-
--radar-text-faint: #
|
|
10
|
+
--radar-text-muted: #475569; /* slate-600, ~7.4:1 on white */
|
|
11
|
+
--radar-text-faint: #64748b; /* slate-500, ~4.8:1 on white */
|
|
12
12
|
--radar-border: #e2e8f0;
|
|
13
13
|
--radar-border-subtle: #cbd5e1;
|
|
14
14
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
--radar-chart-edge: #f8fafc;
|
|
25
25
|
--radar-chart-ring: #cbd5e1;
|
|
26
26
|
--radar-chart-divider: #cbd5e1;
|
|
27
|
-
--radar-chart-label: #
|
|
27
|
+
--radar-chart-label: #475569;
|
|
28
28
|
--radar-chart-band: 15, 23, 42;
|
|
29
29
|
--radar-dot-stroke: #ffffff;
|
|
30
30
|
|
|
@@ -64,16 +64,16 @@
|
|
|
64
64
|
--radar-bg-secondary: #1e293b;
|
|
65
65
|
--radar-text: #e2e8f0;
|
|
66
66
|
--radar-text-secondary: #cbd5e1;
|
|
67
|
-
--radar-text-muted: #
|
|
68
|
-
--radar-text-faint: #94a3b8;
|
|
67
|
+
--radar-text-muted: #a3b1c1; /* ~8.3:1 on #0f172a */
|
|
68
|
+
--radar-text-faint: #94a3b8; /* ~5.6:1 on #0f172a */
|
|
69
69
|
--radar-border: #334155;
|
|
70
|
-
--radar-border-subtle: #
|
|
70
|
+
--radar-border-subtle: #566375; /* brighter for visibility, ~3.5:1 */
|
|
71
71
|
|
|
72
72
|
/* Interactive */
|
|
73
73
|
--radar-hover-bg: #334155;
|
|
74
74
|
--radar-link: #60a5fa;
|
|
75
|
-
--radar-active-bg: #
|
|
76
|
-
--radar-active-text: #
|
|
75
|
+
--radar-active-bg: #2563eb; /* deeper blue, 5.6:1 with white text */
|
|
76
|
+
--radar-active-text: #ffffff;
|
|
77
77
|
|
|
78
78
|
/* Radar SVG */
|
|
79
79
|
--radar-chart-center: #334155;
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
--radar-chart-edge: #0f172a;
|
|
82
82
|
--radar-chart-ring: #475569;
|
|
83
83
|
--radar-chart-divider: #475569;
|
|
84
|
-
--radar-chart-label: #
|
|
84
|
+
--radar-chart-label: #a3b1c1;
|
|
85
85
|
--radar-chart-band: 226, 232, 240;
|
|
86
86
|
--radar-dot-stroke: #0f172a;
|
|
87
87
|
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
--radar-prose-links: #60a5fa;
|
|
110
110
|
--radar-prose-bold: #e2e8f0;
|
|
111
111
|
--radar-prose-code: #a78bfa;
|
|
112
|
-
--radar-prose-quotes: #
|
|
112
|
+
--radar-prose-quotes: #a3b1c1;
|
|
113
113
|
--radar-prose-hr: #475569;
|
|
114
114
|
}
|
|
115
115
|
}
|
|
@@ -121,16 +121,16 @@
|
|
|
121
121
|
--radar-bg-secondary: #1e293b;
|
|
122
122
|
--radar-text: #e2e8f0;
|
|
123
123
|
--radar-text-secondary: #cbd5e1;
|
|
124
|
-
--radar-text-muted: #
|
|
124
|
+
--radar-text-muted: #a3b1c1;
|
|
125
125
|
--radar-text-faint: #94a3b8;
|
|
126
126
|
--radar-border: #334155;
|
|
127
|
-
--radar-border-subtle: #
|
|
127
|
+
--radar-border-subtle: #566375;
|
|
128
128
|
|
|
129
129
|
/* Interactive */
|
|
130
130
|
--radar-hover-bg: #334155;
|
|
131
131
|
--radar-link: #60a5fa;
|
|
132
|
-
--radar-active-bg: #
|
|
133
|
-
--radar-active-text: #
|
|
132
|
+
--radar-active-bg: #2563eb;
|
|
133
|
+
--radar-active-text: #ffffff;
|
|
134
134
|
|
|
135
135
|
/* Radar SVG */
|
|
136
136
|
--radar-chart-center: #334155;
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
--radar-chart-edge: #0f172a;
|
|
139
139
|
--radar-chart-ring: #475569;
|
|
140
140
|
--radar-chart-divider: #475569;
|
|
141
|
-
--radar-chart-label: #
|
|
141
|
+
--radar-chart-label: #a3b1c1;
|
|
142
142
|
--radar-chart-band: 226, 232, 240;
|
|
143
143
|
--radar-dot-stroke: #0f172a;
|
|
144
144
|
|
|
@@ -166,6 +166,6 @@
|
|
|
166
166
|
--radar-prose-links: #60a5fa;
|
|
167
167
|
--radar-prose-bold: #e2e8f0;
|
|
168
168
|
--radar-prose-code: #a78bfa;
|
|
169
|
-
--radar-prose-quotes: #
|
|
169
|
+
--radar-prose-quotes: #a3b1c1;
|
|
170
170
|
--radar-prose-hr: #475569;
|
|
171
171
|
}
|