@nfcard/validation 0.18.0 → 0.20.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/dist/index.d.ts +273 -2
- package/dist/index.js +0 -0
- package/package.json +2 -2
- package/src/elements.test.ts +42 -0
- package/src/index.ts +0 -0
- package/src/printability.test.ts +107 -0
- package/src/printability.ts +124 -10
- package/src/tipJar.test.ts +77 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as _nfcard_types from '@nfcard/types';
|
|
2
2
|
import { CardAsset, CardElement, ElementTransform } from '@nfcard/types';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Manufacturing-printability envelope for the free-transform element layer
|
|
@@ -49,6 +50,18 @@ declare function obbCorners(t: ElementTransform, wMm: number, hMm: number): Pt[]
|
|
|
49
50
|
/** Are all corners inside the ID-1 rim minus the safety margin? */
|
|
50
51
|
declare function cornersOnCard(corners: Pt[], margin?: number): boolean;
|
|
51
52
|
type ChipAxis = 0 | 1 | 2;
|
|
53
|
+
/**
|
|
54
|
+
* The chip PLACEMENT envelope: the region the chip *centre* must stay inside so
|
|
55
|
+
* the pattern-engine's hole-masking + rim clearance can keep the NFC pocket
|
|
56
|
+
* enclosed. Sized so the (scaled) Ø27.5·scale chip island clears the rim margin
|
|
57
|
+
* with a 1 mm buffer. Scale-aware so a grown chip tightens the envelope. Mirrors
|
|
58
|
+
* cardgen `chip_placement_max_xy`; `chipMaxXY()` is exactly this at scale 1 (the
|
|
59
|
+
* domain of the legacy 3×3 anchor lattice).
|
|
60
|
+
*/
|
|
61
|
+
declare function chipPlacementMaxXY(scale: number): {
|
|
62
|
+
maxX: number;
|
|
63
|
+
maxY: number;
|
|
64
|
+
};
|
|
52
65
|
/** Grid cell → card-local mm centre (matches pattern-engine `chipCentreForAnchor`). */
|
|
53
66
|
declare function chipAnchorToMm(col: ChipAxis, row: ChipAxis): Pt;
|
|
54
67
|
/** Free mm → nearest grid cell (matches pattern-engine `nearestChipAnchor`). The
|
|
@@ -66,6 +79,33 @@ interface Violation {
|
|
|
66
79
|
elementId: string;
|
|
67
80
|
message: string;
|
|
68
81
|
}
|
|
82
|
+
/** Unknown glyphs (outside the tables): reserve a generous em so any realistic exotic
|
|
83
|
+
* char OVER-reserves. (Not a mathematical bound — a pathological single-codepoint wide
|
|
84
|
+
* ligature can exceed it — but such glyphs aren't in the card font at all, so the print
|
|
85
|
+
* pipeline rejects them upstream of this estimate.) */
|
|
86
|
+
declare const ADVANCE_FALLBACK_EM = 1.1;
|
|
87
|
+
/** ITALIC ink overhang: slanted glyphs poke past their advance box (measured against the
|
|
88
|
+
* webfont: ≤ 0.20 em at 400, ≤ 0.23 em at 700 — descender tails like "jjj" are the worst
|
|
89
|
+
* case; upright overhang is exactly 0). Charged once per text box so the advance-sum
|
|
90
|
+
* width stays a true upper bound on the rendered ink for the italic toggle too. */
|
|
91
|
+
declare const ITALIC_OVERHANG_EM = 0.25;
|
|
92
|
+
/** Empty-text width floor — the OLD flat estimate's minimum (1 char × 0.6 em), kept so a
|
|
93
|
+
* blank element still has a box. NOT a live glyph metric (blank text is separately gated
|
|
94
|
+
* by `textEmpty`); it just anchors degenerate OBBs. */
|
|
95
|
+
declare const EMPTY_TEXT_FLOOR_EM = 0.6;
|
|
96
|
+
/** Baseline-to-baseline line advance for multi-line text (mirrors the render stack's
|
|
97
|
+
* TEXT_LINE_FACTOR in nfcard-web pocShared + cardgen text2d). */
|
|
98
|
+
declare const TEXT_LINE_FACTOR_EM = 1.32;
|
|
99
|
+
/** Advance-sum width of ONE line in em, in the weight's metric set (≥600 → SemiBold). */
|
|
100
|
+
declare function textAdvanceEm(line: string, weight: number): number;
|
|
101
|
+
/** Estimated card footprint (w×h, pre-scale) used for the on-card OBB check. Text is the
|
|
102
|
+
* per-glyph advance sum of the WIDEST line (untrimmed — spaces cost their real advance)
|
|
103
|
+
* with the multi-line stack in the height; a guaranteed upper bound on the rendered ink
|
|
104
|
+
* (see the table note above), so the gate can never accept an overflowing placement. */
|
|
105
|
+
declare function footprintOf(el: CardElement): {
|
|
106
|
+
w: number;
|
|
107
|
+
h: number;
|
|
108
|
+
};
|
|
69
109
|
/** All hard manufacturing violations for one element. Empty ⇒ printable. */
|
|
70
110
|
declare function elementPrintability(el: CardElement, ctx: PrintabilityCtx): Violation[];
|
|
71
111
|
|
|
@@ -5142,6 +5182,15 @@ declare const ctaButtonsSchema: z.ZodEffects<z.ZodArray<z.ZodObject<{
|
|
|
5142
5182
|
url?: string | undefined;
|
|
5143
5183
|
label?: string | undefined;
|
|
5144
5184
|
}[]>;
|
|
5185
|
+
declare const tipJarSchema: z.ZodEffects<z.ZodUnknown, {
|
|
5186
|
+
provider: _nfcard_types.TipProviderId;
|
|
5187
|
+
handle: string;
|
|
5188
|
+
label: string;
|
|
5189
|
+
} | {
|
|
5190
|
+
provider: _nfcard_types.TipProviderId;
|
|
5191
|
+
handle: string;
|
|
5192
|
+
label?: undefined;
|
|
5193
|
+
} | undefined, unknown>;
|
|
5145
5194
|
declare const digitalConfigSchema: z.ZodObject<{
|
|
5146
5195
|
templateId: z.ZodEnum<["minimal", "bold", "card", "aurora", "classic", "spotlight", "marquee", "riso"]>;
|
|
5147
5196
|
accentColor: z.ZodString;
|
|
@@ -5229,6 +5278,15 @@ declare const digitalConfigSchema: z.ZodObject<{
|
|
|
5229
5278
|
url?: string | undefined;
|
|
5230
5279
|
label?: string | undefined;
|
|
5231
5280
|
}[]>>;
|
|
5281
|
+
tipJar: z.ZodOptional<z.ZodEffects<z.ZodUnknown, {
|
|
5282
|
+
provider: _nfcard_types.TipProviderId;
|
|
5283
|
+
handle: string;
|
|
5284
|
+
label: string;
|
|
5285
|
+
} | {
|
|
5286
|
+
provider: _nfcard_types.TipProviderId;
|
|
5287
|
+
handle: string;
|
|
5288
|
+
label?: undefined;
|
|
5289
|
+
} | undefined, unknown>>;
|
|
5232
5290
|
}, "strip", z.ZodTypeAny, {
|
|
5233
5291
|
fontKey: string;
|
|
5234
5292
|
templateId: "minimal" | "bold" | "card" | "aurora" | "classic" | "spotlight" | "marquee" | "riso";
|
|
@@ -5264,6 +5322,15 @@ declare const digitalConfigSchema: z.ZodObject<{
|
|
|
5264
5322
|
url: string;
|
|
5265
5323
|
label: string;
|
|
5266
5324
|
}[] | undefined;
|
|
5325
|
+
tipJar?: {
|
|
5326
|
+
provider: _nfcard_types.TipProviderId;
|
|
5327
|
+
handle: string;
|
|
5328
|
+
label: string;
|
|
5329
|
+
} | {
|
|
5330
|
+
provider: _nfcard_types.TipProviderId;
|
|
5331
|
+
handle: string;
|
|
5332
|
+
label?: undefined;
|
|
5333
|
+
} | undefined;
|
|
5267
5334
|
}, {
|
|
5268
5335
|
fontKey: string;
|
|
5269
5336
|
templateId: "minimal" | "bold" | "card" | "aurora" | "classic" | "spotlight" | "marquee" | "riso";
|
|
@@ -5299,6 +5366,7 @@ declare const digitalConfigSchema: z.ZodObject<{
|
|
|
5299
5366
|
url?: string | undefined;
|
|
5300
5367
|
label?: string | undefined;
|
|
5301
5368
|
}[] | undefined;
|
|
5369
|
+
tipJar?: unknown;
|
|
5302
5370
|
}>;
|
|
5303
5371
|
declare const entrySourceSchema: z.ZodObject<{
|
|
5304
5372
|
output: z.ZodOptional<z.ZodEnum<["with-card", "digital-only"]>>;
|
|
@@ -8792,6 +8860,15 @@ declare const configurationCreateSchema: z.ZodObject<{
|
|
|
8792
8860
|
url?: string | undefined;
|
|
8793
8861
|
label?: string | undefined;
|
|
8794
8862
|
}[]>>;
|
|
8863
|
+
tipJar: z.ZodOptional<z.ZodEffects<z.ZodUnknown, {
|
|
8864
|
+
provider: _nfcard_types.TipProviderId;
|
|
8865
|
+
handle: string;
|
|
8866
|
+
label: string;
|
|
8867
|
+
} | {
|
|
8868
|
+
provider: _nfcard_types.TipProviderId;
|
|
8869
|
+
handle: string;
|
|
8870
|
+
label?: undefined;
|
|
8871
|
+
} | undefined, unknown>>;
|
|
8795
8872
|
}, "strip", z.ZodTypeAny, {
|
|
8796
8873
|
fontKey: string;
|
|
8797
8874
|
templateId: "minimal" | "bold" | "card" | "aurora" | "classic" | "spotlight" | "marquee" | "riso";
|
|
@@ -8827,6 +8904,15 @@ declare const configurationCreateSchema: z.ZodObject<{
|
|
|
8827
8904
|
url: string;
|
|
8828
8905
|
label: string;
|
|
8829
8906
|
}[] | undefined;
|
|
8907
|
+
tipJar?: {
|
|
8908
|
+
provider: _nfcard_types.TipProviderId;
|
|
8909
|
+
handle: string;
|
|
8910
|
+
label: string;
|
|
8911
|
+
} | {
|
|
8912
|
+
provider: _nfcard_types.TipProviderId;
|
|
8913
|
+
handle: string;
|
|
8914
|
+
label?: undefined;
|
|
8915
|
+
} | undefined;
|
|
8830
8916
|
}, {
|
|
8831
8917
|
fontKey: string;
|
|
8832
8918
|
templateId: "minimal" | "bold" | "card" | "aurora" | "classic" | "spotlight" | "marquee" | "riso";
|
|
@@ -8862,6 +8948,7 @@ declare const configurationCreateSchema: z.ZodObject<{
|
|
|
8862
8948
|
url?: string | undefined;
|
|
8863
8949
|
label?: string | undefined;
|
|
8864
8950
|
}[] | undefined;
|
|
8951
|
+
tipJar?: unknown;
|
|
8865
8952
|
}>;
|
|
8866
8953
|
quantity: z.ZodDefault<z.ZodNumber>;
|
|
8867
8954
|
existingProfileId: z.ZodOptional<z.ZodString>;
|
|
@@ -9246,6 +9333,15 @@ declare const configurationCreateSchema: z.ZodObject<{
|
|
|
9246
9333
|
url: string;
|
|
9247
9334
|
label: string;
|
|
9248
9335
|
}[] | undefined;
|
|
9336
|
+
tipJar?: {
|
|
9337
|
+
provider: _nfcard_types.TipProviderId;
|
|
9338
|
+
handle: string;
|
|
9339
|
+
label: string;
|
|
9340
|
+
} | {
|
|
9341
|
+
provider: _nfcard_types.TipProviderId;
|
|
9342
|
+
handle: string;
|
|
9343
|
+
label?: undefined;
|
|
9344
|
+
} | undefined;
|
|
9249
9345
|
};
|
|
9250
9346
|
quantity: number;
|
|
9251
9347
|
existingProfileId?: string | undefined;
|
|
@@ -9612,6 +9708,7 @@ declare const configurationCreateSchema: z.ZodObject<{
|
|
|
9612
9708
|
url?: string | undefined;
|
|
9613
9709
|
label?: string | undefined;
|
|
9614
9710
|
}[] | undefined;
|
|
9711
|
+
tipJar?: unknown;
|
|
9615
9712
|
};
|
|
9616
9713
|
quantity?: number | undefined;
|
|
9617
9714
|
existingProfileId?: string | undefined;
|
|
@@ -13090,6 +13187,15 @@ declare const configurationUpdateSchema: z.ZodObject<{
|
|
|
13090
13187
|
url?: string | undefined;
|
|
13091
13188
|
label?: string | undefined;
|
|
13092
13189
|
}[]>>;
|
|
13190
|
+
tipJar: z.ZodOptional<z.ZodEffects<z.ZodUnknown, {
|
|
13191
|
+
provider: _nfcard_types.TipProviderId;
|
|
13192
|
+
handle: string;
|
|
13193
|
+
label: string;
|
|
13194
|
+
} | {
|
|
13195
|
+
provider: _nfcard_types.TipProviderId;
|
|
13196
|
+
handle: string;
|
|
13197
|
+
label?: undefined;
|
|
13198
|
+
} | undefined, unknown>>;
|
|
13093
13199
|
}, "strip", z.ZodTypeAny, {
|
|
13094
13200
|
fontKey: string;
|
|
13095
13201
|
templateId: "minimal" | "bold" | "card" | "aurora" | "classic" | "spotlight" | "marquee" | "riso";
|
|
@@ -13125,6 +13231,15 @@ declare const configurationUpdateSchema: z.ZodObject<{
|
|
|
13125
13231
|
url: string;
|
|
13126
13232
|
label: string;
|
|
13127
13233
|
}[] | undefined;
|
|
13234
|
+
tipJar?: {
|
|
13235
|
+
provider: _nfcard_types.TipProviderId;
|
|
13236
|
+
handle: string;
|
|
13237
|
+
label: string;
|
|
13238
|
+
} | {
|
|
13239
|
+
provider: _nfcard_types.TipProviderId;
|
|
13240
|
+
handle: string;
|
|
13241
|
+
label?: undefined;
|
|
13242
|
+
} | undefined;
|
|
13128
13243
|
}, {
|
|
13129
13244
|
fontKey: string;
|
|
13130
13245
|
templateId: "minimal" | "bold" | "card" | "aurora" | "classic" | "spotlight" | "marquee" | "riso";
|
|
@@ -13160,6 +13275,7 @@ declare const configurationUpdateSchema: z.ZodObject<{
|
|
|
13160
13275
|
url?: string | undefined;
|
|
13161
13276
|
label?: string | undefined;
|
|
13162
13277
|
}[] | undefined;
|
|
13278
|
+
tipJar?: unknown;
|
|
13163
13279
|
}>>;
|
|
13164
13280
|
quantity: z.ZodOptional<z.ZodNumber>;
|
|
13165
13281
|
existingProfileId: z.ZodOptional<z.ZodString>;
|
|
@@ -13515,6 +13631,15 @@ declare const configurationUpdateSchema: z.ZodObject<{
|
|
|
13515
13631
|
url: string;
|
|
13516
13632
|
label: string;
|
|
13517
13633
|
}[] | undefined;
|
|
13634
|
+
tipJar?: {
|
|
13635
|
+
provider: _nfcard_types.TipProviderId;
|
|
13636
|
+
handle: string;
|
|
13637
|
+
label: string;
|
|
13638
|
+
} | {
|
|
13639
|
+
provider: _nfcard_types.TipProviderId;
|
|
13640
|
+
handle: string;
|
|
13641
|
+
label?: undefined;
|
|
13642
|
+
} | undefined;
|
|
13518
13643
|
} | undefined;
|
|
13519
13644
|
quantity?: number | undefined;
|
|
13520
13645
|
existingProfileId?: string | undefined;
|
|
@@ -13870,6 +13995,7 @@ declare const configurationUpdateSchema: z.ZodObject<{
|
|
|
13870
13995
|
url?: string | undefined;
|
|
13871
13996
|
label?: string | undefined;
|
|
13872
13997
|
}[] | undefined;
|
|
13998
|
+
tipJar?: unknown;
|
|
13873
13999
|
} | undefined;
|
|
13874
14000
|
quantity?: number | undefined;
|
|
13875
14001
|
existingProfileId?: string | undefined;
|
|
@@ -13976,6 +14102,15 @@ declare const cardUpdateSchema: z.ZodObject<{
|
|
|
13976
14102
|
url?: string | undefined;
|
|
13977
14103
|
label?: string | undefined;
|
|
13978
14104
|
}[]>>>>;
|
|
14105
|
+
tipJar: z.ZodOptional<z.ZodOptional<z.ZodOptional<z.ZodEffects<z.ZodUnknown, {
|
|
14106
|
+
provider: _nfcard_types.TipProviderId;
|
|
14107
|
+
handle: string;
|
|
14108
|
+
label: string;
|
|
14109
|
+
} | {
|
|
14110
|
+
provider: _nfcard_types.TipProviderId;
|
|
14111
|
+
handle: string;
|
|
14112
|
+
label?: undefined;
|
|
14113
|
+
} | undefined, unknown>>>>;
|
|
13979
14114
|
profilePhotoUrl: z.ZodOptional<z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>>;
|
|
13980
14115
|
companyLogoUrl: z.ZodOptional<z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>>;
|
|
13981
14116
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -14015,6 +14150,15 @@ declare const cardUpdateSchema: z.ZodObject<{
|
|
|
14015
14150
|
url: string;
|
|
14016
14151
|
label: string;
|
|
14017
14152
|
}[] | undefined;
|
|
14153
|
+
tipJar?: {
|
|
14154
|
+
provider: _nfcard_types.TipProviderId;
|
|
14155
|
+
handle: string;
|
|
14156
|
+
label: string;
|
|
14157
|
+
} | {
|
|
14158
|
+
provider: _nfcard_types.TipProviderId;
|
|
14159
|
+
handle: string;
|
|
14160
|
+
label?: undefined;
|
|
14161
|
+
} | undefined;
|
|
14018
14162
|
}, {
|
|
14019
14163
|
profilePhotoUrl?: string | undefined;
|
|
14020
14164
|
companyLogoUrl?: string | undefined;
|
|
@@ -14052,6 +14196,7 @@ declare const cardUpdateSchema: z.ZodObject<{
|
|
|
14052
14196
|
url?: string | undefined;
|
|
14053
14197
|
label?: string | undefined;
|
|
14054
14198
|
}[] | undefined;
|
|
14199
|
+
tipJar?: unknown;
|
|
14055
14200
|
}>>;
|
|
14056
14201
|
status: z.ZodOptional<z.ZodEnum<["active", "suspended", "archived"]>>;
|
|
14057
14202
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -14093,6 +14238,15 @@ declare const cardUpdateSchema: z.ZodObject<{
|
|
|
14093
14238
|
url: string;
|
|
14094
14239
|
label: string;
|
|
14095
14240
|
}[] | undefined;
|
|
14241
|
+
tipJar?: {
|
|
14242
|
+
provider: _nfcard_types.TipProviderId;
|
|
14243
|
+
handle: string;
|
|
14244
|
+
label: string;
|
|
14245
|
+
} | {
|
|
14246
|
+
provider: _nfcard_types.TipProviderId;
|
|
14247
|
+
handle: string;
|
|
14248
|
+
label?: undefined;
|
|
14249
|
+
} | undefined;
|
|
14096
14250
|
} | undefined;
|
|
14097
14251
|
}, {
|
|
14098
14252
|
status?: "active" | "suspended" | "archived" | undefined;
|
|
@@ -14133,6 +14287,7 @@ declare const cardUpdateSchema: z.ZodObject<{
|
|
|
14133
14287
|
url?: string | undefined;
|
|
14134
14288
|
label?: string | undefined;
|
|
14135
14289
|
}[] | undefined;
|
|
14290
|
+
tipJar?: unknown;
|
|
14136
14291
|
} | undefined;
|
|
14137
14292
|
}>;
|
|
14138
14293
|
declare const adminCardCreateSchema: z.ZodObject<{
|
|
@@ -14312,6 +14467,15 @@ declare const adminCardCreateSchema: z.ZodObject<{
|
|
|
14312
14467
|
url?: string | undefined;
|
|
14313
14468
|
label?: string | undefined;
|
|
14314
14469
|
}[]>>;
|
|
14470
|
+
tipJar: z.ZodOptional<z.ZodEffects<z.ZodUnknown, {
|
|
14471
|
+
provider: _nfcard_types.TipProviderId;
|
|
14472
|
+
handle: string;
|
|
14473
|
+
label: string;
|
|
14474
|
+
} | {
|
|
14475
|
+
provider: _nfcard_types.TipProviderId;
|
|
14476
|
+
handle: string;
|
|
14477
|
+
label?: undefined;
|
|
14478
|
+
} | undefined, unknown>>;
|
|
14315
14479
|
}, "strip", z.ZodTypeAny, {
|
|
14316
14480
|
fontKey: string;
|
|
14317
14481
|
templateId: "minimal" | "bold" | "card" | "aurora" | "classic" | "spotlight" | "marquee" | "riso";
|
|
@@ -14347,6 +14511,15 @@ declare const adminCardCreateSchema: z.ZodObject<{
|
|
|
14347
14511
|
url: string;
|
|
14348
14512
|
label: string;
|
|
14349
14513
|
}[] | undefined;
|
|
14514
|
+
tipJar?: {
|
|
14515
|
+
provider: _nfcard_types.TipProviderId;
|
|
14516
|
+
handle: string;
|
|
14517
|
+
label: string;
|
|
14518
|
+
} | {
|
|
14519
|
+
provider: _nfcard_types.TipProviderId;
|
|
14520
|
+
handle: string;
|
|
14521
|
+
label?: undefined;
|
|
14522
|
+
} | undefined;
|
|
14350
14523
|
}, {
|
|
14351
14524
|
fontKey: string;
|
|
14352
14525
|
templateId: "minimal" | "bold" | "card" | "aurora" | "classic" | "spotlight" | "marquee" | "riso";
|
|
@@ -14382,6 +14555,7 @@ declare const adminCardCreateSchema: z.ZodObject<{
|
|
|
14382
14555
|
url?: string | undefined;
|
|
14383
14556
|
label?: string | undefined;
|
|
14384
14557
|
}[] | undefined;
|
|
14558
|
+
tipJar?: unknown;
|
|
14385
14559
|
}>;
|
|
14386
14560
|
physicalConfig: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
14387
14561
|
material: z.ZodEnum<["plastic", "3dprint_standard", "metal", "bamboo"]>;
|
|
@@ -17730,6 +17904,15 @@ declare const adminCardCreateSchema: z.ZodObject<{
|
|
|
17730
17904
|
url: string;
|
|
17731
17905
|
label: string;
|
|
17732
17906
|
}[] | undefined;
|
|
17907
|
+
tipJar?: {
|
|
17908
|
+
provider: _nfcard_types.TipProviderId;
|
|
17909
|
+
handle: string;
|
|
17910
|
+
label: string;
|
|
17911
|
+
} | {
|
|
17912
|
+
provider: _nfcard_types.TipProviderId;
|
|
17913
|
+
handle: string;
|
|
17914
|
+
label?: undefined;
|
|
17915
|
+
} | undefined;
|
|
17733
17916
|
};
|
|
17734
17917
|
targetUserId: string;
|
|
17735
17918
|
physicalConfig?: {
|
|
@@ -18083,6 +18266,7 @@ declare const adminCardCreateSchema: z.ZodObject<{
|
|
|
18083
18266
|
url?: string | undefined;
|
|
18084
18267
|
label?: string | undefined;
|
|
18085
18268
|
}[] | undefined;
|
|
18269
|
+
tipJar?: unknown;
|
|
18086
18270
|
};
|
|
18087
18271
|
targetUserId: string;
|
|
18088
18272
|
physicalConfig?: {
|
|
@@ -18647,6 +18831,15 @@ declare const profileCreateSchema: z.ZodObject<{
|
|
|
18647
18831
|
url?: string | undefined;
|
|
18648
18832
|
label?: string | undefined;
|
|
18649
18833
|
}[]>>;
|
|
18834
|
+
tipJar: z.ZodOptional<z.ZodEffects<z.ZodUnknown, {
|
|
18835
|
+
provider: _nfcard_types.TipProviderId;
|
|
18836
|
+
handle: string;
|
|
18837
|
+
label: string;
|
|
18838
|
+
} | {
|
|
18839
|
+
provider: _nfcard_types.TipProviderId;
|
|
18840
|
+
handle: string;
|
|
18841
|
+
label?: undefined;
|
|
18842
|
+
} | undefined, unknown>>;
|
|
18650
18843
|
}, "strip", z.ZodTypeAny, {
|
|
18651
18844
|
fontKey: string;
|
|
18652
18845
|
templateId: "minimal" | "bold" | "card" | "aurora" | "classic" | "spotlight" | "marquee" | "riso";
|
|
@@ -18682,6 +18875,15 @@ declare const profileCreateSchema: z.ZodObject<{
|
|
|
18682
18875
|
url: string;
|
|
18683
18876
|
label: string;
|
|
18684
18877
|
}[] | undefined;
|
|
18878
|
+
tipJar?: {
|
|
18879
|
+
provider: _nfcard_types.TipProviderId;
|
|
18880
|
+
handle: string;
|
|
18881
|
+
label: string;
|
|
18882
|
+
} | {
|
|
18883
|
+
provider: _nfcard_types.TipProviderId;
|
|
18884
|
+
handle: string;
|
|
18885
|
+
label?: undefined;
|
|
18886
|
+
} | undefined;
|
|
18685
18887
|
}, {
|
|
18686
18888
|
fontKey: string;
|
|
18687
18889
|
templateId: "minimal" | "bold" | "card" | "aurora" | "classic" | "spotlight" | "marquee" | "riso";
|
|
@@ -18717,6 +18919,7 @@ declare const profileCreateSchema: z.ZodObject<{
|
|
|
18717
18919
|
url?: string | undefined;
|
|
18718
18920
|
label?: string | undefined;
|
|
18719
18921
|
}[] | undefined;
|
|
18922
|
+
tipJar?: unknown;
|
|
18720
18923
|
}>;
|
|
18721
18924
|
visibility: z.ZodDefault<z.ZodEnum<["public", "private", "link_only"]>>;
|
|
18722
18925
|
accessPin: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
@@ -18781,6 +18984,15 @@ declare const profileCreateSchema: z.ZodObject<{
|
|
|
18781
18984
|
url: string;
|
|
18782
18985
|
label: string;
|
|
18783
18986
|
}[] | undefined;
|
|
18987
|
+
tipJar?: {
|
|
18988
|
+
provider: _nfcard_types.TipProviderId;
|
|
18989
|
+
handle: string;
|
|
18990
|
+
label: string;
|
|
18991
|
+
} | {
|
|
18992
|
+
provider: _nfcard_types.TipProviderId;
|
|
18993
|
+
handle: string;
|
|
18994
|
+
label?: undefined;
|
|
18995
|
+
} | undefined;
|
|
18784
18996
|
};
|
|
18785
18997
|
label?: string | undefined;
|
|
18786
18998
|
cardId?: string | undefined;
|
|
@@ -18846,6 +19058,7 @@ declare const profileCreateSchema: z.ZodObject<{
|
|
|
18846
19058
|
url?: string | undefined;
|
|
18847
19059
|
label?: string | undefined;
|
|
18848
19060
|
}[] | undefined;
|
|
19061
|
+
tipJar?: unknown;
|
|
18849
19062
|
};
|
|
18850
19063
|
label?: string | undefined;
|
|
18851
19064
|
visibility?: "public" | "private" | "link_only" | undefined;
|
|
@@ -19038,6 +19251,15 @@ declare const signupWithProfileSchema: z.ZodObject<{
|
|
|
19038
19251
|
url?: string | undefined;
|
|
19039
19252
|
label?: string | undefined;
|
|
19040
19253
|
}[]>>;
|
|
19254
|
+
tipJar: z.ZodOptional<z.ZodEffects<z.ZodUnknown, {
|
|
19255
|
+
provider: _nfcard_types.TipProviderId;
|
|
19256
|
+
handle: string;
|
|
19257
|
+
label: string;
|
|
19258
|
+
} | {
|
|
19259
|
+
provider: _nfcard_types.TipProviderId;
|
|
19260
|
+
handle: string;
|
|
19261
|
+
label?: undefined;
|
|
19262
|
+
} | undefined, unknown>>;
|
|
19041
19263
|
}, "strip", z.ZodTypeAny, {
|
|
19042
19264
|
fontKey: string;
|
|
19043
19265
|
templateId: "minimal" | "bold" | "card" | "aurora" | "classic" | "spotlight" | "marquee" | "riso";
|
|
@@ -19073,6 +19295,15 @@ declare const signupWithProfileSchema: z.ZodObject<{
|
|
|
19073
19295
|
url: string;
|
|
19074
19296
|
label: string;
|
|
19075
19297
|
}[] | undefined;
|
|
19298
|
+
tipJar?: {
|
|
19299
|
+
provider: _nfcard_types.TipProviderId;
|
|
19300
|
+
handle: string;
|
|
19301
|
+
label: string;
|
|
19302
|
+
} | {
|
|
19303
|
+
provider: _nfcard_types.TipProviderId;
|
|
19304
|
+
handle: string;
|
|
19305
|
+
label?: undefined;
|
|
19306
|
+
} | undefined;
|
|
19076
19307
|
}, {
|
|
19077
19308
|
fontKey: string;
|
|
19078
19309
|
templateId: "minimal" | "bold" | "card" | "aurora" | "classic" | "spotlight" | "marquee" | "riso";
|
|
@@ -19108,6 +19339,7 @@ declare const signupWithProfileSchema: z.ZodObject<{
|
|
|
19108
19339
|
url?: string | undefined;
|
|
19109
19340
|
label?: string | undefined;
|
|
19110
19341
|
}[] | undefined;
|
|
19342
|
+
tipJar?: unknown;
|
|
19111
19343
|
}>;
|
|
19112
19344
|
}, "strip", z.ZodTypeAny, {
|
|
19113
19345
|
email: string;
|
|
@@ -19170,6 +19402,15 @@ declare const signupWithProfileSchema: z.ZodObject<{
|
|
|
19170
19402
|
url: string;
|
|
19171
19403
|
label: string;
|
|
19172
19404
|
}[] | undefined;
|
|
19405
|
+
tipJar?: {
|
|
19406
|
+
provider: _nfcard_types.TipProviderId;
|
|
19407
|
+
handle: string;
|
|
19408
|
+
label: string;
|
|
19409
|
+
} | {
|
|
19410
|
+
provider: _nfcard_types.TipProviderId;
|
|
19411
|
+
handle: string;
|
|
19412
|
+
label?: undefined;
|
|
19413
|
+
} | undefined;
|
|
19173
19414
|
};
|
|
19174
19415
|
password: string;
|
|
19175
19416
|
acceptedTerms: true;
|
|
@@ -19239,6 +19480,7 @@ declare const signupWithProfileSchema: z.ZodObject<{
|
|
|
19239
19480
|
url?: string | undefined;
|
|
19240
19481
|
label?: string | undefined;
|
|
19241
19482
|
}[] | undefined;
|
|
19483
|
+
tipJar?: unknown;
|
|
19242
19484
|
};
|
|
19243
19485
|
password: string;
|
|
19244
19486
|
acceptedTerms: true;
|
|
@@ -19427,6 +19669,15 @@ declare const profileUpdateSchema: z.ZodObject<{
|
|
|
19427
19669
|
url?: string | undefined;
|
|
19428
19670
|
label?: string | undefined;
|
|
19429
19671
|
}[]>>>>;
|
|
19672
|
+
tipJar: z.ZodOptional<z.ZodOptional<z.ZodOptional<z.ZodEffects<z.ZodUnknown, {
|
|
19673
|
+
provider: _nfcard_types.TipProviderId;
|
|
19674
|
+
handle: string;
|
|
19675
|
+
label: string;
|
|
19676
|
+
} | {
|
|
19677
|
+
provider: _nfcard_types.TipProviderId;
|
|
19678
|
+
handle: string;
|
|
19679
|
+
label?: undefined;
|
|
19680
|
+
} | undefined, unknown>>>>;
|
|
19430
19681
|
profilePhotoUrl: z.ZodOptional<z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>>;
|
|
19431
19682
|
companyLogoUrl: z.ZodOptional<z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>>;
|
|
19432
19683
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -19466,6 +19717,15 @@ declare const profileUpdateSchema: z.ZodObject<{
|
|
|
19466
19717
|
url: string;
|
|
19467
19718
|
label: string;
|
|
19468
19719
|
}[] | undefined;
|
|
19720
|
+
tipJar?: {
|
|
19721
|
+
provider: _nfcard_types.TipProviderId;
|
|
19722
|
+
handle: string;
|
|
19723
|
+
label: string;
|
|
19724
|
+
} | {
|
|
19725
|
+
provider: _nfcard_types.TipProviderId;
|
|
19726
|
+
handle: string;
|
|
19727
|
+
label?: undefined;
|
|
19728
|
+
} | undefined;
|
|
19469
19729
|
}, {
|
|
19470
19730
|
profilePhotoUrl?: string | undefined;
|
|
19471
19731
|
companyLogoUrl?: string | undefined;
|
|
@@ -19503,6 +19763,7 @@ declare const profileUpdateSchema: z.ZodObject<{
|
|
|
19503
19763
|
url?: string | undefined;
|
|
19504
19764
|
label?: string | undefined;
|
|
19505
19765
|
}[] | undefined;
|
|
19766
|
+
tipJar?: unknown;
|
|
19506
19767
|
}>>;
|
|
19507
19768
|
visibility: z.ZodOptional<z.ZodEnum<["public", "private", "link_only"]>>;
|
|
19508
19769
|
accessPin: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
@@ -19570,6 +19831,15 @@ declare const profileUpdateSchema: z.ZodObject<{
|
|
|
19570
19831
|
url: string;
|
|
19571
19832
|
label: string;
|
|
19572
19833
|
}[] | undefined;
|
|
19834
|
+
tipJar?: {
|
|
19835
|
+
provider: _nfcard_types.TipProviderId;
|
|
19836
|
+
handle: string;
|
|
19837
|
+
label: string;
|
|
19838
|
+
} | {
|
|
19839
|
+
provider: _nfcard_types.TipProviderId;
|
|
19840
|
+
handle: string;
|
|
19841
|
+
label?: undefined;
|
|
19842
|
+
} | undefined;
|
|
19573
19843
|
} | undefined;
|
|
19574
19844
|
icon?: string | undefined;
|
|
19575
19845
|
accessPin?: string | undefined;
|
|
@@ -19637,6 +19907,7 @@ declare const profileUpdateSchema: z.ZodObject<{
|
|
|
19637
19907
|
url?: string | undefined;
|
|
19638
19908
|
label?: string | undefined;
|
|
19639
19909
|
}[] | undefined;
|
|
19910
|
+
tipJar?: unknown;
|
|
19640
19911
|
} | undefined;
|
|
19641
19912
|
icon?: string | undefined;
|
|
19642
19913
|
accessPin?: string | undefined;
|
|
@@ -19785,4 +20056,4 @@ declare function validatePhysicalConfig(data: unknown): ValidationResult<z.infer
|
|
|
19785
20056
|
declare function validateDigitalConfig(data: unknown): ValidationResult<z.infer<typeof digitalConfigSchema>>;
|
|
19786
20057
|
declare function validateConfiguration(data: unknown): ValidationResult<z.infer<typeof configurationCreateSchema>>;
|
|
19787
20058
|
|
|
19788
|
-
export { CARD_H_MM, CARD_W_MM, CHIP_RADIUS_MM, CHIP_SCALE_FLOOR, type ChipAxis, EDGE_MARGIN_MM, MAX_CTA_BUTTONS, MIN_WALL_MM, NFC_COIL_DIAMETER_MM, NFC_RADIAL_CLEARANCE_MM, type PrintabilityCtx, type Pt, QR_MATRIX_CELLS, QR_MIN_SIZE_MM, QR_MODULE_MIN_MM, TEXT_MIN_EM_MM, type ValidationResult, type Violation, adminCardCreateSchema, cardAssetSchema, cardDesignMaterialSchema, cardDesignPatternSchema, cardDesignSchema, cardElementSchema, cardElementsSchema, cardProfileUpdateSchema, cardUpdateSchema, chipAnchorToMm, comboIdSchema, configurationCreateSchema, configurationUpdateSchema, contactDetailTogglesSchema, contactExchangeSchema, cornersOnCard, createOrderSchema, ctaButtonSchema, ctaButtonsSchema, digitalConfigSchema, digitalVisibilitySchema, elementPrintability, elementTransformSchema, entrySourceSchema, hubConfigSchema, hubProfilesUpdateSchema, isFiniteTransform, materialSchema, mmToNearestChipAnchor, obbCorners, patternFamilySchema, physicalConfigSchema, physicalContentOverridesSchema, profileAccessTokenCreateSchema, profileCreateSchema, profilePinVerifySchema, profileUpdateSchema, profileUserDataSchema, profileVisibilitySchema, sanitizeHttpUrl, sanitizeText, signupWithProfileSchema, socialLinksSchema, socialOverridesSchema, userDataSchema, validateConfiguration, validateDigitalConfig, validatePhysicalConfig, validateUserData, webAddressSchema };
|
|
20059
|
+
export { ADVANCE_FALLBACK_EM, CARD_H_MM, CARD_W_MM, CHIP_RADIUS_MM, CHIP_SCALE_FLOOR, type ChipAxis, EDGE_MARGIN_MM, EMPTY_TEXT_FLOOR_EM, ITALIC_OVERHANG_EM, MAX_CTA_BUTTONS, MIN_WALL_MM, NFC_COIL_DIAMETER_MM, NFC_RADIAL_CLEARANCE_MM, type PrintabilityCtx, type Pt, QR_MATRIX_CELLS, QR_MIN_SIZE_MM, QR_MODULE_MIN_MM, TEXT_LINE_FACTOR_EM, TEXT_MIN_EM_MM, type ValidationResult, type Violation, adminCardCreateSchema, cardAssetSchema, cardDesignMaterialSchema, cardDesignPatternSchema, cardDesignSchema, cardElementSchema, cardElementsSchema, cardProfileUpdateSchema, cardUpdateSchema, chipAnchorToMm, chipPlacementMaxXY, comboIdSchema, configurationCreateSchema, configurationUpdateSchema, contactDetailTogglesSchema, contactExchangeSchema, cornersOnCard, createOrderSchema, ctaButtonSchema, ctaButtonsSchema, digitalConfigSchema, digitalVisibilitySchema, elementPrintability, elementTransformSchema, entrySourceSchema, footprintOf, hubConfigSchema, hubProfilesUpdateSchema, isFiniteTransform, materialSchema, mmToNearestChipAnchor, obbCorners, patternFamilySchema, physicalConfigSchema, physicalContentOverridesSchema, profileAccessTokenCreateSchema, profileCreateSchema, profilePinVerifySchema, profileUpdateSchema, profileUserDataSchema, profileVisibilitySchema, sanitizeHttpUrl, sanitizeText, signupWithProfileSchema, socialLinksSchema, socialOverridesSchema, textAdvanceEm, tipJarSchema, userDataSchema, validateConfiguration, validateDigitalConfig, validatePhysicalConfig, validateUserData, webAddressSchema };
|
package/dist/index.js
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nfcard/validation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Shared Zod validation schemas for the NFCard product family.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"zod": "^3.24.0",
|
|
20
|
-
"@nfcard/types": "0.
|
|
20
|
+
"@nfcard/types": "0.14.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"tsup": "^8.0.0"
|
package/src/elements.test.ts
CHANGED
|
@@ -163,6 +163,48 @@ describe('cardDesignSchema — free-transform element layer (NFCARD-197/198)', (
|
|
|
163
163
|
expect(codes(res)).toContain('elementOffCard')
|
|
164
164
|
})
|
|
165
165
|
|
|
166
|
+
it('NFCARD-421: rejects a chip nudged past the pattern-engine placement envelope', () => {
|
|
167
|
+
// The repro design (order 99d2ef93): chip near the top-left corner at
|
|
168
|
+
// (−24.2, −11.15), scale 1. |y| 11.15 > envelope maxY 10.75, so the pocket
|
|
169
|
+
// wall gets cut at cardgen batch time. The looser elementOffCard OBB check
|
|
170
|
+
// (rim 25.5) let it through checkout before this gate.
|
|
171
|
+
const res = cardDesignSchema.safeParse(
|
|
172
|
+
withElements([chipEl({ transform: { xMm: -24.2, yMm: -11.15, rotationDeg: 0, scale: 1, z: 0 } })], {
|
|
173
|
+
layout: { chipAnchor: { col: 0, row: 0 } },
|
|
174
|
+
}),
|
|
175
|
+
)
|
|
176
|
+
expect(res.success).toBe(false)
|
|
177
|
+
expect(codes(res)).toContain('chipOutsideEnvelope')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('NFCARD-421: accepts a chip sitting exactly at the envelope edge', () => {
|
|
181
|
+
// maxY at scale 1 = 54/2 − 1.5 − 13.75 − 1 = 10.75.
|
|
182
|
+
const res = cardDesignSchema.safeParse(
|
|
183
|
+
withElements([chipEl({ transform: { xMm: 0, yMm: 10.75, rotationDeg: 0, scale: 1, z: 0 } })], {
|
|
184
|
+
layout: { chipAnchor: { col: 1, row: 2 } },
|
|
185
|
+
}),
|
|
186
|
+
)
|
|
187
|
+
expect(res.success).toBe(true)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('NFCARD-421: the placement envelope tightens as the chip scales up', () => {
|
|
191
|
+
// A chip at y = 9 is inside the scale-1 envelope (10.75) but a chip scaled to
|
|
192
|
+
// 1.3 (island Ø35.75) no longer clears the rim there — envelope maxY ≈ 6.63.
|
|
193
|
+
const ok = cardDesignSchema.safeParse(
|
|
194
|
+
withElements([chipEl({ transform: { xMm: 0, yMm: 9, rotationDeg: 0, scale: 1, z: 0 } })], {
|
|
195
|
+
layout: { chipAnchor: { col: 1, row: 2 } },
|
|
196
|
+
}),
|
|
197
|
+
)
|
|
198
|
+
expect(ok.success).toBe(true)
|
|
199
|
+
const tooBig = cardDesignSchema.safeParse(
|
|
200
|
+
withElements([chipEl({ transform: { xMm: 0, yMm: 9, rotationDeg: 0, scale: 1.3, z: 0 } })], {
|
|
201
|
+
layout: { chipAnchor: { col: 1, row: 2 } },
|
|
202
|
+
}),
|
|
203
|
+
)
|
|
204
|
+
expect(tooBig.success).toBe(false)
|
|
205
|
+
expect(codes(tooBig)).toContain('chipOutsideEnvelope')
|
|
206
|
+
})
|
|
207
|
+
|
|
166
208
|
it('rejects duplicate element ids', () => {
|
|
167
209
|
const res = cardDesignSchema.safeParse(withElements([chipEl(), textEl({ id: 'chip1' })]))
|
|
168
210
|
expect(res.success).toBe(false)
|
package/src/index.ts
CHANGED
|
Binary file
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import type { CardElement } from '@nfcard/types'
|
|
3
|
+
import {
|
|
4
|
+
ADVANCE_FALLBACK_EM,
|
|
5
|
+
ITALIC_OVERHANG_EM,
|
|
6
|
+
TEXT_LINE_FACTOR_EM,
|
|
7
|
+
elementPrintability,
|
|
8
|
+
footprintOf,
|
|
9
|
+
textAdvanceEm,
|
|
10
|
+
CARD_W_MM,
|
|
11
|
+
EDGE_MARGIN_MM,
|
|
12
|
+
} from './printability'
|
|
13
|
+
|
|
14
|
+
// ── NFCARD-530 — per-glyph advance-table text footprint ──
|
|
15
|
+
// The flat 0.6 em/char estimate over-reserved ~25 % on lowercase strings (false
|
|
16
|
+
// elementOffCard far from the rim) and UNDER-reserved wide glyphs (W = 1.089 em).
|
|
17
|
+
// The tables are generated from the Instrument Sans webfont; sum-of-advances is a
|
|
18
|
+
// verified upper bound on the kerned ink (kerning in this font only subtracts).
|
|
19
|
+
|
|
20
|
+
const textEl = (over: Partial<Record<string, unknown>> = {}): CardElement =>
|
|
21
|
+
({
|
|
22
|
+
id: 't1',
|
|
23
|
+
type: 'text',
|
|
24
|
+
text: 'Anna Kovács',
|
|
25
|
+
sizeMm: 4,
|
|
26
|
+
weight: 400,
|
|
27
|
+
role: 'text',
|
|
28
|
+
transform: { xMm: 0, yMm: 0, rotationDeg: 0, scale: 1, z: 1 },
|
|
29
|
+
...over,
|
|
30
|
+
}) as unknown as CardElement
|
|
31
|
+
|
|
32
|
+
const ctx = { assetsById: new Map() }
|
|
33
|
+
|
|
34
|
+
describe('NFCARD-530 — textAdvanceEm (per-glyph advance tables)', () => {
|
|
35
|
+
it('sums real glyph advances — narrow and wide glyphs differ (the flat 0.6 could not)', () => {
|
|
36
|
+
expect(textAdvanceEm('l', 400)).toBeCloseTo(0.24, 6)
|
|
37
|
+
expect(textAdvanceEm('W', 400)).toBeCloseTo(1.089, 6)
|
|
38
|
+
expect(textAdvanceEm('Hello', 400)).toBeCloseTo(0.736 + 0.564 + 0.24 + 0.24 + 0.584, 6)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('weight ≥ 600 uses the SemiBold metric set', () => {
|
|
42
|
+
expect(textAdvanceEm('a', 600)).toBeCloseTo(0.571, 6)
|
|
43
|
+
expect(textAdvanceEm('a', 700)).toBeCloseTo(0.571, 6)
|
|
44
|
+
expect(textAdvanceEm('a', 400)).toBeCloseTo(0.533, 6)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('unknown glyphs reserve the generous fallback (over-reserve, never under)', () => {
|
|
48
|
+
expect(textAdvanceEm('あ', 400)).toBeCloseTo(ADVANCE_FALLBACK_EM, 6)
|
|
49
|
+
expect(textAdvanceEm('a😀b', 400)).toBeCloseTo(0.533 + ADVANCE_FALLBACK_EM + 0.606, 6)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('NFCARD-530 — footprintOf text branch', () => {
|
|
54
|
+
it('width = widest line advance sum × sizeMm; spaces cost their real advance (untrimmed)', () => {
|
|
55
|
+
const el = textEl({ text: 'a a', sizeMm: 4 })
|
|
56
|
+
expect(footprintOf(el).w).toBeCloseTo((0.533 + 0.2 + 0.533) * 4, 6)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('multi-line: widest line wins the width; height gains the 1.32 em line stack', () => {
|
|
60
|
+
const el = textEl({ text: 'mmmm\nil', sizeMm: 4 })
|
|
61
|
+
const wide = textAdvanceEm('mmmm', 400)
|
|
62
|
+
expect(footprintOf(el).w).toBeCloseTo(wide * 4, 6)
|
|
63
|
+
expect(footprintOf(el).h).toBeCloseTo(4 * (1.3 + TEXT_LINE_FACTOR_EM), 6)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('ITALIC charges the slant-overhang term — the ink of slanted tails pokes past the advance box', () => {
|
|
67
|
+
// Measured against the webfont: italic ink exceeds the advance sum by ≤ 0.23 em
|
|
68
|
+
// ("jjj" descender tails are the worst case); upright overhang is exactly 0. Without
|
|
69
|
+
// this term the "upper bound on rendered ink" claim silently broke for the italic toggle.
|
|
70
|
+
const upright = footprintOf(textEl({ text: 'jjj' }))
|
|
71
|
+
const italic = footprintOf(textEl({ text: 'jjj', italic: true }))
|
|
72
|
+
expect(italic.w - upright.w).toBeCloseTo(ITALIC_OVERHANG_EM * 4, 6)
|
|
73
|
+
expect(italic.h).toBeCloseTo(upright.h, 6)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('empty text keeps the historical 0.6 em floor', () => {
|
|
77
|
+
expect(footprintOf(textEl({ text: '' })).w).toBeCloseTo(0.6 * 4, 6)
|
|
78
|
+
expect(footprintOf(textEl({ text: '' })).h).toBeCloseTo(4 * 1.3, 6)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('SAFETY: wide-glyph strings now reserve MORE than the old flat 0.6 em/char', () => {
|
|
82
|
+
const el = textEl({ text: 'WWWW' })
|
|
83
|
+
const oldFlat = 4 * 4 * 0.6
|
|
84
|
+
expect(footprintOf(el).w).toBeGreaterThan(oldFlat)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('NFCARD-530 — elementOffCard verdict with the accurate estimate', () => {
|
|
89
|
+
const rimX = CARD_W_MM / 2 - EDGE_MARGIN_MM
|
|
90
|
+
|
|
91
|
+
it('no longer flags a long lowercase string sitting clearly inside the rim', () => {
|
|
92
|
+
// 18-char lowercase @ 2 mm × scale 2: flat estimate = 43.2 mm wide; advance sum = 38.3 mm.
|
|
93
|
+
// Place it in the band where the FLAT box crossed the rim but the accurate one does not.
|
|
94
|
+
const text = 'asdasdqwet4t6 7e56'
|
|
95
|
+
const el = textEl({ text, sizeMm: 2, transform: { xMm: -21, yMm: 0, rotationDeg: 0, scale: 2, z: 1 } })
|
|
96
|
+
const accurateHalf = (textAdvanceEm(text, 400) * 2 * 2) / 2
|
|
97
|
+
const flatHalf = (text.length * 2 * 0.6 * 2) / 2
|
|
98
|
+
expect(-21 - flatHalf).toBeLessThan(-rimX) // the OLD estimate tripped here…
|
|
99
|
+
expect(-21 - accurateHalf).toBeGreaterThan(-rimX) // …the accurate one fits…
|
|
100
|
+
expect(elementPrintability(el, ctx).map((v) => v.code)).toEqual([]) // …and the gate agrees.
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('still rejects text genuinely past the rim (the historical xMm 60 case)', () => {
|
|
104
|
+
const el = textEl({ transform: { xMm: 60, yMm: 0, rotationDeg: 0, scale: 1, z: 1 } })
|
|
105
|
+
expect(elementPrintability(el, ctx).map((v) => v.code)).toContain('elementOffCard')
|
|
106
|
+
})
|
|
107
|
+
})
|
package/src/printability.ts
CHANGED
|
@@ -98,13 +98,25 @@ export function cornersOnCard(corners: Pt[], margin = EDGE_MARGIN_MM): boolean {
|
|
|
98
98
|
const ANCHOR_NORM = [-0.85, 0, 0.85] as const
|
|
99
99
|
export type ChipAxis = 0 | 1 | 2
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
/**
|
|
102
|
+
* The chip PLACEMENT envelope: the region the chip *centre* must stay inside so
|
|
103
|
+
* the pattern-engine's hole-masking + rim clearance can keep the NFC pocket
|
|
104
|
+
* enclosed. Sized so the (scaled) Ø27.5·scale chip island clears the rim margin
|
|
105
|
+
* with a 1 mm buffer. Scale-aware so a grown chip tightens the envelope. Mirrors
|
|
106
|
+
* cardgen `chip_placement_max_xy`; `chipMaxXY()` is exactly this at scale 1 (the
|
|
107
|
+
* domain of the legacy 3×3 anchor lattice).
|
|
108
|
+
*/
|
|
109
|
+
export function chipPlacementMaxXY(scale: number): { maxX: number; maxY: number } {
|
|
102
110
|
return {
|
|
103
|
-
maxX: CARD_W_MM / 2 - EDGE_MARGIN_MM - CHIP_RADIUS_MM - 1,
|
|
104
|
-
maxY: CARD_H_MM / 2 - EDGE_MARGIN_MM - CHIP_RADIUS_MM - 1,
|
|
111
|
+
maxX: CARD_W_MM / 2 - EDGE_MARGIN_MM - CHIP_RADIUS_MM * scale - 1,
|
|
112
|
+
maxY: CARD_H_MM / 2 - EDGE_MARGIN_MM - CHIP_RADIUS_MM * scale - 1,
|
|
105
113
|
}
|
|
106
114
|
}
|
|
107
115
|
|
|
116
|
+
function chipMaxXY(): { maxX: number; maxY: number } {
|
|
117
|
+
return chipPlacementMaxXY(1)
|
|
118
|
+
}
|
|
119
|
+
|
|
108
120
|
/** Grid cell → card-local mm centre (matches pattern-engine `chipCentreForAnchor`). */
|
|
109
121
|
export function chipAnchorToMm(col: ChipAxis, row: ChipAxis): Pt {
|
|
110
122
|
const { maxX, maxY } = chipMaxXY()
|
|
@@ -141,9 +153,88 @@ export interface Violation {
|
|
|
141
153
|
message: string
|
|
142
154
|
}
|
|
143
155
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
156
|
+
// ── Text width estimate: per-glyph advance tables (NFCARD-530) ──
|
|
157
|
+
// GENERATED from the Instrument Sans webfont the card actually renders with (canvas
|
|
158
|
+
// measureText at 100 px → em, Chrome). The font ships two metric sets — <600 maps to the
|
|
159
|
+
// Regular face, ≥600 to the SemiBold face — and italic advances equal the upright ones
|
|
160
|
+
// (the italic INK overhang past the advance box is charged separately, ITALIC_OVERHANG_EM).
|
|
161
|
+
// Kerning in this font only ever SUBTRACTS (verified across mixed-case/HU samples:
|
|
162
|
+
// max real/sum ratio = 1.0), so a plain sum of advances is a guaranteed UPPER bound on
|
|
163
|
+
// the rendered ink, typically within 1–8 %. This replaces the flat 0.6 em/char estimate,
|
|
164
|
+
// which over-reserved ~25 % on lowercase strings (false `elementOffCard` far from the
|
|
165
|
+
// rim — NFCARD-530) while UNDER-reserving wide glyphs (`W` = 1.089 em — a real print
|
|
166
|
+
// overflow hole). cardgen `validate.py` mirrors these tables 1:1 (parity golden).
|
|
167
|
+
const ADVANCE_EM_REGULAR: Record<string, number> = {
|
|
168
|
+
'0': 0.666, '1': 0.391, '2': 0.545, '3': 0.574, '4': 0.6, '5': 0.574, '6': 0.599, '7': 0.532,
|
|
169
|
+
'8': 0.582, '9': 0.61, ' ': 0.2, '!': 0.273, '"': 0.384, '#': 0.716, '$': 0.608, '%': 0.786,
|
|
170
|
+
'&': 0.755, "'": 0.232, '(': 0.406, ')': 0.406, '*': 0.408, '+': 0.531, ',': 0.255, '-': 0.506,
|
|
171
|
+
'.': 0.255, '/': 0.443, ':': 0.255, ';': 0.255, '<': 0.531, '=': 0.531, '>': 0.531, '?': 0.567,
|
|
172
|
+
'@': 0.853, 'A': 0.728, 'B': 0.636, 'C': 0.741, 'D': 0.752, 'E': 0.638, 'F': 0.602, 'G': 0.765,
|
|
173
|
+
'H': 0.736, 'I': 0.254, 'J': 0.455, 'K': 0.692, 'L': 0.588, 'M': 0.906, 'N': 0.736, 'O': 0.786,
|
|
174
|
+
'P': 0.656, 'Q': 0.787, 'R': 0.656, 'S': 0.608, 'T': 0.648, 'U': 0.712, 'V': 0.728, 'W': 1.089,
|
|
175
|
+
'X': 0.688, 'Y': 0.676, 'Z': 0.623, '[': 0.406, '\\': 0.443, ']': 0.406, '^': 0.531, '_': 0.426,
|
|
176
|
+
'`': 0.354, 'a': 0.533, 'b': 0.606, 'c': 0.533, 'd': 0.606, 'e': 0.564, 'f': 0.354, 'g': 0.606,
|
|
177
|
+
'h': 0.599, 'i': 0.24, 'j': 0.24, 'k': 0.535, 'l': 0.24, 'm': 0.922, 'n': 0.599, 'o': 0.584,
|
|
178
|
+
'p': 0.606, 'q': 0.606, 'r': 0.375, 's': 0.473, 't': 0.377, 'u': 0.589, 'v': 0.523, 'w': 0.767,
|
|
179
|
+
'x': 0.551, 'y': 0.523, 'z': 0.496, '{': 0.406, '|': 0.242, '}': 0.406, '~': 0.531, 'Á': 0.728,
|
|
180
|
+
'É': 0.638, 'Í': 0.254, 'Ó': 0.786, 'Ö': 0.786, 'Ő': 0.786, 'Ú': 0.712, 'Ü': 0.712, 'Ű': 0.712,
|
|
181
|
+
'á': 0.533, 'é': 0.564, 'í': 0.24, 'ó': 0.584, 'ö': 0.584, 'ő': 0.584, 'ú': 0.589, 'ü': 0.589,
|
|
182
|
+
'ű': 0.589, 'Ä': 0.728, 'ä': 0.533, 'Ë': 0.638, 'ë': 0.564, 'Ï': 0.254, 'ï': 0.24, 'Ñ': 0.736,
|
|
183
|
+
'ñ': 0.599, 'Ç': 0.741, 'ç': 0.533, 'ß': 0.612, 'ẞ': 0.713, '€': 0.701, '°': 0.386, '§': 0.498,
|
|
184
|
+
'·': 0.103, '•': 0.306, '–': 0.586, '—': 0.886, '…': 0.655, '‘': 0.255, '’': 0.255, '“': 0.409,
|
|
185
|
+
'”': 0.409, '„': 0.398, '±': 0.5488, '×': 0.531, '÷': 0.531,
|
|
186
|
+
}
|
|
187
|
+
const ADVANCE_EM_BOLD: Record<string, number> = {
|
|
188
|
+
'0': 0.682, '1': 0.385, '2': 0.563, '3': 0.582, '4': 0.622, '5': 0.585, '6': 0.623, '7': 0.575,
|
|
189
|
+
'8': 0.606, '9': 0.624, ' ': 0.19, '!': 0.306, '"': 0.482, '#': 0.732, '$': 0.652, '%': 0.786,
|
|
190
|
+
'&': 0.774, "'": 0.268, '(': 0.456, ')': 0.456, '*': 0.441, '+': 0.551, ',': 0.285, '-': 0.486,
|
|
191
|
+
'.': 0.285, '/': 0.445, ':': 0.285, ';': 0.285, '<': 0.551, '=': 0.551, '>': 0.551, '?': 0.587,
|
|
192
|
+
'@': 0.874, 'A': 0.736, 'B': 0.649, 'C': 0.75, 'D': 0.756, 'E': 0.626, 'F': 0.59, 'G': 0.763,
|
|
193
|
+
'H': 0.72, 'I': 0.254, 'J': 0.413, 'K': 0.722, 'L': 0.59, 'M': 0.894, 'N': 0.72, 'O': 0.798,
|
|
194
|
+
'P': 0.672, 'Q': 0.815, 'R': 0.668, 'S': 0.652, 'T': 0.672, 'U': 0.7, 'V': 0.736, 'W': 1.086,
|
|
195
|
+
'X': 0.716, 'Y': 0.712, 'Z': 0.635, '[': 0.456, '\\': 0.445, ']': 0.456, '^': 0.551, '_': 0.486,
|
|
196
|
+
'`': 0.354, 'a': 0.571, 'b': 0.628, 'c': 0.562, 'd': 0.628, 'e': 0.574, 'f': 0.378, 'g': 0.628,
|
|
197
|
+
'h': 0.619, 'i': 0.27, 'j': 0.27, 'k': 0.569, 'l': 0.27, 'm': 0.95, 'n': 0.619, 'o': 0.604,
|
|
198
|
+
'p': 0.628, 'q': 0.628, 'r': 0.398, 's': 0.504, 't': 0.421, 'u': 0.611, 'v': 0.543, 'w': 0.816,
|
|
199
|
+
'x': 0.605, 'y': 0.543, 'z': 0.522, '{': 0.456, '|': 0.23, '}': 0.456, '~': 0.551, 'Á': 0.736,
|
|
200
|
+
'É': 0.626, 'Í': 0.254, 'Ó': 0.798, 'Ö': 0.798, 'Ő': 0.798, 'Ú': 0.7, 'Ü': 0.7, 'Ű': 0.7,
|
|
201
|
+
'á': 0.571, 'é': 0.574, 'í': 0.27, 'ó': 0.604, 'ö': 0.604, 'ő': 0.604, 'ú': 0.611, 'ü': 0.611,
|
|
202
|
+
'ű': 0.611, 'Ä': 0.736, 'ä': 0.571, 'Ë': 0.626, 'ë': 0.574, 'Ï': 0.254, 'ï': 0.27, 'Ñ': 0.72,
|
|
203
|
+
'ñ': 0.619, 'Ç': 0.75, 'ç': 0.562, 'ß': 0.64, 'ẞ': 0.717, '€': 0.722, '°': 0.366, '§': 0.53,
|
|
204
|
+
'·': 0.165, '•': 0.351, '–': 0.606, '—': 0.906, '…': 0.765, '‘': 0.285, '’': 0.285, '“': 0.502,
|
|
205
|
+
'”': 0.502, '„': 0.497, '±': 0.5488, '×': 0.551, '÷': 0.551,
|
|
206
|
+
}
|
|
207
|
+
/** Unknown glyphs (outside the tables): reserve a generous em so any realistic exotic
|
|
208
|
+
* char OVER-reserves. (Not a mathematical bound — a pathological single-codepoint wide
|
|
209
|
+
* ligature can exceed it — but such glyphs aren't in the card font at all, so the print
|
|
210
|
+
* pipeline rejects them upstream of this estimate.) */
|
|
211
|
+
export const ADVANCE_FALLBACK_EM = 1.1
|
|
212
|
+
/** ITALIC ink overhang: slanted glyphs poke past their advance box (measured against the
|
|
213
|
+
* webfont: ≤ 0.20 em at 400, ≤ 0.23 em at 700 — descender tails like "jjj" are the worst
|
|
214
|
+
* case; upright overhang is exactly 0). Charged once per text box so the advance-sum
|
|
215
|
+
* width stays a true upper bound on the rendered ink for the italic toggle too. */
|
|
216
|
+
export const ITALIC_OVERHANG_EM = 0.25
|
|
217
|
+
/** Empty-text width floor — the OLD flat estimate's minimum (1 char × 0.6 em), kept so a
|
|
218
|
+
* blank element still has a box. NOT a live glyph metric (blank text is separately gated
|
|
219
|
+
* by `textEmpty`); it just anchors degenerate OBBs. */
|
|
220
|
+
export const EMPTY_TEXT_FLOOR_EM = 0.6
|
|
221
|
+
/** Baseline-to-baseline line advance for multi-line text (mirrors the render stack's
|
|
222
|
+
* TEXT_LINE_FACTOR in nfcard-web pocShared + cardgen text2d). */
|
|
223
|
+
export const TEXT_LINE_FACTOR_EM = 1.32
|
|
224
|
+
|
|
225
|
+
/** Advance-sum width of ONE line in em, in the weight's metric set (≥600 → SemiBold). */
|
|
226
|
+
export function textAdvanceEm(line: string, weight: number): number {
|
|
227
|
+
const table = weight >= 600 ? ADVANCE_EM_BOLD : ADVANCE_EM_REGULAR
|
|
228
|
+
let em = 0
|
|
229
|
+
for (const ch of line) em += table[ch] ?? ADVANCE_FALLBACK_EM
|
|
230
|
+
return em
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Estimated card footprint (w×h, pre-scale) used for the on-card OBB check. Text is the
|
|
234
|
+
* per-glyph advance sum of the WIDEST line (untrimmed — spaces cost their real advance)
|
|
235
|
+
* with the multi-line stack in the height; a guaranteed upper bound on the rendered ink
|
|
236
|
+
* (see the table note above), so the gate can never accept an overflowing placement. */
|
|
237
|
+
export function footprintOf(el: CardElement): { w: number; h: number } {
|
|
147
238
|
switch (el.type) {
|
|
148
239
|
case 'chip':
|
|
149
240
|
return { w: 2 * CHIP_RADIUS_MM, h: 2 * CHIP_RADIUS_MM }
|
|
@@ -153,9 +244,14 @@ function footprintOf(el: CardElement): { w: number; h: number } {
|
|
|
153
244
|
case 'logo':
|
|
154
245
|
return { w: el.widthMm, h: el.heightMm }
|
|
155
246
|
case 'text': {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
247
|
+
const lines = el.text.split('\n')
|
|
248
|
+
let maxEm = 0
|
|
249
|
+
for (const line of lines) maxEm = Math.max(maxEm, textAdvanceEm(line, el.weight))
|
|
250
|
+
const widthEm = Math.max(EMPTY_TEXT_FLOOR_EM, maxEm) + (el.italic ? ITALIC_OVERHANG_EM : 0)
|
|
251
|
+
return {
|
|
252
|
+
w: widthEm * el.sizeMm,
|
|
253
|
+
h: el.sizeMm * (1.3 + (lines.length - 1) * TEXT_LINE_FACTOR_EM),
|
|
254
|
+
}
|
|
159
255
|
}
|
|
160
256
|
}
|
|
161
257
|
}
|
|
@@ -171,11 +267,29 @@ export function elementPrintability(el: CardElement, ctx: PrintabilityCtx): Viol
|
|
|
171
267
|
}
|
|
172
268
|
|
|
173
269
|
switch (el.type) {
|
|
174
|
-
case 'chip':
|
|
270
|
+
case 'chip': {
|
|
175
271
|
if (el.transform.scale < CHIP_SCALE_FLOOR - EPS) {
|
|
176
272
|
push('chipScaleBelowCoil', 'The chip cannot shrink below the 25 mm NFC coil.')
|
|
177
273
|
}
|
|
274
|
+
// NFCARD-421: the NFC pocket is only guaranteed enclosable when the chip
|
|
275
|
+
// island sits inside the pattern-engine PLACEMENT envelope — the domain
|
|
276
|
+
// under which hole-masking + rim clearance keep the perforation off the
|
|
277
|
+
// pocket wall. The generic `elementOffCard` OBB check is far looser (it lets
|
|
278
|
+
// the island edge reach the rim), so a chip nudged just past the envelope
|
|
279
|
+
// passes checkout yet fails cardgen's pocket-enclosure invariant at batch
|
|
280
|
+
// time — the buyer paid for an un-manufacturable card.
|
|
281
|
+
const env = chipPlacementMaxXY(el.transform.scale)
|
|
282
|
+
if (
|
|
283
|
+
Math.abs(el.transform.xMm) > env.maxX + EPS ||
|
|
284
|
+
Math.abs(el.transform.yMm) > env.maxY + EPS
|
|
285
|
+
) {
|
|
286
|
+
push(
|
|
287
|
+
'chipOutsideEnvelope',
|
|
288
|
+
'The NFC chip is too close to the card edge — the pocket cannot be sealed.',
|
|
289
|
+
)
|
|
290
|
+
}
|
|
178
291
|
break
|
|
292
|
+
}
|
|
179
293
|
case 'qr':
|
|
180
294
|
if (el.sizeMm * el.transform.scale < QR_MIN_SIZE_MM - EPS) {
|
|
181
295
|
push('qrTooSmall', 'The QR code is below the scannable size floor.')
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { tipJarSchema, digitalConfigSchema } from './index'
|
|
3
|
+
|
|
4
|
+
const baseConfig = {
|
|
5
|
+
templateId: 'classic' as const,
|
|
6
|
+
accentColor: '#123456',
|
|
7
|
+
fontKey: 'inter',
|
|
8
|
+
showAvatar: true,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('tipJarSchema', () => {
|
|
12
|
+
it('accepts a valid provider + handle and stores only { provider, handle }', () => {
|
|
13
|
+
expect(tipJarSchema.parse({ provider: 'kofi', handle: 'ricsi' })).toEqual({
|
|
14
|
+
provider: 'kofi',
|
|
15
|
+
handle: 'ricsi',
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('normalizes a pasted full URL down to the handle', () => {
|
|
20
|
+
expect(tipJarSchema.parse({ provider: 'kofi', handle: 'https://ko-fi.com/ricsi?x=1' })).toEqual(
|
|
21
|
+
{ provider: 'kofi', handle: 'ricsi' },
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('lowercases revolut handles', () => {
|
|
26
|
+
expect(tipJarSchema.parse({ provider: 'revolut', handle: 'RicSi' })).toEqual({
|
|
27
|
+
provider: 'revolut',
|
|
28
|
+
handle: 'ricsi',
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('keeps a sanitized custom label, capped at 40 chars', () => {
|
|
33
|
+
const out = tipJarSchema.parse({
|
|
34
|
+
provider: 'kofi',
|
|
35
|
+
handle: 'ricsi',
|
|
36
|
+
label: '<b>Támogass</b> ' + 'x'.repeat(60),
|
|
37
|
+
})
|
|
38
|
+
expect(out?.provider).toBe('kofi')
|
|
39
|
+
expect(out?.label).not.toContain('<')
|
|
40
|
+
expect((out?.label ?? '').length).toBeLessThanOrEqual(40)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('drops everything invalid to undefined instead of throwing (never 400)', () => {
|
|
44
|
+
expect(tipJarSchema.parse(undefined)).toBeUndefined()
|
|
45
|
+
expect(tipJarSchema.parse(null)).toBeUndefined()
|
|
46
|
+
expect(tipJarSchema.parse('nonsense')).toBeUndefined()
|
|
47
|
+
expect(tipJarSchema.parse({})).toBeUndefined()
|
|
48
|
+
expect(tipJarSchema.parse({ provider: 'stripe', handle: 'ricsi' })).toBeUndefined()
|
|
49
|
+
expect(tipJarSchema.parse({ provider: 'kofi', handle: 'x' })).toBeUndefined() // too short
|
|
50
|
+
expect(tipJarSchema.parse({ provider: 'kofi', handle: 'https://evil.com/ricsi' })).toBeUndefined()
|
|
51
|
+
expect(tipJarSchema.parse({ provider: 'kofi', handle: 'javascript:alert(1)' })).toBeUndefined()
|
|
52
|
+
expect(tipJarSchema.parse({ provider: 'kofi', handle: 123 })).toBeUndefined()
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('digitalConfigSchema — tipJar field', () => {
|
|
57
|
+
it('round-trips a valid tipJar', () => {
|
|
58
|
+
const parsed = digitalConfigSchema.parse({
|
|
59
|
+
...baseConfig,
|
|
60
|
+
tipJar: { provider: 'revolut', handle: 'ricsi' },
|
|
61
|
+
})
|
|
62
|
+
expect(parsed.tipJar).toEqual({ provider: 'revolut', handle: 'ricsi' })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('drops an invalid tipJar without failing the whole config save', () => {
|
|
66
|
+
const parsed = digitalConfigSchema.parse({
|
|
67
|
+
...baseConfig,
|
|
68
|
+
tipJar: { provider: 'kofi', handle: 'https://evil.com/x' },
|
|
69
|
+
})
|
|
70
|
+
expect(parsed.tipJar).toBeUndefined()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('is optional — a config without a tipJar parses fine', () => {
|
|
74
|
+
const parsed = digitalConfigSchema.parse(baseConfig)
|
|
75
|
+
expect(parsed.tipJar).toBeUndefined()
|
|
76
|
+
})
|
|
77
|
+
})
|