@estiva-app/ui 0.53.1 → 0.55.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.
Files changed (54) hide show
  1. package/dist/BlockHandle.d.ts +47 -0
  2. package/dist/BlockHandle.d.ts.map +1 -0
  3. package/dist/EditorBlockHandle.d.ts +57 -0
  4. package/dist/EditorBlockHandle.d.ts.map +1 -0
  5. package/dist/NavItem.d.ts +14 -1
  6. package/dist/NavItem.d.ts.map +1 -1
  7. package/dist/NavTree.d.ts +93 -13
  8. package/dist/NavTree.d.ts.map +1 -1
  9. package/dist/UnreadDot.d.ts +12 -0
  10. package/dist/UnreadDot.d.ts.map +1 -0
  11. package/dist/editor.d.ts +1 -0
  12. package/dist/editor.d.ts.map +1 -1
  13. package/dist/editor.js +127 -11
  14. package/dist/editor.js.map +4 -4
  15. package/dist/eslint/index.js +1 -1
  16. package/dist/eslint/index.js.map +2 -2
  17. package/dist/eslint/no-native-title.d.ts +1 -1
  18. package/dist/eslint/no-native-title.d.ts.map +1 -1
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +801 -584
  22. package/dist/index.js.map +4 -4
  23. package/dist/looks.d.ts +1 -1
  24. package/dist/looks.d.ts.map +1 -1
  25. package/package.json +1 -1
  26. package/registry.json +362 -11
  27. package/src/BlockHandle.mdx +58 -0
  28. package/src/BlockHandle.stories.tsx +44 -0
  29. package/src/BlockHandle.test.tsx +216 -0
  30. package/src/BlockHandle.tsx +220 -0
  31. package/src/EditorBlockHandle.mdx +65 -0
  32. package/src/EditorBlockHandle.stories.tsx +81 -0
  33. package/src/EditorBlockHandle.tsx +181 -0
  34. package/src/NavItem.mdx +14 -1
  35. package/src/NavItem.stories.tsx +15 -0
  36. package/src/NavItem.test.tsx +31 -0
  37. package/src/NavItem.tsx +49 -12
  38. package/src/NavTree.mdx +25 -0
  39. package/src/NavTree.stories.tsx +57 -1
  40. package/src/NavTree.test.tsx +78 -1
  41. package/src/NavTree.tsx +175 -40
  42. package/src/SectionLabel.stories.tsx +2 -3
  43. package/src/UnreadDot.mdx +57 -0
  44. package/src/UnreadDot.stories.tsx +22 -0
  45. package/src/UnreadDot.test.tsx +17 -0
  46. package/src/UnreadDot.tsx +17 -0
  47. package/src/editor.ts +1 -0
  48. package/src/eslint/no-native-title.ts +1 -1
  49. package/src/index.ts +3 -1
  50. package/src/looks.ts +2 -1
  51. package/src/registry/registry.test.ts +5 -5
  52. package/stories/Choosing.mdx +3 -0
  53. package/tailwind-preset.js +1 -0
  54. package/tokens.css +5 -0
package/src/NavTree.tsx CHANGED
@@ -1,8 +1,8 @@
1
- import type { MouseEvent, ReactNode } from 'react'
1
+ import { useEffect, useRef, type MouseEvent, type ReactNode } from 'react'
2
2
  import { CollapsibleSection } from './CollapsibleSection'
3
3
  import { EmptyState } from './EmptyState'
4
4
  import { NavItem } from './NavItem'
5
- import { SectionHeader } from './SectionHeader'
5
+ import { SectionHeader, type SectionAction } from './SectionHeader'
6
6
  import { SkeletonList } from './Skeleton'
7
7
 
8
8
  /** One row of the tree, and the rows under it. */
@@ -18,40 +18,103 @@ export interface NavTreeNode {
18
18
  countLabel?: string
19
19
  /** Shown only while the row is pointed at or focused, left of the count: a neutral `Chip`, typically. */
20
20
  hint?: ReactNode
21
+ /** Something new here: NavItem's `unread`. */
22
+ unread?: boolean
23
+ /** With `unread`: the warning badge in the dot's place. */
24
+ urgent?: boolean
21
25
  /** The row's "More options" menu: `MenuItem`s. */
22
26
  menu?: ReactNode
23
- /** Rows under this one. Their row folds them; open unless `defaultOpen` says otherwise. */
24
- children?: NavTreeNode[]
27
+ /**
28
+ * Rows under this one. `null` while they are being read: the row has its
29
+ * arrow, and a skeleton under it once open. Absent with `hasChildren`, the
30
+ * same — for rows read only when the row opens.
31
+ */
32
+ children?: NavTreeNode[] | null
33
+ /** The row has rows under it that are not read yet: it gets its arrow, and `onOpenChange` says when to read them. */
34
+ hasChildren?: boolean
35
+ /** Said under the row, level with its rows, in place of them: "Nothing in this yet.", "This could not be read." The caller's words. */
36
+ childrenMessage?: string
25
37
  defaultOpen?: boolean
38
+ /** The caller owns whether its rows are open. */
39
+ open?: boolean
40
+ onOpenChange?: (open: boolean) => void
26
41
  }
27
42
 
28
43
  /** A group heading over its rows: the top of the tree. */
29
44
  export interface NavTreeGroup {
30
45
  id: string
31
46
  title: string
32
- nodes: NavTreeNode[]
47
+ /** The rows. `null` while they are being read: a skeleton in their place. */
48
+ nodes: NavTreeNode[] | null
49
+ /** Said in the rows' place, level with them: "Nothing in this yet.", "This could not be read." The caller's words. */
50
+ message?: string
51
+ /** Beside the title, always shown: an `UnreadDot`, typically. */
52
+ trailing?: ReactNode
53
+ /** Beside the title, shown on hover or focus: "Open", typically. */
54
+ actions?: SectionAction[]
55
+ defaultOpen?: boolean
56
+ /** The caller owns whether the group is open. */
57
+ open?: boolean
58
+ onOpenChange?: (open: boolean) => void
59
+ /**
60
+ * The pointer rests on the group, or the keyboard reaches it: read its rows
61
+ * now, so opening it draws them. Resting, not passing — a pointer sweeping
62
+ * down the column calls nothing.
63
+ */
64
+ onIntent?: () => void
33
65
  }
34
66
 
35
67
  export interface NavTreeProps {
36
68
  /** The quiet label over the groups. Absent, none is drawn. */
37
69
  title?: string
38
- /** `null` while the first read is on its way: a skeleton in the rows' place. */
39
- groups: NavTreeGroup[] | null
70
+ /** Beside the label, always shown: "New", typically. */
71
+ titleActions?: SectionAction[]
72
+ /**
73
+ * `null` while the first read is on its way: a skeleton in the groups' place.
74
+ * Absent when the app draws its own groups as `children`.
75
+ */
76
+ groups?: NavTreeGroup[] | null
77
+ /**
78
+ * The groups, drawn by the app: `NavTreeSection`s with `NavItem` rows — for
79
+ * an app whose groups and rows each read their own data. Used when `groups`
80
+ * is absent.
81
+ */
82
+ children?: ReactNode
40
83
  /** The row that is current: `active`, and its group starts open. */
41
84
  selected?: string
42
85
  /** A row was clicked. A router app prevents the default and navigates. */
43
86
  onSelect?: (node: NavTreeNode, event: MouseEvent<HTMLAnchorElement>) => void
44
- /** What to say when there are no groups. The words are the caller's. */
45
- emptyMessage: string
87
+ /** What to say when `groups` is empty. The words are the caller's; absent, nothing is said. */
88
+ emptyMessage?: string
46
89
  /** Remember each group open or closed in this browser, under this key and the group's id. The app prefixes it. */
47
90
  storageKey?: string
48
91
  }
49
92
 
50
- function holds(nodes: NavTreeNode[], id: string | undefined): boolean {
51
- return id !== undefined && nodes.some((node) => node.id === id || holds(node.children ?? [], id))
93
+ /** How long the pointer rests on a group before `onIntent` (Peek's FolderTree, 150ms). */
94
+ export const NAV_TREE_INTENT_MS = 150
95
+
96
+ function holds(nodes: NavTreeNode[] | null, id: string | undefined): boolean {
97
+ return !!nodes && id !== undefined && nodes.some((node) => node.id === id || holds(node.children ?? null, id))
98
+ }
99
+
100
+ /** A line in the rows' place: level with the rows, which pad themselves (Katerina, 14 September). */
101
+ function Line({ message }: { message: string }) {
102
+ return (
103
+ <div className="px-2 py-1">
104
+ <EmptyState scope="section" message={message} />
105
+ </div>
106
+ )
52
107
  }
53
108
 
54
109
  function Row({ node, selected, onSelect }: { node: NavTreeNode } & Pick<NavTreeProps, 'selected' | 'onSelect'>) {
110
+ const under =
111
+ node.childrenMessage !== undefined ? (
112
+ <Line message={node.childrenMessage} />
113
+ ) : node.children?.length ? (
114
+ node.children.map((child) => <Row key={child.id} node={child} selected={selected} onSelect={onSelect} />)
115
+ ) : node.children === null || (node.hasChildren && node.children === undefined) ? (
116
+ <SkeletonList rows={2} />
117
+ ) : undefined
55
118
  return (
56
119
  <NavItem
57
120
  href={node.href}
@@ -60,52 +123,124 @@ function Row({ node, selected, onSelect }: { node: NavTreeNode } & Pick<NavTreeP
60
123
  count={node.count}
61
124
  countLabel={node.countLabel}
62
125
  hint={node.hint}
126
+ unread={node.unread}
127
+ urgent={node.urgent}
63
128
  menu={node.menu}
64
129
  active={node.id === selected}
65
130
  defaultOpen={node.defaultOpen ?? true}
131
+ open={node.open}
132
+ onOpenChange={node.onOpenChange}
66
133
  onClick={onSelect ? (event) => onSelect(node, event) : undefined}
67
134
  >
68
- {node.children?.length
69
- ? node.children.map((child) => <Row key={child.id} node={child} selected={selected} onSelect={onSelect} />)
70
- : undefined}
135
+ {under}
71
136
  </NavItem>
72
137
  )
73
138
  }
74
139
 
140
+ function Group({ group, selected, onSelect, storageKey }: { group: NavTreeGroup } & Pick<NavTreeProps, 'selected' | 'onSelect' | 'storageKey'>) {
141
+ return (
142
+ <NavTreeSection
143
+ id={group.id}
144
+ title={group.title}
145
+ message={group.message}
146
+ loading={group.nodes === null}
147
+ trailing={group.trailing}
148
+ actions={group.actions}
149
+ defaultOpen={group.defaultOpen ?? holds(group.nodes, selected)}
150
+ open={group.open}
151
+ onOpenChange={group.onOpenChange}
152
+ storageKey={storageKey ? `${storageKey}.${group.id}` : undefined}
153
+ onIntent={group.onIntent}
154
+ >
155
+ {group.nodes?.map((node) => <Row key={node.id} node={node} selected={selected} onSelect={onSelect} />)}
156
+ </NavTreeSection>
157
+ )
158
+ }
159
+
160
+ export interface NavTreeSectionProps {
161
+ title: string
162
+ /** Marks the group's box as `data-nav-group`, for tests and photo runs to find it. */
163
+ id?: string
164
+ /** Said in the rows' place, level with them. The caller's words. */
165
+ message?: string
166
+ /** Beside the title, always shown: an `UnreadDot`, typically. */
167
+ trailing?: ReactNode
168
+ /** Beside the title, shown on hover or focus: "Open", typically. */
169
+ actions?: SectionAction[]
170
+ /** Open unless told otherwise. */
171
+ defaultOpen?: boolean
172
+ /** The caller owns whether the group is open. */
173
+ open?: boolean
174
+ onOpenChange?: (open: boolean) => void
175
+ /** The pointer rests on the group, or the keyboard reaches it: read its rows now. */
176
+ onIntent?: () => void
177
+ /** Its rows are being read: a skeleton in their place. */
178
+ loading?: boolean
179
+ /** Remember it open or closed in this browser, under this key. */
180
+ storageKey?: string
181
+ /** The rows: `NavItem`s. */
182
+ children?: ReactNode
183
+ }
184
+
185
+ /**
186
+ * One group of a `NavTree`, for an app that draws its own: the standard
187
+ * heading folding its rows, lightly indented under its title, with the
188
+ * group's line, skeleton, actions and `onIntent`. `NavTree` draws these from
189
+ * `groups`; an app whose groups each read their own data draws them itself,
190
+ * as `NavTree`'s children.
191
+ */
192
+ export function NavTreeSection({ id, title, message, loading = false, trailing, actions, defaultOpen = true, open, onOpenChange, storageKey, onIntent, children }: NavTreeSectionProps) {
193
+ const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
194
+ useEffect(() => () => clearTimeout(timer.current), [])
195
+ const intent = onIntent
196
+ return (
197
+ <div
198
+ className="shrink-0"
199
+ data-nav-group={id}
200
+ onPointerEnter={intent ? () => { clearTimeout(timer.current); timer.current = setTimeout(intent, NAV_TREE_INTENT_MS) } : undefined}
201
+ onPointerLeave={intent ? () => clearTimeout(timer.current) : undefined}
202
+ onFocus={intent}
203
+ >
204
+ <CollapsibleSection
205
+ title={title}
206
+ indent
207
+ defaultOpen={defaultOpen}
208
+ open={open}
209
+ onOpenChange={onOpenChange}
210
+ storageKey={storageKey}
211
+ trailing={trailing}
212
+ actions={actions}
213
+ className="mt-2 shrink-0"
214
+ contentClassName="gap-px"
215
+ >
216
+ {message !== undefined ? <Line message={message} /> : loading ? <SkeletonList rows={3} /> : children}
217
+ </CollapsibleSection>
218
+ </div>
219
+ )
220
+ }
221
+
75
222
  /**
76
223
  * A sidebar's tree, from one nested list (Katerina, 2 October: Leaf's folder
77
- * tree): a quiet label, then each group as the standard heading folding its
78
- * rows, lightly indented under its title; rows under rows to any depth, each
79
- * row a link whose icon becomes the arrow that folds what is under it. Only
80
- * the group that holds the current row starts open.
224
+ * tree, then Peek's and Ship's): a quiet label with its actions, then each
225
+ * group as the standard heading folding its rows, lightly indented under its
226
+ * title; rows under rows to any depth, each row a link whose icon becomes the
227
+ * arrow that folds what is under it. Groups and rows can be read when they
228
+ * open. Only the group that holds the current row starts open, unless the
229
+ * caller owns it.
81
230
  */
82
- export function NavTree({ title, groups, selected, onSelect, emptyMessage, storageKey }: NavTreeProps) {
231
+ export function NavTree({ title, titleActions, groups, children, selected, onSelect, emptyMessage = '', storageKey }: NavTreeProps) {
83
232
  return (
84
233
  <>
85
- {title && <SectionHeader title={title} look="quiet" className="mt-4 shrink-0" />}
86
- {groups === null ? (
234
+ {title && <SectionHeader title={title} look="quiet" showActions="always" actions={titleActions} className="mt-4 shrink-0" />}
235
+ {groups === undefined ? (
236
+ children
237
+ ) : groups === null ? (
87
238
  <SkeletonList rows={5} />
88
239
  ) : groups.length === 0 ? (
89
- // Level with the rows: a row pads itself, so the line takes a row's inset (Katerina, 14 September).
90
- <div className="px-2 py-1">
91
- <EmptyState scope="section" message={emptyMessage} />
92
- </div>
240
+ // No words, no line: an app that says nothing when it has no groups.
241
+ emptyMessage ? <Line message={emptyMessage} /> : null
93
242
  ) : (
94
- groups.map((group) => (
95
- <CollapsibleSection
96
- key={group.id}
97
- title={group.title}
98
- indent
99
- defaultOpen={holds(group.nodes, selected)}
100
- storageKey={storageKey ? `${storageKey}.${group.id}` : undefined}
101
- className="mt-2 shrink-0"
102
- contentClassName="gap-px"
103
- >
104
- {group.nodes.map((node) => (
105
- <Row key={node.id} node={node} selected={selected} onSelect={onSelect} />
106
- ))}
107
- </CollapsibleSection>
108
- ))
243
+ groups.map((group) => <Group key={group.id} group={group} selected={selected} onSelect={onSelect} storageKey={storageKey} />)
109
244
  )}
110
245
  </>
111
246
  )
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { NavItem } from './NavItem'
2
3
  import { SectionLabel } from './SectionLabel'
3
4
 
4
5
  /** Every section title renders through this span — a style change is one edit. Under Signal it becomes the mono uppercase micro-label. */
@@ -22,9 +23,7 @@ export const HeadingAList: Story = {
22
23
  <SectionLabel>Recent</SectionLabel>
23
24
  </div>
24
25
  {['Quarterly plan', 'Reading list', 'Archive'].map((row) => (
25
- <div key={row} className="rounded-lg px-2 py-1.5 text-body-2 text-text-primary hover:bg-bg-hover">
26
- {row}
27
- </div>
26
+ <NavItem key={row} href="#" label={row} />
28
27
  ))}
29
28
  </div>
30
29
  ),
@@ -0,0 +1,57 @@
1
+ import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks'
2
+ import * as UnreadDotStories from './UnreadDot.stories'
3
+
4
+ <Meta of={UnreadDotStories} />
5
+
6
+ # UnreadDot
7
+
8
+ The accent dot that says "something new here": a 6px disc centred in a 24px
9
+ slot, so it sits exactly where a control would and the row does not shift
10
+ when one replaces the other.
11
+
12
+ <Canvas of={UnreadDotStories.InARow} />
13
+
14
+ ## When
15
+
16
+ - On any row or card that can be unread: a place in a sidebar, a
17
+ conversation, a reply.
18
+ - Anywhere a control appears on hover and the dot is what is there at rest.
19
+ The 24px slot keeps the two the same size.
20
+ - In a sidebar row, pass **NavItem** `unread` rather than placing it: the row
21
+ draws it, and brightens its words.
22
+
23
+ ## When not
24
+
25
+ - As the only thing that says "unread". A coloured disc reads as nothing to a
26
+ screen reader, so the row that draws it must say "unread" in its own
27
+ accessible name. This part carries no label of its own.
28
+ - For a count. It says that there is something new, never how much: a count
29
+ is **NavItem** `count` or a **Chip**.
30
+
31
+ ## How
32
+
33
+ ```tsx
34
+ import { UnreadDot } from '@estiva-app/ui'
35
+
36
+ <div className="flex items-center">
37
+ <span>{title}</span>
38
+ {isUnread && <UnreadDot />}
39
+ </div>
40
+ ```
41
+
42
+ - It renders unconditionally, so the caller decides whether there is
43
+ anything new.
44
+
45
+ ## What it owns
46
+
47
+ It does these by itself. Write none of them around it.
48
+
49
+ | It does | So you never write | Refused when written by hand |
50
+ |---|---|---|
51
+ | The 6px disc in a 24px slot | a sized box around a round div | |
52
+ | `data-unread`, the attribute tests and photo runs select on | a marker of your own | |
53
+ | Keeps its size in a tight row | `shrink-0` | |
54
+
55
+ ## Props
56
+
57
+ <Controls of={UnreadDotStories.Default} />
@@ -0,0 +1,22 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { UnreadDot } from './UnreadDot'
3
+
4
+ const meta = {
5
+ title: 'Primitives/UnreadDot',
6
+ component: UnreadDot,
7
+ } satisfies Meta<typeof UnreadDot>
8
+
9
+ export default meta
10
+ type Story = StoryObj<typeof meta>
11
+
12
+ export const Default: Story = {}
13
+
14
+ /** Beside a line of text, as a list row draws it: the slot keeps the row's height. */
15
+ export const InARow: Story = {
16
+ render: (args) => (
17
+ <div className="flex w-64 items-center gap-1 text-body-2 text-text-primary">
18
+ <span className="min-w-0 flex-1 truncate">A row with something new</span>
19
+ <UnreadDot {...args} />
20
+ </div>
21
+ ),
22
+ }
@@ -0,0 +1,17 @@
1
+ // @vitest-environment jsdom
2
+ /** What the page claims: the 6px disc in a 24px slot, marked for tests and photo runs. */
3
+ import { afterEach, describe, expect, it } from 'vitest'
4
+ import { cleanup, render } from '@testing-library/react'
5
+ import { UnreadDot } from './UnreadDot'
6
+
7
+ afterEach(cleanup)
8
+
9
+ describe('UnreadDot', () => {
10
+ it('is a 24px slot holding a 6px disc, marked data-unread', () => {
11
+ const { container } = render(<UnreadDot />)
12
+ const slot = container.firstElementChild as HTMLElement
13
+ expect(slot.hasAttribute('data-unread')).toBe(true)
14
+ expect(slot.className).toContain('w-6')
15
+ expect((slot.firstElementChild as HTMLElement).className).toContain('w-1.5')
16
+ })
17
+ })
@@ -0,0 +1,17 @@
1
+ import { cn } from './cn'
2
+
3
+ /**
4
+ * The accent dot that says "something new here": a 6px disc centred in a
5
+ * 24px slot, so it sits where a control would and the row does not shift when
6
+ * one replaces the other (Peek's UnreadDot, moved in on 2 October, Katerina).
7
+ *
8
+ * Purely visual: the row that draws it says "unread" in its own accessible
9
+ * name, because a coloured disc reads as nothing to a screen reader.
10
+ */
11
+ export function UnreadDot({ className }: { className?: string }) {
12
+ return (
13
+ <div className={cn('flex h-6 w-6 shrink-0 items-center justify-center', className)} data-unread>
14
+ <div className="h-1.5 w-1.5 rounded-full bg-accent-primary" />
15
+ </div>
16
+ )
17
+ }
package/src/editor.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
  export { KeptSelection, SelectionToolbar, normalizeHref, type MarkId, type SelectionToolbarProps } from './SelectionToolbar'
11
11
  export { suggestionPopup, isSuggestionActive, isSuggestionOpen, type SuggestionPopupOptions } from './suggestionPopup'
12
+ export { EditorBlockHandle, blockDropCursor, deleteBlock, moveBlock, duplicateBlock, startBlockDrag, type EditorBlockHandleProps, type BlockTurnInto } from './EditorBlockHandle'
12
13
  export {
13
14
  PersonMention,
14
15
  UrgentPersonMention,
@@ -8,7 +8,7 @@ import { bindingOfTag, partsOfFile, type Node, type Parser, type PartBinding } f
8
8
  * the browser shows it as its tooltip. `no-native-title.test.ts` holds this
9
9
  * list to the catalogue: a part that gains a `title` of its own joins it.
10
10
  */
11
- export const PARTS_WITH_A_TITLE = ['CollapsibleSection', 'ConfirmDialog', 'ContainerHeader', 'DialogShell', 'ListColumn', 'NavTree', 'Panel', 'SectionHeader'] as const
11
+ export const PARTS_WITH_A_TITLE = ['CollapsibleSection', 'ConfirmDialog', 'ContainerHeader', 'DialogShell', 'ListColumn', 'NavTree', 'NavTreeSection', 'Panel', 'SectionHeader'] as const
12
12
 
13
13
  const child = (node: Node, key: string) => node[key] as Node | null | undefined
14
14
  const nameOf = (node: Node | null | undefined): string | undefined =>
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export { AttachmentCard, type AttachmentCardProps, type AttachmentCardState } fr
14
14
  export { Avatar, hueFor, initialsFor, type AvatarProps } from './Avatar'
15
15
  export { AvatarGroup, type AvatarGroupMember, type AvatarGroupProps } from './AvatarGroup'
16
16
  export { Banner, type BannerProps, type BannerTone } from './Banner'
17
+ export { BlockHandle, blockAtY, type BlockHandleProps, type BlockHandleEditing, type HandleBlock } from './BlockHandle'
17
18
  export { Button, type ButtonProps, type ButtonSize, type ButtonVariant } from './Button'
18
19
  export { Card, type CardAttention, type CardFill, type CardHover, type CardProps } from './Card'
19
20
  export { Checkbox, type CheckboxProps } from './Checkbox'
@@ -70,7 +71,8 @@ export { SkeletonBar, SkeletonList, SkeletonRow } from './Skeleton'
70
71
  export { Breadcrumb, type BreadcrumbProps, type Crumb } from './Breadcrumb'
71
72
  export { EnterHint, Menu, MenuItem, MenuPanel, MenuRow, MenuSection, MenuSeparator, MenuSub, type MenuItemProps, type MenuPanelProps, type MenuProps, type MenuSubProps } from './Menu'
72
73
  export { NavItem, type NavItemProps } from './NavItem'
73
- export { NavTree, type NavTreeGroup, type NavTreeNode, type NavTreeProps } from './NavTree'
74
+ export { UnreadDot } from './UnreadDot'
75
+ export { NavTree, NavTreeSection, type NavTreeGroup, type NavTreeNode, type NavTreeProps, type NavTreeSectionProps } from './NavTree'
74
76
  export { Popover, type PopoverProps } from './Popover'
75
77
  export { SuggestionMenu, type SuggestionMenuHandle, type SuggestionMenuProps, type SuggestionMenuRow, type SuggestionMenuSection } from './SuggestionMenu'
76
78
  export { ScrollArea, type ScrollAreaProps } from './ScrollArea'
package/src/looks.ts CHANGED
@@ -44,7 +44,8 @@ export const CHIP_TEXT_CLASSES = 'text-chip signal:font-mono signal:text-small s
44
44
  export const BARE_INPUT_CLASSES = 'bg-transparent text-text-primary placeholder:text-text-muted outline-none'
45
45
 
46
46
  /** A sidebar row's shape and its words at rest, brightening under the pointer: NavItem, and SectionHeader's `row` (a folder among rows). */
47
- export const SIDEBAR_ROW_CLASSES = 'rounded-md transition-colors'
47
+ // 8px corners, a MenuItem row's (Katerina, 2 October).
48
+ export const SIDEBAR_ROW_CLASSES = 'rounded-lg transition-colors'
48
49
  export const SIDEBAR_ROW_TEXT_CLASSES = 'text-text-secondary hover:text-text-primary'
49
50
 
50
51
  /**
@@ -48,20 +48,20 @@ describe('the registry builds', () => {
48
48
  expect(registry.entries.map((one) => one.name).sort()).toEqual(values.map((one) => one.name).sort())
49
49
  })
50
50
 
51
- it('counts 120 names over 66 files: 96 components, 23 helpers and 1 hook (NavTree and the composer triggers joined on 2 October, CON-27)', () => {
51
+ it('counts 130 names over 69 files: 100 components, 29 helpers and 1 hook (NavTree, NavTreeSection, the composer triggers and UnreadDot joined on 2 October; BlockHandle and EditorBlockHandle (RIC-18) on 2 October, and moveBlock and blockDropCursor, the drop in the gutter, on 5 October)', () => {
52
52
  // The reconciliation GATES.md §24 explains. If this changes, that changes.
53
53
  // The ten `EDITOR_EXTENSIONS` count as components by their names; they are
54
54
  // editor extensions, documented on SelectionToolbar's, BlockNodes' and
55
55
  // ComposerTriggers' pages.
56
56
  const kinds = registry.entries.reduce<Record<string, number>>((all, one) => ({ ...all, [one.kind]: (all[one.kind] ?? 0) + 1 }), {})
57
- expect(kinds).toEqual({ component: 96, helper: 23, hook: 1 })
58
- expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(66)
57
+ expect(kinds).toEqual({ component: 100, helper: 29, hook: 1 })
58
+ expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(69)
59
59
  })
60
60
 
61
61
  it('gives every entry a purpose, from its own page or from the comment above it', () => {
62
62
  expect(registry.entries.filter((one) => one.purpose.trim() === '')).toEqual([])
63
- expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(62)
64
- expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(58)
63
+ expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(65)
64
+ expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(65)
65
65
  })
66
66
 
67
67
  it('takes a name with a page of its own from the page, and a name without one from the code', () => {
@@ -70,6 +70,8 @@ fork freely, owe nothing back, mention it on the package's ticket.
70
70
  | Bold, italic and the like over selected text in an editor | **SelectionToolbar**, from `@estiva-app/ui/editor` — it follows the selection by itself |
71
71
  | A composer's `@`, `!@`, `[` and `/` — mention a person, reference something, format or insert | **ComposerTriggers** — `PersonMention`, `UrgentPersonMention`, `CaptionedReference`, `ReferenceTrigger` and `SlashCommands` from `@estiva-app/ui/editor`; the app passes its people, its items and its own `/` rows |
72
72
  | An editor that saves a block document and must keep its block ids | **BlockNodes** — `BlockId`, `UnknownBlock`, `ReferenceNode` and `AttachmentNode` from `@estiva-app/ui/editor`, beside StarterKit and TableKit |
73
+ | Move, turn, duplicate or delete one block of a document being edited | **EditorBlockHandle**, from `@estiva-app/ui/editor`, with your rows after its own |
74
+ | Offer a link or a comment on one block of a document being read | **BlockHandle** — your rows only, and no Tiptap loaded |
73
75
  | Anything with a title, body and footer | **DialogShell** |
74
76
  | "Are you sure?" | **ConfirmDialog** |
75
77
  | A picture full screen, from a thumbnail | **Lightbox** — an **AttachmentCard** opens one by itself |
@@ -82,6 +84,7 @@ fork freely, owe nothing back, mention it on the package's ticket.
82
84
  | The bar across the top of a column: its title, and the buttons that act on the whole column | **ContainerHeader** — inside the package's columns; in an app, the header comes with **Panel** or **ListColumn** |
83
85
  | A box that can hold more than fits — a list that grows with its data counts, even when today's data fits | **ScrollArea** — its bar is drawn over the content and takes no width |
84
86
  | A status, a count or a category, in a small pill | **Chip** — six colour types; the colour is the message, the label makes it legible |
87
+ | Something new on a row or a card, no count | **UnreadDot** — the 6px dot in a 24px slot; in a sidebar row, **NavItem** `unread` draws it |
85
88
  | One thing — a conversation, a project, an object — as a box you can pick | **Card** — the fill by what it sits on; `href` for a link, `hover="fill"` in a feed |
86
89
  | A section heading, chevron, hover actions | **SectionHeader** |
87
90
  | A group of rows that opens and closes, and stays how you left it | **CollapsibleSection** — `storageKey` remembers |
@@ -149,6 +149,7 @@ export default {
149
149
  'bg-hover': 'var(--bg-hover)',
150
150
  'bg-selected': 'var(--bg-selected)',
151
151
  'bg-active': 'var(--bg-active)',
152
+ 'bg-nav-active': 'var(--bg-nav-active)',
152
153
  'bg-disabled': 'var(--bg-disabled)',
153
154
  'bg-tooltip': 'var(--bg-tooltip)',
154
155
  'bg-tooltip-key': 'var(--bg-tooltip-key)',
package/tokens.css CHANGED
@@ -29,6 +29,7 @@
29
29
  --bg-hover: #E9E9EB;
30
30
  --bg-selected: #E4E4E7;
31
31
  --bg-active: #D4D4D8;
32
+ --bg-nav-active: #D4D4D8; /* the current row in a sidebar */
32
33
  --bg-disabled: #F4F4F5;
33
34
 
34
35
  /* text */
@@ -113,6 +114,7 @@
113
114
  --bg-hover: #232326;
114
115
  --bg-selected: #1F1F22;
115
116
  --bg-active: #2D2D31;
117
+ --bg-nav-active: #2D2D31; /* the current row in a sidebar */
116
118
  --bg-disabled: #1C1C1F;
117
119
 
118
120
  /* text */
@@ -198,6 +200,7 @@
198
200
  --bg-hover: rgba(255, 255, 255, 0.05);
199
201
  --bg-selected: rgba(86, 200, 255, 0.11);
200
202
  --bg-active: rgba(255, 255, 255, 0.08);
203
+ --bg-nav-active: rgba(86, 200, 255, 0.11); /* the current row in a sidebar: Peek's selected row (Katerina, 2 October) */
201
204
  --bg-disabled: #191d24;
202
205
 
203
206
  /* text */
@@ -285,6 +288,7 @@
285
288
  --bg-hover: rgba(255, 255, 255, 0.05);
286
289
  --bg-selected: rgba(75, 88, 212, 0.16);
287
290
  --bg-active: rgba(255, 255, 255, 0.08);
291
+ --bg-nav-active: rgba(255, 255, 255, 0.08); /* the current row in a sidebar */
288
292
  --bg-disabled: #16181f;
289
293
 
290
294
  /* text */
@@ -390,6 +394,7 @@
390
394
  --bg-hover: rgba(0, 0, 0, 0.064);
391
395
  --bg-selected: #e5e5e6; /* --color-bg-tertiary */
392
396
  --bg-active: #e1e1e2; /* the selected sidebar row (lch 89.54%) */
397
+ --bg-nav-active: #e1e1e2; /* the current row in a sidebar */
393
398
  --bg-disabled: #efeff0; /* the base, as light has it */
394
399
 
395
400
  /* text */