@jjlmoya/utils-tabletop 1.45.0 → 1.46.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-tabletop",
3
- "version": "1.45.0",
3
+ "version": "1.46.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -24,7 +24,7 @@
24
24
  "start": "astro dev",
25
25
  "prestart": "node scripts/postinstall.mjs",
26
26
  "build": "astro build",
27
- "prebuild": "node scripts/postinstall.mjs",
27
+ "prebuild": "node scripts/postinstall.mjs && node scripts/validate-icons.mjs",
28
28
  "preview": "astro preview",
29
29
  "astro": "astro",
30
30
  "lint": "eslint src/ --max-warnings 0 && stylelint \"src/**/*.{css,astro}\"",
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from 'node:module';
4
+ import { readFileSync, readdirSync } from 'node:fs';
5
+ import { dirname, join, relative, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const packageJsonPath = require.resolve('@iconify-json/mdi/package.json');
10
+ const packageRoot = dirname(packageJsonPath);
11
+ const iconSet = JSON.parse(readFileSync(join(packageRoot, 'icons.json'), 'utf8'));
12
+ const availableIcons = new Set([
13
+ ...Object.keys(iconSet.icons ?? {}),
14
+ ...Object.keys(iconSet.aliases ?? {}),
15
+ ]);
16
+
17
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
18
+ const sourceRoot = resolve(repoRoot, 'src');
19
+ const extensions = new Set(['.astro', '.js', '.mjs', '.ts', '.tsx']);
20
+ const iconPattern = /\bmdi:([a-z0-9-]+)\b/g;
21
+ const failures = [];
22
+ let references = 0;
23
+
24
+ function walk(directory) {
25
+ const entries = readdirSync(directory, { withFileTypes: true });
26
+ for (const entry of entries) {
27
+ const path = join(directory, entry.name);
28
+ if (entry.isDirectory()) {
29
+ if (entry.name !== 'node_modules' && entry.name !== 'dist' && entry.name !== 'tests') {
30
+ walk(path);
31
+ }
32
+ continue;
33
+ }
34
+ if (!extensions.has(path.slice(path.lastIndexOf('.')))) continue;
35
+
36
+ const source = readFileSync(path, 'utf8');
37
+ for (const match of source.matchAll(iconPattern)) {
38
+ references += 1;
39
+ const iconName = match[1];
40
+ if (availableIcons.has(iconName)) continue;
41
+
42
+ const line = source.slice(0, match.index).split('\n').length;
43
+ failures.push(`${relative(repoRoot, path)}:${line} — mdi:${iconName}`);
44
+ }
45
+ }
46
+ }
47
+
48
+ walk(sourceRoot);
49
+
50
+ if (failures.length > 0) {
51
+ console.error('Invalid MDI icons found:');
52
+ for (const failure of failures) console.error(`- ${failure}`);
53
+ console.error(`Checked ${references} MDI icon references against @iconify-json/mdi.`);
54
+ process.exitCode = 1;
55
+ } else {
56
+ console.log(`MDI icon validation passed: ${references} references checked.`);
57
+ }
@@ -0,0 +1,196 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import { SEORenderer } from '@jjlmoya/utils-shared';
4
+ import ProductionBreadcrumb from '../components/ProductionBreadcrumb.astro';
5
+ import ProductionPage from '../layouts/ProductionPage.astro';
6
+ import { getCategoryUi } from '../mfe/category-ui';
7
+ import { getCategoryPath } from '../mfe/routes';
8
+ import type { CategoryLocaleContent, KnownLocale } from '../types';
9
+ import './tabletop-home.css';
10
+
11
+ interface CategoryTool {
12
+ id: string;
13
+ icon: string;
14
+ title: string;
15
+ description: string;
16
+ href: string;
17
+ }
18
+
19
+ interface Props {
20
+ locale: KnownLocale;
21
+ category: CategoryLocaleContent;
22
+ tools: CategoryTool[];
23
+ alternates: { language: KnownLocale; url: string }[];
24
+ image?: string | undefined;
25
+ }
26
+
27
+ const { locale, category, tools, alternates, image } = Astro.props;
28
+ const publicPath = getCategoryPath(locale, category.slug);
29
+ const ui = getCategoryUi(locale);
30
+ const firstTool = tools[0];
31
+ const toolCount = String(tools.length).padStart(2, '0');
32
+
33
+ if (!firstTool) throw new Error('The tabletop category needs at least one tool');
34
+
35
+ ---
36
+
37
+ <ProductionPage title={category.title} description={category.description} {locale} {publicPath} {alternates} {image}>
38
+ <div class="tabletop-home" data-tabletop-home>
39
+ <div class="tabletop-felt-speckle" aria-hidden="true"></div>
40
+ <div class="tabletop-frame">
41
+ <ProductionBreadcrumb locale={locale} currentTitle={category.title} />
42
+
43
+ <header class="tabletop-deck-intro">
44
+ <div class="tabletop-hero-meta" aria-hidden="true">
45
+ <span>01</span>
46
+ <span>{toolCount}</span>
47
+ <span>TABLETOP</span>
48
+ </div>
49
+
50
+ <div class="tabletop-title-lockup">
51
+ <span class="tabletop-title-mark" aria-hidden="true">+</span>
52
+ <h2 class="tabletop-hero-title">{ui.heroTitle}</h2>
53
+ <p>{category.description}</p>
54
+ <a class="tabletop-hero-cta" href="#tabletop-tools">{ui.catalogTitle} <b aria-hidden="true">↓</b></a>
55
+ </div>
56
+
57
+ <div class="tabletop-manifesto" aria-hidden="true">
58
+ <span class="tabletop-manifesto-number">{toolCount}</span>
59
+ <span class="tabletop-manifesto-copy">{ui.catalogTitle}</span>
60
+ <span class="tabletop-manifesto-mark">+</span>
61
+ </div>
62
+ </header>
63
+
64
+ <section class="tabletop-play-zone" aria-labelledby="tabletop-play-title">
65
+ <div class="tabletop-play-label">
66
+ <span id="tabletop-play-title">{ui.relatedEyebrow}</span>
67
+ <span aria-hidden="true">01 / {String(tools.length).padStart(2, '0')}</span>
68
+ </div>
69
+
70
+ <div class="tabletop-feature-wrap" data-featured-tool>
71
+ <article class="tabletop-feature-card">
72
+ <div class="tabletop-card-top">
73
+ <span>01</span>
74
+ <span class="tabletop-card-index" aria-hidden="true">{toolCount}</span>
75
+ </div>
76
+ <div class="tabletop-card-art" aria-hidden="true">
77
+ <span class="tabletop-card-art-ring"></span>
78
+ <span class="tabletop-card-art-star">*</span>
79
+ <Icon name={firstTool.icon} />
80
+ </div>
81
+ <div class="tabletop-card-body">
82
+ <a href={firstTool.href} data-featured-title>{firstTool.title}</a>
83
+ <p data-featured-description aria-live="polite">{firstTool.description}</p>
84
+ </div>
85
+ <div class="tabletop-card-bottom">
86
+ <button type="button" data-draw-tool>{ui.shuffleTool}</button>
87
+ <span aria-hidden="true">{ui.useTool} <b>↗</b></span>
88
+ </div>
89
+ </article>
90
+ </div>
91
+
92
+ <div class="tabletop-dice-bay" aria-hidden="true">
93
+ <div class="tabletop-d20">
94
+ <svg viewBox="0 0 240 220">
95
+ <polygon class="tabletop-d20-face" points="120,8 224,72 187,194 53,194 16,72" />
96
+ <path class="tabletop-d20-line" d="M120 8v186M16 72l171 122M224 72L53 194M16 72h208M120 8L53 194M120 8l67 186" />
97
+ <text x="120" y="139">20</text>
98
+ </svg>
99
+ </div>
100
+ <div class="tabletop-meeples">
101
+ <svg class="tabletop-meeple tabletop-meeple-red" viewBox="0 0 100 120"><path d="M50 8c-15 0-25 12-25 27 0 9 3 16 9 22C19 65 8 78 8 98v14h84V98c0-20-11-33-26-41 6-6 9-13 9-22C75 20 65 8 50 8Z" /></svg>
102
+ <svg class="tabletop-meeple tabletop-meeple-blue" viewBox="0 0 100 120"><path d="M50 8c-15 0-25 12-25 27 0 9 3 16 9 22C19 65 8 78 8 98v14h84V98c0-20-11-33-26-41 6-6 9-13 9-22C75 20 65 8 50 8Z" /></svg>
103
+ <svg class="tabletop-meeple tabletop-meeple-gold" viewBox="0 0 100 120"><path d="M50 8c-15 0-25 12-25 27 0 9 3 16 9 22C19 65 8 78 8 98v14h84V98c0-20-11-33-26-41 6-6 9-13 9-22C75 20 65 8 50 8Z" /></svg>
104
+ </div>
105
+ <span class="tabletop-dice-caption">{toolCount} / D20</span>
106
+ </div>
107
+ </section>
108
+
109
+ <section class="tabletop-catalog" id="tabletop-tools" aria-labelledby="tabletop-catalog-title">
110
+ <div class="tabletop-catalog-head">
111
+ <div>
112
+ <span class="tabletop-catalog-kicker" aria-hidden="true">01 — {toolCount}</span>
113
+ <h2 id="tabletop-catalog-title">{ui.catalogTitle}</h2>
114
+ </div>
115
+ <span class="tabletop-catalog-scribble" aria-hidden="true">*</span>
116
+ </div>
117
+
118
+ <div class="tabletop-card-grid">
119
+ {tools.map((tool, index) => (
120
+ <a
121
+ href={tool.href}
122
+ class:list={['tabletop-tool-card', 'tabletop-tool-card-' + ((index % 4) + 1)]}
123
+ data-tool-card
124
+ data-tool-title={tool.title}
125
+ data-tool-description={tool.description}
126
+ >
127
+ <div class="tabletop-tool-card-face">
128
+ <div class="tabletop-tool-card-top">
129
+ <span>{String(index + 1).padStart(2, '0')}</span>
130
+ <span aria-hidden="true">{toolCount}</span>
131
+ </div>
132
+ <div class="tabletop-tool-card-art" aria-hidden="true">
133
+ <Icon name={tool.icon} />
134
+ </div>
135
+ <h3>{tool.title}</h3>
136
+ <p>{tool.description}</p>
137
+ <span class="tabletop-tool-card-action">{ui.useTool} <b aria-hidden="true">↗</b></span>
138
+ <span class="tabletop-tool-card-corner" aria-hidden="true">#{String(index + 1).padStart(2, '0')}</span>
139
+ </div>
140
+ </a>
141
+ ))}
142
+ </div>
143
+ </section>
144
+
145
+ <section class="tabletop-rulebook" aria-labelledby="tabletop-rulebook-title">
146
+ <div class="tabletop-rulebook-spine" aria-hidden="true">
147
+ <span>{toolCount}</span><span>INFO</span>
148
+ </div>
149
+ <div class="tabletop-rulebook-copy">
150
+ <span class="tabletop-rulebook-kicker">{ui.relatedEyebrow}</span>
151
+ <h2 id="tabletop-rulebook-title">{category.title}</h2>
152
+ <div class="tabletop-seo-copy"><SEORenderer content={{ locale, sections: category.seo }} /></div>
153
+ </div>
154
+ </section>
155
+
156
+ </div>
157
+ </div>
158
+ </ProductionPage>
159
+
160
+ <script>
161
+ const root = document.querySelector<HTMLElement>('[data-tabletop-home]');
162
+
163
+ if (root) {
164
+ const button = root.querySelector<HTMLButtonElement>('[data-draw-tool]');
165
+ const featureWrap = root.querySelector<HTMLElement>('[data-featured-tool]');
166
+ const featureLink = root.querySelector<HTMLAnchorElement>('[data-featured-title]');
167
+ const featureDescription = root.querySelector<HTMLElement>('[data-featured-description]');
168
+ const cards = Array.from(root.querySelectorAll<HTMLElement>('[data-tool-card]'));
169
+ let activeIndex = 0;
170
+ let isShuffling = false;
171
+
172
+ button?.addEventListener('click', () => {
173
+ if (!featureLink || !featureDescription || cards.length === 0 || isShuffling) return;
174
+ isShuffling = true;
175
+ featureWrap?.classList.add('is-shuffling');
176
+ button.setAttribute('aria-busy', 'true');
177
+ activeIndex = (activeIndex + 1) % cards.length;
178
+ const card = cards[activeIndex];
179
+ if (!card) {
180
+ isShuffling = false;
181
+ featureWrap?.classList.remove('is-shuffling');
182
+ button.removeAttribute('aria-busy');
183
+ return;
184
+ }
185
+ window.setTimeout(() => {
186
+ featureLink.href = card.getAttribute('href') ?? '#';
187
+ featureLink.textContent = card.dataset.toolTitle ?? '';
188
+ featureDescription.textContent = card.dataset.toolDescription ?? '';
189
+ cards.forEach((item, index) => item.toggleAttribute('data-selected', index === activeIndex));
190
+ featureWrap?.classList.remove('is-shuffling');
191
+ button.removeAttribute('aria-busy');
192
+ isShuffling = false;
193
+ }, 360);
194
+ });
195
+ }
196
+ </script>