@estiva-app/ui 0.22.0 → 0.24.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/README.md +21 -0
- package/dist/AppShell.d.ts.map +1 -1
- package/dist/AttachmentCard.d.ts.map +1 -1
- package/dist/Banner.d.ts +20 -2
- package/dist/Banner.d.ts.map +1 -1
- package/dist/ContainerHeader.d.ts +25 -0
- package/dist/ContainerHeader.d.ts.map +1 -0
- package/dist/EmptyState.d.ts +10 -1
- package/dist/EmptyState.d.ts.map +1 -1
- package/dist/Form.d.ts.map +1 -1
- package/dist/IconButton.d.ts +9 -1
- package/dist/IconButton.d.ts.map +1 -1
- package/dist/PreviewCard.d.ts +9 -2
- package/dist/PreviewCard.d.ts.map +1 -1
- package/dist/SectionHeader.d.ts +7 -1
- package/dist/SectionHeader.d.ts.map +1 -1
- package/dist/Select.d.ts +11 -3
- package/dist/Select.d.ts.map +1 -1
- package/dist/Tooltip.d.ts +13 -1
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/eslint/index.js +2 -1
- package/dist/eslint/index.js.map +2 -2
- package/dist/eslint/no-restyled-part.d.ts.map +1 -1
- package/dist/gates/app-checks.d.ts.map +1 -1
- package/dist/gates/{chunk-ZGJ2J5NU.js → chunk-EA33NP5B.js} +275 -11
- package/dist/gates/chunk-EA33NP5B.js.map +7 -0
- package/dist/gates/{chunk-AUXD4GCY.js → chunk-GTQZEHPC.js} +15 -1
- package/dist/gates/chunk-GTQZEHPC.js.map +7 -0
- package/dist/gates/cli.js +1 -1
- package/dist/gates/create-app.d.ts +2 -0
- package/dist/gates/create-app.d.ts.map +1 -1
- package/dist/gates/create-app.js +1 -1
- package/dist/gates/index.js +25 -3
- package/dist/gates/index.js.map +2 -2
- package/dist/gates/status.d.ts +2 -0
- package/dist/gates/status.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +196 -156
- package/dist/index.js.map +4 -4
- package/dist/registry/app-DQI6WLHR.js +10 -0
- package/dist/registry/app.d.ts +21 -0
- package/dist/registry/app.d.ts.map +1 -0
- package/dist/registry/build-LGFCCOLR.js +27 -0
- package/dist/registry/build-LGFCCOLR.js.map +7 -0
- package/dist/registry/build.d.ts +68 -2
- package/dist/registry/build.d.ts.map +1 -1
- package/dist/registry/{chunk-MRSBS5OP.js → chunk-E4JNV7PC.js} +68 -12
- package/dist/registry/chunk-E4JNV7PC.js.map +7 -0
- package/dist/registry/{chunk-QDYGB3QN.js → chunk-NJN4MQAM.js} +60 -15
- package/dist/registry/chunk-NJN4MQAM.js.map +7 -0
- package/dist/registry/{chunk-IJNCYVH4.js → chunk-W2B2G7OE.js} +34 -9
- package/dist/registry/chunk-W2B2G7OE.js.map +7 -0
- package/dist/registry/chunk-WMFF3MPP.js +648 -0
- package/dist/registry/chunk-WMFF3MPP.js.map +7 -0
- package/dist/registry/cli.js +81 -26
- package/dist/registry/cli.js.map +3 -3
- package/dist/registry/find.d.ts +12 -1
- package/dist/registry/find.d.ts.map +1 -1
- package/dist/registry/index.d.ts +5 -2
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +11 -3
- package/dist/registry/schema.d.ts +83 -6
- package/dist/registry/schema.d.ts.map +1 -1
- package/package.json +2 -2
- package/registry.json +283 -95
- package/src/AppShell.mdx +10 -0
- package/src/AppShell.test.tsx +16 -0
- package/src/AppShell.tsx +4 -1
- package/src/AttachmentCard.mdx +9 -0
- package/src/AttachmentCard.test.tsx +10 -0
- package/src/AttachmentCard.tsx +8 -5
- package/src/Avatar.mdx +13 -3
- package/src/AvatarGroup.mdx +4 -0
- package/src/Banner.mdx +30 -7
- package/src/Banner.stories.tsx +22 -0
- package/src/Banner.test.tsx +35 -0
- package/src/Banner.tsx +34 -9
- package/src/Breadcrumb.mdx +10 -0
- package/src/Button.mdx +14 -3
- package/src/Card.mdx +9 -0
- package/src/Checkbox.mdx +10 -0
- package/src/Chip.mdx +4 -0
- package/src/ChipInput.mdx +12 -0
- package/src/CollapsibleSection.mdx +9 -0
- package/src/CommandPalette.mdx +14 -2
- package/src/ConfirmDialog.mdx +11 -0
- package/src/ContainerHeader.mdx +60 -0
- package/src/ContainerHeader.stories.tsx +62 -0
- package/src/ContainerHeader.test.tsx +47 -0
- package/src/ContainerHeader.tsx +45 -0
- package/src/DialogShell.mdx +14 -0
- package/src/Divider.mdx +9 -1
- package/src/EditableText.mdx +11 -0
- package/src/EmptyState.mdx +27 -4
- package/src/EmptyState.stories.tsx +6 -1
- package/src/EmptyState.test.tsx +18 -1
- package/src/EmptyState.tsx +14 -1
- package/src/Field.mdx +10 -1
- package/src/FieldLine.mdx +9 -1
- package/src/FilePicker.mdx +8 -0
- package/src/Form.mdx +10 -0
- package/src/Form.test.tsx +26 -0
- package/src/Form.tsx +7 -0
- package/src/IconButton.mdx +16 -2
- package/src/IconButton.stories.tsx +3 -0
- package/src/IconButton.test.tsx +25 -0
- package/src/IconButton.tsx +61 -46
- package/src/IdentityMenu.mdx +9 -0
- package/src/InlineChip.mdx +8 -0
- package/src/Kbd.mdx +4 -0
- package/src/Link.mdx +10 -0
- package/src/Menu.mdx +16 -1
- package/src/MenuItem.mdx +12 -2
- package/src/NavItem.mdx +8 -0
- package/src/Person.mdx +4 -0
- package/src/PersonTrigger.mdx +9 -0
- package/src/Popover.mdx +21 -0
- package/src/PreviewCard.mdx +19 -4
- package/src/PreviewCard.tsx +11 -4
- package/src/ProgressBar.mdx +8 -0
- package/src/Property.mdx +4 -0
- package/src/Rail.mdx +9 -1
- package/src/RailItem.mdx +8 -0
- package/src/Reaction.mdx +9 -0
- package/src/ReactionPicker.mdx +8 -0
- package/src/ScrollArea.mdx +13 -2
- package/src/SearchInput.mdx +9 -0
- package/src/SectionHeader.mdx +13 -0
- package/src/SectionHeader.stories.tsx +9 -0
- package/src/SectionHeader.test.tsx +9 -0
- package/src/SectionHeader.tsx +8 -2
- package/src/SectionLabel.mdx +4 -0
- package/src/Select.mdx +21 -2
- package/src/Select.stories.tsx +4 -1
- package/src/Select.test.tsx +17 -0
- package/src/Select.tsx +34 -13
- package/src/Sidebar.mdx +8 -0
- package/src/Skeleton.mdx +4 -0
- package/src/Tabs.mdx +12 -1
- package/src/TextInput.mdx +8 -0
- package/src/Textarea.mdx +8 -0
- package/src/Toast.mdx +12 -1
- package/src/Toolbar.mdx +11 -0
- package/src/Tooltip.mdx +26 -2
- package/src/Tooltip.stories.tsx +26 -0
- package/src/Tooltip.test.tsx +29 -0
- package/src/Tooltip.tsx +17 -4
- package/src/TopBar.mdx +4 -0
- package/src/eslint/no-rebuilt-behaviour.ts +1 -1
- package/src/eslint/no-restyled-part.ts +1 -0
- package/src/gates/app-checks.ts +23 -1
- package/src/gates/create-app.test.ts +77 -6
- package/src/gates/create-app.ts +288 -15
- package/src/gates/status.ts +16 -0
- package/src/index.ts +1 -0
- package/src/pages.test.ts +142 -0
- package/src/registry/app.test.ts +562 -0
- package/src/registry/app.ts +854 -0
- package/src/registry/build.ts +114 -25
- package/src/registry/cli.ts +105 -42
- package/src/registry/find.ts +45 -8
- package/src/registry/index.ts +8 -1
- package/src/registry/registry.test.ts +5 -5
- package/src/registry/schema.ts +163 -12
- package/tailwind-preset.js +22 -1
- package/dist/gates/chunk-AUXD4GCY.js.map +0 -7
- package/dist/gates/chunk-ZGJ2J5NU.js.map +0 -7
- package/dist/registry/build-GOVLABI6.js +0 -13
- package/dist/registry/chunk-IJNCYVH4.js.map +0 -7
- package/dist/registry/chunk-MRSBS5OP.js.map +0 -7
- package/dist/registry/chunk-QDYGB3QN.js.map +0 -7
- /package/dist/registry/{build-GOVLABI6.js.map → app-DQI6WLHR.js.map} +0 -0
package/src/SectionHeader.tsx
CHANGED
|
@@ -59,6 +59,12 @@ export interface SectionHeaderProps {
|
|
|
59
59
|
actions?: SectionAction[]
|
|
60
60
|
/** `hover` reveals the actions while the row is hovered or focused; `always` keeps them. */
|
|
61
61
|
showActions?: 'hover' | 'always'
|
|
62
|
+
/**
|
|
63
|
+
* `fill` lights the row under the pointer when it does something (a toggle,
|
|
64
|
+
* actions); `none` keeps it still — a heading whose actions are always shown,
|
|
65
|
+
* where the buttons light up on their own (UIG-14, Katerina, 19 September).
|
|
66
|
+
*/
|
|
67
|
+
hover?: 'fill' | 'none'
|
|
62
68
|
/**
|
|
63
69
|
* What the title renders as, in Base UI's manner. A plain button with
|
|
64
70
|
* `onToggle` by default; `CollapsibleSection` hands in `Collapsible.Trigger`.
|
|
@@ -67,7 +73,7 @@ export interface SectionHeaderProps {
|
|
|
67
73
|
className?: string
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
export function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, trailing, actions, showActions = 'hover', render, className }: SectionHeaderProps) {
|
|
76
|
+
export function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, trailing, actions, showActions = 'hover', hover = 'fill', render, className }: SectionHeaderProps) {
|
|
71
77
|
const titleElement = useRender({
|
|
72
78
|
render: render ?? (chevron ? <button type="button" onClick={onToggle} aria-expanded={isExpanded} /> : <span />),
|
|
73
79
|
props: {
|
|
@@ -97,7 +103,7 @@ export function SectionHeader({ title, chevron = false, isExpanded = true, onTog
|
|
|
97
103
|
// The fill says "this does something": a row with a toggle or actions
|
|
98
104
|
// lights up, a fixed heading over rows does not (2026-09-09, the
|
|
99
105
|
// Sidebar's fixed group).
|
|
100
|
-
(chevron || (actions && actions.length > 0)) && 'hover:bg-bg-hover',
|
|
106
|
+
hover === 'fill' && (chevron || (actions && actions.length > 0)) && 'hover:bg-bg-hover',
|
|
101
107
|
className,
|
|
102
108
|
)}
|
|
103
109
|
>
|
package/src/SectionLabel.mdx
CHANGED
|
@@ -38,6 +38,10 @@ around it. Read it `tone="secondary"` where it labels rather than leads — menu
|
|
|
38
38
|
headings and the command palette's groups do (Katerina, 2026-09-01). A colour
|
|
39
39
|
passed in `className` is refused by the lint (UIG-9).
|
|
40
40
|
|
|
41
|
+
## What it owns
|
|
42
|
+
|
|
43
|
+
Nothing. It only draws. Clicks and keys are yours.
|
|
44
|
+
|
|
41
45
|
## Props
|
|
42
46
|
|
|
43
47
|
<Controls of={SectionLabelStories.Default} />
|
package/src/Select.mdx
CHANGED
|
@@ -29,8 +29,10 @@ corners and scrolling panels.
|
|
|
29
29
|
things.
|
|
30
30
|
- Several values → **ChipInput**.
|
|
31
31
|
- Switching views of one thing → **Tabs**.
|
|
32
|
-
- `disabled` explains nothing by itself —
|
|
33
|
-
|
|
32
|
+
- `disabled` explains nothing by itself — give it `disabledReason`, which says
|
|
33
|
+
why and keeps it reachable by Tab. Busy for a moment while an action runs
|
|
34
|
+
needs no reason.
|
|
35
|
+
- Ticking several on and off in a list → **Checkbox** with `row`.
|
|
34
36
|
|
|
35
37
|
## How
|
|
36
38
|
|
|
@@ -59,10 +61,27 @@ import { Select } from '@estiva-app/ui'
|
|
|
59
61
|
smaller — and scrolls inside that. Measured in a bottom-right corner:
|
|
60
62
|
the list flips above the trigger, slides left to stay on screen, and
|
|
61
63
|
stands at its full 288.
|
|
64
|
+
- **`disabledReason="Read only"`** holds it shut and keeps it reachable by Tab,
|
|
65
|
+
showing the reason on hover and on keyboard focus — as `Button` does. Plain
|
|
66
|
+
`disabled` is a native disabled control: Tab skips it, so it cannot say why.
|
|
62
67
|
- **The trigger is a `combobox`, not a `button`.** That is the ARIA pattern
|
|
63
68
|
for this control and Base UI's doing; a test looking for it by role has
|
|
64
69
|
to ask for `combobox`.
|
|
65
70
|
|
|
71
|
+
## What it owns
|
|
72
|
+
|
|
73
|
+
It does these by itself. Write none of them around it.
|
|
74
|
+
|
|
75
|
+
| It does | So you never write | Refused when written by hand |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| Floats on top of the page | a portal, or a `z-index` to win | ✓ |
|
|
78
|
+
| Stays attached to its anchor on scroll and resize, and hides when its trigger scrolls out of sight | a scroll or resize listener, or placement arithmetic | ✓ |
|
|
79
|
+
| Closes on a press outside | a click listener on the page | ✓ |
|
|
80
|
+
| Takes its keys by itself: Enter and Space pick, Escape and Tab close | a key listener on the page | ✓ |
|
|
81
|
+
| Moves through its items with the arrow keys, and jumps to an option by its first letters | an arrow-key handler | ✓ |
|
|
82
|
+
| Says what it is to assistive technology: a list of options | `role="listbox"` or `role="option"` | ✓ |
|
|
83
|
+
| Keeps the highlighted option in view, and gives focus back to the trigger | scroll or focus code | |
|
|
84
|
+
|
|
66
85
|
## Keys
|
|
67
86
|
|
|
68
87
|
| Input | What happens |
|
package/src/Select.stories.tsx
CHANGED
|
@@ -89,9 +89,12 @@ export const InAField: Story = {
|
|
|
89
89
|
),
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/** Unavailable — the same disabled treatment as every other control.
|
|
92
|
+
/** Unavailable — the same disabled treatment as every other control. To say why, `WithAReason`. */
|
|
93
93
|
export const Disabled: Story = { render: (args) => <Demo {...args} initial="done" disabled /> }
|
|
94
94
|
|
|
95
|
+
/** `disabledReason`: it looks disabled and will not open, but Tab reaches it and the reason shows on hover and on focus. */
|
|
96
|
+
export const WithAReason: Story = { render: (args) => <Demo {...args} initial="done" disabledReason="Read only: you are a guest here" /> }
|
|
97
|
+
|
|
95
98
|
/** Longer labels than the trigger is wide: the trigger truncates rather than growing; the menu takes the trigger's width as a minimum and grows past it. */
|
|
96
99
|
export const LongLabels: Story = {
|
|
97
100
|
render: (args) => (
|
package/src/Select.test.tsx
CHANGED
|
@@ -57,6 +57,23 @@ describe('Select', () => {
|
|
|
57
57
|
expect(trigger.getAttribute('aria-expanded')).toBe('false')
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
+
it('disabledReason: held shut, reachable by Tab, and it says why', async () => {
|
|
61
|
+
const user = userEvent.setup()
|
|
62
|
+
const onChange = vi.fn()
|
|
63
|
+
render(<Select value="todo" onChange={onChange} options={STATUSES} ariaLabel="Status" disabledReason="Read only: you are a guest" />)
|
|
64
|
+
const trigger = screen.getByRole('combobox', { name: 'Status' })
|
|
65
|
+
expect(trigger.getAttribute('aria-disabled')).toBe('true')
|
|
66
|
+
expect(trigger.hasAttribute('disabled')).toBe(false)
|
|
67
|
+
await user.click(trigger)
|
|
68
|
+
await user.keyboard('{ArrowDown}{Enter}')
|
|
69
|
+
expect(screen.queryByRole('listbox')).toBeNull()
|
|
70
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
71
|
+
await user.tab()
|
|
72
|
+
await user.tab({ shift: true })
|
|
73
|
+
expect(document.activeElement).toBe(trigger)
|
|
74
|
+
expect((await screen.findByRole('tooltip')).textContent).toBe('Read only: you are a guest')
|
|
75
|
+
})
|
|
76
|
+
|
|
60
77
|
it('shows the placeholder when nothing matches', () => {
|
|
61
78
|
render(<Controlled initial="" placeholder="Choose" />)
|
|
62
79
|
expect(screen.getByRole('combobox', { name: 'Status' }).textContent).toContain('Choose')
|
package/src/Select.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import type { ReactNode } from 'react'
|
|
|
4
4
|
import { cn } from './cn'
|
|
5
5
|
import { ScrollArea } from './ScrollArea'
|
|
6
6
|
import { MenuPanel, menuItemClassName } from './Menu'
|
|
7
|
+
import { TooltipTrigger } from './Tooltip'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Peek's Select (2026-08-28), verbatim, plus what Ship added: an option may
|
|
@@ -29,8 +30,9 @@ import { MenuPanel, menuItemClassName } from './Menu'
|
|
|
29
30
|
* highlight is managed rather than counted, and a value that can be part of a
|
|
30
31
|
* form.
|
|
31
32
|
*
|
|
32
|
-
* Two sizes
|
|
33
|
-
*
|
|
33
|
+
* Two sizes. `disabled` explains nothing by itself; `disabledReason` says
|
|
34
|
+
* why, as Button's does: the select looks disabled, will not open, stays
|
|
35
|
+
* reachable by Tab, and shows the reason on hover and on keyboard focus.
|
|
34
36
|
*/
|
|
35
37
|
export interface SelectOption {
|
|
36
38
|
value: string
|
|
@@ -49,6 +51,13 @@ export interface SelectProps {
|
|
|
49
51
|
'aria-required'?: boolean | 'true' | 'false'
|
|
50
52
|
placeholder?: string
|
|
51
53
|
disabled?: boolean
|
|
54
|
+
/**
|
|
55
|
+
* Why it cannot be changed right now (UIG-14, Katerina, 19 September). It
|
|
56
|
+
* looks disabled and will not open, but Tab still reaches it, and the reason
|
|
57
|
+
* shows as its tooltip — a native disabled button cannot be focused, so it
|
|
58
|
+
* could never say why. Takes the place of wrapping it in `WithTooltip`.
|
|
59
|
+
*/
|
|
60
|
+
disabledReason?: string
|
|
52
61
|
className?: string
|
|
53
62
|
}
|
|
54
63
|
|
|
@@ -57,20 +66,16 @@ export interface SelectProps {
|
|
|
57
66
|
const GAP = 4
|
|
58
67
|
const VIEWPORT_PAD = 8
|
|
59
68
|
|
|
60
|
-
export function Select({ value, onChange, options, size = 'default', ariaLabel, placeholder = 'Select…', disabled, className, ...aria }: SelectProps) {
|
|
69
|
+
export function Select({ value, onChange, options, size = 'default', ariaLabel, placeholder = 'Select…', disabled, disabledReason, className, ...aria }: SelectProps) {
|
|
61
70
|
const selected = options.find((o) => o.value === value)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
disabled={disabled}
|
|
67
|
-
// Non-modal, as it has always been: the page behind stays scrollable and
|
|
68
|
-
// keeps its scrollbar, so opening a select never shifts the layout.
|
|
69
|
-
modal={false}
|
|
70
|
-
>
|
|
71
|
+
// With a reason it is held shut rather than disabled: a disabled trigger is
|
|
72
|
+
// a native `disabled` button, which Tab skips and a tooltip cannot open on.
|
|
73
|
+
const held = Boolean(disabledReason) && !disabled
|
|
74
|
+
const trigger = (
|
|
71
75
|
<BaseSelect.Trigger
|
|
72
76
|
aria-label={ariaLabel}
|
|
73
77
|
aria-required={aria['aria-required']}
|
|
78
|
+
aria-disabled={held || undefined}
|
|
74
79
|
className={cn(
|
|
75
80
|
/*
|
|
76
81
|
* `min-w-0 max-w-full`: a trigger must never outgrow its container
|
|
@@ -85,7 +90,9 @@ export function Select({ value, onChange, options, size = 'default', ariaLabel,
|
|
|
85
90
|
// The focused border survives a hover: hover alone strengthens the
|
|
86
91
|
// hairline, but hover while focused must not grey the focus colour —
|
|
87
92
|
// the stacked variant outranks plain hover by specificity.
|
|
88
|
-
'hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus
|
|
93
|
+
!held && 'hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus',
|
|
94
|
+
'disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled',
|
|
95
|
+
held && 'cursor-not-allowed bg-bg-disabled text-text-disabled',
|
|
89
96
|
'focus-visible:border-border-focus aria-expanded:border-border-focus',
|
|
90
97
|
'signal:transition-shadow signal:focus-visible:shadow-focus-ring',
|
|
91
98
|
size === 'default' && 'px-3 py-2 text-input-value',
|
|
@@ -101,6 +108,20 @@ export function Select({ value, onChange, options, size = 'default', ariaLabel,
|
|
|
101
108
|
render={<IconChevronDown size={size === 'small' ? 14 : 16} stroke={1.5} className="shrink-0 text-text-secondary" />}
|
|
102
109
|
/>
|
|
103
110
|
</BaseSelect.Trigger>
|
|
111
|
+
)
|
|
112
|
+
return (
|
|
113
|
+
<BaseSelect.Root
|
|
114
|
+
value={value}
|
|
115
|
+
onValueChange={(next) => {
|
|
116
|
+
if (!held) onChange(next as string)
|
|
117
|
+
}}
|
|
118
|
+
{...(held ? { open: false, onOpenChange: () => {} } : {})}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
// Non-modal, as it has always been: the page behind stays scrollable and
|
|
121
|
+
// keeps its scrollbar, so opening a select never shifts the layout.
|
|
122
|
+
modal={false}
|
|
123
|
+
>
|
|
124
|
+
{held ? <TooltipTrigger label={disabledReason ?? ''}>{trigger}</TooltipTrigger> : trigger}
|
|
104
125
|
|
|
105
126
|
<BaseSelect.Portal>
|
|
106
127
|
<BaseSelect.Positioner
|
package/src/Sidebar.mdx
CHANGED
|
@@ -59,6 +59,14 @@ compressed instead — rows must keep their height.
|
|
|
59
59
|
It is a `nav` region — pass `aria-label` when "Workspace" is not the
|
|
60
60
|
right name for what it navigates.
|
|
61
61
|
|
|
62
|
+
## What it owns
|
|
63
|
+
|
|
64
|
+
It does these by itself. Write none of them around it.
|
|
65
|
+
|
|
66
|
+
| It does | So you never write | Refused when written by hand |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| Scrolls its rows in a ScrollArea when they overflow | `overflow-auto` on the column | |
|
|
69
|
+
|
|
62
70
|
## Props
|
|
63
71
|
|
|
64
72
|
<Controls of={SidebarStories.Composed} />
|
package/src/Skeleton.mdx
CHANGED
|
@@ -37,3 +37,7 @@ import { SkeletonList, SkeletonBar } from '@estiva-app/ui'
|
|
|
37
37
|
load never flashes a skeleton. Building your own placeholder from the
|
|
38
38
|
bar? Put that class on its container for the same manner.
|
|
39
39
|
- The list is `aria-hidden` — a placeholder has nothing to read.
|
|
40
|
+
|
|
41
|
+
## What it owns
|
|
42
|
+
|
|
43
|
+
Nothing. It only draws. Clicks and keys are yours.
|
package/src/Tabs.mdx
CHANGED
|
@@ -23,7 +23,7 @@ is a neutral fill, not the accent — selection is a place, not a highlight.
|
|
|
23
23
|
|
|
24
24
|
## When not
|
|
25
25
|
|
|
26
|
-
- Going to a different place →
|
|
26
|
+
- Going to a different place → **Link**, **NavItem** or **Breadcrumb**.
|
|
27
27
|
- Choosing a value that gets saved → **Select**.
|
|
28
28
|
- Actions → **Button**s; a tab never *does*, it *shows*.
|
|
29
29
|
|
|
@@ -54,6 +54,17 @@ import { Tabs } from '@estiva-app/ui'
|
|
|
54
54
|
that already names it takes `aria-labelledby` instead. Unnamed is fine for
|
|
55
55
|
the only tab row on a page.
|
|
56
56
|
|
|
57
|
+
## What it owns
|
|
58
|
+
|
|
59
|
+
It does these by itself. Write none of them around it.
|
|
60
|
+
|
|
61
|
+
| It does | So you never write | Refused when written by hand |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| Takes its keys by itself: Home and End select the first and last tab | a key listener on the page | ✓ |
|
|
64
|
+
| Moves through its items with the arrow keys: ← → select the next tab, and wrap at the ends | an arrow-key handler | ✓ |
|
|
65
|
+
| Says what it is to assistive technology: a set of tabs and their panels | `role="tablist"`, `tab` or `tabpanel` | ✓ |
|
|
66
|
+
| Is one Tab stop: Tab lands on the selected tab | a `tabIndex` on each tab | |
|
|
67
|
+
|
|
57
68
|
## Keys
|
|
58
69
|
|
|
59
70
|
| Key | Does |
|
package/src/TextInput.mdx
CHANGED
|
@@ -46,6 +46,14 @@ import { TextInput } from '@estiva-app/ui'
|
|
|
46
46
|
- `size` is this component's (`default` or `small`), not the native attribute
|
|
47
47
|
that counts characters. Set a width with the layout around it.
|
|
48
48
|
|
|
49
|
+
## What it owns
|
|
50
|
+
|
|
51
|
+
It does these by itself. Write none of them around it.
|
|
52
|
+
|
|
53
|
+
| It does | So you never write | Refused when written by hand |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| Inside a `Field`, takes its name from the Field's label, and its error state | an `id` / `htmlFor` pair, or `aria-invalid` | |
|
|
56
|
+
|
|
49
57
|
## Props
|
|
50
58
|
|
|
51
59
|
<Controls of={TextInputStories.Empty} />
|
package/src/Textarea.mdx
CHANGED
|
@@ -35,6 +35,14 @@ import { Textarea } from '@estiva-app/ui'
|
|
|
35
35
|
- **Inside a `Field`, the label names it. Add nothing.** Outside one, give
|
|
36
36
|
it an `aria-label`.
|
|
37
37
|
|
|
38
|
+
## What it owns
|
|
39
|
+
|
|
40
|
+
It does these by itself. Write none of them around it.
|
|
41
|
+
|
|
42
|
+
| It does | So you never write | Refused when written by hand |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| Inside a `Field`, takes its name from the Field's label, and its error state | an `id` / `htmlFor` pair, or `aria-invalid` | |
|
|
45
|
+
|
|
38
46
|
## Props
|
|
39
47
|
|
|
40
48
|
<Controls of={TextareaStories.Empty} />
|
package/src/Toast.mdx
CHANGED
|
@@ -33,7 +33,7 @@ the positioning (bottom-left), the portal, and the auto-dismiss.
|
|
|
33
33
|
- Something the person must read or act on → put it in the surface; a
|
|
34
34
|
toast dismisses itself.
|
|
35
35
|
- A question before the action → **ConfirmDialog**.
|
|
36
|
-
- Standing status →
|
|
36
|
+
- Standing status → **Chip**, or your surface's own chrome.
|
|
37
37
|
|
|
38
38
|
## How
|
|
39
39
|
|
|
@@ -70,6 +70,17 @@ showToast({ label: 'Link copied', type: 'success' })
|
|
|
70
70
|
- `useToast` outside a provider throws — mount the provider at the root,
|
|
71
71
|
once.
|
|
72
72
|
|
|
73
|
+
## What it owns
|
|
74
|
+
|
|
75
|
+
It does these by itself. Write none of them around it.
|
|
76
|
+
|
|
77
|
+
| It does | So you never write | Refused when written by hand |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| Floats on top of the page | a portal, or a `z-index` to win | ✓ |
|
|
80
|
+
| Says what it is to assistive technology: a message, announced as it arrives | `role="status"` or `aria-live` | ✓ |
|
|
81
|
+
| F6 moves focus to the toasts from anywhere; Escape closes one | a key listener | |
|
|
82
|
+
| Leaves by itself after a while | a timer | |
|
|
83
|
+
|
|
73
84
|
## Keys
|
|
74
85
|
|
|
75
86
|
| Key | What happens |
|
package/src/Toolbar.mdx
CHANGED
|
@@ -82,6 +82,17 @@ why the placement is its job and not arithmetic of ours.
|
|
|
82
82
|
|
|
83
83
|
<Canvas of={ToolbarStories.OnAnExistingSurface} />
|
|
84
84
|
|
|
85
|
+
## What it owns
|
|
86
|
+
|
|
87
|
+
It does these by itself. Write none of them around it.
|
|
88
|
+
|
|
89
|
+
| It does | So you never write | Refused when written by hand |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| Takes its keys by itself: Enter and Space press the focused control | a key listener on the page | ✓ |
|
|
92
|
+
| Moves through its items with the arrow keys, and wraps at the ends | an arrow-key handler | ✓ |
|
|
93
|
+
| Says what it is to assistive technology: a toolbar | `role="toolbar"` | ✓ |
|
|
94
|
+
| Is one Tab stop, and remembers where the walk left off | `tabIndex` on each control | |
|
|
95
|
+
|
|
85
96
|
## Keys
|
|
86
97
|
|
|
87
98
|
| Input | What happens |
|
package/src/Tooltip.mdx
CHANGED
|
@@ -21,6 +21,7 @@ shows it beside a trigger — on hover, and on keyboard focus.
|
|
|
21
21
|
- **The key that does the same thing** — pass `shortcut` and it is drawn as
|
|
22
22
|
the `Kbd` chip after the label. For an icon-only control whose only other
|
|
23
23
|
affordance is a keyboard shortcut, the tooltip is the only place to say so.
|
|
24
|
+
- A detail behind a short word in running text — the time behind "edited".
|
|
24
25
|
|
|
25
26
|
<Canvas of={TooltipStories.ShortcutComparison} />
|
|
26
27
|
|
|
@@ -30,8 +31,10 @@ shows it beside a trigger — on hover, and on keyboard focus.
|
|
|
30
31
|
|
|
31
32
|
- Anything the person must read to proceed → put it in the surface, not
|
|
32
33
|
behind a hover.
|
|
33
|
-
- Anything
|
|
34
|
-
|
|
34
|
+
- Anything with controls inside → **Popover**, opened by a click. A list of
|
|
35
|
+
actions → **Menu**. A tooltip disappears as the pointer moves onto it, so
|
|
36
|
+
nothing inside it can be clicked.
|
|
37
|
+
- Something worth reading, scrolling or copying on hover → **PreviewCard**.
|
|
35
38
|
|
|
36
39
|
## How
|
|
37
40
|
|
|
@@ -47,6 +50,11 @@ import { WithTooltip } from '@estiva-app/ui'
|
|
|
47
50
|
shrinks nothing, so a wrapped block collapses to its content width.
|
|
48
51
|
- A truncating label keeps truncating if you pass
|
|
49
52
|
`wrapperClassName="min-w-0 shrink"`.
|
|
53
|
+
- **`inline`** for a trigger inside a line of text — a reference, a name in a
|
|
54
|
+
sentence: the wrapper is a `<span>`, because a `<div>` is not allowed inside
|
|
55
|
+
a paragraph.
|
|
56
|
+
- A label longer than 320px wraps onto more lines rather than running off the
|
|
57
|
+
screen; a short one stays one line at 30px.
|
|
50
58
|
- **`Button` and `IconButton` need no wrapper**: pass `tooltip` (or
|
|
51
59
|
`disabledReason`) and the button itself becomes the trigger. That is what
|
|
52
60
|
lets a `Dialog.Close` or a `Menu.Trigger` be one of them — the component's
|
|
@@ -56,6 +64,10 @@ import { WithTooltip } from '@estiva-app/ui'
|
|
|
56
64
|
looking at — so pass the finished label. `IconButton` forwards its own
|
|
57
65
|
`tooltipShortcut` here.
|
|
58
66
|
|
|
67
|
+
<Canvas of={TooltipStories.InsideText} />
|
|
68
|
+
|
|
69
|
+
<Canvas of={TooltipStories.LongLabel} />
|
|
70
|
+
|
|
59
71
|
### The delay, and why a toolbar only pauses once
|
|
60
72
|
|
|
61
73
|
A tooltip waits **300ms** before it opens. Mount **one `TooltipProvider` at
|
|
@@ -82,6 +94,18 @@ beneath one — and fades out over **80ms**. Moving between triggers inside a
|
|
|
82
94
|
warm group skips the animation entirely, so a toolbar sweep does not flicker.
|
|
83
95
|
`prefers-reduced-motion` removes it.
|
|
84
96
|
|
|
97
|
+
## What it owns
|
|
98
|
+
|
|
99
|
+
It does these by itself. Write none of them around it.
|
|
100
|
+
|
|
101
|
+
| It does | So you never write | Refused when written by hand |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| Floats on top of the page | a portal, or a `z-index` to win | ✓ |
|
|
104
|
+
| Stays attached to its anchor on scroll and resize | a scroll or resize listener, or placement arithmetic | ✓ |
|
|
105
|
+
| Says what it is to assistive technology: a tooltip | `role="tooltip"` | ✓ |
|
|
106
|
+
| Opens after the pointer rests, and at once on keyboard focus | a timer, or hover and focus handlers | |
|
|
107
|
+
| Closes on Escape, on a click of its control, and when the pointer leaves | close handlers | |
|
|
108
|
+
|
|
85
109
|
## Keys
|
|
86
110
|
|
|
87
111
|
| Input | What happens |
|
package/src/Tooltip.stories.tsx
CHANGED
|
@@ -31,6 +31,32 @@ export const ShortcutComparison: Story = {
|
|
|
31
31
|
),
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** A label longer than 320px wraps onto more lines, rather than running off the screen. A short one stays one line at 30px. */
|
|
35
|
+
export const LongLabel: Story = {
|
|
36
|
+
parameters: { controls: { disable: true } },
|
|
37
|
+
render: () => (
|
|
38
|
+
<div className="flex flex-col items-start gap-2">
|
|
39
|
+
<Tooltip label="Comment" />
|
|
40
|
+
<Tooltip label="naddr1qvzqqqr4gupzq9h35qgq6n8ll0xyyv8gurjzjrx9sjwp4hry6ejnlks8cqcmzp6tqyfhwumn8ghj7mmxve3ksctfdch8qatz9uq3wamnwvaz7tmjv4kxz7fwwpexjmtpdshxuet59uq3qamnwvaz7tm99ehx2aqqz9mhxue69uhkummnw3ez6un9d3shjtnwda4k7tnr" />
|
|
41
|
+
<Tooltip label="Item one, with a title long enough that it no longer fits on one line of a tooltip" />
|
|
42
|
+
</div>
|
|
43
|
+
),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** `inline`: a tooltip on a word inside a sentence. The wrapper is a span, so the paragraph stays valid HTML. Hover the word. */
|
|
47
|
+
export const InsideText: Story = {
|
|
48
|
+
parameters: { controls: { disable: true } },
|
|
49
|
+
render: () => (
|
|
50
|
+
<p className="max-w-sm text-body-2 text-text-primary">
|
|
51
|
+
The plan is in{' '}
|
|
52
|
+
<WithTooltip inline label="Item one, with a title long enough that it no longer fits on one line of a tooltip">
|
|
53
|
+
<span className="font-semibold">Item one</span>
|
|
54
|
+
</WithTooltip>
|
|
55
|
+
, and the rest follows.
|
|
56
|
+
</p>
|
|
57
|
+
),
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
/** Hover the button — WithTooltip portals the tooltip above the trigger. */
|
|
35
61
|
export const OnHoverTop: Story = {
|
|
36
62
|
parameters: { controls: { disable: true } },
|
package/src/Tooltip.test.tsx
CHANGED
|
@@ -27,6 +27,14 @@ describe('Tooltip, the surface', () => {
|
|
|
27
27
|
expect(tip.textContent).toBe('BoldCmd+B')
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
+
it('wraps a long label at 320px instead of running it off the screen, and stays 30px on one line', () => {
|
|
31
|
+
render(<Tooltip label="A label long enough to wrap onto a second line of the tooltip" />)
|
|
32
|
+
const pill = screen.getByRole('tooltip')
|
|
33
|
+
expect(pill.className).toContain('max-w-80')
|
|
34
|
+
expect(pill.className).toContain('min-h-[30px]')
|
|
35
|
+
expect(pill.querySelector('span')?.className).not.toContain('whitespace-nowrap')
|
|
36
|
+
})
|
|
37
|
+
|
|
30
38
|
it('takes a caller class without losing its own', () => {
|
|
31
39
|
render(<Tooltip label="Bold" className="w-40" />)
|
|
32
40
|
const tip = screen.getByRole('tooltip')
|
|
@@ -49,6 +57,27 @@ describe('WithTooltip', () => {
|
|
|
49
57
|
expect(wrapper.firstElementChild?.tagName).toBe('BUTTON')
|
|
50
58
|
})
|
|
51
59
|
|
|
60
|
+
it('inline: a span wrapper, so it can sit inside a paragraph without React warning', () => {
|
|
61
|
+
const errors: unknown[] = []
|
|
62
|
+
const original = console.error
|
|
63
|
+
console.error = (...args: unknown[]) => { errors.push(args) }
|
|
64
|
+
try {
|
|
65
|
+
render(
|
|
66
|
+
<p>
|
|
67
|
+
The plan is in{' '}
|
|
68
|
+
<WithTooltip inline label="Item one">
|
|
69
|
+
<span>Item one</span>
|
|
70
|
+
</WithTooltip>
|
|
71
|
+
.
|
|
72
|
+
</p>,
|
|
73
|
+
)
|
|
74
|
+
} finally {
|
|
75
|
+
console.error = original
|
|
76
|
+
}
|
|
77
|
+
expect(screen.getByText('Item one').parentElement?.tagName).toBe('SPAN')
|
|
78
|
+
expect(errors).toEqual([])
|
|
79
|
+
})
|
|
80
|
+
|
|
52
81
|
it('takes wrapperClassName, which is what keeps a truncating label truncating', () => {
|
|
53
82
|
const { container } = render(
|
|
54
83
|
<WithTooltip label="A long name" wrapperClassName="min-w-0 shrink">
|
package/src/Tooltip.tsx
CHANGED
|
@@ -64,10 +64,16 @@ export interface TooltipProps extends Omit<ComponentPropsWithRef<'div'>, 'childr
|
|
|
64
64
|
shortcut?: string
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* One line at 30px, as it has always been; a label longer than 320px wraps
|
|
69
|
+
* onto more lines rather than running off the screen (Katerina, 19 September:
|
|
70
|
+
* a reference's full address drew a pill 950px wide). `py-1` only shows once
|
|
71
|
+
* it wraps: a single line is centred in the 30px.
|
|
72
|
+
*/
|
|
67
73
|
export function Tooltip({ label, shortcut, className, ...props }: TooltipProps) {
|
|
68
74
|
return (
|
|
69
|
-
<div role="tooltip" className={cn('bg-bg-elevated border border-border-default rounded-lg h-[30px] flex items-center justify-center gap-1.5 px-2 shadow-lg', className)} {...props}>
|
|
70
|
-
<span className="text-caption text-text-primary
|
|
75
|
+
<div role="tooltip" className={cn('bg-bg-elevated border border-border-default rounded-lg min-h-[30px] max-w-80 flex items-center justify-center gap-1.5 px-2 py-1 shadow-lg', className)} {...props}>
|
|
76
|
+
<span className="min-w-0 text-caption text-text-primary break-words">{label}</span>
|
|
71
77
|
{shortcut && <Kbd>{shortcut}</Kbd>}
|
|
72
78
|
</div>
|
|
73
79
|
)
|
|
@@ -80,6 +86,12 @@ export interface WithTooltipProps {
|
|
|
80
86
|
placement?: 'top' | 'bottom'
|
|
81
87
|
/** Extra classes on the wrapper — e.g. `min-w-0 shrink` so a truncating label keeps truncating inside it. */
|
|
82
88
|
wrapperClassName?: string
|
|
89
|
+
/**
|
|
90
|
+
* For a trigger inside a line of text — a reference, a name in a sentence:
|
|
91
|
+
* the wrapper is a `<span>`, because a `<div>` is not allowed inside a
|
|
92
|
+
* paragraph (UIG-14, Katerina, 19 September).
|
|
93
|
+
*/
|
|
94
|
+
inline?: boolean
|
|
83
95
|
children: ReactNode
|
|
84
96
|
}
|
|
85
97
|
|
|
@@ -150,7 +162,8 @@ function TooltipSurface({ label, shortcut, placement }: { label: string; shortcu
|
|
|
150
162
|
* an icon. It wraps what it is given in an element that carries the handlers.
|
|
151
163
|
* A control that can be the trigger does it itself: `IconButton` takes a
|
|
152
164
|
* `tooltip` prop, and `Button` shows a `disabledReason` the same way. */
|
|
153
|
-
export function WithTooltip({ label, shortcut, placement = 'top', wrapperClassName, children }: WithTooltipProps) {
|
|
165
|
+
export function WithTooltip({ label, shortcut, placement = 'top', wrapperClassName, inline = false, children }: WithTooltipProps) {
|
|
166
|
+
const Wrapper = inline ? 'span' : 'div'
|
|
154
167
|
return (
|
|
155
168
|
<BaseTooltip.Root disableHoverablePopup>
|
|
156
169
|
{/* The wrapper stays the trigger: `WithTooltip` wraps whatever it is
|
|
@@ -158,7 +171,7 @@ export function WithTooltip({ label, shortcut, placement = 'top', wrapperClassNa
|
|
|
158
171
|
can carry the handlers for all of them. A control that IS the trigger
|
|
159
172
|
(Button, IconButton) composes the part onto itself instead, which is
|
|
160
173
|
what lets a `Dialog.Close` or a `Menu.Trigger` be one of those. */}
|
|
161
|
-
<BaseTooltip.Trigger delay={OPEN_DELAY} render={<
|
|
174
|
+
<BaseTooltip.Trigger delay={OPEN_DELAY} render={<Wrapper className={cn('inline-flex shrink-0', wrapperClassName)} />}>
|
|
162
175
|
{children}
|
|
163
176
|
</BaseTooltip.Trigger>
|
|
164
177
|
<TooltipSurface label={label} shortcut={shortcut} placement={placement} />
|
package/src/TopBar.mdx
CHANGED
|
@@ -57,6 +57,10 @@ import { TopBar, IdentityMenu, SearchInput } from '@estiva-app/ui'
|
|
|
57
57
|
- The frame is desktop-only for now — there is no narrow-screen
|
|
58
58
|
behaviour; the menu button collapses panels, it does not open a drawer.
|
|
59
59
|
|
|
60
|
+
## What it owns
|
|
61
|
+
|
|
62
|
+
Nothing. It only draws. Clicks and keys are yours.
|
|
63
|
+
|
|
60
64
|
## Props
|
|
61
65
|
|
|
62
66
|
<Controls of={TopBarStories.Solid} />
|
|
@@ -202,7 +202,7 @@ export const OWNED_BEHAVIOURS: OwnedBehaviour[] = [
|
|
|
202
202
|
},
|
|
203
203
|
{
|
|
204
204
|
id: 'page-keys',
|
|
205
|
-
behaviour: '
|
|
205
|
+
behaviour: 'Takes its keys by itself',
|
|
206
206
|
baseUi: ['Dialog', 'AlertDialog', 'Popover', 'Menu', 'Select', 'Combobox', 'Tooltip', 'PreviewCard', 'Toast'],
|
|
207
207
|
owners: ['DialogShell', 'Popover', 'Menu', 'Select', 'Tabs', 'Toolbar'],
|
|
208
208
|
reads: 'a keydown, keyup or keypress listener on window or document',
|
package/src/gates/app-checks.ts
CHANGED
|
@@ -43,6 +43,7 @@ export const APP_TICKET_TITLES: Record<string, string> = {
|
|
|
43
43
|
'UIG-7': 'Lint rule — every remaining raw element',
|
|
44
44
|
'UIG-8': 'Lint rule — forbid the reach',
|
|
45
45
|
'UIG-9': 'Lint rule — the className allow-list',
|
|
46
|
+
'UIG-10': 'create-app — a command that makes a new Estiva app that runs',
|
|
46
47
|
'UIG-13': "The registry widens to Peek's 115 and Ship's 74, with classification",
|
|
47
48
|
'UIG-19': 'Lock the contract in CI, and make the three Storybooks one search',
|
|
48
49
|
'UIG-20': 'The Claude skill, reading the registry',
|
|
@@ -164,8 +165,29 @@ export function appChecks(h: GateHelpers, { app = '.', page, chain = { ref: 'UIG
|
|
|
164
165
|
} },
|
|
165
166
|
{ what: 'a real page gets no error', run: realPage },
|
|
166
167
|
]),
|
|
168
|
+
// UIG-10, reopened 18 September: a tab holds one relay client, the way Peek and Ship hold
|
|
169
|
+
// theirs and a made app is written to. Measured on both mains that day: one each, and no
|
|
170
|
+
// socket opened any other way. A second holder, or a socket of the app's own, is a second
|
|
171
|
+
// connection the relay signs in and budgets separately.
|
|
172
|
+
ticket('UIG-10', [
|
|
173
|
+
{ what: 'the app holds one relay client, at module level, and opens no socket of its own', run: () => {
|
|
174
|
+
const sources = h.listFiles(at('src'), (n) => /\.tsx?$/.test(n) && !/\.(test|stories)\.tsx?$/.test(n))
|
|
175
|
+
const own = sources.filter((f) => /\bcreateLiveClient\(|\bcreateLiveRelay\(|\bnew WebSocket\(/.test(h.read(f)))
|
|
176
|
+
if (own.length) return h.FAIL(`opens a socket of its own (${own.join(', ')}): hold one client from createLiveClientHolder()`)
|
|
177
|
+
const holders = sources.filter((f) => /^\s*(?:export\s+)?const\s+\w+\s*=\s*createLiveClientHolder\(\)/m.test(h.read(f)))
|
|
178
|
+
if (holders.length !== 1) return h.FAIL(holders.length ? `${holders.length} client holders (${holders.join(', ')}): a tab holds one` : 'no module-level createLiveClientHolder(): the app has no relay client')
|
|
179
|
+
return h.PASS(`one client for the tab, held in ${holders[0]}`)
|
|
180
|
+
} },
|
|
181
|
+
]),
|
|
182
|
+
// Built by UIG-13, 18 September. An app's catalogue is built fresh from its code
|
|
183
|
+
// and never committed: the package is public and the apps are private (Katerina).
|
|
167
184
|
ticket('UIG-13', [
|
|
168
|
-
{ what:
|
|
185
|
+
{ what: "npm run ui:find is the package's search, over the package and this app", run: () => h.contains(at('package.json'), /"ui:find":\s*"estiva-ui find\b/, "ui:find runs the package's estiva-ui find") },
|
|
186
|
+
{ what: "npm run registry:check is the package's check", run: () => h.contains(at('package.json'), /"registry:check":\s*"estiva-ui check\b/, 'registry:check runs estiva-ui check') },
|
|
187
|
+
{ what: "CI's job gate refuses a part with no description", run: () => h.ciJob('gate', 'registry:check') },
|
|
188
|
+
// In the .gitignore of the app's own folder, where `estiva-ui build` would write it.
|
|
189
|
+
{ what: 'the catalogue is never committed: git ignores registry.json', run: () => h.contains(at('.gitignore'), /^\/?registry\.json\r?$/m, `${at('.gitignore')} ignores registry.json`) },
|
|
190
|
+
{ what: 'every part is described and sorted, and every file is accounted for', run: () => h.catalogue(at('.')) },
|
|
169
191
|
]),
|
|
170
192
|
ticket('UIG-19', [
|
|
171
193
|
{ what: 'the usage-page contract runs in CI', run: () => h.ci(/[\w:-]*contract[\w:-]*/) },
|