@charcuterie/ui 4.7.1 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/Menu/Menu.d.ts +66 -5
- package/dist/Menu/Menu.d.ts.map +1 -1
- package/dist/Menu/Menu.js +25 -3
- package/dist/Menu/Menu.js.map +1 -1
- package/dist/Overlay/useAnchoredOverlay.d.ts +17 -2
- package/dist/Overlay/useAnchoredOverlay.d.ts.map +1 -1
- package/dist/Overlay/useAnchoredOverlay.js +30 -8
- package/dist/Overlay/useAnchoredOverlay.js.map +1 -1
- package/dist/ProgressBar/ProgressBar.d.ts +3 -1
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +15 -7
- package/dist/ProgressBar/ProgressBar.js.map +1 -1
- package/dist/ProgressCard/ProgressCard.d.ts +30 -0
- package/dist/ProgressCard/ProgressCard.d.ts.map +1 -0
- package/dist/ProgressCard/ProgressCard.js +15 -0
- package/dist/ProgressCard/ProgressCard.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Menu/Menu.mdx +36 -0
- package/src/Menu/Menu.stories.tsx +99 -1
- package/src/Menu/Menu.test.tsx +120 -0
- package/src/Menu/Menu.tsx +89 -4
- package/src/Overlay/useAnchoredOverlay.ts +53 -10
- package/src/ProgressBar/ProgressBar.tsx +54 -30
- package/src/ProgressCard/ProgressCard.mdx +43 -0
- package/src/ProgressCard/ProgressCard.stories.tsx +178 -0
- package/src/ProgressCard/ProgressCard.test.tsx +118 -0
- package/src/ProgressCard/ProgressCard.tsx +152 -0
- package/src/index.ts +12 -0
- package/src/sourceRules.test.ts +4 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
import * as ProgressCardStories from "./ProgressCard.stories";
|
|
3
|
+
|
|
4
|
+
<Meta of={ProgressCardStories} />
|
|
5
|
+
|
|
6
|
+
# ProgressCard
|
|
7
|
+
|
|
8
|
+
A progress-first card with a large value over a filled band and distinct metric blocks.
|
|
9
|
+
Use it for transfers, imports, backups, or other measurable work. `Card` owns the shell;
|
|
10
|
+
`ProgressBar` owns clamping, the named ARIA widget, and indeterminate motion.
|
|
11
|
+
|
|
12
|
+
<Canvas of={ProgressCardStories.Playground} />
|
|
13
|
+
|
|
14
|
+
## Ownership
|
|
15
|
+
|
|
16
|
+
The library owns layout, responsive typography, metric contrast, and whole-card intent.
|
|
17
|
+
The app supplies identity, media, formatted values, actions, and meaningful issue text.
|
|
18
|
+
Neither an absent diagnosis nor a numeric percentage implies a warning here.
|
|
19
|
+
|
|
20
|
+
`intent` colors the entire card; `progressIntent` colors the fill independently.
|
|
21
|
+
`valueText` is app-formatted text, while `value` and `max` drive measurable progress.
|
|
22
|
+
Set `isIndeterminate` when progress cannot yet be measured and use a state such as
|
|
23
|
+
"Preparing" for `valueText`. Set `isValueEmphasized={false}` for terminal state text.
|
|
24
|
+
The band overlay stays outside the progressbar role so its content retains semantics.
|
|
25
|
+
|
|
26
|
+
<Controls />
|
|
27
|
+
|
|
28
|
+
## Layouts and states
|
|
29
|
+
|
|
30
|
+
`band` is the default. `hierarchy` places the same headline above a thin progress track.
|
|
31
|
+
Metrics stack when their own content area is narrow, even if media makes the whole card
|
|
32
|
+
wide. The viewport does not decide this. Labels should be unique.
|
|
33
|
+
|
|
34
|
+
<Canvas of={ProgressCardStories.AllVariants} />
|
|
35
|
+
<Canvas of={ProgressCardStories.AllStates} />
|
|
36
|
+
<Canvas of={ProgressCardStories.Responsive} />
|
|
37
|
+
|
|
38
|
+
## App-owned controls
|
|
39
|
+
|
|
40
|
+
Action slots use the same controls as the rest of the app. The card does not define cancel,
|
|
41
|
+
retry, or confirmation policy. Additional issue details belong in `children`.
|
|
42
|
+
|
|
43
|
+
<Canvas of={ProgressCardStories.Interactive} />
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
|
|
5
|
+
import { intentArgType } from "../argTypes.storyHelpers.ts"
|
|
6
|
+
import { Button } from "../Button/Button.tsx"
|
|
7
|
+
import {
|
|
8
|
+
ContainerBoard,
|
|
9
|
+
StoryCell,
|
|
10
|
+
StoryGrid,
|
|
11
|
+
} from "../board.storyHelpers.tsx"
|
|
12
|
+
import { ProgressCard } from "./ProgressCard.tsx"
|
|
13
|
+
|
|
14
|
+
const metrics = [
|
|
15
|
+
{ label: "Speed", value: "17.2 MB/s" },
|
|
16
|
+
{ label: "Remaining", value: "9m" },
|
|
17
|
+
{ label: "Finishes", value: "4:44 AM" },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const meta = {
|
|
21
|
+
title: "Components/Feedback/ProgressCard",
|
|
22
|
+
component: ProgressCard,
|
|
23
|
+
parameters: { layout: "padded" },
|
|
24
|
+
argTypes: {
|
|
25
|
+
intent: intentArgType,
|
|
26
|
+
progressIntent: intentArgType,
|
|
27
|
+
},
|
|
28
|
+
args: {
|
|
29
|
+
"aria-label": "Archive transfer",
|
|
30
|
+
header: "Archive transfer",
|
|
31
|
+
layout: "band",
|
|
32
|
+
metrics,
|
|
33
|
+
progressLabel: "Transfer progress",
|
|
34
|
+
status: "Transferring",
|
|
35
|
+
value: 82.5,
|
|
36
|
+
valueText: "82.5%",
|
|
37
|
+
},
|
|
38
|
+
} satisfies Meta<typeof ProgressCard>
|
|
39
|
+
|
|
40
|
+
export default meta
|
|
41
|
+
type Story = StoryObj<typeof meta>
|
|
42
|
+
|
|
43
|
+
export const Playground: Story = {
|
|
44
|
+
parameters: playgroundParameters,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const AllVariants: Story = {
|
|
48
|
+
render: (props) => (
|
|
49
|
+
<StoryGrid columns={2}>
|
|
50
|
+
{(["band", "hierarchy"] as const).map((layout) => (
|
|
51
|
+
<StoryCell
|
|
52
|
+
align="stretch"
|
|
53
|
+
key={layout}
|
|
54
|
+
label={layout}
|
|
55
|
+
>
|
|
56
|
+
<ProgressCard
|
|
57
|
+
{...props}
|
|
58
|
+
aria-label={`Transfer ${layout}`}
|
|
59
|
+
layout={layout}
|
|
60
|
+
/>
|
|
61
|
+
</StoryCell>
|
|
62
|
+
))}
|
|
63
|
+
</StoryGrid>
|
|
64
|
+
),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const AllStates: Story = {
|
|
68
|
+
render: (props) => (
|
|
69
|
+
<StoryGrid columns={2}>
|
|
70
|
+
<StoryCell align="stretch" label="Preparing">
|
|
71
|
+
<ProgressCard
|
|
72
|
+
{...props}
|
|
73
|
+
aria-label="Preparing transfer"
|
|
74
|
+
isIndeterminate
|
|
75
|
+
isValueEmphasized={false}
|
|
76
|
+
metrics={[]}
|
|
77
|
+
status={undefined}
|
|
78
|
+
valueText="Preparing"
|
|
79
|
+
/>
|
|
80
|
+
</StoryCell>
|
|
81
|
+
<StoryCell align="stretch" label="Warning">
|
|
82
|
+
<ProgressCard
|
|
83
|
+
{...props}
|
|
84
|
+
aria-label="Transfer warning"
|
|
85
|
+
intent="warning"
|
|
86
|
+
progressIntent="warning"
|
|
87
|
+
status="Needs attention"
|
|
88
|
+
>
|
|
89
|
+
<p className="mt-2 text-intent-warning-content text-sm">
|
|
90
|
+
The source stopped responding.
|
|
91
|
+
</p>
|
|
92
|
+
</ProgressCard>
|
|
93
|
+
</StoryCell>
|
|
94
|
+
<StoryCell align="stretch" label="Failed">
|
|
95
|
+
<ProgressCard
|
|
96
|
+
{...props}
|
|
97
|
+
aria-label="Failed transfer"
|
|
98
|
+
intent="danger"
|
|
99
|
+
isValueEmphasized={false}
|
|
100
|
+
metrics={[]}
|
|
101
|
+
progressIntent="danger"
|
|
102
|
+
status={undefined}
|
|
103
|
+
valueText="Failed"
|
|
104
|
+
/>
|
|
105
|
+
</StoryCell>
|
|
106
|
+
<StoryCell align="stretch" label="Complete">
|
|
107
|
+
<ProgressCard
|
|
108
|
+
{...props}
|
|
109
|
+
aria-label="Complete transfer"
|
|
110
|
+
isValueEmphasized={false}
|
|
111
|
+
metrics={[]}
|
|
112
|
+
progressIntent="success"
|
|
113
|
+
status={undefined}
|
|
114
|
+
value={100}
|
|
115
|
+
valueText="Complete"
|
|
116
|
+
/>
|
|
117
|
+
</StoryCell>
|
|
118
|
+
</StoryGrid>
|
|
119
|
+
),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const Responsive: Story = {
|
|
123
|
+
render: (props) => (
|
|
124
|
+
<ContainerBoard>
|
|
125
|
+
{(width) => (
|
|
126
|
+
<ProgressCard
|
|
127
|
+
{...props}
|
|
128
|
+
aria-label={`Transfer at ${width}`}
|
|
129
|
+
header="ARCHIVE_TRANSFER_WITH_A_VERY_LONG_UNBROKEN_NAME"
|
|
130
|
+
media={
|
|
131
|
+
<span className="rounded-md bg-surface-sunken p-2">
|
|
132
|
+
07
|
|
133
|
+
</span>
|
|
134
|
+
}
|
|
135
|
+
actions={
|
|
136
|
+
<Button
|
|
137
|
+
appearance="outline"
|
|
138
|
+
intent="danger"
|
|
139
|
+
size="sm"
|
|
140
|
+
>
|
|
141
|
+
Cancel
|
|
142
|
+
</Button>
|
|
143
|
+
}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
</ContainerBoard>
|
|
147
|
+
),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const ControlledProgressCard = () => {
|
|
151
|
+
const [isCanceled, setIsCanceled] = useState(false)
|
|
152
|
+
return (
|
|
153
|
+
<ProgressCard
|
|
154
|
+
aria-label="Cancelable transfer"
|
|
155
|
+
header="Archive transfer"
|
|
156
|
+
actions={
|
|
157
|
+
<Button
|
|
158
|
+
appearance="outline"
|
|
159
|
+
intent="danger"
|
|
160
|
+
isDisabled={isCanceled}
|
|
161
|
+
onClick={() => setIsCanceled(true)}
|
|
162
|
+
>
|
|
163
|
+
Cancel
|
|
164
|
+
</Button>
|
|
165
|
+
}
|
|
166
|
+
isValueEmphasized={!isCanceled}
|
|
167
|
+
metrics={isCanceled ? [] : metrics}
|
|
168
|
+
progressLabel="Transfer progress"
|
|
169
|
+
status={isCanceled ? undefined : "Transferring"}
|
|
170
|
+
value={82.5}
|
|
171
|
+
valueText={isCanceled ? "Canceled" : "82.5%"}
|
|
172
|
+
/>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const Interactive: Story = {
|
|
177
|
+
render: () => <ControlledProgressCard />,
|
|
178
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { composeStories } from "@storybook/react"
|
|
2
|
+
import { expect, userEvent, within } from "storybook/test"
|
|
3
|
+
import { test } from "vitest"
|
|
4
|
+
|
|
5
|
+
import { mountStory } from "../mountStory.testHelpers.ts"
|
|
6
|
+
import { expectAgentDrivable } from "../testing/index.ts"
|
|
7
|
+
import {
|
|
8
|
+
DESKTOP,
|
|
9
|
+
setViewport,
|
|
10
|
+
} from "../viewport.testHelpers.ts"
|
|
11
|
+
import * as stories from "./ProgressCard.stories.tsx"
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
Playground,
|
|
15
|
+
AllVariants,
|
|
16
|
+
AllStates,
|
|
17
|
+
Responsive,
|
|
18
|
+
Interactive,
|
|
19
|
+
} = composeStories(stories)
|
|
20
|
+
|
|
21
|
+
test("the large value overlays a real named progressbar and the metrics stay outside it", async () => {
|
|
22
|
+
const { canvas } = await mountStory(Playground)
|
|
23
|
+
const card = expectAgentDrivable(canvas, {
|
|
24
|
+
role: "region",
|
|
25
|
+
name: "Archive transfer",
|
|
26
|
+
})
|
|
27
|
+
const bar = within(card).getByRole("progressbar", {
|
|
28
|
+
name: "Transfer progress",
|
|
29
|
+
})
|
|
30
|
+
const percentage = within(card).getByText("82.5%")
|
|
31
|
+
await expect(bar).toHaveAttribute("aria-valuenow", "82.5")
|
|
32
|
+
await expect(bar).not.toContainElement(percentage)
|
|
33
|
+
await expect(
|
|
34
|
+
bar.getBoundingClientRect().height,
|
|
35
|
+
).toBeGreaterThan(50)
|
|
36
|
+
await expect(
|
|
37
|
+
bar.getBoundingClientRect().top,
|
|
38
|
+
).toBeLessThan(percentage.getBoundingClientRect().top)
|
|
39
|
+
await expect(
|
|
40
|
+
within(card).getAllByRole("term"),
|
|
41
|
+
).toHaveLength(3)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test("both layouts preserve progress semantics", async () => {
|
|
45
|
+
const { canvas } = await mountStory(AllVariants)
|
|
46
|
+
for (const layout of ["band", "hierarchy"]) {
|
|
47
|
+
const card = expectAgentDrivable(canvas, {
|
|
48
|
+
role: "region",
|
|
49
|
+
name: `Transfer ${layout}`,
|
|
50
|
+
})
|
|
51
|
+
await expect(
|
|
52
|
+
within(card).getByRole("progressbar"),
|
|
53
|
+
).toHaveAttribute("aria-valuenow", "82.5")
|
|
54
|
+
await expect(
|
|
55
|
+
within(card).getByText("82.5%"),
|
|
56
|
+
).toBeVisible()
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("issues tint the complete card and indeterminate progress has no invented value", async () => {
|
|
61
|
+
const { canvas } = await mountStory(AllStates)
|
|
62
|
+
const warning = expectAgentDrivable(canvas, {
|
|
63
|
+
role: "region",
|
|
64
|
+
name: "Transfer warning",
|
|
65
|
+
})
|
|
66
|
+
await expect(warning).toHaveClass(
|
|
67
|
+
"bg-intent-warning-surface",
|
|
68
|
+
)
|
|
69
|
+
await expect(
|
|
70
|
+
canvas.getByRole("region", { name: "Failed transfer" }),
|
|
71
|
+
).toHaveClass("bg-intent-danger-surface")
|
|
72
|
+
const preparing = canvas.getByRole("region", {
|
|
73
|
+
name: "Preparing transfer",
|
|
74
|
+
})
|
|
75
|
+
await expect(
|
|
76
|
+
within(preparing).getByRole("progressbar"),
|
|
77
|
+
).not.toHaveAttribute("aria-valuenow")
|
|
78
|
+
await expect(
|
|
79
|
+
within(preparing).queryByRole("term"),
|
|
80
|
+
).not.toBeInTheDocument()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test("the card fits each container width with a long title, media, and actions", async () => {
|
|
84
|
+
await setViewport(DESKTOP)
|
|
85
|
+
const { canvas } = await mountStory(Responsive)
|
|
86
|
+
for (const width of ["15rem", "24rem", "34rem"]) {
|
|
87
|
+
const card = expectAgentDrivable(canvas, {
|
|
88
|
+
role: "region",
|
|
89
|
+
name: `Transfer at ${width}`,
|
|
90
|
+
})
|
|
91
|
+
await expect(card.scrollWidth).toBeLessThanOrEqual(
|
|
92
|
+
card.clientWidth,
|
|
93
|
+
)
|
|
94
|
+
const metrics = card.querySelector("dl") as HTMLElement
|
|
95
|
+
await expect(
|
|
96
|
+
getComputedStyle(metrics).gridTemplateColumns.split(
|
|
97
|
+
" ",
|
|
98
|
+
),
|
|
99
|
+
).toHaveLength(width === "34rem" ? 3 : 1)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test("the app action remains keyboard-operable above the progress band", async () => {
|
|
104
|
+
const { canvas } = await mountStory(Interactive)
|
|
105
|
+
const card = expectAgentDrivable(canvas, {
|
|
106
|
+
role: "region",
|
|
107
|
+
name: "Cancelable transfer",
|
|
108
|
+
})
|
|
109
|
+
const cancel = within(card).getByRole("button", {
|
|
110
|
+
name: "Cancel",
|
|
111
|
+
})
|
|
112
|
+
cancel.focus()
|
|
113
|
+
await userEvent.keyboard("{Enter}")
|
|
114
|
+
await expect(
|
|
115
|
+
within(card).getByText("Canceled"),
|
|
116
|
+
).toBeVisible()
|
|
117
|
+
await expect(cancel).toBeDisabled()
|
|
118
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { IntentName } from "@charcuterie/tokens"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
|
|
4
|
+
import { Card, type CardProps } from "../Card/Card.tsx"
|
|
5
|
+
import { INTENT_APPEARANCE_CLASS } from "../intentStyles.ts"
|
|
6
|
+
import { ProgressBar } from "../ProgressBar/ProgressBar.tsx"
|
|
7
|
+
import { toClassName } from "../toClassName.ts"
|
|
8
|
+
|
|
9
|
+
export type ProgressCardMetric = {
|
|
10
|
+
label: string
|
|
11
|
+
value: ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ProgressCardLayout = "band" | "hierarchy"
|
|
15
|
+
|
|
16
|
+
export type ProgressCardProps = Omit<
|
|
17
|
+
CardProps,
|
|
18
|
+
"heading" | "footer"
|
|
19
|
+
> & {
|
|
20
|
+
"aria-label": string
|
|
21
|
+
/** App-owned identity: a title, an identifier, or a format mark. */
|
|
22
|
+
header: ReactNode
|
|
23
|
+
/** Whole-card intent is independent of the progress fill. */
|
|
24
|
+
intent?: IntentName
|
|
25
|
+
isIndeterminate?: boolean
|
|
26
|
+
isValueEmphasized?: boolean
|
|
27
|
+
layout?: ProgressCardLayout
|
|
28
|
+
max?: number
|
|
29
|
+
media?: ReactNode
|
|
30
|
+
metrics?: readonly ProgressCardMetric[]
|
|
31
|
+
progressIntent?: IntentName
|
|
32
|
+
progressLabel: string
|
|
33
|
+
status?: string
|
|
34
|
+
value?: number
|
|
35
|
+
/** App-formatted percentage or terminal state; never inferred from intent. */
|
|
36
|
+
valueText: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A progress-first card. The app owns state, formatting, actions, and diagnosis. */
|
|
40
|
+
export const ProgressCard = ({
|
|
41
|
+
actions,
|
|
42
|
+
children,
|
|
43
|
+
className,
|
|
44
|
+
header,
|
|
45
|
+
intent = "neutral",
|
|
46
|
+
isIndeterminate = false,
|
|
47
|
+
isValueEmphasized = true,
|
|
48
|
+
layout = "band",
|
|
49
|
+
max,
|
|
50
|
+
media,
|
|
51
|
+
metrics = [],
|
|
52
|
+
progressIntent = "accent",
|
|
53
|
+
progressLabel,
|
|
54
|
+
status,
|
|
55
|
+
value,
|
|
56
|
+
valueText,
|
|
57
|
+
...cardProps
|
|
58
|
+
}: ProgressCardProps): ReactNode => {
|
|
59
|
+
const headline = (
|
|
60
|
+
<div
|
|
61
|
+
className={toClassName(
|
|
62
|
+
"relative flex flex-wrap items-end justify-between gap-2",
|
|
63
|
+
layout === "band" ? "p-3" : "mb-3",
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
{status ? (
|
|
67
|
+
<span className="min-w-0 wrap-anywhere font-medium text-content-secondary text-sm">
|
|
68
|
+
{status}
|
|
69
|
+
</span>
|
|
70
|
+
) : null}
|
|
71
|
+
<span
|
|
72
|
+
className={toClassName(
|
|
73
|
+
"min-w-0 wrap-anywhere text-end tabular-nums",
|
|
74
|
+
isValueEmphasized
|
|
75
|
+
? "font-bold text-4xl leading-none tracking-tight cq-sm:text-[3.25rem]"
|
|
76
|
+
: "font-semibold text-xl",
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
79
|
+
{valueText}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Card
|
|
86
|
+
{...cardProps}
|
|
87
|
+
className={toClassName(
|
|
88
|
+
"relative rounded-2xl",
|
|
89
|
+
intent === "neutral"
|
|
90
|
+
? undefined
|
|
91
|
+
: INTENT_APPEARANCE_CLASS[intent].soft,
|
|
92
|
+
"text-content-primary",
|
|
93
|
+
className,
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
<div className="flex gap-3.5">
|
|
97
|
+
{media ? (
|
|
98
|
+
<div className="flex shrink-0 flex-col items-start gap-2">
|
|
99
|
+
{media}
|
|
100
|
+
</div>
|
|
101
|
+
) : null}
|
|
102
|
+
<div className="min-w-0 flex-1">
|
|
103
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
104
|
+
<div className="min-w-0 flex-1 basis-40 wrap-anywhere">
|
|
105
|
+
{header}
|
|
106
|
+
</div>
|
|
107
|
+
{actions ? (
|
|
108
|
+
<div className="relative z-10 flex flex-wrap items-center gap-2">
|
|
109
|
+
{actions}
|
|
110
|
+
</div>
|
|
111
|
+
) : null}
|
|
112
|
+
</div>
|
|
113
|
+
<div className="mt-5">
|
|
114
|
+
{layout === "hierarchy" ? headline : null}
|
|
115
|
+
<ProgressBar
|
|
116
|
+
intent={progressIntent}
|
|
117
|
+
isIndeterminate={isIndeterminate}
|
|
118
|
+
label={progressLabel}
|
|
119
|
+
max={max}
|
|
120
|
+
overlay={
|
|
121
|
+
layout === "band" ? headline : undefined
|
|
122
|
+
}
|
|
123
|
+
value={value}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
{metrics.length > 0 ? (
|
|
127
|
+
<div className="@container">
|
|
128
|
+
<dl className="mt-3 grid grid-cols-1 gap-2 cq-sm:grid-cols-3">
|
|
129
|
+
{metrics.map(
|
|
130
|
+
({ label, value: metricValue }) => (
|
|
131
|
+
<div
|
|
132
|
+
className="flex items-baseline justify-between gap-2 rounded-lg border border-border-subtle bg-surface-sunken/70 px-2.5 py-2 cq-sm:block"
|
|
133
|
+
key={label}
|
|
134
|
+
>
|
|
135
|
+
<dt className="min-w-0 wrap-anywhere font-semibold text-content-muted text-xs uppercase tracking-wide">
|
|
136
|
+
{label}
|
|
137
|
+
</dt>
|
|
138
|
+
<dd className="m-0 min-w-0 wrap-anywhere font-semibold text-lg tabular-nums cq-sm:mt-1">
|
|
139
|
+
{metricValue ?? "—"}
|
|
140
|
+
</dd>
|
|
141
|
+
</div>
|
|
142
|
+
),
|
|
143
|
+
)}
|
|
144
|
+
</dl>
|
|
145
|
+
</div>
|
|
146
|
+
) : null}
|
|
147
|
+
{children}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</Card>
|
|
151
|
+
)
|
|
152
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -334,6 +334,12 @@ export type {
|
|
|
334
334
|
NavBarProps,
|
|
335
335
|
} from "./NavBar/NavBar.tsx"
|
|
336
336
|
export { NavBar } from "./NavBar/NavBar.tsx"
|
|
337
|
+
/**
|
|
338
|
+
* What a `Menu` in anchor mode hangs off: an element, or a virtual
|
|
339
|
+
* one that is only a rectangle. A context menu passes the second,
|
|
340
|
+
* holding the point that was pressed.
|
|
341
|
+
*/
|
|
342
|
+
export type { AnchoredOverlayAnchor } from "./Overlay/useAnchoredOverlay.ts"
|
|
337
343
|
export type {
|
|
338
344
|
PickerOption,
|
|
339
345
|
PickerProps,
|
|
@@ -358,6 +364,12 @@ export {
|
|
|
358
364
|
toProgressPercent,
|
|
359
365
|
toProgressValue,
|
|
360
366
|
} from "./ProgressBar/progressValue.ts"
|
|
367
|
+
export type {
|
|
368
|
+
ProgressCardLayout,
|
|
369
|
+
ProgressCardMetric,
|
|
370
|
+
ProgressCardProps,
|
|
371
|
+
} from "./ProgressCard/ProgressCard.tsx"
|
|
372
|
+
export { ProgressCard } from "./ProgressCard/ProgressCard.tsx"
|
|
361
373
|
export type {
|
|
362
374
|
QueryBuilderLabels,
|
|
363
375
|
QueryBuilderProps,
|
package/src/sourceRules.test.ts
CHANGED
|
@@ -251,6 +251,8 @@ test("a container-query component is never storied in a shrink-to-fit cell", ()
|
|
|
251
251
|
// answer neither, because a track is about the same width in
|
|
252
252
|
// both.
|
|
253
253
|
"PortraitTiles",
|
|
254
|
+
// Metrics query the space remaining beside the media, not the entire card.
|
|
255
|
+
"ProgressCard",
|
|
254
256
|
// `Stepper`'s container is the one that decides whether a
|
|
255
257
|
// `horizontal` sequence is actually horizontal. Below `--cq-md`
|
|
256
258
|
// it falls back to the vertical column, because four steps side
|
|
@@ -612,7 +614,8 @@ test("the barrel is the only place components are re-exported", async () => {
|
|
|
612
614
|
// consumer deciding whether an item is on screen must ask the
|
|
613
615
|
// question the same way the bars are placed, or the two answers
|
|
614
616
|
// drift by a day and nothing says so.
|
|
615
|
-
|
|
617
|
+
// ProgressCard composes Card and ProgressBar for progress-first dashboards.
|
|
618
|
+
expect(componentNames.length).toBe(67)
|
|
616
619
|
|
|
617
620
|
for (const name of componentNames) {
|
|
618
621
|
expect(barrel).toContain(`export { ${name} }`)
|