@nfcard/validation 0.24.0 → 0.25.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 +68 -10
- package/dist/index.js +10 -5
- package/package.json +2 -2
- package/src/profileLayout.test.ts +108 -7
- package/src/profileLayout.ts +43 -11
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _nfcard_types from '@nfcard/types';
|
|
2
|
-
import { CardAsset, CardElement, ElementTransform, ProfileLayout } from '@nfcard/types';
|
|
2
|
+
import { CardAsset, CardElement, ElementTransform, ProfileObjectKind, ProfileLayout } from '@nfcard/types';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -145,17 +145,17 @@ declare const textObjectStyleSchema: z.ZodObject<{
|
|
|
145
145
|
fontKey?: string | undefined;
|
|
146
146
|
italic?: boolean | undefined;
|
|
147
147
|
underline?: boolean | undefined;
|
|
148
|
-
color?: string | undefined;
|
|
149
|
-
sizeStep?: "sm" | "md" | "lg" | undefined;
|
|
150
148
|
align?: "start" | "center" | undefined;
|
|
149
|
+
sizeStep?: "sm" | "md" | "lg" | undefined;
|
|
150
|
+
color?: string | undefined;
|
|
151
151
|
}, {
|
|
152
152
|
weight?: "bold" | "normal" | undefined;
|
|
153
153
|
fontKey?: string | undefined;
|
|
154
154
|
italic?: boolean | undefined;
|
|
155
155
|
underline?: boolean | undefined;
|
|
156
|
-
color?: string | undefined;
|
|
157
|
-
sizeStep?: "sm" | "md" | "lg" | undefined;
|
|
158
156
|
align?: "start" | "center" | undefined;
|
|
157
|
+
sizeStep?: "sm" | "md" | "lg" | undefined;
|
|
158
|
+
color?: string | undefined;
|
|
159
159
|
}>;
|
|
160
160
|
declare const imageObjectStyleSchema: z.ZodObject<{
|
|
161
161
|
crop: z.ZodOptional<z.ZodObject<{
|
|
@@ -193,16 +193,74 @@ declare const imageObjectStyleSchema: z.ZodObject<{
|
|
|
193
193
|
borderColor?: string | undefined;
|
|
194
194
|
borderPx?: number | undefined;
|
|
195
195
|
}>;
|
|
196
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Contact rows and social chips: text minus `align` and `sizeStep`. Row
|
|
198
|
+
* alignment is the template's grid; and a row is a COMPOUND (label/value
|
|
199
|
+
* spans with their own px sizes), so no wrapper-level em can scale it
|
|
200
|
+
* without destroying the internal hierarchy — either would be a
|
|
201
|
+
* stored-but-dead field.
|
|
202
|
+
*/
|
|
203
|
+
declare const rowTextStyleSchema: z.ZodObject<Omit<{
|
|
204
|
+
color: z.ZodOptional<z.ZodString>;
|
|
205
|
+
sizeStep: z.ZodOptional<z.ZodEnum<["sm", "md", "lg"]>>;
|
|
206
|
+
align: z.ZodOptional<z.ZodEnum<["start", "center"]>>;
|
|
207
|
+
weight: z.ZodOptional<z.ZodEnum<["normal", "bold"]>>;
|
|
208
|
+
italic: z.ZodOptional<z.ZodBoolean>;
|
|
209
|
+
underline: z.ZodOptional<z.ZodBoolean>;
|
|
210
|
+
fontKey: z.ZodOptional<z.ZodString>;
|
|
211
|
+
}, "align" | "sizeStep">, "strict", z.ZodTypeAny, {
|
|
212
|
+
weight?: "bold" | "normal" | undefined;
|
|
213
|
+
fontKey?: string | undefined;
|
|
214
|
+
italic?: boolean | undefined;
|
|
215
|
+
underline?: boolean | undefined;
|
|
216
|
+
color?: string | undefined;
|
|
217
|
+
}, {
|
|
218
|
+
weight?: "bold" | "normal" | undefined;
|
|
219
|
+
fontKey?: string | undefined;
|
|
220
|
+
italic?: boolean | undefined;
|
|
221
|
+
underline?: boolean | undefined;
|
|
222
|
+
color?: string | undefined;
|
|
223
|
+
}>;
|
|
224
|
+
/** Label styling (the text vocabulary's `color`/`weight`/`italic`/`fontKey`,
|
|
225
|
+
* DERIVED so the validators stay in lockstep) + `bg` fill. No `sizeStep`
|
|
226
|
+
* in v1 — button geometry is the template's. `variant` is GONE (0.24.0
|
|
227
|
+
* accepted it and the render did style it): NO SURFACE COULD EVER WRITE IT
|
|
228
|
+
* — the toolbar exposes no variant control and none is planned — and a
|
|
229
|
+
* stored-but-unwritable field is dead weight on every future vintage. No
|
|
230
|
+
* deployed tag ever accepted one, so nothing stored carries it. */
|
|
231
|
+
declare const buttonObjectStyleSchema: z.ZodObject<Pick<{
|
|
232
|
+
color: z.ZodOptional<z.ZodString>;
|
|
233
|
+
sizeStep: z.ZodOptional<z.ZodEnum<["sm", "md", "lg"]>>;
|
|
234
|
+
align: z.ZodOptional<z.ZodEnum<["start", "center"]>>;
|
|
235
|
+
weight: z.ZodOptional<z.ZodEnum<["normal", "bold"]>>;
|
|
236
|
+
italic: z.ZodOptional<z.ZodBoolean>;
|
|
237
|
+
underline: z.ZodOptional<z.ZodBoolean>;
|
|
238
|
+
fontKey: z.ZodOptional<z.ZodString>;
|
|
239
|
+
}, "weight" | "fontKey" | "italic" | "color"> & {
|
|
197
240
|
bg: z.ZodOptional<z.ZodString>;
|
|
198
|
-
variant: z.ZodOptional<z.ZodEnum<["solid", "soft", "outline"]>>;
|
|
199
241
|
}, "strict", z.ZodTypeAny, {
|
|
200
|
-
|
|
242
|
+
weight?: "bold" | "normal" | undefined;
|
|
243
|
+
fontKey?: string | undefined;
|
|
244
|
+
italic?: boolean | undefined;
|
|
245
|
+
color?: string | undefined;
|
|
201
246
|
bg?: string | undefined;
|
|
202
247
|
}, {
|
|
203
|
-
|
|
248
|
+
weight?: "bold" | "normal" | undefined;
|
|
249
|
+
fontKey?: string | undefined;
|
|
250
|
+
italic?: boolean | undefined;
|
|
251
|
+
color?: string | undefined;
|
|
204
252
|
bg?: string | undefined;
|
|
205
253
|
}>;
|
|
254
|
+
/**
|
|
255
|
+
* Which style vocabulary each kind accepts — TOTAL over the kind vocabulary
|
|
256
|
+
* since the any-text wave, so "unknown kind" can only mean an id this
|
|
257
|
+
* schema's vintage cannot dispatch (already refused by `objectKindOf`).
|
|
258
|
+
*
|
|
259
|
+
* EXPORTED for the web toolbar: `toolbarControlsFor` derives its control
|
|
260
|
+
* matrix from these shapes instead of hand-mirroring them, so a vocabulary
|
|
261
|
+
* change here reshapes the UI by construction.
|
|
262
|
+
*/
|
|
263
|
+
declare const STYLE_SCHEMA_FOR_KIND: Record<ProfileObjectKind, z.ZodObject<z.ZodRawShape>>;
|
|
206
264
|
declare const profileLayoutSchema: z.ZodEffects<z.ZodObject<{
|
|
207
265
|
v: z.ZodLiteral<1>;
|
|
208
266
|
sectionOrder: z.ZodOptional<z.ZodArray<z.ZodEnum<["identity", "bio", "contact", "social", "actions", "cta", "footer"]>, "many">>;
|
|
@@ -22665,4 +22723,4 @@ declare function validatePhysicalConfig(data: unknown): ValidationResult<z.infer
|
|
|
22665
22723
|
declare function validateDigitalConfig(data: unknown): ValidationResult<z.infer<typeof digitalConfigSchema>>;
|
|
22666
22724
|
declare function validateConfiguration(data: unknown): ValidationResult<z.infer<typeof configurationCreateSchema>>;
|
|
22667
22725
|
|
|
22668
|
-
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, bookingSchema, buttonObjectStyleSchema, 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, imageObjectStyleSchema, isFiniteTransform, materialSchema, mmToNearestChipAnchor, obbCorners, patternFamilySchema, physicalConfigSchema, physicalContentOverridesSchema, profileAccessTokenCreateSchema, profileBackgroundSchema, profileCreateSchema, profileLayoutSchema, profilePinVerifySchema, profileUpdateSchema, profileUserDataSchema, profileVisibilitySchema, qrModeSchema, safeProfileLayout, sanitizeHttpUrl, sanitizeText, signupWithProfileSchema, socialLinksSchema, socialOverridesSchema, textAdvanceEm, textObjectStyleSchema, tipJarSchema, userDataSchema, validateConfiguration, validateDigitalConfig, validatePhysicalConfig, validateUserData, webAddressSchema };
|
|
22726
|
+
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, STYLE_SCHEMA_FOR_KIND, TEXT_LINE_FACTOR_EM, TEXT_MIN_EM_MM, type ValidationResult, type Violation, adminCardCreateSchema, bookingSchema, buttonObjectStyleSchema, 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, imageObjectStyleSchema, isFiniteTransform, materialSchema, mmToNearestChipAnchor, obbCorners, patternFamilySchema, physicalConfigSchema, physicalContentOverridesSchema, profileAccessTokenCreateSchema, profileBackgroundSchema, profileCreateSchema, profileLayoutSchema, profilePinVerifySchema, profileUpdateSchema, profileUserDataSchema, profileVisibilitySchema, qrModeSchema, rowTextStyleSchema, safeProfileLayout, sanitizeHttpUrl, sanitizeText, signupWithProfileSchema, socialLinksSchema, socialOverridesSchema, textAdvanceEm, textObjectStyleSchema, tipJarSchema, userDataSchema, validateConfiguration, validateDigitalConfig, validatePhysicalConfig, validateUserData, webAddressSchema };
|
package/dist/index.js
CHANGED
|
@@ -484,13 +484,16 @@ var imageObjectStyleSchema = z.object({
|
|
|
484
484
|
borderPx: z.number().int().min(0).max(8).optional(),
|
|
485
485
|
shape: z.enum(["circle", "rounded", "square"]).optional()
|
|
486
486
|
}).strict();
|
|
487
|
-
var
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
})
|
|
487
|
+
var rowTextStyleSchema = textObjectStyleSchema.omit({
|
|
488
|
+
align: true,
|
|
489
|
+
sizeStep: true
|
|
490
|
+
});
|
|
491
|
+
var buttonObjectStyleSchema = textObjectStyleSchema.pick({ color: true, weight: true, italic: true, fontKey: true }).extend({ bg: z.string().regex(HEX_RE).optional() }).strict();
|
|
491
492
|
var STYLE_SCHEMA_FOR_KIND = {
|
|
492
493
|
text: textObjectStyleSchema,
|
|
493
494
|
freeText: textObjectStyleSchema,
|
|
495
|
+
contactRow: rowTextStyleSchema,
|
|
496
|
+
socialLink: rowTextStyleSchema,
|
|
494
497
|
image: imageObjectStyleSchema,
|
|
495
498
|
ctaButton: buttonObjectStyleSchema,
|
|
496
499
|
linkOut: buttonObjectStyleSchema,
|
|
@@ -562,7 +565,7 @@ var profileLayoutSchema = z.object({
|
|
|
562
565
|
ctx.addIssue({
|
|
563
566
|
code: z.ZodIssueCode.custom,
|
|
564
567
|
path: ["objects", key, "style"],
|
|
565
|
-
message: `kind '${kind}'
|
|
568
|
+
message: `kind '${kind}' has no style vocabulary in this build`
|
|
566
569
|
});
|
|
567
570
|
continue;
|
|
568
571
|
}
|
|
@@ -1186,6 +1189,7 @@ export {
|
|
|
1186
1189
|
QR_MATRIX_CELLS,
|
|
1187
1190
|
QR_MIN_SIZE_MM,
|
|
1188
1191
|
QR_MODULE_MIN_MM,
|
|
1192
|
+
STYLE_SCHEMA_FOR_KIND,
|
|
1189
1193
|
TEXT_LINE_FACTOR_EM,
|
|
1190
1194
|
TEXT_MIN_EM_MM,
|
|
1191
1195
|
adminCardCreateSchema,
|
|
@@ -1235,6 +1239,7 @@ export {
|
|
|
1235
1239
|
profileUserDataSchema,
|
|
1236
1240
|
profileVisibilitySchema,
|
|
1237
1241
|
qrModeSchema,
|
|
1242
|
+
rowTextStyleSchema,
|
|
1238
1243
|
safeProfileLayout,
|
|
1239
1244
|
sanitizeHttpUrl,
|
|
1240
1245
|
sanitizeText,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nfcard/validation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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.19.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"tsup": "^8.0.0"
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* strings spelled out.
|
|
8
8
|
*/
|
|
9
9
|
import { describe, expect, it } from 'vitest'
|
|
10
|
-
import { CANONICAL_OBJECT_ORDER, ctaObjectIndex, type ProfileLayout } from '@nfcard/types'
|
|
10
|
+
import { CANONICAL_OBJECT_ORDER, PROFILE_OBJECT_KINDS, ctaObjectIndex, type ProfileLayout } from '@nfcard/types'
|
|
11
11
|
import { MAX_CTA_BUTTONS, digitalConfigSchema } from './index'
|
|
12
|
-
import { profileLayoutSchema, safeProfileLayout } from './profileLayout'
|
|
12
|
+
import { STYLE_SCHEMA_FOR_KIND, profileLayoutSchema, safeProfileLayout } from './profileLayout'
|
|
13
13
|
|
|
14
14
|
const okLayout: ProfileLayout = {
|
|
15
15
|
v: 1,
|
|
@@ -21,8 +21,8 @@ const okLayout: ProfileLayout = {
|
|
|
21
21
|
objects: {
|
|
22
22
|
name: { style: { color: '#112233', sizeStep: 'lg', weight: 'bold' } },
|
|
23
23
|
avatar: { style: { crop: { x: 0.5, y: 0.25, zoom: 1.6 }, borderColor: '#ffffff', borderPx: 3 } },
|
|
24
|
-
'cta-0': { style: { bg: '#16a34a',
|
|
25
|
-
booking: { style: {
|
|
24
|
+
'cta-0': { style: { bg: '#16a34a', color: '#ffffff' } },
|
|
25
|
+
booking: { style: { weight: 'bold' } },
|
|
26
26
|
},
|
|
27
27
|
ownedObjects: [{ id: 'ft-abcd', kind: 'freeText', section: 'bio', text: 'Hello there' }],
|
|
28
28
|
}
|
|
@@ -69,10 +69,10 @@ describe('profileLayoutSchema — rejects abuse', () => {
|
|
|
69
69
|
expect(bad({ v: 1, objectOrder: { unknownSection: ['social-x'] } })).toBe(false)
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
it('objects: unknown ids,
|
|
72
|
+
it('objects: unknown ids, off-vocabulary props, injection-shaped values', () => {
|
|
73
73
|
expect(bad({ v: 1, objects: { 'not-a-thing': { style: {} } } })).toBe(false)
|
|
74
|
-
// contactRow is
|
|
75
|
-
//
|
|
74
|
+
// contactRow is TEXT-stylable since the any-text wave, but `bg` never
|
|
75
|
+
// joined its vocabulary — a row has no fill of its own.
|
|
76
76
|
expect(bad({ v: 1, objects: { 'contact-email': { style: { bg: '#112233' } } } })).toBe(false)
|
|
77
77
|
expect(bad({ v: 1, objects: { name: { style: { color: 'red;}{' } } } })).toBe(false)
|
|
78
78
|
expect(bad({ v: 1, objects: { name: { style: { color: '#12345' } } } })).toBe(false)
|
|
@@ -116,6 +116,107 @@ describe('profileLayoutSchema — rejects abuse', () => {
|
|
|
116
116
|
})
|
|
117
117
|
})
|
|
118
118
|
|
|
119
|
+
describe('the any-text wave: per-kind style vocabulary (kind × prop matrix)', () => {
|
|
120
|
+
const style = (id: string, s: Record<string, unknown>) =>
|
|
121
|
+
profileLayoutSchema.safeParse({ v: 1, objects: { [id]: { style: s } } }).success
|
|
122
|
+
|
|
123
|
+
it('rows (contactRow / socialLink) take the text vocabulary minus align/sizeStep', () => {
|
|
124
|
+
const full = {
|
|
125
|
+
color: '#112233',
|
|
126
|
+
weight: 'bold',
|
|
127
|
+
italic: true,
|
|
128
|
+
underline: true,
|
|
129
|
+
fontKey: 'inter',
|
|
130
|
+
}
|
|
131
|
+
expect(style('contact-email', full)).toBe(true)
|
|
132
|
+
expect(style('social-instagram', full)).toBe(true)
|
|
133
|
+
// Row alignment is the template's grid — align stays refused. And a row
|
|
134
|
+
// is a compound (label/value spans with own px sizes), so sizeStep has
|
|
135
|
+
// nothing honest to render — refused rather than stored dead.
|
|
136
|
+
expect(style('contact-email', { align: 'center' })).toBe(false)
|
|
137
|
+
expect(style('social-instagram', { align: 'start' })).toBe(false)
|
|
138
|
+
expect(style('contact-email', { sizeStep: 'sm' })).toBe(false)
|
|
139
|
+
expect(style('social-instagram', { sizeStep: 'lg' })).toBe(false)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('buttons take label styling; geometry props stay out', () => {
|
|
143
|
+
const label = { color: '#112233', weight: 'bold', italic: true, fontKey: 'inter' }
|
|
144
|
+
expect(style('cta-0', { ...label, bg: '#16a34a' })).toBe(true)
|
|
145
|
+
expect(style('booking', { ...label, bg: '#16a34a' })).toBe(true)
|
|
146
|
+
// No sizeStep (button geometry is the template's), no underline, no align.
|
|
147
|
+
expect(style('cta-0', { sizeStep: 'lg' })).toBe(false)
|
|
148
|
+
expect(style('cta-0', { underline: true })).toBe(false)
|
|
149
|
+
expect(style('booking', { align: 'center' })).toBe(false)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('🔴 variant is GONE for every button kind — no surface could ever write it', () => {
|
|
153
|
+
expect(style('act-share', { color: '#112233', bg: '#16a34a', weight: 'bold' })).toBe(true)
|
|
154
|
+
for (const id of ['cta-0', 'booking', 'act-share']) {
|
|
155
|
+
expect(style(id, { variant: 'solid' })).toBe(false)
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('🔴 the vocabulary is TOTAL: every kind dispatches to a schema', () => {
|
|
160
|
+
// Driven off PROFILE_OBJECT_KINDS, not a hardcoded list — a NINTH kind
|
|
161
|
+
// added to types without a row here fails THIS line, not a production
|
|
162
|
+
// render. (tsc would catch it too, but tsc does not run on the publish
|
|
163
|
+
// path — validation 0.24.0 shipped while tsc was red.)
|
|
164
|
+
for (const kind of PROFILE_OBJECT_KINDS) {
|
|
165
|
+
expect(STYLE_SCHEMA_FOR_KIND[kind], `no style schema for kind '${kind}'`).toBeDefined()
|
|
166
|
+
}
|
|
167
|
+
// And through the real dispatch: one id per kind; `{}` is the least
|
|
168
|
+
// style. (The empty-style accept is also the additive guarantee: old
|
|
169
|
+
// documents keep parsing as vocabularies widen.)
|
|
170
|
+
const representative: Record<string, string> = {
|
|
171
|
+
text: 'name',
|
|
172
|
+
freeText: 'ft-abcd',
|
|
173
|
+
contactRow: 'contact-email',
|
|
174
|
+
socialLink: 'social-instagram',
|
|
175
|
+
image: 'avatar',
|
|
176
|
+
ctaButton: 'cta-0',
|
|
177
|
+
linkOut: 'booking',
|
|
178
|
+
action: 'act-share',
|
|
179
|
+
}
|
|
180
|
+
for (const kind of PROFILE_OBJECT_KINDS) {
|
|
181
|
+
const id = representative[kind]
|
|
182
|
+
expect(id, `no representative id for kind '${kind}' — extend this test`).toBeDefined()
|
|
183
|
+
expect(style(id, {})).toBe(true)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('🔴 a kind the table misses is a REFUSAL, never a throw', () => {
|
|
188
|
+
// The cross-package skew case: a types vintage that knows a kind this
|
|
189
|
+
// validation build does not. The guard must add an issue — an exception
|
|
190
|
+
// in superRefine ESCAPES safeParse (zod 3.25), and safeProfileLayout's
|
|
191
|
+
// "the public page never 500s over decoration" contract dies with it.
|
|
192
|
+
const stolen = STYLE_SCHEMA_FOR_KIND.action
|
|
193
|
+
delete (STYLE_SCHEMA_FOR_KIND as Partial<typeof STYLE_SCHEMA_FOR_KIND>).action
|
|
194
|
+
try {
|
|
195
|
+
const r = profileLayoutSchema.safeParse({
|
|
196
|
+
v: 1,
|
|
197
|
+
objects: { 'act-share': { style: { weight: 'bold' } } },
|
|
198
|
+
})
|
|
199
|
+
expect(r.success).toBe(false)
|
|
200
|
+
// A style-LESS override on the unknown kind keeps the old accept.
|
|
201
|
+
expect(
|
|
202
|
+
profileLayoutSchema.safeParse({ v: 1, objects: { 'act-share': {} } }).success,
|
|
203
|
+
).toBe(true)
|
|
204
|
+
} finally {
|
|
205
|
+
STYLE_SCHEMA_FOR_KIND.action = stolen
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('the distinct-font cap counts row and button fontKeys too', () => {
|
|
210
|
+
const fonts = Object.fromEntries(
|
|
211
|
+
['name', 'contact-email', 'social-instagram', 'cta-0', 'booking'].map((id, i) => [
|
|
212
|
+
id,
|
|
213
|
+
{ style: { fontKey: `font-${i}` } },
|
|
214
|
+
]),
|
|
215
|
+
)
|
|
216
|
+
expect(profileLayoutSchema.safeParse({ v: 1, objects: fonts }).success).toBe(false)
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
119
220
|
describe('digitalConfigSchema integration', () => {
|
|
120
221
|
const baseConfig = {
|
|
121
222
|
templateId: 'classic',
|
package/src/profileLayout.ts
CHANGED
|
@@ -83,22 +83,47 @@ export const imageObjectStyleSchema = z
|
|
|
83
83
|
})
|
|
84
84
|
.strict()
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Contact rows and social chips: text minus `align` and `sizeStep`. Row
|
|
88
|
+
* alignment is the template's grid; and a row is a COMPOUND (label/value
|
|
89
|
+
* spans with their own px sizes), so no wrapper-level em can scale it
|
|
90
|
+
* without destroying the internal hierarchy — either would be a
|
|
91
|
+
* stored-but-dead field.
|
|
92
|
+
*/
|
|
93
|
+
export const rowTextStyleSchema = textObjectStyleSchema.omit({
|
|
94
|
+
align: true,
|
|
95
|
+
sizeStep: true,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
/** Label styling (the text vocabulary's `color`/`weight`/`italic`/`fontKey`,
|
|
99
|
+
* DERIVED so the validators stay in lockstep) + `bg` fill. No `sizeStep`
|
|
100
|
+
* in v1 — button geometry is the template's. `variant` is GONE (0.24.0
|
|
101
|
+
* accepted it and the render did style it): NO SURFACE COULD EVER WRITE IT
|
|
102
|
+
* — the toolbar exposes no variant control and none is planned — and a
|
|
103
|
+
* stored-but-unwritable field is dead weight on every future vintage. No
|
|
104
|
+
* deployed tag ever accepted one, so nothing stored carries it. */
|
|
105
|
+
export const buttonObjectStyleSchema = textObjectStyleSchema
|
|
106
|
+
.pick({ color: true, weight: true, italic: true, fontKey: true })
|
|
107
|
+
.extend({ bg: z.string().regex(HEX_RE).optional() })
|
|
91
108
|
.strict()
|
|
92
109
|
|
|
93
110
|
/**
|
|
94
|
-
* Which style vocabulary each kind accepts
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
111
|
+
* Which style vocabulary each kind accepts — TOTAL over the kind vocabulary
|
|
112
|
+
* since the any-text wave, so "unknown kind" can only mean an id this
|
|
113
|
+
* schema's vintage cannot dispatch (already refused by `objectKindOf`).
|
|
114
|
+
*
|
|
115
|
+
* EXPORTED for the web toolbar: `toolbarControlsFor` derives its control
|
|
116
|
+
* matrix from these shapes instead of hand-mirroring them, so a vocabulary
|
|
117
|
+
* change here reshapes the UI by construction.
|
|
98
118
|
*/
|
|
99
|
-
const STYLE_SCHEMA_FOR_KIND:
|
|
119
|
+
export const STYLE_SCHEMA_FOR_KIND: Record<
|
|
120
|
+
ProfileObjectKind,
|
|
121
|
+
z.ZodObject<z.ZodRawShape>
|
|
122
|
+
> = {
|
|
100
123
|
text: textObjectStyleSchema,
|
|
101
124
|
freeText: textObjectStyleSchema,
|
|
125
|
+
contactRow: rowTextStyleSchema,
|
|
126
|
+
socialLink: rowTextStyleSchema,
|
|
102
127
|
image: imageObjectStyleSchema,
|
|
103
128
|
ctaButton: buttonObjectStyleSchema,
|
|
104
129
|
linkOut: buttonObjectStyleSchema,
|
|
@@ -191,11 +216,18 @@ export const profileLayoutSchema = z
|
|
|
191
216
|
}
|
|
192
217
|
const style = overrideParsed.data.style
|
|
193
218
|
if (style === undefined) continue
|
|
219
|
+
// The table is TOTAL by type, but tsc does not run on the publish
|
|
220
|
+
// path and `objectKindOf` lives in @nfcard/types behind a ^range —
|
|
221
|
+
// a types vintage that knows a NINTH kind can resolve under a
|
|
222
|
+
// validation build whose table stops at eight. Without this guard
|
|
223
|
+
// that skew is a TypeError that ESCAPES safeParse (verified against
|
|
224
|
+
// zod 3.25), turning `safeProfileLayout`'s promised null into a 500
|
|
225
|
+
// on the public render. Refusal, never a throw.
|
|
194
226
|
if (!styleSchema) {
|
|
195
227
|
ctx.addIssue({
|
|
196
228
|
code: z.ZodIssueCode.custom,
|
|
197
229
|
path: ['objects', key, 'style'],
|
|
198
|
-
message: `kind '${kind}'
|
|
230
|
+
message: `kind '${kind}' has no style vocabulary in this build`,
|
|
199
231
|
})
|
|
200
232
|
continue
|
|
201
233
|
}
|