mtg-card 0.1.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 ADDED
@@ -0,0 +1,308 @@
1
+ # mtg-card
2
+
3
+ A React component that renders pixel-perfect Magic: The Gathering cards entirely in the browser. No canvas, no images of card frames — just SVG layers, CSS theming, and one `<MtgCard />` component.
4
+
5
+ ![Card showcase](showcase.png)
6
+
7
+ ## Why this library?
8
+
9
+ - **Zero card-frame images** — Every part of the card frame (borders, fields, edges, P/T box) is an inline SVG themed with CSS custom properties. Colors adapt automatically to the mana cost.
10
+ - **Automatic color theming** — Pass `manaCost={['R', 'G']}` and the card renders with the correct Gruul dual-color frame, texture, and field tints. Mono, dual, tri+, gold, artifact, colorless, and land frames all work out of the box.
11
+ - **All major frame types** — Standard creatures, noncreature spells, planeswalkers, sagas, vehicles, adventures, mutate, basic lands, and nonbasic lands.
12
+ - **Dynamic set symbols via CDN** — Pass a `setCode` and `rarity` to fetch the real set icon at runtime from [mtg-vectors](https://github.com/Investigamer/mtg-vectors). Zero bundle impact.
13
+ - **Inline mana symbols** — Rules text like `"{T}: Add {G}."` automatically renders tap and mana symbols inline.
14
+ - **Lightweight** — Ships as a single ESM bundle with CSS injected by JS. No external CSS file to import.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install mtg-card
20
+ ```
21
+
22
+ Peer dependencies: `react >= 18` and `react-dom >= 18`.
23
+
24
+ ## Quick start — Llanowar Elves
25
+
26
+ ```tsx
27
+ import { MtgCard } from 'mtg-card'
28
+
29
+ function App() {
30
+ return (
31
+ <MtgCard
32
+ cardName="Llanowar Elves"
33
+ manaCost={['G']}
34
+ typeLine="Creature — Elf Druid"
35
+ rulesText="{T}: Add {G}."
36
+ flavorText="As patient and generous as life, as harsh and merciless as nature."
37
+ power="1"
38
+ toughness="1"
39
+ cardArt="/your-art.jpg"
40
+ setCode="GN3"
41
+ rarity="common"
42
+ cardNumber="101"
43
+ totalCards="136"
44
+ language="EN"
45
+ artist="Chris Rahn"
46
+ year="2022"
47
+ />
48
+ )
49
+ }
50
+ ```
51
+
52
+ <p align="center">
53
+ <img src="preview.png" alt="Llanowar Elves" width="336" />
54
+ </p>
55
+
56
+ ## Props
57
+
58
+ ### Base props (all frame types)
59
+
60
+ | Prop | Type | Description |
61
+ |------|------|-------------|
62
+ | `cardName` | `string` | Card name displayed in the title bar |
63
+ | `manaCost` | `string[]` | Mana symbols — drives color theming. E.g. `['2', 'R', 'R']` |
64
+ | `typeLine` | `string` | Full type line. E.g. `"Legendary Creature — Goblin Warrior"` |
65
+ | `cardArt` | `string?` | URL or path to the card illustration |
66
+ | `legendary` | `boolean?` | Adds the legendary crown overlay |
67
+ | `setCode` | `string?` | 3-letter set code (e.g. `"MKM"`). Fetches the real set symbol from CDN |
68
+ | `rarity` | `string?` | `"common"`, `"uncommon"`, `"rare"`, `"mythic"` (or `"C"`, `"U"`, `"R"`, `"M"`) |
69
+ | `setSymbolUrl` | `string?` | Direct URL to a custom set symbol SVG. Overrides `setCode` |
70
+ | `cardNumber` | `string?` | Collector number |
71
+ | `totalCards` | `string?` | Total cards in set |
72
+ | `language` | `string?` | Language code (e.g. `"EN"`) |
73
+ | `artist` | `string?` | Artist credit |
74
+ | `year` | `string?` | Copyright year |
75
+
76
+ ### Standard frame (default)
77
+
78
+ ```tsx
79
+ <MtgCard
80
+ cardName="Lightning Bolt"
81
+ manaCost={['R']}
82
+ typeLine="Instant"
83
+ rulesText="Lightning Bolt deals 3 damage to any target."
84
+ flavorText={'"The spark that satisfies."'}
85
+ />
86
+ ```
87
+
88
+ | Prop | Type | Description |
89
+ |------|------|-------------|
90
+ | `frame` | `"standard"?` | Optional, default frame |
91
+ | `rulesText` | `string?` | Rules text. Supports `{T}`, `{G}`, `{W/U}` etc. |
92
+ | `flavorText` | `string?` | Italic flavor text below the divider bar |
93
+ | `power` | `string?` | Power (shows P/T box when both power and toughness are set) |
94
+ | `toughness` | `string?` | Toughness |
95
+ | `landSymbol` | `string?` | Large centered mana symbol for basic lands |
96
+
97
+ ### Vehicle frame
98
+
99
+ ```tsx
100
+ <MtgCard
101
+ frame="vehicle"
102
+ cardName="Smuggler's Copter"
103
+ manaCost={['2']}
104
+ typeLine="Artifact — Vehicle"
105
+ rulesText="Flying\nCrew 1"
106
+ power="3"
107
+ toughness="3"
108
+ />
109
+ ```
110
+
111
+ ### Land frame
112
+
113
+ ```tsx
114
+ // Basic land — large centered mana symbol
115
+ <MtgCard
116
+ frame="land"
117
+ cardName="Forest"
118
+ manaCost={['G']}
119
+ typeLine="Basic Land — Forest"
120
+ landSymbol="G"
121
+ />
122
+
123
+ // Nonbasic land — rules text box
124
+ <MtgCard
125
+ frame="land"
126
+ cardName="Command Tower"
127
+ manaCost={[]}
128
+ typeLine="Land"
129
+ rulesText="{T}: Add one mana of any color in your commander's color identity."
130
+ />
131
+ ```
132
+
133
+ ### Planeswalker frame
134
+
135
+ ```tsx
136
+ <MtgCard
137
+ frame="planeswalker"
138
+ cardName="Jace, the Mind Sculptor"
139
+ manaCost={['2', 'U', 'U']}
140
+ typeLine="Legendary Planeswalker — Jace"
141
+ legendary
142
+ loyaltyAbilities={[
143
+ { cost: '+2', text: "Look at the top card of target player's library." },
144
+ { cost: '0', text: 'Draw three cards, then put two cards from your hand on top.' },
145
+ { cost: '-1', text: "Return target creature to its owner's hand." },
146
+ { cost: '-12', text: "Exile all cards from target player's library." },
147
+ ]}
148
+ startingLoyalty="3"
149
+ />
150
+ ```
151
+
152
+ | Prop | Type | Description |
153
+ |------|------|-------------|
154
+ | `frame` | `"planeswalker"` | Required |
155
+ | `loyaltyAbilities` | `{ cost: string; text: string }[]` | Loyalty abilities with `+N`, `0`, or `-N` costs |
156
+ | `startingLoyalty` | `string` | Starting loyalty counter value |
157
+
158
+ ### Saga frame
159
+
160
+ ```tsx
161
+ <MtgCard
162
+ frame="saga"
163
+ cardName="The Antiquities War"
164
+ manaCost={['3', 'U']}
165
+ typeLine="Enchantment — Saga"
166
+ chapters={[
167
+ { numerals: 'I', text: 'Look at the top five cards. Reveal up to two artifacts.' },
168
+ { numerals: 'II', text: 'Artifacts you control become 5/5 creatures.' },
169
+ { numerals: 'III', text: "Destroy all artifacts your opponents control." },
170
+ ]}
171
+ />
172
+ ```
173
+
174
+ | Prop | Type | Description |
175
+ |------|------|-------------|
176
+ | `frame` | `"saga"` | Required |
177
+ | `chapters` | `{ numerals: string; text: string }[]` | Chapter entries with Roman numeral labels |
178
+ | `reminderText` | `string?` | Override the default saga reminder text |
179
+
180
+ ### Adventure frame
181
+
182
+ ```tsx
183
+ <MtgCard
184
+ frame="adventure"
185
+ cardName="Bonecrusher Giant"
186
+ manaCost={['2', 'R']}
187
+ typeLine="Creature — Giant"
188
+ rulesText="Whenever Bonecrusher Giant becomes the target of a spell, it deals 2 damage to that spell's controller."
189
+ adventureName="Stomp"
190
+ adventureManaCost={['1', 'R']}
191
+ adventureTypeLine="Instant — Adventure"
192
+ adventureRulesText="Damage can't be prevented this turn. Stomp deals 2 damage to any target."
193
+ power="4"
194
+ toughness="3"
195
+ />
196
+ ```
197
+
198
+ ### Mutate frame
199
+
200
+ ```tsx
201
+ <MtgCard
202
+ frame="mutate"
203
+ cardName="Gemrazer"
204
+ manaCost={['3', 'G']}
205
+ typeLine="Creature — Beast"
206
+ rulesText="Mutate {1}{G}{G}\nReach, trample\nWhenever this creature mutates, destroy target artifact or enchantment."
207
+ power="4"
208
+ toughness="4"
209
+ />
210
+ ```
211
+
212
+ ## Color theming
213
+
214
+ Color is derived entirely from `manaCost`. You don't need to specify colors manually.
215
+
216
+ | Mana cost | Frame style |
217
+ |-----------|-------------|
218
+ | `['G']` | Green mono |
219
+ | `['1', 'W', 'U']` | Azorius dual |
220
+ | `['W', 'U', 'B', 'R', 'G']` | Gold (3+ colors) |
221
+ | `['4']` or `['0']` | Artifact |
222
+ | `[]` | Colorless |
223
+
224
+ All 10 two-color pairs have dedicated dual-frame textures.
225
+
226
+ ## Set symbols
227
+
228
+ Three ways to display set symbols, in priority order:
229
+
230
+ ```tsx
231
+ // 1. Direct URL (highest priority)
232
+ <MtgCard setSymbolUrl="https://example.com/my-set.svg" ... />
233
+
234
+ // 2. Set code + rarity — fetched from CDN at runtime
235
+ <MtgCard setCode="MKM" rarity="rare" ... />
236
+
237
+ // 3. No props — falls back to a generic bundled symbol
238
+ <MtgCard ... />
239
+ ```
240
+
241
+ You can also use the `getSetSymbolUrl` helper to build URLs yourself:
242
+
243
+ ```tsx
244
+ import { getSetSymbolUrl } from 'mtg-card'
245
+
246
+ getSetSymbolUrl('MKM', 'rare')
247
+ // → "https://cdn.jsdelivr.net/gh/Investigamer/mtg-vectors@main/svg/optimized/set/MKM/R.svg"
248
+ ```
249
+
250
+ ## Mana symbols in text
251
+
252
+ Rules text automatically parses `{X}` tokens into inline mana symbols:
253
+
254
+ ```
255
+ "{T}: Add {G}." → tap symbol + green mana
256
+ "{2}{W}{W}" → 2 + white + white
257
+ "{W/U}" → hybrid white/blue
258
+ ```
259
+
260
+ Supported in `rulesText`, `adventureRulesText`, loyalty ability text, and saga chapter text.
261
+
262
+ ## Fonts
263
+
264
+ The component uses these font families (bring your own or let the browser fall back):
265
+
266
+ - **Beleren** — Card name and type line
267
+ - **Merriweather** — Rules and flavor text
268
+ - **Source Sans Pro** — Metadata (collector info, artist)
269
+
270
+ ## Exports
271
+
272
+ ```tsx
273
+ import {
274
+ MtgCard, // Main component
275
+ ManaSymbol, // Standalone mana symbol component
276
+ getColorTheme, // Get color theme object from mana cost
277
+ getSetSymbolUrl, // Build CDN URL for a set symbol
278
+ parseRulesText, // Parse "{T}: Add {G}" into React nodes
279
+ } from 'mtg-card'
280
+ ```
281
+
282
+ ## TypeScript
283
+
284
+ All props are fully typed with discriminated unions:
285
+
286
+ ```tsx
287
+ import type {
288
+ MtgCardProps, // Union of all frame types
289
+ StandardCardProps,
290
+ AdventureCardProps,
291
+ PlaneswalkerCardProps,
292
+ SagaCardProps,
293
+ CardColorTheme,
294
+ } from 'mtg-card'
295
+ ```
296
+
297
+ ## Known issues
298
+
299
+ - Vehicle frames are not pixel-perfect yet (texture and P/T box coloring may differ from official cards).
300
+ - Legendary cards have minor visual differences from official frames (crown clip-path and overlay positioning).
301
+
302
+ ## Acknowledgments
303
+
304
+ Card frame SVGs and layout are based on the [MTG Card Designer](https://www.figma.com/community/file/773439497184575668) Figma community file by [@ux_andrew](https://x.com/ux_andrew). Thanks for making such a detailed reference available to the community.
305
+
306
+ ## License
307
+
308
+ MIT
@@ -0,0 +1,2 @@
1
+ import { AdventureCardProps } from './types';
2
+ export default function AdventureCard({ cardName, manaCost, cardArt, typeLine, legendary, rulesText, flavorText, power, toughness, adventureName, adventureManaCost, adventureTypeLine, adventureRulesText, cardNumber, totalCards, rarity, setCode, setSymbolUrl, language, artist, year, }: AdventureCardProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ interface ManaSymbolProps {
2
+ symbol: string;
3
+ size?: number;
4
+ margin?: number;
5
+ }
6
+ export default function ManaSymbol({ symbol, size, margin }: ManaSymbolProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,2 @@
1
+ import { MtgCardProps } from './types';
2
+ export default function MtgCard(props: MtgCardProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ import { PlaneswalkerCardProps } from './types';
2
+ export default function PlaneswalkerCard({ cardName, manaCost, cardArt, typeLine, loyaltyAbilities, startingLoyalty, cardNumber, totalCards, rarity, setCode, setSymbolUrl, language, artist, year, }: PlaneswalkerCardProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ import { SagaCardProps } from './types';
2
+ export default function SagaCard({ cardName, manaCost, cardArt, typeLine, chapters, reminderText, cardNumber, totalCards, rarity, setCode, setSymbolUrl, language, artist, year, }: SagaCardProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ import { StandardCardProps } from './types';
2
+ export default function StandardCard({ frame, cardName, manaCost, cardArt, typeLine, legendary, rulesText, flavorText, power, toughness, landSymbol, cardNumber, totalCards, rarity, setCode, setSymbolUrl, language, artist, year, }: StandardCardProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,18 @@
1
+ export interface CardColorTheme {
2
+ card: string;
3
+ nameType: string;
4
+ text: string;
5
+ border: string;
6
+ legendFilter: string;
7
+ }
8
+ /**
9
+ * Determine the card's color identity from its mana cost symbols.
10
+ * Returns the appropriate color theme.
11
+ *
12
+ * Rules:
13
+ * - Single color (only one of WUBRG in cost) → that color's theme
14
+ * - Two colors → gold theme with pair-specific text box color
15
+ * - Three+ colors → gold theme
16
+ * - No colors (generic/colorless only) → colorless theme
17
+ */
18
+ export declare function getColorTheme(manaCost: string[], frame?: string): CardColorTheme;
@@ -0,0 +1,6 @@
1
+ export { default as MtgCard } from './MtgCard';
2
+ export { default as ManaSymbol } from './ManaSymbol';
3
+ export { getColorTheme } from './colors';
4
+ export { parseRulesText, DropShadow, getSetSymbolUrl } from './shared';
5
+ export type { MtgCardProps, StandardCardProps, AdventureCardProps, PlaneswalkerCardProps, SagaCardProps } from './types';
6
+ export type { CardColorTheme } from './colors';
@@ -0,0 +1,66 @@
1
+ import { CSSProperties, ReactNode } from 'react';
2
+ import { CardColorTheme } from './colors';
3
+ export declare function getTextureUrl(manaCost: string[], frame?: string): string;
4
+ /** Inline SVG helper — renders raw SVG as DOM so CSS custom properties inherit */
5
+ export declare function Svg({ html, className, style }: {
6
+ html: string;
7
+ className?: string;
8
+ style?: CSSProperties;
9
+ }): import("react/jsx-runtime").JSX.Element;
10
+ /** Dark drop shadow on the left edge of the card border (edges-mask.svg from Figma) */
11
+ export declare function DropShadow({ className, style }: {
12
+ className: string;
13
+ style?: CSSProperties;
14
+ }): import("react/jsx-runtime").JSX.Element;
15
+ /** Derive theme + texture from mana cost */
16
+ export declare function useCardTheme(manaCost: string[], frame?: string): {
17
+ theme: CardColorTheme;
18
+ textureUrl: string;
19
+ };
20
+ /** CSS variable objects for SVG theming */
21
+ export declare function getThemeVars(theme: CardColorTheme): {
22
+ frameVars: CSSProperties;
23
+ fieldVars: CSSProperties;
24
+ borderVars: CSSProperties;
25
+ legendVars: CSSProperties;
26
+ ptVars: CSSProperties;
27
+ };
28
+ /** Parse rules text, replacing {T}, {G}, {1}, {W/U} etc. with inline ManaSymbol components */
29
+ export declare function parseRulesText(text: string, size?: number): ReactNode[];
30
+ /** Mana cost row */
31
+ export declare function ManaCostRow({ manaCost, className }: {
32
+ manaCost: string[];
33
+ className: string;
34
+ }): import("react/jsx-runtime").JSX.Element;
35
+ export declare function getSetSymbolUrl(setCode: string, rarity?: string): string;
36
+ /** Set symbol icon */
37
+ export declare function SetSymbolIcon({ className, innerClassName, fillClassName, setCode, rarity, setSymbolUrl }: {
38
+ className: string;
39
+ innerClassName: string;
40
+ fillClassName: string;
41
+ setCode?: string;
42
+ rarity?: string;
43
+ setSymbolUrl?: string;
44
+ }): import("react/jsx-runtime").JSX.Element;
45
+ /** Metadata block (bottom-left) */
46
+ export declare function Metadata({ className, rowClassName, numClassName, rarityClassName, creditClassName, setClassName, artistIconClassName, artistNameClassName, cardNumber, totalCards, rarity, setCode, language, artist, }: {
47
+ className: string;
48
+ rowClassName: string;
49
+ numClassName: string;
50
+ rarityClassName: string;
51
+ creditClassName: string;
52
+ setClassName: string;
53
+ artistIconClassName: string;
54
+ artistNameClassName: string;
55
+ cardNumber?: string;
56
+ totalCards?: string;
57
+ rarity?: string;
58
+ setCode?: string;
59
+ language?: string;
60
+ artist?: string;
61
+ }): import("react/jsx-runtime").JSX.Element;
62
+ /** Copyright (bottom-right) */
63
+ export declare function Copyright({ className, year }: {
64
+ className: string;
65
+ year?: string;
66
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,51 @@
1
+ export interface MtgCardBaseProps {
2
+ cardName: string;
3
+ manaCost: string[];
4
+ cardArt?: string;
5
+ typeLine: string;
6
+ legendary?: boolean;
7
+ cardNumber?: string;
8
+ totalCards?: string;
9
+ rarity?: string;
10
+ setCode?: string;
11
+ setSymbolUrl?: string;
12
+ language?: string;
13
+ artist?: string;
14
+ year?: string;
15
+ }
16
+ export interface StandardCardProps extends MtgCardBaseProps {
17
+ frame?: 'standard' | 'vehicle' | 'mutate' | 'land';
18
+ rulesText?: string;
19
+ flavorText?: string;
20
+ power?: string;
21
+ toughness?: string;
22
+ landSymbol?: string;
23
+ }
24
+ export interface AdventureCardProps extends MtgCardBaseProps {
25
+ frame: 'adventure';
26
+ rulesText?: string;
27
+ flavorText?: string;
28
+ power?: string;
29
+ toughness?: string;
30
+ adventureName: string;
31
+ adventureManaCost: string[];
32
+ adventureTypeLine: string;
33
+ adventureRulesText: string;
34
+ }
35
+ export interface PlaneswalkerCardProps extends MtgCardBaseProps {
36
+ frame: 'planeswalker';
37
+ loyaltyAbilities: {
38
+ cost: string;
39
+ text: string;
40
+ }[];
41
+ startingLoyalty: string;
42
+ }
43
+ export interface SagaCardProps extends MtgCardBaseProps {
44
+ frame: 'saga';
45
+ chapters: {
46
+ numerals: string;
47
+ text: string;
48
+ }[];
49
+ reminderText?: string;
50
+ }
51
+ export type MtgCardProps = StandardCardProps | AdventureCardProps | PlaneswalkerCardProps | SagaCardProps;
@@ -0,0 +1,2 @@
1
+ export { MtgCard, ManaSymbol, getColorTheme, getSetSymbolUrl } from './components/MtgCard';
2
+ export type { MtgCardProps, StandardCardProps, PlaneswalkerCardProps, SagaCardProps, CardColorTheme } from './components/MtgCard';