@estiva-app/ui 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/ui",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Estiva's design tokens (the contract) and a small set of primitives (a convenience) for every Estiva app.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/Card.mdx CHANGED
@@ -25,8 +25,10 @@ move it.
25
25
  hover.
26
26
  - `hover="fill"` — a card in a feed: it lights up when pointed at, so the light
27
27
  says which one you are on. `quietUntilHover` hides its hairline until then.
28
+ - `hovered` — hold that look while the pointer is elsewhere: a card whose own
29
+ menu or picker is open stays lit, because the menu opens outside the card.
28
30
  - `selected` — the one you are on. `active` — the one being changed in place.
29
- Neither answers the pointer.
31
+ Neither answers the pointer, and `hovered` changes neither.
30
32
  - `attention` — a hairline that asks to be looked at: `accent` for something
31
33
  new, `warning` for something urgent.
32
34
  - `unreadable` — it stands for something that could not be read: the hairline
@@ -60,8 +62,12 @@ import { Card } from '@estiva-app/ui'
60
62
  - With `href` it renders `Link`'s anchor, with every anchor prop passed through,
61
63
  so a router app takes the click and the address stays real.
62
64
  - Without `href`, a card that responds to a click gets `cursor-pointer` from its
63
- `hover` and your `onClick`. Controls inside it should stop their own clicks
64
- from reaching the card.
65
+ `hover` and your `onClick` the selected one too, since it can open again;
66
+ the one being changed does not. Controls inside it should stop their own
67
+ clicks from reaching the card.
68
+ - Keep a card lit while its menu is open by passing `hovered` from your own
69
+ state: set it when the pointer enters, and clear it when the pointer leaves
70
+ and nothing of the card's is open.
65
71
 
66
72
  ## Props
67
73
 
@@ -75,6 +75,9 @@ export const InAFeed: Story = {
75
75
  ),
76
76
  }
77
77
 
78
+ /** Its menu is open: the pointer is on the menu, outside the card, and the card stays lit under it. */
79
+ export const HeldWhileItsMenuIsOpen: Story = { args: { hover: 'fill', quietUntilHover: true, hovered: true, onClick: () => {}, className: 'p-3' } }
80
+
78
81
  /** Being changed in place: the selected fill and the accent hairline. */
79
82
  export const Active: Story = { args: { active: true, hover: 'fill' } }
80
83
 
package/src/Card.test.tsx CHANGED
@@ -84,6 +84,47 @@ describe('Card', () => {
84
84
  expect(active.some((c) => c.startsWith('hover:'))).toBe(false)
85
85
  })
86
86
 
87
+ it('hovered holds the hover look without the pointer: a feed card stays lit, a link card keeps its stronger hairline', () => {
88
+ const feed = classesOf(card(<Card hover="fill" quietUntilHover hovered onClick={() => {}}>Item one</Card>))
89
+ expect(feed).toContain('bg-bg-hover')
90
+ expect(feed).toContain('border-border-default')
91
+ expect(feed).not.toContain('bg-bg-surface')
92
+ expect(feed).not.toContain('border-transparent')
93
+ cleanup()
94
+ const link = classesOf(card(<Card href="#" hovered>Item one</Card>))
95
+ expect(link).toContain('border-border-strong')
96
+ cleanup()
97
+ const inset = classesOf(card(<Card fill="inset" href="#" hovered>Item one</Card>))
98
+ expect(inset).toContain('border-border-default')
99
+ expect(inset).not.toContain('border-border-subtle')
100
+ cleanup()
101
+ // Nothing to hold on a card that does not answer the pointer.
102
+ const still = classesOf(card(<Card hovered>Item one</Card>))
103
+ expect(still).toContain('bg-bg-surface')
104
+ expect(still).not.toContain('bg-bg-hover')
105
+ })
106
+
107
+ it('hovered is ignored by the one you are on and the one being changed; attention still colours the hairline', () => {
108
+ const selected = classesOf(card(<Card hover="fill" hovered selected>Item one</Card>))
109
+ expect(selected).toContain('bg-bg-selected')
110
+ expect(selected).not.toContain('bg-bg-hover')
111
+ cleanup()
112
+ const active = classesOf(card(<Card hover="fill" hovered active>Item one</Card>))
113
+ expect(active).toContain('border-accent-primary')
114
+ expect(active).not.toContain('bg-bg-hover')
115
+ cleanup()
116
+ const urgent = classesOf(card(<Card hover="fill" quietUntilHover hovered attention="warning">Item one</Card>))
117
+ expect(urgent).toContain('bg-bg-hover')
118
+ expect(urgent).toContain('border-warning-muted')
119
+ expect(urgent).not.toContain('border-border-default')
120
+ })
121
+
122
+ it('the one you are on can still be clicked, and says so; the one being changed does not', () => {
123
+ expect(classesOf(card(<Card hover="fill" selected onClick={() => {}}>Item one</Card>))).toContain('cursor-pointer')
124
+ cleanup()
125
+ expect(classesOf(card(<Card hover="fill" active onClick={() => {}}>Item one</Card>))).not.toContain('cursor-pointer')
126
+ })
127
+
87
128
  it('attention recolours the hairline, at rest and on hover', () => {
88
129
  const classes = classesOf(card(<Card hover="fill" quietUntilHover attention="warning">Item one</Card>))
89
130
  expect(classes).toContain('border-warning-muted')
package/src/Card.tsx CHANGED
@@ -44,6 +44,14 @@ const HAIRLINE_HOVER_CLASSES: Record<CardFill, string> = {
44
44
  none: 'hover:border-border-default',
45
45
  }
46
46
 
47
+ /** The same step, held by `hovered` rather than by the pointer. */
48
+ const HAIRLINE_HELD_CLASSES: Record<CardFill, string> = {
49
+ surface: 'border-border-strong',
50
+ elevated: 'border-border-strong',
51
+ inset: 'border-border-default',
52
+ none: 'border-border-default',
53
+ }
54
+
47
55
  const ATTENTION_CLASSES: Record<CardAttention, string> = {
48
56
  accent: 'border-accent-muted hover:border-accent-muted',
49
57
  warning: 'border-warning-muted hover:border-warning-muted',
@@ -58,6 +66,12 @@ export interface CardProps extends ComponentPropsWithRef<'div'> {
58
66
  hover?: CardHover
59
67
  /** No hairline until it is pointed at — a row in a feed that shows its edge only when you are on it. */
60
68
  quietUntilHover?: boolean
69
+ /**
70
+ * Draw the hover look now, whatever the pointer does. For a card whose own menu or picker is open: the menu
71
+ * opens outside the card, so the pointer on it leaves the card, and the card would go dark under its own menu.
72
+ * Ignored while `selected` or `active`, like the pointer.
73
+ */
74
+ hovered?: boolean
61
75
  /** The one you are on: the selected fill, a subtle hairline, and no hover. */
62
76
  selected?: boolean
63
77
  /** Being changed in place: the selected fill with the accent hairline, and no hover. */
@@ -74,6 +88,7 @@ export function Card({
74
88
  href,
75
89
  hover = href ? 'hairline' : 'none',
76
90
  quietUntilHover = false,
91
+ hovered = false,
77
92
  selected = false,
78
93
  active = false,
79
94
  attention,
@@ -91,7 +106,10 @@ export function Card({
91
106
  quietUntilHover && 'border-transparent',
92
107
  !still && hover === 'hairline' && HAIRLINE_HOVER_CLASSES[fill],
93
108
  !still && hover === 'fill' && 'hover:bg-bg-hover hover:border-border-default',
94
- !still && hover !== 'none' && !href && props.onClick && 'cursor-pointer',
109
+ !still && hovered && hover === 'hairline' && HAIRLINE_HELD_CLASSES[fill],
110
+ !still && hovered && hover === 'fill' && 'bg-bg-hover border-border-default',
111
+ // The one you are on can still be clicked (it opens again); the one being changed cannot.
112
+ !active && hover !== 'none' && !href && props.onClick && 'cursor-pointer',
95
113
  selected && 'bg-bg-selected border-border-subtle',
96
114
  active && 'bg-bg-selected border-accent-primary',
97
115
  !active && attention && ATTENTION_CLASSES[attention],