@estiva-app/ui 0.37.2 → 0.39.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 +5 -2
- package/dist/AppShell.d.ts.map +1 -1
- package/dist/AttachmentCard.d.ts.map +1 -1
- package/dist/Kbd.d.ts +7 -1
- package/dist/Kbd.d.ts.map +1 -1
- package/dist/Lightbox.d.ts.map +1 -1
- package/dist/SectionLabel.d.ts.map +1 -1
- package/dist/Sidebar.d.ts +7 -1
- package/dist/Sidebar.d.ts.map +1 -1
- package/dist/Toast.d.ts.map +1 -1
- package/dist/TopBar.d.ts +5 -2
- package/dist/TopBar.d.ts.map +1 -1
- package/dist/cn.d.ts.map +1 -1
- package/dist/eslint/index.js +1 -0
- package/dist/eslint/index.js.map +2 -2
- package/dist/eslint/no-restyled-part.d.ts.map +1 -1
- package/dist/index.js +486 -477
- package/dist/index.js.map +4 -4
- package/dist/looks.d.ts +2 -2
- package/dist/looks.d.ts.map +1 -1
- package/package.json +2 -1
- package/registry.json +62 -43
- package/src/AppShell.mdx +11 -3
- package/src/AppShell.stories.tsx +23 -12
- package/src/AppShell.test.tsx +20 -0
- package/src/AppShell.tsx +43 -24
- package/src/AttachmentCard.tsx +0 -1
- package/src/CommandPalette.tsx +1 -1
- package/src/ErrorBoundary.mdx +2 -2
- package/src/Kbd.tsx +10 -1
- package/src/Lightbox.tsx +4 -2
- package/src/Menu.tsx +1 -1
- package/src/MenuItem.stories.tsx +4 -9
- package/src/Popover.stories.tsx +3 -0
- package/src/SectionLabel.tsx +2 -0
- package/src/Select.tsx +2 -2
- package/src/Sidebar.tsx +10 -2
- package/src/TextInput.test.tsx +1 -1
- package/src/Toast.tsx +7 -6
- package/src/Tooltip.stories.tsx +9 -9
- package/src/Tooltip.test.tsx +1 -1
- package/src/Tooltip.tsx +3 -3
- package/src/TopBar.mdx +6 -4
- package/src/TopBar.tsx +6 -3
- package/src/cn.ts +1 -1
- package/src/eslint/no-restyled-part.ts +1 -0
- package/src/looks.ts +2 -2
- package/stories/TokensPage.tsx +17 -3
- package/tailwind-preset.js +10 -0
- package/tokens.css +144 -0
package/src/MenuItem.stories.tsx
CHANGED
|
@@ -48,15 +48,10 @@ export const WithAShortcut: Story = {
|
|
|
48
48
|
args: { label: 'Copy link', leading: <IconCopy size={16} stroke={1.5} className="text-text-secondary" />, shortcut: 'Ctrl+C' },
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** The row opens another menu. */
|
|
52
|
-
export const Submenu: Story = {
|
|
53
|
-
args: { label: 'Move to…', submenu: true },
|
|
54
|
-
}
|
|
55
|
-
|
|
56
51
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
52
|
+
* A row that opens another menu, working: such a row can only be tried inside
|
|
53
|
+
* a real menu, so this is one (Katerina, 28 September: one story, the working
|
|
54
|
+
* one; the chevron is the row's whole affordance).
|
|
60
55
|
*
|
|
61
56
|
* Hover **Move to…**, or arrow onto it and press →. The panel opens beside the
|
|
62
57
|
* row, flips to the other side at a screen edge, and stays open while you
|
|
@@ -66,7 +61,7 @@ export const Submenu: Story = {
|
|
|
66
61
|
* into it, and ← closes it and puts the highlight back on the row. Measured in
|
|
67
62
|
* that order — → then ← alone does nothing, because focus has not entered yet.
|
|
68
63
|
*/
|
|
69
|
-
export const
|
|
64
|
+
export const Submenu: Story = {
|
|
70
65
|
parameters: { controls: { disable: true } },
|
|
71
66
|
decorators: [(Story) => <Story />],
|
|
72
67
|
render: () => (
|
package/src/Popover.stories.tsx
CHANGED
|
@@ -190,6 +190,9 @@ export const FromASelection: Story = {
|
|
|
190
190
|
side="top"
|
|
191
191
|
ariaLabel="Formatting"
|
|
192
192
|
className="w-auto min-w-0 p-1"
|
|
193
|
+
/* No padding of the popup's own: the panel's 4px is the Toolbar's, and
|
|
194
|
+
the default 8px on top of it drew the panel 16px taller (Katerina, 28 September). */
|
|
195
|
+
contentClassName="p-0"
|
|
193
196
|
>
|
|
194
197
|
<Toolbar aria-label="Formatting" surface={false}>
|
|
195
198
|
<ToolbarButton aria-label="Bold" tooltip="Bold">
|
package/src/SectionLabel.tsx
CHANGED
|
@@ -39,6 +39,8 @@ export function SectionLabel({
|
|
|
39
39
|
<span
|
|
40
40
|
className={cn(
|
|
41
41
|
'text-h5 leading-3 signal:font-mono signal:text-small signal:uppercase signal:tracking-widest',
|
|
42
|
+
// Leaf's section labels are Signal's type (Katerina, 28 September).
|
|
43
|
+
'leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest',
|
|
42
44
|
tone === 'secondary' ? 'text-text-secondary' : tone === 'muted' ? 'text-text-muted' : 'text-text-primary',
|
|
43
45
|
truncate && 'min-w-0 truncate',
|
|
44
46
|
className,
|
package/src/Select.tsx
CHANGED
|
@@ -88,8 +88,8 @@ export function Select({ value, onChange, options, size = 'default', ariaLabel,
|
|
|
88
88
|
* `truncate` from ever engaging; in a block container max-w-full is
|
|
89
89
|
* the cap. Full-width callers are unaffected.
|
|
90
90
|
*/
|
|
91
|
-
'flex w-full min-w-0 max-w-full items-center justify-between gap-2 rounded-lg border bg-bg-
|
|
92
|
-
'border-border-
|
|
91
|
+
'flex w-full min-w-0 max-w-full items-center justify-between gap-2 rounded-lg border bg-bg-field shadow-field text-left',
|
|
92
|
+
'border-border-field outline-none transition-colors',
|
|
93
93
|
FIELD_TEXT_CLASSES,
|
|
94
94
|
// The focused border survives a hover: hover alone strengthens the
|
|
95
95
|
// hairline, but hover while focused must not grey the focus colour —
|
package/src/Sidebar.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { createContext, useContext, type ReactNode } from 'react'
|
|
2
2
|
import { cn } from './cn'
|
|
3
3
|
import { ScrollArea } from './ScrollArea'
|
|
4
4
|
|
|
@@ -10,6 +10,10 @@ import { ScrollArea } from './ScrollArea'
|
|
|
10
10
|
* What goes inside is the caller's: NavItem rows, a SectionLabel heading
|
|
11
11
|
* in a 32px row over a group. Desktop only — there is no narrow-screen
|
|
12
12
|
* drawer.
|
|
13
|
+
*
|
|
14
|
+
* In AppShell's `inset` frame it stands on the ground instead: no hairline,
|
|
15
|
+
* no surface, the content's card beside it (Katerina, 28 September). The
|
|
16
|
+
* frame tells it so; the caller passes nothing.
|
|
13
17
|
*/
|
|
14
18
|
export interface SidebarProps {
|
|
15
19
|
/** Names the navigation region for assistive tech. */
|
|
@@ -18,11 +22,15 @@ export interface SidebarProps {
|
|
|
18
22
|
className?: string
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
/** Set by AppShell's `inset` frame: the sidebar stands on the ground, with no line or surface of its own. */
|
|
26
|
+
export const SidebarOnGround = createContext(false)
|
|
27
|
+
|
|
21
28
|
export function Sidebar({ 'aria-label': ariaLabel = 'Workspace', children, className }: SidebarProps) {
|
|
29
|
+
const onGround = useContext(SidebarOnGround)
|
|
22
30
|
return (
|
|
23
31
|
<nav
|
|
24
32
|
aria-label={ariaLabel}
|
|
25
|
-
className={cn('flex w-60 shrink-0 flex-col border-r border-border-default bg-bg-surface', className)}
|
|
33
|
+
className={cn('flex w-60 shrink-0 flex-col', !onGround && 'border-r border-border-default bg-bg-surface', className)}
|
|
26
34
|
>
|
|
27
35
|
{/* The rows scroll in a ScrollArea: the bar takes no width, so the
|
|
28
36
|
column's padding reads the same with a long list as with a short
|
package/src/TextInput.test.tsx
CHANGED
|
@@ -40,6 +40,6 @@ describe('TextInput', () => {
|
|
|
40
40
|
render(<TextInput aria-label="Title" />)
|
|
41
41
|
const classes = screen.getByRole('textbox').className.split(' ')
|
|
42
42
|
// Both survive cn(); Tailwind emits focus after hover, so the order in CSS decides, not here.
|
|
43
|
-
for (const c of ['border-border-
|
|
43
|
+
for (const c of ['border-border-field', 'hover:border-border-strong', 'focus:border-border-focus']) expect(classes).toContain(c)
|
|
44
44
|
})
|
|
45
45
|
})
|
package/src/Toast.tsx
CHANGED
|
@@ -44,13 +44,14 @@ export interface ToastProps {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// Signal: every toast is the same dark overlay pill (v3) — the type lives in
|
|
47
|
-
// the icon color + glow, not the surface.
|
|
47
|
+
// the icon color + glow, not the surface. Leaf draws them the same way, on its
|
|
48
|
+
// own greys (Katerina, 28 September).
|
|
48
49
|
const SURFACE_STYLES: Record<ToastType, string> = {
|
|
49
|
-
success: 'bg-success-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-md',
|
|
50
|
-
brand: 'bg-accent-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-md',
|
|
51
|
-
neutral: 'bg-bg-inset border border-border-subtle signal:border-border-default signal:shadow-md',
|
|
52
|
-
warning: 'bg-warning-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-md',
|
|
53
|
-
error: 'bg-error-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-md',
|
|
50
|
+
success: 'bg-success-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-md leaf:bg-bg-inset leaf:border leaf:border-border-default leaf:shadow-md',
|
|
51
|
+
brand: 'bg-accent-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-md leaf:bg-bg-inset leaf:border leaf:border-border-default leaf:shadow-md',
|
|
52
|
+
neutral: 'bg-bg-inset border border-border-subtle signal:border-border-default signal:shadow-md leaf:bg-bg-inset leaf:border leaf:border-border-default leaf:shadow-md',
|
|
53
|
+
warning: 'bg-warning-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-md leaf:bg-bg-inset leaf:border leaf:border-border-default leaf:shadow-md',
|
|
54
|
+
error: 'bg-error-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-md leaf:bg-bg-inset leaf:border leaf:border-border-default leaf:shadow-md',
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/**
|
package/src/Tooltip.stories.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { IconArchive, IconPin, IconTrash } from '@tabler/icons-react'
|
|
|
3
3
|
import { Button } from './Button'
|
|
4
4
|
import { IconButton } from './IconButton'
|
|
5
5
|
import { Tooltip, WithTooltip } from './Tooltip'
|
|
6
|
-
import {
|
|
6
|
+
import { Toolbar, ToolbarButton } from './Toolbar'
|
|
7
7
|
|
|
8
8
|
const meta = {
|
|
9
9
|
title: 'Primitives/Tooltip',
|
|
@@ -100,16 +100,16 @@ export const OnADisabledControl: Story = {
|
|
|
100
100
|
export const InAToolbar: Story = {
|
|
101
101
|
parameters: { controls: { disable: true } },
|
|
102
102
|
render: () => (
|
|
103
|
-
<
|
|
104
|
-
<
|
|
103
|
+
<Toolbar aria-label="Actions">
|
|
104
|
+
<ToolbarButton aria-label="Pin" tooltip="Pin">
|
|
105
105
|
<IconPin size={16} stroke={1.5} />
|
|
106
|
-
</
|
|
107
|
-
<
|
|
106
|
+
</ToolbarButton>
|
|
107
|
+
<ToolbarButton aria-label="Archive" tooltip="Archive">
|
|
108
108
|
<IconArchive size={16} stroke={1.5} />
|
|
109
|
-
</
|
|
110
|
-
<
|
|
109
|
+
</ToolbarButton>
|
|
110
|
+
<ToolbarButton aria-label="Delete" tooltip="Delete" tooltipShortcut="Del">
|
|
111
111
|
<IconTrash size={16} stroke={1.5} />
|
|
112
|
-
</
|
|
113
|
-
</
|
|
112
|
+
</ToolbarButton>
|
|
113
|
+
</Toolbar>
|
|
114
114
|
),
|
|
115
115
|
}
|
package/src/Tooltip.test.tsx
CHANGED
|
@@ -39,7 +39,7 @@ describe('Tooltip, the surface', () => {
|
|
|
39
39
|
render(<Tooltip label="Bold" className="w-40" />)
|
|
40
40
|
const tip = screen.getByRole('tooltip')
|
|
41
41
|
expect(tip.className).toContain('w-40')
|
|
42
|
-
expect(tip.className).toContain('bg-bg-
|
|
42
|
+
expect(tip.className).toContain('bg-bg-tooltip')
|
|
43
43
|
})
|
|
44
44
|
})
|
|
45
45
|
|
package/src/Tooltip.tsx
CHANGED
|
@@ -73,9 +73,9 @@ export interface TooltipProps extends Omit<ComponentPropsWithRef<'div'>, 'childr
|
|
|
73
73
|
*/
|
|
74
74
|
export function Tooltip({ label, shortcut, className, ...props }: TooltipProps) {
|
|
75
75
|
return (
|
|
76
|
-
<div role="tooltip" className={cn(FLOATING_SURFACE_CLASSES, 'min-h-[30px] max-w-80 flex items-center justify-center gap-1.5 px-2 py-1', className)} {...props}>
|
|
77
|
-
<span className="min-w-0 text-caption text-text-
|
|
78
|
-
{shortcut && <Kbd>{shortcut}</Kbd>}
|
|
76
|
+
<div role="tooltip" className={cn(FLOATING_SURFACE_CLASSES, 'border-border-tooltip bg-bg-tooltip', 'min-h-[30px] max-w-80 flex items-center justify-center gap-1.5 px-2 py-1', className)} {...props}>
|
|
77
|
+
<span className="min-w-0 text-caption text-text-tooltip break-words">{label}</span>
|
|
78
|
+
{shortcut && <Kbd tone="tooltip">{shortcut}</Kbd>}
|
|
79
79
|
</div>
|
|
80
80
|
)
|
|
81
81
|
}
|
package/src/TopBar.mdx
CHANGED
|
@@ -6,10 +6,10 @@ import * as TopBarStories from './TopBar.stories'
|
|
|
6
6
|
# TopBar
|
|
7
7
|
|
|
8
8
|
The 52px top bar: `menu` and `logo` on the left, a `search` slot in the
|
|
9
|
-
centre, the identity cluster on the right.
|
|
10
|
-
flow with a hairline and a surface
|
|
11
|
-
|
|
12
|
-
reachable.
|
|
9
|
+
centre, the identity cluster on the right. Three manners — **`solid`**,
|
|
10
|
+
in flow with a hairline and a surface; **`inset`**, in flow with neither,
|
|
11
|
+
standing on the ground; and **`floating`**, an overlay with neither, whose
|
|
12
|
+
bar ignores the pointer so the content underneath stays reachable.
|
|
13
13
|
|
|
14
14
|
<Canvas of={TopBarStories.Solid} />
|
|
15
15
|
|
|
@@ -19,6 +19,8 @@ reachable.
|
|
|
19
19
|
|
|
20
20
|
- **`solid`** — the structured frame's first row, above the sidebar
|
|
21
21
|
(AppShell's `solid` variant wires exactly this).
|
|
22
|
+
- **`inset`** — the first row of AppShell's `inset` frame, on the ground
|
|
23
|
+
above a sidebar that is on the ground too.
|
|
22
24
|
- **`floating`** — an app whose content runs to the top edge; the bar
|
|
23
25
|
floats over it and only its clusters catch clicks (AppShell's
|
|
24
26
|
`floating` variant).
|
package/src/TopBar.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react'
|
|
|
2
2
|
import { cn } from './cn'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* The 52px top bar, in
|
|
5
|
+
* The 52px top bar, in three manners:
|
|
6
6
|
*
|
|
7
7
|
* - `solid` — in flow: a hairline under it, the surface behind it, the
|
|
8
8
|
* frame's first row (2026-09-02, verbatim from the structured app; its
|
|
@@ -11,6 +11,9 @@ import { cn } from './cn'
|
|
|
11
11
|
* background, and the bar itself ignores the pointer — only its clusters
|
|
12
12
|
* catch clicks, so the content underneath stays reachable (2026-09-02,
|
|
13
13
|
* verbatim from the floating app).
|
|
14
|
+
* - `inset` — in flow like `solid`, but standing on the ground: no hairline
|
|
15
|
+
* and no surface, above a sidebar on the ground and the content's card
|
|
16
|
+
* (Katerina, 28 September).
|
|
14
17
|
*
|
|
15
18
|
* Geometry both apps agreed on before extraction: 52px tall, `pl-5
|
|
16
19
|
* pr-[26px]`, right cluster `gap-[6px]`. The left region is two slots
|
|
@@ -20,7 +23,7 @@ import { cn } from './cn'
|
|
|
20
23
|
* searched; it only knows where those go.
|
|
21
24
|
*/
|
|
22
25
|
export interface TopBarProps {
|
|
23
|
-
variant?: 'solid' | 'floating'
|
|
26
|
+
variant?: 'solid' | 'floating' | 'inset'
|
|
24
27
|
/** Far left: the menu button, in an app whose navigation collapses. Stays the caller's — the bar only places it. */
|
|
25
28
|
menu?: ReactNode
|
|
26
29
|
/** Beside the menu button: the app's logo mark, or its name as text. */
|
|
@@ -40,7 +43,7 @@ export function TopBar({ variant = 'solid', menu, logo, search, right, className
|
|
|
40
43
|
'flex h-[52px] items-center pl-5 pr-[26px]',
|
|
41
44
|
floating
|
|
42
45
|
? 'pointer-events-none absolute left-0 right-0 top-0 z-10'
|
|
43
|
-
: 'shrink-0 gap-4 border-b border-border-default bg-bg-surface',
|
|
46
|
+
: cn('shrink-0 gap-4', variant === 'solid' && 'border-b border-border-default bg-bg-surface'),
|
|
44
47
|
className,
|
|
45
48
|
)}
|
|
46
49
|
>
|
package/src/cn.ts
CHANGED
|
@@ -29,7 +29,7 @@ const FONT_SIZE_TOKENS = [
|
|
|
29
29
|
* `shadow-glow-warning` beside `shadow-md` kept both (measured 2026-09-06).
|
|
30
30
|
* `cn.test.ts` pins these to the preset too.
|
|
31
31
|
*/
|
|
32
|
-
const BOX_SHADOW_TOKENS = ['sm', 'md', 'lg', 'focus-ring', 'glow-warning', 'glow-success', 'glow-accent', 'highlight-inset']
|
|
32
|
+
const BOX_SHADOW_TOKENS = ['sm', 'md', 'lg', 'focus-ring', 'field', 'glow-warning', 'glow-success', 'glow-accent', 'highlight-inset']
|
|
33
33
|
const DROP_SHADOW_TOKENS = ['glow-success', 'glow-accent', 'glow-warning']
|
|
34
34
|
|
|
35
35
|
const twMerge = extendTailwindMerge({
|
|
@@ -118,6 +118,7 @@ export const PART_LOOK_PROPS: Record<string, string[]> = {
|
|
|
118
118
|
FieldLine: ['tone'],
|
|
119
119
|
IconButton: ['variant', 'pressed', 'glow'],
|
|
120
120
|
InputChip: ['truncate'],
|
|
121
|
+
Kbd: ['tone'],
|
|
121
122
|
Link: ['variant', 'truncate'],
|
|
122
123
|
MenuItem: ['size', 'selected', 'destructive'],
|
|
123
124
|
MenuSub: ['selected'],
|
package/src/looks.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/** A field's box: TextInput and Textarea. The hairline strengthens on hover; focus comes after it, so a focused field keeps its colour under the pointer. */
|
|
15
|
-
export const FIELD_BOX_CLASSES = 'bg-bg-
|
|
15
|
+
export const FIELD_BOX_CLASSES = 'bg-bg-field border border-border-field shadow-field hover:border-border-strong focus:border-border-focus rounded-lg'
|
|
16
16
|
|
|
17
17
|
/** What a field's words look like, typed or waiting: TextInput, Textarea and Select. */
|
|
18
18
|
export const FIELD_TEXT_CLASSES = 'text-text-primary placeholder:text-text-muted'
|
|
@@ -30,7 +30,7 @@ export const FIELD_SIZE_CLASSES = {
|
|
|
30
30
|
} as const
|
|
31
31
|
|
|
32
32
|
/** The box around an input that sits inside it, before its focus look: SearchInput and ChipInput. */
|
|
33
|
-
export const FIELD_SHELL_CLASSES = 'bg-bg-
|
|
33
|
+
export const FIELD_SHELL_CLASSES = 'bg-bg-field border border-border-field shadow-field hover:border-border-strong rounded-lg transition-colors'
|
|
34
34
|
|
|
35
35
|
/** A panel that floats over the page: Menu's panel and Tooltip. */
|
|
36
36
|
export const FLOATING_SURFACE_CLASSES = 'rounded-lg border border-border-default bg-bg-elevated shadow-lg'
|
package/stories/TokensPage.tsx
CHANGED
|
@@ -55,14 +55,14 @@ function resolveVar(name: string): string {
|
|
|
55
55
|
|
|
56
56
|
/* ── The vocabulary (mirrors tailwind-preset.js; tokens.test.ts holds the two together) ── */
|
|
57
57
|
|
|
58
|
-
type Swatch = 'fill' | 'text' | 'text-inverse' | 'border' | 'outline' | 'shadow' | 'drop-shadow'
|
|
58
|
+
type Swatch = 'fill' | 'text' | 'text-inverse' | 'text-tooltip' | 'border' | 'outline' | 'shadow' | 'drop-shadow'
|
|
59
59
|
type Token = { key: string; cssVar: string; cls: string; swatch: Swatch; note?: string }
|
|
60
60
|
type Family = { label: string; blurb: string; utilities: string; tokens: Token[] }
|
|
61
61
|
|
|
62
62
|
const colour = (prefix: string, key: string, swatch: Swatch, note?: string): Token => ({
|
|
63
63
|
key: `${prefix}-${key}`,
|
|
64
64
|
cssVar: `--${prefix}-${key}`,
|
|
65
|
-
cls: `${swatch === 'text' || swatch === 'text-inverse' ? 'text' : swatch === 'border' || swatch === 'outline' ? 'border' : 'bg'}-${prefix}-${key}`,
|
|
65
|
+
cls: `${swatch === 'text' || swatch === 'text-inverse' || swatch === 'text-tooltip' ? 'text' : swatch === 'border' || swatch === 'outline' ? 'border' : 'bg'}-${prefix}-${key}`,
|
|
66
66
|
swatch,
|
|
67
67
|
note,
|
|
68
68
|
})
|
|
@@ -86,6 +86,18 @@ const FAMILIES: Family[] = [
|
|
|
86
86
|
utilities: COLOUR_UTILITIES,
|
|
87
87
|
tokens: ['subtle', 'default', 'strong', 'focus'].map((k) => colour('border', k, 'border')),
|
|
88
88
|
},
|
|
89
|
+
{
|
|
90
|
+
label: 'Tooltip',
|
|
91
|
+
blurb: "A tooltip's own colours, so a theme can draw it apart from its menus and popups: its ground, its text and its edge, then the same three for a shortcut key shown on it.",
|
|
92
|
+
utilities: COLOUR_UTILITIES,
|
|
93
|
+
tokens: [colour('bg', 'tooltip', 'fill'), colour('text', 'tooltip', 'text-tooltip'), colour('border', 'tooltip', 'border'), colour('bg', 'tooltip-key', 'fill'), colour('text', 'tooltip-key', 'text-tooltip'), colour('border', 'tooltip-key', 'border')],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
label: 'Field',
|
|
97
|
+
blurb: "A field's own box, so a theme can draw its text boxes apart from the ground it uses for chips and keys: its fill, its line and its shadow.",
|
|
98
|
+
utilities: COLOUR_UTILITIES,
|
|
99
|
+
tokens: [colour('bg', 'field', 'fill'), colour('border', 'field', 'border')],
|
|
100
|
+
},
|
|
89
101
|
{
|
|
90
102
|
label: 'Accent',
|
|
91
103
|
blurb: 'The brand colour: the primary button, the checked box, the brand chip. Muted is its opaque wash, wash the translucent one a row tints with on hover, outline its thin border.',
|
|
@@ -121,6 +133,7 @@ const FAMILIES: Family[] = [
|
|
|
121
133
|
{ key: 'md', cssVar: '--shadow-md', cls: 'shadow-md', swatch: 'shadow' },
|
|
122
134
|
{ key: 'lg', cssVar: '--shadow-lg', cls: 'shadow-lg', swatch: 'shadow' },
|
|
123
135
|
{ key: 'focus-ring', cssVar: '--focus-ring', cls: 'shadow-focus-ring', swatch: 'shadow' },
|
|
136
|
+
{ key: 'field', cssVar: '--shadow-field', cls: 'shadow-field', swatch: 'shadow' },
|
|
124
137
|
{ key: 'glow-warning', cssVar: '--glow-warning', cls: 'shadow-glow-warning', swatch: 'shadow' },
|
|
125
138
|
{ key: 'highlight-inset', cssVar: '--highlight-inset', cls: 'shadow-highlight-inset', swatch: 'shadow' },
|
|
126
139
|
{ key: 'glow-success', cssVar: '--glow-success', cls: 'drop-shadow-glow-success', swatch: 'drop-shadow' },
|
|
@@ -180,8 +193,9 @@ function SwatchBox({ token }: { token: Token }) {
|
|
|
180
193
|
return <div className={`${base} border border-border-subtle`} style={{ background: v }} />
|
|
181
194
|
case 'text':
|
|
182
195
|
case 'text-inverse':
|
|
196
|
+
case 'text-tooltip':
|
|
183
197
|
return (
|
|
184
|
-
<div className={`${base} flex items-center justify-center border border-border-subtle ${token.swatch === 'text-inverse' ? 'bg-accent-primary' : 'bg-bg-surface'}`}>
|
|
198
|
+
<div className={`${base} flex items-center justify-center border border-border-subtle ${token.swatch === 'text-inverse' ? 'bg-accent-primary' : token.swatch === 'text-tooltip' ? 'bg-bg-tooltip' : 'bg-bg-surface'}`}>
|
|
185
199
|
<span className="text-body-2-strong leading-none" style={{ color: v }}>Ag</span>
|
|
186
200
|
</div>
|
|
187
201
|
)
|
package/tailwind-preset.js
CHANGED
|
@@ -70,6 +70,7 @@ export default {
|
|
|
70
70
|
plugin(({ addVariant, addComponents }) => {
|
|
71
71
|
addVariant('signal', '.signal &')
|
|
72
72
|
addVariant('ship', "[data-theme='ship'] &")
|
|
73
|
+
addVariant('leaf', "[data-theme='leaf'] &")
|
|
73
74
|
// The Signal canvas: the control-room dot grid on the app's dark ground,
|
|
74
75
|
// in the Signal theme only. AppShell's floating frame carries
|
|
75
76
|
// `signal-canvas`. Peek's, verbatim (UIG-14, Katerina, 19 September: the
|
|
@@ -149,6 +150,9 @@ export default {
|
|
|
149
150
|
'bg-selected': 'var(--bg-selected)',
|
|
150
151
|
'bg-active': 'var(--bg-active)',
|
|
151
152
|
'bg-disabled': 'var(--bg-disabled)',
|
|
153
|
+
'bg-tooltip': 'var(--bg-tooltip)',
|
|
154
|
+
'bg-tooltip-key': 'var(--bg-tooltip-key)',
|
|
155
|
+
'bg-field': 'var(--bg-field)',
|
|
152
156
|
// text
|
|
153
157
|
'text-primary': 'var(--text-primary)',
|
|
154
158
|
'text-secondary': 'var(--text-secondary)',
|
|
@@ -156,11 +160,16 @@ export default {
|
|
|
156
160
|
'text-disabled': 'var(--text-disabled)',
|
|
157
161
|
'text-inverse': 'var(--text-inverse)',
|
|
158
162
|
'text-interactive': 'var(--text-interactive)',
|
|
163
|
+
'text-tooltip': 'var(--text-tooltip)',
|
|
164
|
+
'text-tooltip-key': 'var(--text-tooltip-key)',
|
|
159
165
|
// border
|
|
160
166
|
'border-subtle': 'var(--border-subtle)',
|
|
161
167
|
'border-default': 'var(--border-default)',
|
|
162
168
|
'border-strong': 'var(--border-strong)',
|
|
163
169
|
'border-focus': 'var(--border-focus)',
|
|
170
|
+
'border-tooltip': 'var(--border-tooltip)',
|
|
171
|
+
'border-tooltip-key': 'var(--border-tooltip-key)',
|
|
172
|
+
'border-field': 'var(--border-field)',
|
|
164
173
|
// accent
|
|
165
174
|
'accent-primary': 'var(--accent-primary)',
|
|
166
175
|
'accent-hover': 'var(--accent-hover)',
|
|
@@ -199,6 +208,7 @@ export default {
|
|
|
199
208
|
// The ring a focused control wears where a theme wants one: Signal's
|
|
200
209
|
// glow. Every theme defines it; the shared inputs use it under `signal:`.
|
|
201
210
|
'focus-ring': 'var(--focus-ring)',
|
|
211
|
+
'field': 'var(--shadow-field)',
|
|
202
212
|
// Signal's glow and inner highlight (D16); the components use them under `signal:`.
|
|
203
213
|
// All three glows are here as box shadows, and the two below are also
|
|
204
214
|
// drop shadows: a glow on a surface is a box shadow, a glow on an icon
|
package/tokens.css
CHANGED
|
@@ -80,6 +80,20 @@
|
|
|
80
80
|
--highlight-inset: inset 0 1px 0 rgba(255, 255, 255, 0.035);
|
|
81
81
|
--scrim: rgba(0, 0, 0, 0.5);
|
|
82
82
|
--scrim-strong: rgba(0, 0, 0, 0.8);
|
|
83
|
+
/* Tooltips have their own colours (Katerina, 27 September); here, the ones they
|
|
84
|
+
always had: the elevated surface, primary text, the default border. */
|
|
85
|
+
--bg-tooltip: #FAFAFA;
|
|
86
|
+
--text-tooltip: #18181B;
|
|
87
|
+
--border-tooltip: #E4E4E7;
|
|
88
|
+
/* A shortcut key on a tooltip: what Kbd always drew here. */
|
|
89
|
+
--bg-tooltip-key: #F4F4F5;
|
|
90
|
+
--border-tooltip-key: #D4D4D8;
|
|
91
|
+
--text-tooltip-key: #52525B;
|
|
92
|
+
/* A field has its own colours (Katerina, 28 September); here, the ones it always had:
|
|
93
|
+
the inset ground, the default line, no shadow. */
|
|
94
|
+
--bg-field: #F4F4F5;
|
|
95
|
+
--border-field: #E4E4E7;
|
|
96
|
+
--shadow-field: 0 0 #0000;
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
/* ─── dark ─── Peek's dark theme (peek/src/index.css .dark, 2026-08-28). */
|
|
@@ -144,6 +158,20 @@
|
|
|
144
158
|
--highlight-inset: inset 0 1px 0 rgba(255, 255, 255, 0.035);
|
|
145
159
|
--scrim: rgba(0, 0, 0, 0.5);
|
|
146
160
|
--scrim-strong: rgba(0, 0, 0, 0.8);
|
|
161
|
+
/* Tooltips have their own colours (Katerina, 27 September); here, the ones they
|
|
162
|
+
always had: the elevated surface, primary text, the default border. */
|
|
163
|
+
--bg-tooltip: #1C1C1F;
|
|
164
|
+
--text-tooltip: #FAFAFA;
|
|
165
|
+
--border-tooltip: #2D2D31;
|
|
166
|
+
/* A shortcut key on a tooltip: what Kbd always drew here. */
|
|
167
|
+
--bg-tooltip-key: #27272A;
|
|
168
|
+
--border-tooltip-key: #52525B;
|
|
169
|
+
--text-tooltip-key: #AEAEB6;
|
|
170
|
+
/* A field has its own colours (Katerina, 28 September); here, the ones it always had:
|
|
171
|
+
the inset ground, the default line, no shadow. */
|
|
172
|
+
--bg-field: #27272A;
|
|
173
|
+
--border-field: #2D2D31;
|
|
174
|
+
--shadow-field: 0 0 #0000;
|
|
147
175
|
}
|
|
148
176
|
|
|
149
177
|
|
|
@@ -212,6 +240,20 @@
|
|
|
212
240
|
--highlight-inset: inset 0 1px 0 rgba(255, 255, 255, 0.035);
|
|
213
241
|
--scrim: rgba(0, 0, 0, 0.5);
|
|
214
242
|
--scrim-strong: rgba(0, 0, 0, 0.8);
|
|
243
|
+
/* Tooltips have their own colours (Katerina, 27 September); here, the ones they
|
|
244
|
+
always had: the elevated surface, primary text, the default border. */
|
|
245
|
+
--bg-tooltip: #191d24;
|
|
246
|
+
--text-tooltip: #eef1f6;
|
|
247
|
+
--border-tooltip: rgba(255, 255, 255, 0.12);
|
|
248
|
+
/* A shortcut key on a tooltip: what Kbd always drew here. */
|
|
249
|
+
--bg-tooltip-key: rgba(255, 255, 255, 0.05);
|
|
250
|
+
--border-tooltip-key: rgba(255, 255, 255, 0.22);
|
|
251
|
+
--text-tooltip-key: #b2b9c6;
|
|
252
|
+
/* A field has its own colours (Katerina, 28 September); here, the ones it always had:
|
|
253
|
+
the inset ground, the default line, no shadow. */
|
|
254
|
+
--bg-field: #20242d;
|
|
255
|
+
--border-field: rgba(255, 255, 255, 0.12);
|
|
256
|
+
--shadow-field: 0 0 #0000;
|
|
215
257
|
}
|
|
216
258
|
|
|
217
259
|
/* ─── ship ─── Ship's palette (Katerina's ruling, 2026-08-28; ship/web/src/index.css). */
|
|
@@ -286,4 +328,106 @@
|
|
|
286
328
|
--highlight-inset: inset 0 1px 0 rgba(255, 255, 255, 0.035);
|
|
287
329
|
--scrim: rgba(0, 0, 0, 0.5);
|
|
288
330
|
--scrim-strong: rgba(0, 0, 0, 0.8);
|
|
331
|
+
/* Tooltips have their own colours (Katerina, 27 September); here, the ones they
|
|
332
|
+
always had: the elevated surface, primary text, the default border. */
|
|
333
|
+
--bg-tooltip: #1c1f28;
|
|
334
|
+
--text-tooltip: #e6e8ee;
|
|
335
|
+
--border-tooltip: rgba(255, 255, 255, 0.12);
|
|
336
|
+
/* A shortcut key on a tooltip: what Kbd always drew here. */
|
|
337
|
+
--bg-tooltip-key: rgba(255, 255, 255, 0.05);
|
|
338
|
+
--border-tooltip-key: rgba(255, 255, 255, 0.22);
|
|
339
|
+
--text-tooltip-key: #b3b9c7;
|
|
340
|
+
/* A field has its own colours (Katerina, 28 September); here, the ones it always had:
|
|
341
|
+
the inset ground, the default line, no shadow. */
|
|
342
|
+
--bg-field: #1f2330;
|
|
343
|
+
--border-field: rgba(255, 255, 255, 0.12);
|
|
344
|
+
--shadow-field: 0 0 #0000;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* ─── leaf ─── Leaf's theme: Linear's light theme on these same tokens (Katerina,
|
|
348
|
+
27–28 September: "I like Linear's theme more", and "get it right, not by
|
|
349
|
+
screenshot"). Read from the Linear app itself: its named colours
|
|
350
|
+
(--color-bg-*, --color-border-*, --color-text-*, --focus-ring-color), dumped
|
|
351
|
+
from a signed-in tab on 28 September and turned from LCH into sRGB by Chrome.
|
|
352
|
+
Where Linear names no colour for one of these tokens, the light theme's stays
|
|
353
|
+
(marked "light"); menus, the toolbar and tooltips follow her rulings. */
|
|
354
|
+
:root[data-theme='leaf'] {
|
|
355
|
+
color-scheme: light;
|
|
356
|
+
|
|
357
|
+
/* bg */
|
|
358
|
+
--bg-base: #efeff0; /* --color-bg-quaternary, the app's ground and sidebar */
|
|
359
|
+
--bg-surface: #f9f9fa; /* --color-bg-primary, the content panel and header */
|
|
360
|
+
--bg-elevated: #ffffff; /* menus and the toolbar are white (Katerina, 27 September) */
|
|
361
|
+
--bg-inset: #e9e9ea; /* --color-bg-secondary: not the ground, or an inset card and a skeleton vanish on it (Katerina, 28 September) */
|
|
362
|
+
/* See-through, so a hover shows on a row that is itself hovered or grey (Katerina, 28 September):
|
|
363
|
+
6.4% black is Linear's --color-bg-secondary (#e9e9ea) on its content panel. */
|
|
364
|
+
--bg-hover: rgba(0, 0, 0, 0.064);
|
|
365
|
+
--bg-selected: #e5e5e6; /* --color-bg-tertiary */
|
|
366
|
+
--bg-active: #e1e1e2; /* the selected sidebar row (lch 89.54%) */
|
|
367
|
+
--bg-disabled: #efeff0; /* the base, as light has it */
|
|
368
|
+
|
|
369
|
+
/* text */
|
|
370
|
+
--text-primary: #1b1b1b; /* --color-text-primary */
|
|
371
|
+
--text-secondary: #5c5c5e; /* --color-text-tertiary: labels, descriptions, icons */
|
|
372
|
+
--text-muted: #9c9d9f; /* --color-text-quaternary: placeholders, counts */
|
|
373
|
+
--text-disabled: #D4D4D8; /* light */
|
|
374
|
+
--text-inverse: #ffffff;
|
|
375
|
+
--text-interactive: #1b1b1b; /* --app-link-color: a link is the text colour */
|
|
376
|
+
|
|
377
|
+
/* borders */
|
|
378
|
+
/* One step darker than Linear's own faintest line (Katerina, 28 September): our cards, chips and
|
|
379
|
+
toasts draw their edge with subtle, and Linear's #f1f1f1 vanished on white and on the ground. */
|
|
380
|
+
--border-subtle: #e5e5e6; /* --color-bg-tertiary */
|
|
381
|
+
--border-default: #dedede; /* --color-border-secondary */
|
|
382
|
+
--border-strong: #d3d3d3; /* --color-border-tertiary */
|
|
383
|
+
--border-focus: #5e69d1; /* --focus-ring-color */
|
|
384
|
+
|
|
385
|
+
/* accent — Linear's indigo */
|
|
386
|
+
--accent-primary: #5e69d1; /* --focus-ring-color, the selection's colour */
|
|
387
|
+
--accent-hover: #6b75df; /* not named in Linear's set: the indigo beside it in the app's own values */
|
|
388
|
+
--accent-muted: #EDE9FE; /* light */
|
|
389
|
+
|
|
390
|
+
/* semantic — light, until Linear's are read */
|
|
391
|
+
--info-default: #3B82F6;
|
|
392
|
+
--info-muted: #DBEAFE;
|
|
393
|
+
--warning-default: #967000; /* Linear's darkest yellow: readable on the warning wash (Katerina, 28 September) */
|
|
394
|
+
--warning-muted: #FEF3C7;
|
|
395
|
+
--success-default: #0EA06F;
|
|
396
|
+
--success-muted: #D1FAE5;
|
|
397
|
+
--error-default: #EF4444;
|
|
398
|
+
--error-muted: #FEE2E2;
|
|
399
|
+
|
|
400
|
+
/* shadows — light */
|
|
401
|
+
--shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.05);
|
|
402
|
+
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.10), 0 2px 4px -2px rgba(0,0,0,0.10);
|
|
403
|
+
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.10), 0 4px 6px -4px rgba(0,0,0,0.10);
|
|
404
|
+
--focus-ring: 0 0 0 1px #5e69d1; /* --focus-ring-outline: 1px of the accent */
|
|
405
|
+
/* Transparent colours (D16): light's strengths, on Linear's indigo. */
|
|
406
|
+
--bg-wash: rgba(0, 0, 0, 0.04);
|
|
407
|
+
--accent-outline: rgba(94, 105, 209, 0.3);
|
|
408
|
+
--info-outline: rgba(59, 130, 246, 0.3);
|
|
409
|
+
--warning-outline: rgba(245, 158, 11, 0.3);
|
|
410
|
+
--success-outline: rgba(14, 160, 111, 0.3);
|
|
411
|
+
--error-outline: rgba(239, 68, 68, 0.3);
|
|
412
|
+
--accent-wash: rgba(94, 105, 209, 0.11);
|
|
413
|
+
--success-wash: rgba(14, 160, 111, 0.11);
|
|
414
|
+
--glow-warning: 0 0 5px rgba(245, 158, 11, 0.4);
|
|
415
|
+
--glow-success: 0 0 5px rgba(14, 160, 111, 0.7);
|
|
416
|
+
--glow-accent: 0 0 5px rgba(94, 105, 209, 0.6);
|
|
417
|
+
--highlight-inset: inset 0 1px 0 rgba(255, 255, 255, 0.035);
|
|
418
|
+
--scrim: rgba(0, 0, 0, 0.5);
|
|
419
|
+
--scrim-strong: rgba(0, 0, 0, 0.8);
|
|
420
|
+
/* Tooltips are dark on the light page (Katerina, 27 September): Linear's text colour as the ground. */
|
|
421
|
+
--bg-tooltip: #1b1b1b;
|
|
422
|
+
--text-tooltip: #ffffff;
|
|
423
|
+
--border-tooltip: #1b1b1b;
|
|
424
|
+
/* A shortcut key on the dark tooltip: a faint light key. */
|
|
425
|
+
--bg-tooltip-key: rgba(255, 255, 255, 0.1);
|
|
426
|
+
--border-tooltip-key: rgba(255, 255, 255, 0.18);
|
|
427
|
+
--text-tooltip-key: rgba(255, 255, 255, 0.75);
|
|
428
|
+
/* A field is Linear's: white, a faint line, a soft two-layer shadow (read from its
|
|
429
|
+
comment box, 28 September: lch(100), lch(97.3), 0 3px 6px -2px at 2% and 0 1px 1px at 4%). */
|
|
430
|
+
--bg-field: #ffffff;
|
|
431
|
+
--border-field: #dedede; /* the default line, so a field shows on a white dialog (Katerina, 28 September); Linear's own is #f7f7f7 */
|
|
432
|
+
--shadow-field: 0 3px 6px -2px rgba(0, 0, 0, 0.02), 0 1px 1px 0 rgba(0, 0, 0, 0.04);
|
|
289
433
|
}
|