@estiva-app/ui 0.23.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/dist/AppShell.d.ts.map +1 -1
- package/dist/AttachmentCard.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/index.js +127 -108
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
- package/registry.json +51 -8
- 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 +13 -3
- 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 +4 -0
- package/src/DialogShell.mdx +14 -0
- package/src/Divider.mdx +9 -1
- package/src/EditableText.mdx +11 -0
- package/src/EmptyState.mdx +15 -4
- 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/pages.test.ts +142 -0
- package/tailwind-preset.js +22 -1
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',
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract every component page keeps (UIG-14, with UIG-15 and UIG-16 folded in).
|
|
3
|
+
*
|
|
4
|
+
* A page opens with a line saying what the part is, then has `When`, `When not`,
|
|
5
|
+
* `How` (with code) and `What it owns`, in that order, before `Keys` and `Props`.
|
|
6
|
+
*
|
|
7
|
+
* `What it owns` reads the checker's own list back. A row ticked ✓ in its third
|
|
8
|
+
* column is a behaviour from `OWNED_BEHAVIOURS` that one of the page's parts owns,
|
|
9
|
+
* worded as `ROW` below; every behaviour the page's parts own has one; and a row
|
|
10
|
+
* worded as a behaviour the parts do not own fails. So the page and the checker
|
|
11
|
+
* cannot say different things. A part that owns nothing says so in one line.
|
|
12
|
+
*
|
|
13
|
+
* A `When not` line names only parts that exist: a bold name is a value export of
|
|
14
|
+
* `index.ts`, or a page.
|
|
15
|
+
*/
|
|
16
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
17
|
+
import { describe, expect, it } from 'vitest'
|
|
18
|
+
import { OWNED_BEHAVIOURS } from './eslint/no-rebuilt-behaviour'
|
|
19
|
+
|
|
20
|
+
/** How a page words each behaviour. `base-ui` is how a part is built, not what it does for a caller. */
|
|
21
|
+
const ROW: Record<string, RegExp> = {
|
|
22
|
+
portal: /^Floats on top of the page\b/,
|
|
23
|
+
'press-outside': /^Closes on a press outside\b/,
|
|
24
|
+
'page-keys': /^Takes its keys by itself\b/,
|
|
25
|
+
focus: /^Holds focus inside while open\b/,
|
|
26
|
+
'scroll-lock': /^Stops the page behind it scrolling\b/,
|
|
27
|
+
follow: /^Stays attached to its anchor\b/,
|
|
28
|
+
walking: /^Moves through its items with the arrow keys\b/,
|
|
29
|
+
role: /^Says what it is to assistive technology\b/,
|
|
30
|
+
'tab-stop': /^Is reachable with Tab\b/,
|
|
31
|
+
scroll: /^Scrolls with our scrollbar\b/,
|
|
32
|
+
}
|
|
33
|
+
const NOT_A_ROW = ['base-ui']
|
|
34
|
+
|
|
35
|
+
/** A page whose part another ticket is changing, while it is; empty when none is. */
|
|
36
|
+
const WAITING: string[] = []
|
|
37
|
+
|
|
38
|
+
const NOTHING = 'Nothing. It only draws. Clicks and keys are yours.'
|
|
39
|
+
|
|
40
|
+
const dir = new URL('.', import.meta.url)
|
|
41
|
+
const read = (file: string) => readFileSync(new URL(file, dir), 'utf8').replace(/\r\n/g, '\n')
|
|
42
|
+
const pages = readdirSync(dir)
|
|
43
|
+
.filter((f) => f.endsWith('.mdx'))
|
|
44
|
+
.map((f) => f.slice(0, -4))
|
|
45
|
+
|
|
46
|
+
const index = read('index.ts')
|
|
47
|
+
/** Value exports of index.ts, by the module they come from. */
|
|
48
|
+
const byModule = new Map<string, string[]>()
|
|
49
|
+
for (const m of index.matchAll(/export\s*\{([^}]*)\}\s*from\s*'\.\/([\w/]+)'/g)) {
|
|
50
|
+
const names = m[1]
|
|
51
|
+
.split(',')
|
|
52
|
+
.map((s) => s.trim())
|
|
53
|
+
.filter((s) => s && !s.startsWith('type '))
|
|
54
|
+
.map((s) => s.split(/\s+as\s+/).pop()!)
|
|
55
|
+
byModule.set(m[2], [...(byModule.get(m[2]) ?? []), ...names])
|
|
56
|
+
}
|
|
57
|
+
const exported = new Set([...byModule.values()].flat())
|
|
58
|
+
|
|
59
|
+
/** The parts a page documents: its own name, and its module's exports that have no page of their own. */
|
|
60
|
+
const partsOf = (page: string) => [page, ...(byModule.get(page) ?? []).filter((n) => n !== page && !pages.includes(n))]
|
|
61
|
+
|
|
62
|
+
const section = (t: string, name: string) => {
|
|
63
|
+
const head = `\n## ${name}\n`
|
|
64
|
+
const at = t.indexOf(head)
|
|
65
|
+
if (at < 0) return null
|
|
66
|
+
const rest = t.slice(at + head.length)
|
|
67
|
+
const end = rest.search(/^## /m)
|
|
68
|
+
return end < 0 ? rest : rest.slice(0, end)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const owns = (t: string) =>
|
|
72
|
+
(section(t, 'What it owns') ?? '')
|
|
73
|
+
.split('\n')
|
|
74
|
+
.filter((l) => l.startsWith('| ') && !l.startsWith('| It does |'))
|
|
75
|
+
.map((l) => l.split('|').map((c) => c.trim()))
|
|
76
|
+
.map(([, does, never, ticked]) => ({ does, never, ticked: ticked === '✓' }))
|
|
77
|
+
|
|
78
|
+
describe('the page contract', () => {
|
|
79
|
+
it('words every behaviour the checker knows', () => {
|
|
80
|
+
const ids = OWNED_BEHAVIOURS.map((b) => b.id).filter((id) => !NOT_A_ROW.includes(id))
|
|
81
|
+
expect(Object.keys(ROW).sort()).toEqual(ids.sort())
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('finds the pages', () => {
|
|
85
|
+
expect(pages.length).toBeGreaterThanOrEqual(54)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
for (const page of pages) {
|
|
89
|
+
describe(page, () => {
|
|
90
|
+
const t = read(`${page}.mdx`)
|
|
91
|
+
const headings = [...t.matchAll(/^## (.+)$/gm)].map((m) => m[1])
|
|
92
|
+
|
|
93
|
+
it('opens with a line saying what it is', () => {
|
|
94
|
+
const opening = t.split(/^# .*$/m)[1]?.split(/\n\s*\n/).map((s) => s.trim()).find((s) => s && !s.startsWith('<'))
|
|
95
|
+
expect(opening, 'no opening line under the title').toBeTruthy()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('has each section once', () => {
|
|
99
|
+
expect(headings.filter((h, i) => headings.indexOf(h) !== i)).toEqual([])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('has its sections in order', () => {
|
|
103
|
+
const want = WAITING.includes(page) ? ['When', 'When not', 'How'] : ['When', 'When not', 'How', 'What it owns']
|
|
104
|
+
const at = want.map((h) => headings.indexOf(h))
|
|
105
|
+
expect(at.filter((i) => i < 0).length, `missing: ${want.filter((_, i) => at[i] < 0).join(', ')}`).toBe(0)
|
|
106
|
+
expect([...at].sort((a, b) => a - b)).toEqual(at)
|
|
107
|
+
for (const after of ['Keys', 'Props']) {
|
|
108
|
+
const i = headings.indexOf(after)
|
|
109
|
+
if (i >= 0) expect(at[at.length - 1], `${want[want.length - 1]} comes before ${after}`).toBeLessThan(i)
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('shows code under How', () => {
|
|
114
|
+
expect(section(t, 'How')).toMatch(/```tsx?\n/)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('names only parts that exist under When not', () => {
|
|
118
|
+
const names = [...(section(t, 'When not') ?? '').matchAll(/\*\*`?([A-Z][A-Za-z0-9]*)`?\*\*/g)].map((m) => m[1])
|
|
119
|
+
expect(names.filter((n) => !exported.has(n) && !pages.includes(n))).toEqual([])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
if (WAITING.includes(page)) return
|
|
123
|
+
|
|
124
|
+
it('says what it owns, as the checker does', () => {
|
|
125
|
+
const parts = partsOf(page)
|
|
126
|
+
const owned = OWNED_BEHAVIOURS.filter((b) => !NOT_A_ROW.includes(b.id) && b.owners.some((o) => parts.includes(o))).map((b) => b.id)
|
|
127
|
+
const rows = owns(t)
|
|
128
|
+
const body = (section(t, 'What it owns') ?? '').trim()
|
|
129
|
+
if (!rows.length) {
|
|
130
|
+
expect(owned, `${page} owns ${owned.join(', ')}; the page says nothing`).toEqual([])
|
|
131
|
+
expect(body).toBe(NOTHING)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
const said = rows.flatMap((r) => Object.entries(ROW).filter(([, re]) => re.test(r.does)).map(([id]) => ({ id, ticked: r.ticked, does: r.does })))
|
|
135
|
+
expect(said.filter((s) => !s.ticked).map((s) => s.does), 'a checker behaviour, not ticked').toEqual([])
|
|
136
|
+
expect(rows.filter((r) => r.ticked && !Object.values(ROW).some((re) => re.test(r.does))).map((r) => r.does), 'ticked, but not worded as a checker behaviour').toEqual([])
|
|
137
|
+
expect(said.map((s) => s.id).sort()).toEqual([...owned].sort())
|
|
138
|
+
expect(rows.every((r) => r.does && r.never), 'every row says what it does and what you never write').toBe(true)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
})
|
package/tailwind-preset.js
CHANGED
|
@@ -67,9 +67,30 @@ export default {
|
|
|
67
67
|
// see tokens.css). Both exist so a treatment can be given to the two apps
|
|
68
68
|
// without changing the plain light/dark themes the docs render in.
|
|
69
69
|
plugins: [
|
|
70
|
-
plugin(({ addVariant }) => {
|
|
70
|
+
plugin(({ addVariant, addComponents }) => {
|
|
71
71
|
addVariant('signal', '.signal &')
|
|
72
72
|
addVariant('ship', "[data-theme='ship'] &")
|
|
73
|
+
// The Signal canvas: the control-room dot grid on the app's dark ground,
|
|
74
|
+
// in the Signal theme only. AppShell's floating frame carries
|
|
75
|
+
// `signal-canvas`. Peek's, verbatim (UIG-14, Katerina, 19 September: the
|
|
76
|
+
// canvas is the theme's, so it moved with the frame). A component here,
|
|
77
|
+
// not base.css, so it reaches every app on the preset and the class lint
|
|
78
|
+
// knows the name. `isolation` and `z-index: -1` keep the grid above
|
|
79
|
+
// the ground and below every child: never over the card, a dialog, a menu.
|
|
80
|
+
addComponents({
|
|
81
|
+
'.signal .signal-canvas': { isolation: 'isolate' },
|
|
82
|
+
'.signal .signal-canvas::before': {
|
|
83
|
+
content: "''",
|
|
84
|
+
position: 'absolute',
|
|
85
|
+
inset: '0',
|
|
86
|
+
zIndex: '-1',
|
|
87
|
+
pointerEvents: 'none',
|
|
88
|
+
backgroundImage: 'radial-gradient(circle, rgba(255, 255, 255, 0.045) 1px, transparent 1px)',
|
|
89
|
+
backgroundSize: '26px 26px',
|
|
90
|
+
maskImage: 'radial-gradient(1100px 700px at 50% 45%, transparent 25%, #000 100%)',
|
|
91
|
+
WebkitMaskImage: 'radial-gradient(1100px 700px at 50% 45%, transparent 25%, #000 100%)',
|
|
92
|
+
},
|
|
93
|
+
})
|
|
73
94
|
}),
|
|
74
95
|
],
|
|
75
96
|
theme: {
|