@estiva-app/ui 0.42.0 → 0.43.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/ui",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
4
4
  "description": "Estiva's design tokens (the contract) and a small set of primitives (a convenience) for every Estiva app.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/registry.json CHANGED
@@ -4,9 +4,9 @@
4
4
  "kind": "package",
5
5
  "repo": "estiva-ui",
6
6
  "package": "@estiva-app/ui",
7
- "packageVersion": "0.42.0",
8
- "exports": 90,
9
- "typeExports": 101,
7
+ "packageVersion": "0.43.0",
8
+ "exports": 91,
9
+ "typeExports": 102,
10
10
  "files": null
11
11
  },
12
12
  "storybook": {
@@ -1280,6 +1280,44 @@
1280
1280
  "app": null,
1281
1281
  "looks": []
1282
1282
  },
1283
+ {
1284
+ "name": "CommandPaletteProposal",
1285
+ "repo": "estiva-ui",
1286
+ "kind": "component",
1287
+ "importPath": "@estiva-app/ui",
1288
+ "sourceFile": "src/CommandPalette.tsx",
1289
+ "purpose": "Something the palette offers to make, before anything is made: a card with where it would be made on top, then what it would be.",
1290
+ "purposeFrom": "comment",
1291
+ "props": [
1292
+ {
1293
+ "name": "source",
1294
+ "takes": "text",
1295
+ "required": true,
1296
+ "note": "Where it would be made: the app and the action — \"App › New item\"."
1297
+ },
1298
+ {
1299
+ "name": "mark",
1300
+ "takes": "anything",
1301
+ "required": false,
1302
+ "note": "The app's mark before the source, as small as the line (about 10px)."
1303
+ },
1304
+ {
1305
+ "name": "children",
1306
+ "takes": "text",
1307
+ "required": true,
1308
+ "note": "What would be made, in a sentence."
1309
+ }
1310
+ ],
1311
+ "variants": [],
1312
+ "ownsBehaviours": [],
1313
+ "status": "stable",
1314
+ "migrationStage": null,
1315
+ "docsId": "overlays-commandpalette--docs",
1316
+ "storyId": "overlays-commandpalette--first-level",
1317
+ "docPage": null,
1318
+ "app": null,
1319
+ "looks": []
1320
+ },
1283
1321
  {
1284
1322
  "name": "CommandPaletteQuote",
1285
1323
  "repo": "estiva-ui",
@@ -23,6 +23,12 @@ form — and the app keeps the levels.
23
23
  above the rows, for a result that is written rather than listed: a line while
24
24
  it is on its way, an answer with marks pointing at the rows it came from, and
25
25
  text written to be used somewhere else.
26
+ - **`CommandPaletteProposal`** — above the rows, for something the palette
27
+ offers to make before anything is made: a card with where it would be made
28
+ (`source`, with the app's `mark`), then what it would be. The rows under it
29
+ say what happens next.
30
+
31
+ <Canvas of={CommandPaletteStories.Proposal} />
26
32
 
27
33
  <Canvas of={CommandPaletteStories.Answer} />
28
34
 
@@ -7,6 +7,7 @@ import {
7
7
  CommandPalette,
8
8
  CommandPaletteAnswer,
9
9
  CommandPaletteForm,
10
+ CommandPaletteProposal,
10
11
  CommandPaletteQuote,
11
12
  CommandPaletteSearch,
12
13
  CommandPaletteWorking,
@@ -54,6 +55,7 @@ type Level =
54
55
  | { kind: 'place'; name: string }
55
56
  | { kind: 'ask' }
56
57
  | { kind: 'draft' }
58
+ | { kind: 'make' }
57
59
  | { kind: 'form' }
58
60
  | { kind: 'one-list' }
59
61
 
@@ -62,7 +64,7 @@ interface Frame {
62
64
  query: string
63
65
  }
64
66
 
65
- type Start = 'first' | 'arriving' | 'nothing' | 'place' | 'working' | 'answer' | 'draft' | 'form' | 'refused' | 'one-list'
67
+ type Start = 'first' | 'arriving' | 'nothing' | 'place' | 'working' | 'answer' | 'draft' | 'make' | 'form' | 'refused' | 'one-list'
66
68
 
67
69
  const PLACES = ['Place one', 'Place two']
68
70
  const ACTIONS = [
@@ -81,6 +83,7 @@ const GROUPS = [
81
83
  ]
82
84
  const ANSWER = 'Item one moved on Monday [1], and item two is waiting on a reply [2].\n\nNothing else changed this week.'
83
85
  const DRAFT = 'Thanks, both. I will look at item one today\nand reply on item two tomorrow.'
86
+ const PROPOSAL = 'An item for the reply on item two, due tomorrow.'
84
87
 
85
88
  function startStack(start: Start): Frame[] {
86
89
  const first: Frame = { level: { kind: 'first' }, query: '' }
@@ -96,6 +99,8 @@ function startStack(start: Start): Frame[] {
96
99
  return [first, { level: { kind: 'ask' }, query: 'What changed this week?' }]
97
100
  case 'draft':
98
101
  return [first, { level: { kind: 'draft' }, query: 'Say I will look today' }]
102
+ case 'make':
103
+ return [first, { level: { kind: 'make' }, query: '' }]
99
104
  case 'form':
100
105
  case 'refused':
101
106
  return [first, { level: { kind: 'place', name: 'Place one' }, query: '' }, { level: { kind: 'form' }, query: '' }]
@@ -180,6 +185,17 @@ function Demo({ start, where, modKey, label }: { start: Start; where?: string; m
180
185
  },
181
186
  ]
182
187
  }
188
+ if (level.kind === 'make') {
189
+ return [
190
+ {
191
+ label: 'Next',
192
+ rows: [
193
+ { id: 'make-open', label: 'Open the form', description: 'Place one › Action one · nothing is made until you submit', icon, onSelect: () => push({ kind: 'form' }) },
194
+ { id: 'make-again', label: 'Suggest something else', icon, onSelect: () => close('Suggest something else') },
195
+ ],
196
+ },
197
+ ]
198
+ }
183
199
  if (level.kind === 'ask' || level.kind === 'draft') {
184
200
  if (!asked?.done) return []
185
201
  const changed = frame.query.trim() !== asked.question.trim()
@@ -236,6 +252,7 @@ function Demo({ start, where, modKey, label }: { start: Start; where?: string; m
236
252
  rows: [
237
253
  { id: 'ask', label: 'Ask about this page', icon, onSelect: () => push({ kind: 'ask' }) },
238
254
  { id: 'draft', label: 'Write a draft', icon, onSelect: () => push({ kind: 'draft' }) },
255
+ { id: 'make', label: 'Make something from this page', icon, onSelect: () => push({ kind: 'make' }) },
239
256
  { id: 'one-list', label: 'Change kind', description: 'Item one', icon, onSelect: () => push({ kind: 'one-list' }) },
240
257
  ],
241
258
  },
@@ -289,6 +306,7 @@ function Demo({ start, where, modKey, label }: { start: Start; where?: string; m
289
306
  }
290
307
 
291
308
  const body = (() => {
309
+ if (level.kind === 'make') return <CommandPaletteProposal source="Place one › Action one" mark={<IconSquareRounded size={10} stroke={1.5} />}>{PROPOSAL}</CommandPaletteProposal>
292
310
  if (level.kind !== 'ask' && level.kind !== 'draft') return undefined
293
311
  if (!asked || !asked.done) return <CommandPaletteWorking>Reading 12 notes…</CommandPaletteWorking>
294
312
  const changed = frame.query.trim() !== asked.question.trim()
@@ -355,6 +373,8 @@ function Demo({ start, where, modKey, label }: { start: Start; where?: string; m
355
373
  ? 'Ask anything about this page'
356
374
  : level.kind === 'draft'
357
375
  ? 'What should it say?'
376
+ : level.kind === 'make'
377
+ ? 'Looking at this page'
358
378
  : 'Search, or choose a place'
359
379
  }
360
380
  groups={groups}
@@ -365,6 +385,8 @@ function Demo({ start, where, modKey, label }: { start: Start; where?: string; m
365
385
  ? { label: 'Ask', onBack: back }
366
386
  : level.kind === 'draft'
367
387
  ? { label: 'Draft', onBack: back }
388
+ : level.kind === 'make'
389
+ ? { label: 'Make', onBack: back }
368
390
  : undefined
369
391
  }
370
392
  pending={level.kind === 'first' && searching ? 'Searching…' : undefined}
@@ -412,6 +434,9 @@ export const Answer: Story = story('answer')
412
434
  export const Quote: Story = story('draft')
413
435
 
414
436
  /** A form. Ctrl+Enter creates; a missing field says so and takes focus; Backspace in an empty field goes back and keeps what was typed. */
437
+ /** Something it offers to make: where, then what. Nothing is made until the form is sent. */
438
+ export const Proposal: Story = story('make')
439
+
415
440
  export const Form: Story = story('form')
416
441
 
417
442
  /** Creating is refused: the fields unlock, the reason sits beside the button, and focus is back where it was. */
@@ -17,6 +17,7 @@ import {
17
17
  CommandPalette,
18
18
  CommandPaletteAnswer,
19
19
  CommandPaletteForm,
20
+ CommandPaletteProposal,
20
21
  CommandPaletteQuote,
21
22
  CommandPaletteSearch,
22
23
  CommandPaletteWorking,
@@ -421,6 +422,19 @@ describe('what sits above the rows', () => {
421
422
  expect(screen.getByText(/Line one/).textContent).toBe('Line one\nLine two')
422
423
  })
423
424
 
425
+ it('draws a proposal as a card: where it would be made, with its mark, then what', () => {
426
+ const { container } = render(
427
+ <CommandPaletteProposal source="Place one › Action one" mark={<svg data-testid="mark" />}>
428
+ An item for tomorrow.
429
+ </CommandPaletteProposal>,
430
+ )
431
+ const lines = container.firstElementChild?.children
432
+ expect(lines).toHaveLength(2)
433
+ expect(lines?.[0].textContent).toBe('Place one › Action one')
434
+ expect(lines?.[0].contains(screen.getByTestId('mark'))).toBe(true)
435
+ expect(lines?.[1].textContent).toBe('An item for tomorrow.')
436
+ })
437
+
424
438
  it('announces what is being worked on, with its grey bars', () => {
425
439
  const { container } = render(<CommandPaletteWorking bars={3}>Reading…</CommandPaletteWorking>)
426
440
  expect(screen.getByRole('status').textContent).toContain('Reading…')
@@ -16,6 +16,7 @@ import type { BaseUIEvent } from '@base-ui/react/types'
16
16
  import { IconLoader2, IconSearch } from '@tabler/icons-react'
17
17
  import { cn } from './cn'
18
18
  import { Button } from './Button'
19
+ import { Card } from './Card'
19
20
  import { InputChip } from './ChipInput'
20
21
  import { EmptyState } from './EmptyState'
21
22
  import { FieldLine } from './Field'
@@ -661,3 +662,31 @@ export interface CommandPaletteQuoteProps {
661
662
  export function CommandPaletteQuote({ children }: CommandPaletteQuoteProps) {
662
663
  return <p className="whitespace-pre-wrap border-l-2 border-border-default pl-3 text-body-2 text-text-primary">{children}</p>
663
664
  }
665
+
666
+ export interface CommandPaletteProposalProps {
667
+ /** Where it would be made: the app and the action — "App › New item". */
668
+ source: string
669
+ /** The app's mark before the source, as small as the line (about 10px). */
670
+ mark?: ReactNode
671
+ /** What would be made, in a sentence. */
672
+ children: string
673
+ }
674
+
675
+ /**
676
+ * Something the palette offers to make, before anything is made: a card with
677
+ * where it would be made on top, then what it would be. A card because it is a
678
+ * thing about to exist, not text to read or use; the source first because two
679
+ * apps may offer something alike (UIG-40, Katerina 28 September: the look Peek's
680
+ * launcher already had, now drawn by the palette, ruling P4).
681
+ */
682
+ export function CommandPaletteProposal({ source, mark, children }: CommandPaletteProposalProps) {
683
+ return (
684
+ <Card fill="surface" className="flex flex-col gap-1 p-3">
685
+ <span className="flex items-center gap-2 text-caption text-text-secondary">
686
+ {mark}
687
+ {source}
688
+ </span>
689
+ <span className="text-body-2 text-text-primary">{children}</span>
690
+ </Card>
691
+ )
692
+ }
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ export {
22
22
  CommandPalette,
23
23
  CommandPaletteAnswer,
24
24
  CommandPaletteForm,
25
+ CommandPaletteProposal,
25
26
  CommandPaletteQuote,
26
27
  CommandPaletteSearch,
27
28
  CommandPaletteWorking,
@@ -30,6 +31,7 @@ export {
30
31
  type CommandPaletteFormProps,
31
32
  type CommandPaletteGroup,
32
33
  type CommandPaletteProps,
34
+ type CommandPaletteProposalProps,
33
35
  type CommandPaletteQuoteProps,
34
36
  type CommandPaletteRow,
35
37
  type CommandPaletteSearchProps,
@@ -45,17 +45,17 @@ describe('the registry builds', () => {
45
45
  expect(registry.entries.map((one) => one.name).sort()).toEqual(values.map((one) => one.name).sort())
46
46
  })
47
47
 
48
- it('counts 90 names over 59 files: 82 components, 7 helpers and 1 hook (RichText and richTextClassName joined on 28 September)', () => {
48
+ it('counts 91 names over 59 files: 83 components, 7 helpers and 1 hook (CommandPaletteProposal joined on 28 September)', () => {
49
49
  // The reconciliation GATES.md §24 explains. If this changes, that changes.
50
50
  const kinds = registry.entries.reduce<Record<string, number>>((all, one) => ({ ...all, [one.kind]: (all[one.kind] ?? 0) + 1 }), {})
51
- expect(kinds).toEqual({ component: 82, helper: 7, hook: 1 })
51
+ expect(kinds).toEqual({ component: 83, helper: 7, hook: 1 })
52
52
  expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(59)
53
53
  })
54
54
 
55
55
  it('gives every entry a purpose, from its own page or from the comment above it', () => {
56
56
  expect(registry.entries.filter((one) => one.purpose.trim() === '')).toEqual([])
57
57
  expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(59)
58
- expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(31)
58
+ expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(32)
59
59
  })
60
60
 
61
61
  it('takes a name with a page of its own from the page, and a name without one from the code', () => {