@stina/extension-api 0.53.1 → 0.57.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
  // -----------------------------------------------------------------------------