@dirsigler/astro-techradar 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dennis Irsigler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,257 @@
1
+ # @dirsigler/astro-techradar
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@dirsigler/astro-techradar)](https://www.npmjs.com/package/@dirsigler/astro-techradar)
4
+ [![Built with Astro](https://img.shields.io/badge/Built%20with-Astro-BC52EE?logo=astro&logoColor=white)](https://astro.build)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ An [Astro](https://astro.build) integration that adds a complete, interactive technology radar to your site. Track technology adoption decisions across your organization with a visual radar chart, categorized segments, and detailed technology pages — all driven by simple Markdown files.
8
+
9
+ **[View Live Demo](https://demo.techradar.irsigler.dev)**
10
+
11
+ ---
12
+
13
+ ## Preview
14
+
15
+ | Radar Overview | Technology Detail |
16
+ | :----------------------------------------------: | :--------------------------------------------------------: |
17
+ | ![Radar Light](docs/screenshots/radar-light.png) | ![Technology Page](docs/screenshots/technology-detail.png) |
18
+ | ![Radar Dark](docs/screenshots/radar-dark.png) | ![Segment Page](docs/screenshots/segment-page.png) |
19
+
20
+ ---
21
+
22
+ ## Features
23
+
24
+ - **Interactive SVG Radar** — Technologies plotted across four rings (Adopt, Trial, Assess, Hold) with hover tooltips and click-through navigation
25
+ - **Markdown-Driven** — Add technologies by dropping `.md` files into `segments/`. No code changes needed
26
+ - **Theming & Color Mode** — Ships with a default theme and [Catppuccin Mocha](https://catppuccin.com). Light/dark toggle, lockable color mode, or create your own theme with CSS custom properties
27
+ - **Fully Static & Fast** — Builds to plain HTML/CSS/JS. Deploy anywhere
28
+ - **Movement Indicators** — Mark technologies as moved in/out to highlight recent changes
29
+ - **SEO Ready** — Open Graph, Twitter Cards, canonical URLs, and custom 404 page
30
+
31
+ ---
32
+
33
+ ## Quick Start
34
+
35
+ ### 1. Install
36
+
37
+ ```bash
38
+ npm install @dirsigler/astro-techradar astro
39
+ ```
40
+
41
+ ### 2. Add the integration
42
+
43
+ ```js
44
+ // astro.config.mjs
45
+ import { defineConfig } from "astro/config";
46
+ import techradar from "@dirsigler/astro-techradar";
47
+
48
+ export default defineConfig({
49
+ site: "https://your-site.example.com",
50
+ integrations: [
51
+ techradar({
52
+ title: "Tech Radar",
53
+ }),
54
+ ],
55
+ });
56
+ ```
57
+
58
+ ### 3. Define the content collections
59
+
60
+ Create `src/content.config.ts`:
61
+
62
+ ```ts
63
+ import { defineCollection } from "astro:content";
64
+ import { glob } from "astro/loaders";
65
+ import { segmentSchema, technologySchema } from "@dirsigler/astro-techradar/schemas";
66
+
67
+ const segments = defineCollection({
68
+ loader: glob({ pattern: "*/index.md", base: "./segments" }),
69
+ schema: segmentSchema,
70
+ });
71
+
72
+ const technologies = defineCollection({
73
+ loader: glob({ pattern: "*/!(index).md", base: "./segments" }),
74
+ schema: technologySchema,
75
+ });
76
+
77
+ export const collections = { segments, technologies };
78
+ ```
79
+
80
+ ### 4. Add your content
81
+
82
+ ```text
83
+ segments/
84
+ ├── cloud/
85
+ │ ├── index.md # Segment metadata
86
+ │ ├── kubernetes.md
87
+ │ └── terraform.md
88
+ ├── frameworks/
89
+ │ ├── index.md
90
+ │ ├── astro.md
91
+ │ └── react.md
92
+ └── ...
93
+ ```
94
+
95
+ Each segment needs an `index.md`:
96
+
97
+ ```markdown
98
+ ---
99
+ title: Cloud
100
+ color: "#3b82f6"
101
+ order: 1
102
+ ---
103
+ ```
104
+
105
+ Each technology file:
106
+
107
+ ```markdown
108
+ ---
109
+ title: Kubernetes
110
+ ring: adopt
111
+ moved: 0
112
+ ---
113
+
114
+ Your description in Markdown. Explain why this technology is in this ring
115
+ and what your experience has been.
116
+ ```
117
+
118
+ ### 5. Run
119
+
120
+ ```bash
121
+ npx astro dev
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Configuration
127
+
128
+ All options are passed to the `techradar()` integration in `astro.config.mjs`:
129
+
130
+ ```js
131
+ techradar({
132
+ // Required
133
+ title: "Tech Radar",
134
+
135
+ // Optional
136
+ basePath: "/techradar", // Mount under a sub-path (e.g. acme.com/techradar/)
137
+ logo: "/logo.svg", // Path to logo in public/
138
+ footerText: "Built by the Platform Team", // Supports HTML
139
+ repositoryUrl: "https://github.com/your-org/your-radar",
140
+ editBaseUrl: "https://github.com/your-org/your-radar/edit/main/segments",
141
+ theme: "default", // 'default' | 'catppuccin-mocha' | path to custom CSS
142
+ color: {
143
+ toggle: true, // Show the light/dark mode toggle (default: true)
144
+ mode: "system", // 'light' | 'dark' | 'system' (default: 'system')
145
+ },
146
+ socialLinks: [
147
+ {
148
+ label: "GitHub",
149
+ href: "https://github.com/your-org/your-radar",
150
+ icon: "github", // Lucide icon name (requires @iconify-json/lucide)
151
+ },
152
+ ],
153
+ });
154
+ ```
155
+
156
+ ### Social Link Icons
157
+
158
+ Social links can optionally display [Lucide](https://lucide.dev) icons. If you use the `icon` field, install the icon set:
159
+
160
+ ```bash
161
+ npm install @iconify-json/lucide
162
+ ```
163
+
164
+ ```js
165
+ socialLinks: [
166
+ { label: "GitHub", href: "https://github.com/your-org", icon: "github" },
167
+ ],
168
+ ```
169
+
170
+ ### Technology Frontmatter
171
+
172
+ | Field | Type | Description |
173
+ | :------ | :----------------------------------------- | :---------------------------------------------- |
174
+ | `title` | `string` | Display name |
175
+ | `ring` | `'adopt' \| 'trial' \| 'assess' \| 'hold'` | Which ring the technology belongs to |
176
+ | `moved` | `-1 \| 0 \| 1` | Movement indicator (-1 = out, 0 = none, 1 = in) |
177
+
178
+ ### Segment Frontmatter
179
+
180
+ | Field | Type | Description |
181
+ | :------ | :------- | :--------------------------- |
182
+ | `title` | `string` | Segment display name |
183
+ | `color` | `string` | Hex color (e.g. `"#3b82f6"`) |
184
+ | `order` | `number` | Quadrant position (1-4) |
185
+
186
+ ---
187
+
188
+ ## Theming
189
+
190
+ Themes are CSS files defining `--radar-*` custom properties.
191
+
192
+ | Theme | Description |
193
+ | :----------------- | :---------------------------------------------------- |
194
+ | `default` | Clean light/dark theme (follows system preference) |
195
+ | `catppuccin-mocha` | [Catppuccin Mocha](https://catppuccin.com) dark theme |
196
+
197
+ To create a custom theme, copy the [default theme](src/themes/default.css), save it in your project, and point to it:
198
+
199
+ ```js
200
+ techradar({
201
+ theme: "./src/my-theme.css",
202
+ });
203
+ ```
204
+
205
+ ### Color Mode
206
+
207
+ Control the light/dark mode behavior with the `color` option:
208
+
209
+ ```js
210
+ // Default — toggle visible, follows system preference
211
+ techradar({ title: "Tech Radar" });
212
+
213
+ // Lock to dark mode, no toggle
214
+ techradar({
215
+ title: "Tech Radar",
216
+ color: { toggle: false, mode: "dark" },
217
+ });
218
+
219
+ // Lock to light mode, no toggle
220
+ techradar({
221
+ title: "Tech Radar",
222
+ color: { toggle: false, mode: "light" },
223
+ });
224
+ ```
225
+
226
+ | Field | Type | Default | Description |
227
+ | :------- | :------------------------------ | :--------- | :--------------------------------------------------- |
228
+ | `toggle` | `boolean` | `true` | Show the light/dark mode toggle in the header |
229
+ | `mode` | `'light' \| 'dark' \| 'system'` | `'system'` | Color mode — locks the mode when `toggle` is `false` |
230
+
231
+ ---
232
+
233
+ ## Base Path
234
+
235
+ Mount the radar under a sub-path when embedding it into a larger Astro site:
236
+
237
+ ```js
238
+ // Serves the radar at acme.com/techradar/
239
+ techradar({
240
+ title: "ACME Tech Radar",
241
+ basePath: "/techradar",
242
+ });
243
+ ```
244
+
245
+ All routes and internal links are automatically prefixed. This works alongside Astro's own `base` config for full flexibility.
246
+
247
+ ---
248
+
249
+ ## Example
250
+
251
+ See the [techradar-demo](https://github.com/dirsigler/techradar-demo) repository for a complete working example.
252
+
253
+ ---
254
+
255
+ ## License
256
+
257
+ [MIT](LICENSE) — Dennis Irsigler
package/config.ts ADDED
@@ -0,0 +1,95 @@
1
+ export interface SocialLink {
2
+ label: string;
3
+ href: string;
4
+ /** Lucide icon name (e.g. "github", "twitter", "globe"). Falls back to label text if unrecognized. */
5
+ icon?: string;
6
+ }
7
+
8
+ export interface ColorModeConfig {
9
+ /** Show the light/dark mode toggle in the header. Default: true */
10
+ toggle?: boolean;
11
+ /** Color mode preference. When toggle is false this locks the mode. Default: 'system' */
12
+ mode?: 'light' | 'dark' | 'system';
13
+ }
14
+
15
+ export interface ResolvedColorMode {
16
+ toggle: boolean;
17
+ mode: 'light' | 'dark' | 'system';
18
+ }
19
+
20
+ export interface TechRadarUserConfig {
21
+ /** Navbar title text */
22
+ title: string;
23
+
24
+ /** Main page headline. Falls back to title if not set. */
25
+ headline?: string;
26
+
27
+ /**
28
+ * URL path prefix where the tech radar is mounted (e.g. "/techradar").
29
+ * Useful when embedding the radar into a larger Astro site.
30
+ * Default: "" (root)
31
+ */
32
+ basePath?: string;
33
+
34
+ /** Logo image — either a path in public/ (e.g. "/logo.svg") or an external HTTPS URL. Omit to show title only. */
35
+ logo?: string;
36
+
37
+ /** Footer text. Supports simple HTML. */
38
+ footerText?: string;
39
+
40
+ /** Show "Edit this page" links on technology pages. Default: true */
41
+ allowEditing?: boolean;
42
+
43
+ /** Base URL for "Edit" links on technology pages (e.g. "https://github.com/org/repo/edit/main/segments"). */
44
+ editBaseUrl?: string;
45
+
46
+ /** Social links shown in the footer. */
47
+ socialLinks?: SocialLink[];
48
+
49
+ /** Theme name — matches a built-in CSS file ('default' | 'catppuccin-mocha') or a path to a custom CSS file. Default: 'default' */
50
+ theme?: string;
51
+
52
+ /** Color mode configuration. */
53
+ color?: ColorModeConfig;
54
+ }
55
+
56
+ export interface ResolvedConfig {
57
+ title: string;
58
+ headline: string;
59
+ /** Normalized base path with leading slash, no trailing slash. Empty string when mounted at root. */
60
+ basePath: string;
61
+ logo?: string;
62
+ footerText: string;
63
+ editBaseUrl?: string;
64
+ socialLinks: SocialLink[];
65
+ theme: string;
66
+ color: ResolvedColorMode;
67
+ }
68
+
69
+ /** Normalize a user-supplied path: ensure leading slash, strip trailing slash. Returns "" for root. */
70
+ function normalizeBasePath(raw?: string): string {
71
+ if (!raw) return '';
72
+ // Strip leading/trailing slashes, then re-add leading slash
73
+ const trimmed = raw.replace(/^\/+|\/+$/g, '');
74
+ return trimmed ? `/${trimmed}` : '';
75
+ }
76
+
77
+ export function resolveConfig(user: TechRadarUserConfig): ResolvedConfig {
78
+ return {
79
+ title: user.title,
80
+ headline: user.headline ?? user.title,
81
+ basePath: normalizeBasePath(user.basePath),
82
+ logo: user.logo,
83
+ footerText: user.footerText ?? '',
84
+ editBaseUrl:
85
+ (user.allowEditing ?? true) === false
86
+ ? undefined
87
+ : user.editBaseUrl,
88
+ socialLinks: user.socialLinks ?? [],
89
+ theme: user.theme ?? 'default',
90
+ color: {
91
+ toggle: user.color?.toggle ?? true,
92
+ mode: user.color?.mode ?? 'system',
93
+ },
94
+ };
95
+ }
package/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ import type { TechRadarUserConfig } from './config';
3
+ import { createIntegration } from './integration';
4
+
5
+ export default function techradar(
6
+ userConfig: TechRadarUserConfig,
7
+ ): AstroIntegration {
8
+ return createIntegration(userConfig);
9
+ }
10
+
11
+ export type { TechRadarUserConfig, ResolvedConfig, SocialLink } from './config';
package/integration.ts ADDED
@@ -0,0 +1,103 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ import type { TechRadarUserConfig } from './config';
3
+ import { resolveConfig } from './config';
4
+ import icon from 'astro-icon';
5
+ import tailwindcss from '@tailwindcss/vite';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { copyFileSync, mkdirSync, readFileSync, existsSync } from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ const PKG_DIR = path.dirname(fileURLToPath(import.meta.url));
11
+
12
+ export function createIntegration(
13
+ userConfig: TechRadarUserConfig,
14
+ ): AstroIntegration {
15
+ const config = resolveConfig(userConfig);
16
+
17
+ return {
18
+ name: '@dirsigler/astro-techradar',
19
+ hooks: {
20
+ 'astro:config:setup'({ injectRoute, updateConfig, config: astroConfig }) {
21
+ const bp = config.basePath; // e.g. "" or "/techradar"
22
+
23
+ // Inject all routes (prefixed with basePath)
24
+ injectRoute({
25
+ pattern: `${bp}/`,
26
+ entrypoint: path.join(PKG_DIR, 'src/pages/index.astro'),
27
+ });
28
+ injectRoute({
29
+ pattern: `${bp}/404`,
30
+ entrypoint: path.join(PKG_DIR, 'src/pages/404.astro'),
31
+ });
32
+ injectRoute({
33
+ pattern: `${bp}/segments/[segment]`,
34
+ entrypoint: path.join(PKG_DIR, 'src/pages/segments/[segment].astro'),
35
+ });
36
+ injectRoute({
37
+ pattern: `${bp}/technology/[...slug]`,
38
+ entrypoint: path.join(
39
+ PKG_DIR,
40
+ 'src/pages/technology/[...slug].astro',
41
+ ),
42
+ });
43
+ // Copy static assets into the consumer's public directory
44
+ const publicDir = fileURLToPath(astroConfig.publicDir);
45
+ mkdirSync(publicDir, { recursive: true });
46
+ for (const file of ['favicon.svg', 'og-image.png']) {
47
+ copyFileSync(
48
+ path.join(PKG_DIR, 'src/assets', file),
49
+ path.join(publicDir, file),
50
+ );
51
+ }
52
+
53
+ // Resolve theme CSS
54
+ let themeCSS = '';
55
+ const builtinPath = path.join(
56
+ PKG_DIR,
57
+ `src/themes/${config.theme}.css`,
58
+ );
59
+ if (existsSync(builtinPath)) {
60
+ themeCSS = readFileSync(builtinPath, 'utf-8');
61
+ } else if (existsSync(config.theme)) {
62
+ // User provided an absolute or relative path to a custom theme
63
+ themeCSS = readFileSync(config.theme, 'utf-8');
64
+ }
65
+
66
+ // Virtual modules for config and theme
67
+ const VIRTUAL_CONFIG = 'virtual:techradar/config';
68
+ const RESOLVED_CONFIG = '\0' + VIRTUAL_CONFIG;
69
+ const VIRTUAL_THEME = 'virtual:techradar/theme';
70
+ const RESOLVED_THEME = '\0' + VIRTUAL_THEME;
71
+
72
+ // Register astro-icon if not already added by the user
73
+ const hasIcon = astroConfig.integrations.some((i) => i.name === 'astro-icon');
74
+ if (!hasIcon) {
75
+ updateConfig({ integrations: [icon()] });
76
+ }
77
+
78
+ updateConfig({
79
+ vite: {
80
+ plugins: [
81
+ tailwindcss(),
82
+ {
83
+ name: 'techradar-virtual-modules',
84
+ resolveId(id: string) {
85
+ if (id === VIRTUAL_CONFIG) return RESOLVED_CONFIG;
86
+ if (id === VIRTUAL_THEME) return RESOLVED_THEME;
87
+ },
88
+ load(id: string) {
89
+ if (id === RESOLVED_CONFIG) {
90
+ return `export default ${JSON.stringify(config)}`;
91
+ }
92
+ if (id === RESOLVED_THEME) {
93
+ return `export default ${JSON.stringify(themeCSS)}`;
94
+ }
95
+ },
96
+ },
97
+ ],
98
+ },
99
+ });
100
+ },
101
+ },
102
+ };
103
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@dirsigler/astro-techradar",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "description": "An interactive technology radar Astro integration — track technology adoption across your organization",
6
+ "license": "MIT",
7
+ "author": "Dennis Irsigler <dennis@irsigler.dev>",
8
+ "homepage": "https://github.com/dirsigler/techradar",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/dirsigler/techradar.git"
12
+ },
13
+ "keywords": [
14
+ "astro-integration",
15
+ "techradar",
16
+ "technology-radar",
17
+ "radar",
18
+ "engineering"
19
+ ],
20
+ "exports": {
21
+ ".": "./index.ts",
22
+ "./schemas": "./schemas.ts"
23
+ },
24
+ "files": [
25
+ "LICENSE",
26
+ "README.md",
27
+ "index.ts",
28
+ "integration.ts",
29
+ "config.ts",
30
+ "schemas.ts",
31
+ "virtual.d.ts",
32
+ "src/**/*"
33
+ ],
34
+ "engines": {
35
+ "node": ">=22.12.0"
36
+ },
37
+ "peerDependencies": {
38
+ "@iconify-json/lucide": "^1.2.98",
39
+ "astro": "^6.0.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "@iconify-json/lucide": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "dependencies": {
47
+ "@tailwindcss/typography": "^0.5.19",
48
+ "@tailwindcss/vite": "^4.2.1",
49
+ "astro-icon": "^1.1.5",
50
+ "tailwindcss": "^4.2.1"
51
+ }
52
+ }
package/schemas.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { z } from 'astro:content';
2
+
3
+ export const segmentSchema = z.object({
4
+ title: z.string(),
5
+ order: z.number().int().min(1).max(4),
6
+ color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
7
+ });
8
+
9
+ export const technologySchema = z.object({
10
+ title: z.string(),
11
+ ring: z.enum(['adopt', 'trial', 'assess', 'hold']),
12
+ moved: z.number().int().min(-1).max(1).default(0),
13
+ });
@@ -0,0 +1,27 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <!-- Concentric rings -->
3
+ <circle cx="16" cy="16" r="15" fill="none" stroke="#cbd5e1" stroke-width="0.75" opacity="0.4"/>
4
+ <circle cx="16" cy="16" r="11" fill="none" stroke="#cbd5e1" stroke-width="0.75" opacity="0.5"/>
5
+ <circle cx="16" cy="16" r="7" fill="none" stroke="#cbd5e1" stroke-width="0.75" opacity="0.6"/>
6
+ <circle cx="16" cy="16" r="3" fill="none" stroke="#cbd5e1" stroke-width="0.75" opacity="0.7"/>
7
+
8
+ <!-- Hold ring (outer) — red dots -->
9
+ <circle cx="5" cy="10" r="1.4" fill="#dc2626"/>
10
+ <circle cx="26" cy="20" r="1.2" fill="#dc2626"/>
11
+ <circle cx="10" cy="27" r="1.1" fill="#dc2626"/>
12
+
13
+ <!-- Assess ring — yellow dots -->
14
+ <circle cx="22" cy="9" r="1.3" fill="#ca8a04"/>
15
+ <circle cx="8" cy="19" r="1.2" fill="#ca8a04"/>
16
+ <circle cx="20" cy="25" r="1.0" fill="#ca8a04"/>
17
+
18
+ <!-- Trial ring — blue dots -->
19
+ <circle cx="12" cy="10" r="1.3" fill="#2563eb"/>
20
+ <circle cx="22" cy="15" r="1.1" fill="#2563eb"/>
21
+ <circle cx="11" cy="22" r="1.2" fill="#2563eb"/>
22
+
23
+ <!-- Adopt ring (inner) — green dots -->
24
+ <circle cx="16" cy="13" r="1.4" fill="#16a34a"/>
25
+ <circle cx="14" cy="18" r="1.2" fill="#16a34a"/>
26
+ <circle cx="19" cy="17" r="1.1" fill="#16a34a"/>
27
+ </svg>
Binary file
@@ -0,0 +1,10 @@
1
+ ---
2
+ interface Props {
3
+ moved: number;
4
+ }
5
+
6
+ const { moved } = Astro.props;
7
+ ---
8
+
9
+ {moved === 1 && <span style="color: var(--radar-moved-in);" title="Moved in">&#9650;</span>}
10
+ {moved === -1 && <span style="color: var(--radar-moved-out);" title="Moved out">&#9660;</span>}