@stina/extension-api 0.53.1 → 0.56.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.
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import type { ChatCardComponent } from './card.schema.js'
|
|
2
3
|
import { CHAT_CARD_COMPONENTS, describeChatCardProfile, validateChatCard } from './card.schema.js'
|
|
3
4
|
|
|
4
5
|
describe('validateChatCard', () => {
|
|
@@ -72,7 +73,6 @@ describe('validateChatCard', () => {
|
|
|
72
73
|
it('accepts components nested through children', () => {
|
|
73
74
|
const result = validateChatCard({
|
|
74
75
|
component: 'VerticalStack',
|
|
75
|
-
gap: 1,
|
|
76
76
|
children: [
|
|
77
77
|
{ component: 'Header', level: 3, title: 'I dag' },
|
|
78
78
|
{ component: 'Timeline', entries: [{ time: '09:00', title: 'Standup', current: true }] },
|
|
@@ -82,6 +82,47 @@ describe('validateChatCard', () => {
|
|
|
82
82
|
expect(result.ok).toBe(true)
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
+
it('spaces every layout in the card, however deep it sits', () => {
|
|
86
|
+
const result = validateChatCard({
|
|
87
|
+
component: 'VerticalStack',
|
|
88
|
+
children: [
|
|
89
|
+
{
|
|
90
|
+
component: 'Grid',
|
|
91
|
+
columns: 2,
|
|
92
|
+
children: [
|
|
93
|
+
{ component: 'HorizontalStack', children: [{ component: 'Label', text: 'A' }] },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
expect(result.ok).toBe(true)
|
|
100
|
+
if (!result.ok) return
|
|
101
|
+
|
|
102
|
+
const grid = (result.card['children'] as ChatCardComponent[])[0]!
|
|
103
|
+
const row = (grid['children'] as ChatCardComponent[])[0]!
|
|
104
|
+
|
|
105
|
+
expect(result.card['gap']).toBe(0.75)
|
|
106
|
+
expect(grid['gap']).toBe(0.75)
|
|
107
|
+
expect(row['gap']).toBe(0.75)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('refuses a gap the caller set itself', () => {
|
|
111
|
+
// The rhythm is the profile's. A number here is a model guessing at a unit
|
|
112
|
+
// it was never told — 14 meaning pixels renders as fourteen rem.
|
|
113
|
+
const result = validateChatCard({
|
|
114
|
+
component: 'VerticalStack',
|
|
115
|
+
gap: 14,
|
|
116
|
+
children: [{ component: 'Label', text: 'A' }],
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
expect(result.ok).toBe(false)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('leaves gap out of the vocabulary the model is given', () => {
|
|
123
|
+
expect(describeChatCardProfile()).not.toContain('gap')
|
|
124
|
+
})
|
|
125
|
+
|
|
85
126
|
describe('refuses anything the user could act on', () => {
|
|
86
127
|
it.each([
|
|
87
128
|
['Button', { component: 'Button', text: 'Spara', onClickAction: 'save' }],
|
|
@@ -64,10 +64,24 @@ const children: z.ZodType<ChatCardComponent[]> = z.lazy(() =>
|
|
|
64
64
|
// Layout
|
|
65
65
|
// -----------------------------------------------------------------------------
|
|
66
66
|
|
|
67
|
+
/*
|
|
68
|
+
* None of the three layouts takes a `gap`.
|
|
69
|
+
*
|
|
70
|
+
* A panel's author sets one and looks at the result; a model writes a number
|
|
71
|
+
* into a field described as `gap?: number` and never sees what it did. The unit
|
|
72
|
+
* is rem, which is not a thing anyone guesses — the first card written against
|
|
73
|
+
* this profile asked for `gap: 14`, meaning pixels, and the second asked for the
|
|
74
|
+
* 8 the validator allowed, which is 128 of them. Both were reasonable readings
|
|
75
|
+
* of a number with no unit on it.
|
|
76
|
+
*
|
|
77
|
+
* So the spacing is the profile's, not the caller's: `applyCardSpacing` stamps
|
|
78
|
+
* the same rhythm onto every stack and grid after validation. One less field to
|
|
79
|
+
* describe, and every card in the conversation is spaced alike.
|
|
80
|
+
*/
|
|
81
|
+
|
|
67
82
|
const VerticalStackCardSchema = z
|
|
68
83
|
.object({
|
|
69
84
|
component: z.literal('VerticalStack'),
|
|
70
|
-
gap: z.number().min(0).max(8).optional(),
|
|
71
85
|
children,
|
|
72
86
|
style,
|
|
73
87
|
})
|
|
@@ -76,7 +90,6 @@ const VerticalStackCardSchema = z
|
|
|
76
90
|
const HorizontalStackCardSchema = z
|
|
77
91
|
.object({
|
|
78
92
|
component: z.literal('HorizontalStack'),
|
|
79
|
-
gap: z.number().min(0).max(8).optional(),
|
|
80
93
|
children,
|
|
81
94
|
style,
|
|
82
95
|
})
|
|
@@ -86,7 +99,6 @@ const GridCardSchema = z
|
|
|
86
99
|
.object({
|
|
87
100
|
component: z.literal('Grid'),
|
|
88
101
|
columns: z.number().min(1).max(6),
|
|
89
|
-
gap: z.number().min(0).max(8).optional(),
|
|
90
102
|
children,
|
|
91
103
|
style,
|
|
92
104
|
})
|
|
@@ -483,6 +495,37 @@ function findMisalignedChart(value: unknown): string | null {
|
|
|
483
495
|
return null
|
|
484
496
|
}
|
|
485
497
|
|
|
498
|
+
/**
|
|
499
|
+
* The space between a card's components, in rem.
|
|
500
|
+
*
|
|
501
|
+
* One value for every stack and every grid: a card is read at a glance, and a
|
|
502
|
+
* rhythm that changes between two of them reads as a mistake rather than as
|
|
503
|
+
* emphasis. It matches what the components' own padding already assumes.
|
|
504
|
+
*/
|
|
505
|
+
const CARD_GAP_REM = 0.75
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Stamp the profile's spacing onto every layout in a validated card.
|
|
509
|
+
*
|
|
510
|
+
* Done here rather than in each client's renderer: the same card object goes to
|
|
511
|
+
* the web, to Electron and to the phone, so a number written once is a rhythm
|
|
512
|
+
* they cannot disagree about. The renderers keep their own default of no gap,
|
|
513
|
+
* which is what an extension panel means by leaving it out.
|
|
514
|
+
*
|
|
515
|
+
* Mutates the parsed copy, which zod built for us and nothing else holds.
|
|
516
|
+
*/
|
|
517
|
+
function applyCardSpacing(card: ChatCardComponent): ChatCardComponent {
|
|
518
|
+
if (card.component === 'VerticalStack' || card.component === 'HorizontalStack' || card.component === 'Grid') {
|
|
519
|
+
card['gap'] = CARD_GAP_REM
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (Array.isArray(card['children'])) {
|
|
523
|
+
for (const child of card['children'] as ChatCardComponent[]) applyCardSpacing(child)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return card
|
|
527
|
+
}
|
|
528
|
+
|
|
486
529
|
/** What came back from validating a card. */
|
|
487
530
|
export type ChatCardValidation =
|
|
488
531
|
| { ok: true; card: ChatCardComponent }
|
|
@@ -527,7 +570,7 @@ export function validateChatCard(card: unknown): ChatCardValidation {
|
|
|
527
570
|
}
|
|
528
571
|
}
|
|
529
572
|
|
|
530
|
-
return { ok: true, card: parsed.data }
|
|
573
|
+
return { ok: true, card: applyCardSpacing(parsed.data) }
|
|
531
574
|
}
|
|
532
575
|
|
|
533
576
|
// -----------------------------------------------------------------------------
|