@tonycodes-ui/mortar 0.0.0-stage → 0.11.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/AGENTS.md +10 -0
- package/CHANGELOG.md +276 -0
- package/CONTRIBUTING.md +48 -0
- package/DESIGN.md +51 -0
- package/LICENSE +21 -0
- package/README.md +155 -2
- package/TESTING.md +83 -0
- package/baseline-policy.json +16 -0
- package/bin/mortar.mjs +258 -0
- package/catalog.json +9703 -0
- package/cli/audit.mjs +943 -0
- package/cli/data.mjs +176 -0
- package/cli/mcp.mjs +216 -0
- package/cli/migrations.mjs +128 -0
- package/cli/packet.mjs +267 -0
- package/cli/search.mjs +246 -0
- package/cli/tools.mjs +229 -0
- package/design-principles.json +30 -0
- package/dist/agent-data.json +1 -0
- package/dist/application.d.ts +340 -0
- package/dist/application.js +240 -0
- package/dist/auth.d.ts +121 -0
- package/dist/auth.js +97 -0
- package/dist/brand.d.ts +69 -0
- package/dist/brand.js +266 -0
- package/dist/calendar.d.ts +75 -0
- package/dist/calendar.js +310 -0
- package/dist/choice.d.ts +36 -0
- package/dist/choice.js +36 -0
- package/dist/composer.d.ts +28 -0
- package/dist/composer.js +47 -0
- package/dist/display.d.ts +136 -0
- package/dist/display.js +75 -0
- package/dist/docs.d.ts +145 -0
- package/dist/docs.js +141 -0
- package/dist/ecommerce.d.ts +261 -0
- package/dist/ecommerce.js +177 -0
- package/dist/feedback.d.ts +80 -0
- package/dist/feedback.js +98 -0
- package/dist/foundation.d.ts +42 -0
- package/dist/foundation.js +121 -0
- package/dist/heading.d.ts +5 -0
- package/dist/heading.js +9 -0
- package/dist/icons.d.ts +50 -0
- package/dist/icons.js +70 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +17 -0
- package/dist/inspection.d.ts +24 -0
- package/dist/inspection.js +14 -0
- package/dist/internal.d.ts +492 -0
- package/dist/internal.js +273 -0
- package/dist/layout.d.ts +107 -0
- package/dist/layout.js +66 -0
- package/dist/marketing-sections.d.ts +161 -0
- package/dist/marketing-sections.js +58 -0
- package/dist/marketing.d.ts +254 -0
- package/dist/marketing.js +150 -0
- package/dist/navigation.d.ts +35 -0
- package/dist/navigation.js +44 -0
- package/dist/primitives.d.ts +258 -0
- package/dist/primitives.js +244 -0
- package/dist/showcase.d.ts +29 -0
- package/dist/showcase.js +10 -0
- package/dist/workspace.d.ts +80 -0
- package/dist/workspace.js +48 -0
- package/docs/agents.md +127 -0
- package/docs/auth-flows.md +71 -0
- package/docs/migrating-to-0.6.md +94 -0
- package/docs/theming.md +170 -0
- package/package.json +94 -4
- package/skills/mortar/SKILL.md +36 -0
- package/src/application.css +1487 -0
- package/src/application.tsx +1413 -0
- package/src/auth.tsx +452 -0
- package/src/brand.ts +335 -0
- package/src/calendar.css +468 -0
- package/src/calendar.tsx +868 -0
- package/src/choice.tsx +240 -0
- package/src/composer.tsx +158 -0
- package/src/data.css +690 -0
- package/src/display.tsx +452 -0
- package/src/docs.css +704 -0
- package/src/docs.tsx +737 -0
- package/src/ecommerce.css +810 -0
- package/src/ecommerce.tsx +1033 -0
- package/src/feedback.tsx +388 -0
- package/src/foundation.css +1867 -0
- package/src/foundation.tsx +227 -0
- package/src/heading.ts +13 -0
- package/src/icons.tsx +237 -0
- package/src/index.ts +230 -0
- package/src/inspection.tsx +91 -0
- package/src/internal.tsx +614 -0
- package/src/layout.tsx +342 -0
- package/src/marketing-sections.tsx +621 -0
- package/src/marketing.css +2400 -0
- package/src/marketing.tsx +1156 -0
- package/src/navigation.tsx +118 -0
- package/src/overlays.css +350 -0
- package/src/primitives.tsx +1135 -0
- package/src/showcase.tsx +120 -0
- package/src/tokens.css +449 -0
- package/src/workspace.tsx +333 -0
- package/styles.css +56 -0
package/dist/calendar.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { SegmentedControl } from "./inspection.js";
|
|
4
|
+
import { ChevronLeft, ChevronRight } from "./icons.js";
|
|
5
|
+
import { childLevel, headingTag } from "./heading.js";
|
|
6
|
+
import { componentRoot, cx } from "./internal.js";
|
|
7
|
+
// Dates are YYYY-MM-DD strings handled in UTC, so no local time zone can shift a day.
|
|
8
|
+
const DAY = 86_400_000;
|
|
9
|
+
const toTime = (date) => {
|
|
10
|
+
const [y, m, d] = date.split("-").map(Number);
|
|
11
|
+
return Date.UTC(y, m - 1, d);
|
|
12
|
+
};
|
|
13
|
+
const toDate = (time) => new Date(time).toISOString().slice(0, 10);
|
|
14
|
+
const addDays = (date, days) => toDate(toTime(date) + days * DAY);
|
|
15
|
+
function addMonths(date, months) {
|
|
16
|
+
const [y, m, d] = date.split("-").map(Number);
|
|
17
|
+
const first = new Date(Date.UTC(y, m - 1 + months, 1));
|
|
18
|
+
const last = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + 1, 0)).getUTCDate();
|
|
19
|
+
return toDate(Date.UTC(first.getUTCFullYear(), first.getUTCMonth(), Math.min(d, last)));
|
|
20
|
+
}
|
|
21
|
+
const monthOf = (date) => date.slice(0, 7);
|
|
22
|
+
/** Column of a date in a week starting on `weekStartsOn` (0 = Sunday). */
|
|
23
|
+
const weekdayIndex = (date, weekStartsOn) => (new Date(toTime(date)).getUTCDay() - weekStartsOn + 7) % 7;
|
|
24
|
+
/** A day button's name: the full date, plus how many events it has. */
|
|
25
|
+
const dayLabel = (name, count) => count ? `${name}, ${count} ${count === 1 ? "event" : "events"}` : name;
|
|
26
|
+
const toMinutes = (time) => {
|
|
27
|
+
const [h, m] = time.split(":").map(Number);
|
|
28
|
+
return h * 60 + (m || 0);
|
|
29
|
+
};
|
|
30
|
+
/** Shortest drawn event, in minutes, so an event at the edge stays visible. */
|
|
31
|
+
const MIN_DRAWN = 30;
|
|
32
|
+
/**
|
|
33
|
+
* Place timed events between `min` and `max` minutes and assign overlapping
|
|
34
|
+
* ones to side-by-side lanes. Events that start before `min` or end after
|
|
35
|
+
* `max` are drawn at the edge and flagged as clipped.
|
|
36
|
+
*/
|
|
37
|
+
function layout(events, min, max) {
|
|
38
|
+
const timed = events
|
|
39
|
+
.filter((e) => e.start)
|
|
40
|
+
.map((e) => {
|
|
41
|
+
const start = toMinutes(e.start);
|
|
42
|
+
const end = e.end ? toMinutes(e.end) : start + 60;
|
|
43
|
+
const top = Math.min(Math.max(start, min), max - MIN_DRAWN);
|
|
44
|
+
const bottom = Math.max(Math.min(end, max), top + MIN_DRAWN);
|
|
45
|
+
return {
|
|
46
|
+
event: e,
|
|
47
|
+
top,
|
|
48
|
+
bottom,
|
|
49
|
+
clippedStart: start < min,
|
|
50
|
+
clippedEnd: end > max,
|
|
51
|
+
};
|
|
52
|
+
})
|
|
53
|
+
.sort((a, b) => a.top - b.top || a.bottom - b.bottom);
|
|
54
|
+
const out = [];
|
|
55
|
+
let cluster = [];
|
|
56
|
+
let laneEnds = [];
|
|
57
|
+
let clusterEnd = -1;
|
|
58
|
+
const flush = () => {
|
|
59
|
+
const lanes = Math.max(1, ...cluster.map((c) => c.lane + 1));
|
|
60
|
+
cluster.forEach((c) => (c.lanes = lanes));
|
|
61
|
+
out.push(...cluster);
|
|
62
|
+
cluster = [];
|
|
63
|
+
laneEnds = [];
|
|
64
|
+
clusterEnd = -1;
|
|
65
|
+
};
|
|
66
|
+
for (const item of timed) {
|
|
67
|
+
if (cluster.length && item.top >= clusterEnd)
|
|
68
|
+
flush();
|
|
69
|
+
let lane = laneEnds.findIndex((end) => end <= item.top);
|
|
70
|
+
if (lane < 0) {
|
|
71
|
+
lane = laneEnds.length;
|
|
72
|
+
laneEnds.push(item.bottom);
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
laneEnds[lane] = item.bottom;
|
|
76
|
+
cluster.push({ ...item, lane, lanes: 1 });
|
|
77
|
+
clusterEnd = Math.max(clusterEnd, item.bottom);
|
|
78
|
+
}
|
|
79
|
+
if (cluster.length)
|
|
80
|
+
flush();
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function localToday() {
|
|
84
|
+
const now = new Date();
|
|
85
|
+
return toDate(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Calendar with year, month, week and day views. One current date (`date`)
|
|
89
|
+
* is shared by every view, so moving through months and then switching to
|
|
90
|
+
* the week view shows a week of the month you reached. The month and year
|
|
91
|
+
* views are grids with keyboard navigation; the week and day views place
|
|
92
|
+
* timed events on an hour grid.
|
|
93
|
+
*/
|
|
94
|
+
export function Calendar({ month: controlledMonth, onMonthChange, date: controlledDate, onDateChange, selected, onSelect, events = [], variant = "compact", weekStartsOn = 1, today: todayProp, locale = "en-GB", maxEvents = 2, view = "month", onViewChange, onEventSelect, agenda = false, dayStartHour = 8, dayEndHour = 19, headingLevel = 2, id, className, }) {
|
|
95
|
+
const today = todayProp || localToday();
|
|
96
|
+
const [ownDate, setOwnDate] = useState(() => {
|
|
97
|
+
const start = selected || today;
|
|
98
|
+
return controlledMonth && monthOf(start) !== controlledMonth
|
|
99
|
+
? `${controlledMonth}-01`
|
|
100
|
+
: start;
|
|
101
|
+
});
|
|
102
|
+
// Follow a changed `selected` or `month` prop when the date is uncontrolled.
|
|
103
|
+
const [seen, setSeen] = useState({ selected, month: controlledMonth });
|
|
104
|
+
if (seen.selected !== selected || seen.month !== controlledMonth) {
|
|
105
|
+
setSeen({ selected, month: controlledMonth });
|
|
106
|
+
if (controlledDate === undefined) {
|
|
107
|
+
let next = selected && selected !== seen.selected ? selected : ownDate;
|
|
108
|
+
if (controlledMonth && monthOf(next) !== controlledMonth)
|
|
109
|
+
next = `${controlledMonth}-01`;
|
|
110
|
+
if (next !== ownDate)
|
|
111
|
+
setOwnDate(next);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const date = controlledDate ?? ownDate;
|
|
115
|
+
const month = controlledMonth ?? monthOf(date);
|
|
116
|
+
const focusDate = monthOf(date) === month ? date : `${month}-01`;
|
|
117
|
+
const moved = useRef(false);
|
|
118
|
+
const grid = useRef(null);
|
|
119
|
+
const headingId = useId();
|
|
120
|
+
const Heading = headingTag(headingLevel);
|
|
121
|
+
const SubHeading = headingTag(childLevel(headingLevel));
|
|
122
|
+
const rootClass = cx("m-calendar", className);
|
|
123
|
+
const formats = useMemo(() => ({
|
|
124
|
+
title: new Intl.DateTimeFormat(locale, {
|
|
125
|
+
month: "long",
|
|
126
|
+
year: "numeric",
|
|
127
|
+
timeZone: "UTC",
|
|
128
|
+
}),
|
|
129
|
+
day: new Intl.DateTimeFormat(locale, {
|
|
130
|
+
weekday: "long",
|
|
131
|
+
day: "numeric",
|
|
132
|
+
month: "long",
|
|
133
|
+
year: "numeric",
|
|
134
|
+
timeZone: "UTC",
|
|
135
|
+
}),
|
|
136
|
+
short: new Intl.DateTimeFormat(locale, {
|
|
137
|
+
weekday: "short",
|
|
138
|
+
timeZone: "UTC",
|
|
139
|
+
}),
|
|
140
|
+
long: new Intl.DateTimeFormat(locale, {
|
|
141
|
+
weekday: "long",
|
|
142
|
+
timeZone: "UTC",
|
|
143
|
+
}),
|
|
144
|
+
narrow: new Intl.DateTimeFormat(locale, {
|
|
145
|
+
weekday: "narrow",
|
|
146
|
+
timeZone: "UTC",
|
|
147
|
+
}),
|
|
148
|
+
monthName: new Intl.DateTimeFormat(locale, {
|
|
149
|
+
month: "long",
|
|
150
|
+
timeZone: "UTC",
|
|
151
|
+
}),
|
|
152
|
+
}), [locale]);
|
|
153
|
+
const byDay = useMemo(() => {
|
|
154
|
+
const map = new Map();
|
|
155
|
+
for (const event of events)
|
|
156
|
+
map.set(event.date, [...(map.get(event.date) || []), event]);
|
|
157
|
+
return map;
|
|
158
|
+
}, [events]);
|
|
159
|
+
const first = `${month}-01`;
|
|
160
|
+
const start = addDays(first, -weekdayIndex(first, weekStartsOn));
|
|
161
|
+
const weeks = Array.from({ length: 6 }, (_, w) => Array.from({ length: 7 }, (_, d) => addDays(start, w * 7 + d)));
|
|
162
|
+
function setDate(next) {
|
|
163
|
+
if (next === date)
|
|
164
|
+
return;
|
|
165
|
+
if (controlledDate === undefined)
|
|
166
|
+
setOwnDate(next);
|
|
167
|
+
onDateChange?.(next);
|
|
168
|
+
if (monthOf(next) !== month)
|
|
169
|
+
onMonthChange?.(monthOf(next));
|
|
170
|
+
}
|
|
171
|
+
const year = date.slice(0, 4);
|
|
172
|
+
function move(next) {
|
|
173
|
+
moved.current = true;
|
|
174
|
+
setDate(next);
|
|
175
|
+
}
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (!moved.current)
|
|
178
|
+
return;
|
|
179
|
+
moved.current = false;
|
|
180
|
+
const target = view === "year" ? date : focusDate;
|
|
181
|
+
grid.current
|
|
182
|
+
?.querySelector(`[data-date="${target}"]`)
|
|
183
|
+
?.focus();
|
|
184
|
+
});
|
|
185
|
+
function onKeyDown(event, day) {
|
|
186
|
+
const weekday = weekdayIndex(day, weekStartsOn);
|
|
187
|
+
const next = {
|
|
188
|
+
ArrowLeft: () => addDays(day, -1),
|
|
189
|
+
ArrowRight: () => addDays(day, 1),
|
|
190
|
+
ArrowUp: () => addDays(day, -7),
|
|
191
|
+
ArrowDown: () => addDays(day, 7),
|
|
192
|
+
Home: () => addDays(day, -weekday),
|
|
193
|
+
End: () => addDays(day, 6 - weekday),
|
|
194
|
+
PageUp: () => addMonths(day, event.shiftKey ? -12 : -1),
|
|
195
|
+
PageDown: () => addMonths(day, event.shiftKey ? 12 : 1),
|
|
196
|
+
}[event.key];
|
|
197
|
+
if (!next)
|
|
198
|
+
return;
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
move(next());
|
|
201
|
+
}
|
|
202
|
+
const step = (months) => setDate(addMonths(focusDate, months));
|
|
203
|
+
const switcher = onViewChange && (_jsx(SegmentedControl, { label: "Calendar view", value: view, options: [
|
|
204
|
+
{ value: "year", label: "Year" },
|
|
205
|
+
{ value: "month", label: "Month" },
|
|
206
|
+
{ value: "week", label: "Week" },
|
|
207
|
+
{ value: "day", label: "Day" },
|
|
208
|
+
], onChange: (next) => onViewChange(next) }));
|
|
209
|
+
if (view === "year") {
|
|
210
|
+
const shiftYear = (by) => setDate(addMonths(date, 12 * by));
|
|
211
|
+
const label = (day) => dayLabel(formats.day.format(new Date(toTime(day))), byDay.get(day)?.length ?? 0);
|
|
212
|
+
return (_jsxs("div", { id: id, ...componentRoot("Calendar"), className: rootClass, "data-view": "year", children: [_jsxs("div", { className: "m-calendar-header", children: [_jsx(Heading, { id: headingId, className: "m-calendar-title", "aria-live": "polite", children: year }), switcher, _jsxs("div", { className: "m-calendar-nav", children: [_jsx(NavButton, { label: "Previous year", direction: "back", onClick: () => shiftYear(-1) }), _jsx(NavButton, { label: "Next year", direction: "forward", onClick: () => shiftYear(1) })] })] }), _jsx("div", { className: "m-calendar-year", ref: grid, children: Array.from({ length: 12 }, (_, i) => {
|
|
213
|
+
const mm = String(i + 1).padStart(2, "0");
|
|
214
|
+
const firstDay = `${year}-${mm}-01`;
|
|
215
|
+
const lead = weekdayIndex(firstDay, weekStartsOn);
|
|
216
|
+
const cells = Array.from({ length: 42 }, (_, n) => addDays(firstDay, n - lead));
|
|
217
|
+
const titleId = `${headingId}-m${mm}`;
|
|
218
|
+
return (_jsxs("section", { className: "m-calendar-month", "aria-labelledby": titleId, children: [_jsx(SubHeading, { id: titleId, className: "m-calendar-month-title", children: formats.monthName.format(new Date(toTime(firstDay))) }), _jsxs("table", { children: [_jsx("thead", { children: _jsx("tr", { children: cells.slice(0, 7).map((day) => {
|
|
219
|
+
const d = new Date(toTime(day));
|
|
220
|
+
return (_jsx("th", { scope: "col", abbr: formats.long.format(d), children: formats.narrow.format(d) }, day));
|
|
221
|
+
}) }) }), _jsx("tbody", { children: Array.from({ length: 6 }, (_, w) => (_jsx("tr", { children: cells.slice(w * 7, w * 7 + 7).map((day) => monthOf(day) === `${year}-${mm}` ? (_jsx("td", { children: _jsxs("button", { type: "button", "data-date": day, tabIndex: day === date ? 0 : -1, "aria-current": day === today ? "date" : undefined, "aria-pressed": day === selected, "aria-label": label(day), onClick: () => {
|
|
222
|
+
setDate(day);
|
|
223
|
+
onSelect?.(day);
|
|
224
|
+
}, onKeyDown: (event) => onKeyDown(event, day), children: [_jsx("span", { className: "m-calendar-day", children: Number(day.slice(8)) }), byDay.has(day) && (_jsx("span", { className: "m-calendar-dot", "aria-hidden": "true" }))] }) }, day)) : (_jsx("td", {}, day))) }, w))) })] })] }, mm));
|
|
225
|
+
}) })] }));
|
|
226
|
+
}
|
|
227
|
+
if (view !== "month")
|
|
228
|
+
return (_jsx(TimeGrid, { view: view, date: date, onDateChange: setDate, events: byDay, today: today, selected: selected, weekStartsOn: weekStartsOn, locale: locale, formats: formats, headingId: headingId, headingLevel: headingLevel, switcher: switcher, onEventSelect: onEventSelect, dayStartHour: dayStartHour, dayEndHour: dayEndHour, id: id, className: rootClass }));
|
|
229
|
+
const monthGrid = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "m-calendar-header", children: [_jsx(Heading, { id: headingId, className: "m-calendar-title", "aria-live": "polite", children: formats.title.format(new Date(toTime(first))) }), switcher, _jsxs("div", { className: "m-calendar-nav", children: [_jsx(NavButton, { label: "Previous month", direction: "back", onClick: () => step(-1) }), _jsx(NavButton, { label: "Next month", direction: "forward", onClick: () => step(1) })] })] }), _jsxs("table", { role: "grid", "aria-labelledby": headingId, ref: grid, children: [_jsx("thead", { children: _jsx("tr", { children: weeks[0].map((day) => {
|
|
230
|
+
const d = new Date(toTime(day));
|
|
231
|
+
return (_jsx("th", { scope: "col", abbr: formats.long.format(d), children: formats.short.format(d) }, day));
|
|
232
|
+
}) }) }), _jsx("tbody", { children: weeks.map((week) => (_jsx("tr", { children: week.map((day) => {
|
|
233
|
+
const dayEvents = byDay.get(day) || [];
|
|
234
|
+
const count = dayEvents.length;
|
|
235
|
+
const name = formats.day.format(new Date(toTime(day)));
|
|
236
|
+
const listed = variant === "full" && count > 0;
|
|
237
|
+
return (_jsxs("td", { "aria-selected": day === selected ? true : undefined, "data-outside": monthOf(day) !== month || undefined, children: [_jsxs("button", { type: "button", "data-date": day, tabIndex: day === focusDate ? 0 : -1, "aria-current": day === today ? "date" : undefined, "aria-pressed": day === selected, "aria-describedby": listed ? `${headingId}-${day}` : undefined, "aria-label": dayLabel(name, count), onClick: () => {
|
|
238
|
+
setDate(day);
|
|
239
|
+
onSelect?.(day);
|
|
240
|
+
}, onKeyDown: (event) => onKeyDown(event, day), children: [_jsx("span", { className: "m-calendar-day", children: Number(day.slice(8)) }), count > 0 && (_jsx("span", { className: "m-calendar-dot", "aria-hidden": "true" }))] }), listed && (_jsxs("ol", { className: "m-calendar-events", id: `${headingId}-${day}`, children: [dayEvents.slice(0, maxEvents).map((event) => (_jsxs("li", { children: [event.time && _jsx("time", { children: event.time }), event.time && " ", event.title] }, event.id))), count > maxEvents && (_jsxs("li", { className: "m-calendar-more", children: ["+", count - maxEvents, " more"] }))] }))] }, day));
|
|
241
|
+
}) }, week[0]))) })] })] }));
|
|
242
|
+
const rootProps = {
|
|
243
|
+
...componentRoot("Calendar"),
|
|
244
|
+
id,
|
|
245
|
+
className: rootClass,
|
|
246
|
+
"data-variant": variant === "full" ? "full" : undefined,
|
|
247
|
+
};
|
|
248
|
+
if (!agenda)
|
|
249
|
+
return _jsx("div", { ...rootProps, children: monthGrid });
|
|
250
|
+
const agendaDay = selected ?? date;
|
|
251
|
+
const agendaEvents = [...(byDay.get(agendaDay) || [])].sort((a, b) => (a.start ?? a.time ?? "").localeCompare(b.start ?? b.time ?? ""));
|
|
252
|
+
const agendaId = `${headingId}-agenda`;
|
|
253
|
+
return (_jsxs("div", { ...rootProps, "data-agenda": "true", children: [_jsx("div", { className: "m-calendar-main", children: monthGrid }), _jsxs("section", { className: "m-calendar-agenda", "aria-labelledby": agendaId, children: [_jsxs(SubHeading, { id: agendaId, className: "m-calendar-agenda-title", "aria-live": "polite", children: ["Schedule for", " ", _jsx("time", { dateTime: agendaDay, children: formats.day.format(new Date(toTime(agendaDay))) })] }), agendaEvents.length ? (_jsx("ol", { className: "m-calendar-agenda-list", children: agendaEvents.map((event) => {
|
|
254
|
+
const when = event.start ? (_jsxs(_Fragment, { children: [_jsx("time", { children: event.start }), event.end && (_jsxs(_Fragment, { children: ["\u2013", _jsx("time", { children: event.end })] }))] })) : event.time ? (_jsx("time", { children: event.time })) : ("All day");
|
|
255
|
+
// The space keeps the title and time apart in a button's name.
|
|
256
|
+
const text = (_jsxs(_Fragment, { children: [_jsx("strong", { children: event.title }), " ", _jsx("span", { className: "m-calendar-event-time", children: when })] }));
|
|
257
|
+
return (_jsxs("li", { children: [event.media, onEventSelect ? (_jsx("button", { type: "button", className: "m-calendar-agenda-text", onClick: () => onEventSelect(event.id), children: text })) : (_jsx("p", { className: "m-calendar-agenda-text", children: text }))] }, event.id));
|
|
258
|
+
}) })) : (_jsx("p", { className: "m-calendar-none", children: "No events" }))] })] }));
|
|
259
|
+
}
|
|
260
|
+
function NavButton({ label, direction, onClick, }) {
|
|
261
|
+
return (_jsx("button", { type: "button", "aria-label": label, onClick: onClick, children: direction === "back" ? _jsx(ChevronLeft, {}) : _jsx(ChevronRight, {}) }));
|
|
262
|
+
}
|
|
263
|
+
/** Week and day views: labelled day groups whose events are positioned on an hour grid. */
|
|
264
|
+
function TimeGrid({ view, date, onDateChange, events, today, selected, weekStartsOn, locale, formats, headingId, headingLevel, switcher, onEventSelect, dayStartHour, dayEndHour, id, className, }) {
|
|
265
|
+
const Heading = headingTag(headingLevel);
|
|
266
|
+
const DayHeading = headingTag(childLevel(headingLevel));
|
|
267
|
+
const offset = weekdayIndex(date, weekStartsOn);
|
|
268
|
+
const days = view === "week"
|
|
269
|
+
? Array.from({ length: 7 }, (_, i) => addDays(date, i - offset))
|
|
270
|
+
: [date];
|
|
271
|
+
const step = view === "week" ? 7 : 1;
|
|
272
|
+
const unit = view === "week" ? "week" : "day";
|
|
273
|
+
const range = new Intl.DateTimeFormat(locale, {
|
|
274
|
+
day: "numeric",
|
|
275
|
+
month: "long",
|
|
276
|
+
year: "numeric",
|
|
277
|
+
timeZone: "UTC",
|
|
278
|
+
});
|
|
279
|
+
const title = view === "week"
|
|
280
|
+
? range.formatRange(new Date(toTime(days[0])), new Date(toTime(days[days.length - 1])))
|
|
281
|
+
: formats.day.format(new Date(toTime(date)));
|
|
282
|
+
const first = dayStartHour * 60;
|
|
283
|
+
const span = Math.max(60, (dayEndHour - dayStartHour) * 60);
|
|
284
|
+
const pct = (n) => `${(Math.min(Math.max(n, 0), 1) * 100).toFixed(4)}%`;
|
|
285
|
+
const hours = Array.from({ length: dayEndHour - dayStartHour }, (_, i) => dayStartHour + i);
|
|
286
|
+
const times = (event) => (_jsxs("span", { className: "m-calendar-event-time", children: [_jsx("time", { children: event.start }), event.end && (_jsxs(_Fragment, { children: ["\u2013", _jsx("time", { children: event.end })] }))] }));
|
|
287
|
+
const item = (event, timed) => {
|
|
288
|
+
// The space keeps the title and time apart in the accessible name.
|
|
289
|
+
const inner = (_jsxs(_Fragment, { children: [event.title, timed && " ", timed && times(event)] }));
|
|
290
|
+
return onEventSelect ? (_jsx("button", { type: "button", className: "m-calendar-event", onClick: () => onEventSelect(event.id), children: inner })) : timed ? (_jsx("div", { className: "m-calendar-event", children: inner })) : (inner);
|
|
291
|
+
};
|
|
292
|
+
return (_jsxs("div", { id: id, ...componentRoot("Calendar"), className: className, "data-view": view, children: [_jsxs("div", { className: "m-calendar-header", children: [_jsx(Heading, { id: headingId, className: "m-calendar-title", "aria-live": "polite", children: title }), switcher, _jsxs("div", { className: "m-calendar-nav", children: [_jsx(NavButton, { label: `Previous ${unit}`, direction: "back", onClick: () => onDateChange(addDays(date, -step)) }), _jsx(NavButton, { label: `Next ${unit}`, direction: "forward", onClick: () => onDateChange(addDays(date, step)) })] })] }), _jsxs("div", { className: "m-calendar-timegrid", style: {
|
|
293
|
+
"--m-hours": hours.length,
|
|
294
|
+
"--m-days": days.length,
|
|
295
|
+
}, children: [_jsx("div", { className: "m-calendar-hours", "aria-hidden": "true", children: hours.map((h) => (_jsx("span", { children: `${String(h).padStart(2, "0")}:00` }, h))) }), _jsx("div", { className: "m-calendar-columns", "data-count": days.length, children: days.map((day) => {
|
|
296
|
+
const dayEvents = events.get(day) || [];
|
|
297
|
+
const allDay = dayEvents.filter((e) => !e.start);
|
|
298
|
+
const timed = layout(dayEvents, first, first + span);
|
|
299
|
+
const d = new Date(toTime(day));
|
|
300
|
+
const labelId = `${headingId}-${day}`;
|
|
301
|
+
return (_jsxs("div", { role: "group", className: "m-calendar-column", "aria-labelledby": labelId, "data-selected": day === selected || undefined, children: [_jsxs(DayHeading, { id: labelId, className: "m-calendar-column-heading", "aria-current": day === today ? "date" : undefined, children: [_jsxs("span", { className: "m-calendar-column-label", "aria-hidden": "true", children: [_jsx("span", { children: formats.short.format(d) }), _jsx("strong", { children: Number(day.slice(8)) })] }), _jsx("span", { className: "m-sr-only", children: formats.day.format(d) })] }), allDay.length > 0 && (_jsx("ul", { className: "m-calendar-allday", "aria-label": "All day", children: allDay.map((event) => (_jsx("li", { children: item(event, false) }, event.id))) })), timed.length > 0 ? (_jsx("ol", { className: "m-calendar-slots", children: timed.map((t) => (_jsx("li", { "data-clipped-start": t.clippedStart || undefined, "data-clipped-end": t.clippedEnd || undefined, style: {
|
|
302
|
+
"--m-top": pct((t.top - first) / span),
|
|
303
|
+
"--m-height": pct((t.bottom - t.top) / span),
|
|
304
|
+
"--m-lane": t.lane,
|
|
305
|
+
"--m-lanes": t.lanes,
|
|
306
|
+
}, children: item(t.event, true) }, t.event.id))) })) : allDay.length ? (
|
|
307
|
+
// Keeps the hour lines in columns that only have all-day events.
|
|
308
|
+
_jsx("div", { className: "m-calendar-slots", "aria-hidden": "true" })) : (_jsx("p", { className: "m-calendar-none", children: "No events" }))] }, day));
|
|
309
|
+
}) })] })] }));
|
|
310
|
+
}
|
package/dist/choice.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface ChoiceOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
/** Avatar, icon or status dot shown before the label. */
|
|
7
|
+
leading?: ReactNode;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface ChoiceProps {
|
|
11
|
+
label: string;
|
|
12
|
+
options: ChoiceOption[];
|
|
13
|
+
value: string | null;
|
|
14
|
+
onChange: (value: string | null) => void;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
hint?: string;
|
|
17
|
+
/** Marks the field invalid; the message is announced when it appears. */
|
|
18
|
+
error?: string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** Marks the field required and adds a decorative marker to the label. */
|
|
21
|
+
required?: boolean;
|
|
22
|
+
/** On the field's root element. */
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Styled single select for rich options (avatars, descriptions). Use Select for plain native choices. */
|
|
26
|
+
export declare function Listbox({ label, options, value, onChange, placeholder, hint, error, disabled, required, className, }: ChoiceProps): import("react").JSX.Element;
|
|
27
|
+
/** Searchable single select; filters options by label as the person types. */
|
|
28
|
+
export declare function Combobox({ label, options, value, onChange, placeholder, hint, error, disabled, required, className, empty, }: ChoiceProps & {
|
|
29
|
+
/** Shown and announced when the typed text matches no option. */
|
|
30
|
+
empty?: {
|
|
31
|
+
title: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
action?: ReactNode;
|
|
34
|
+
};
|
|
35
|
+
}): import("react").JSX.Element;
|
|
36
|
+
export {};
|
package/dist/choice.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Field, Label, Description, Listbox as HeadlessListbox, ListboxButton, ListboxOptions, ListboxOption, Combobox as HeadlessCombobox, ComboboxInput, ComboboxButton, ComboboxOptions, ComboboxOption, } from "@headlessui/react";
|
|
4
|
+
import { Check, ChevronDown } from "./icons.js";
|
|
5
|
+
import { RequiredMark, componentRoot, cx } from "./internal.js";
|
|
6
|
+
function OptionContent({ option }) {
|
|
7
|
+
return (_jsxs(_Fragment, { children: [option.leading && (_jsx("span", { className: "m-choice-leading", "aria-hidden": "true", children: option.leading })), _jsxs("span", { className: "m-choice-text", children: [_jsx("span", { children: option.label }), option.description && _jsx("small", { children: option.description })] }), _jsx(Check, { className: "m-choice-check" })] }));
|
|
8
|
+
}
|
|
9
|
+
/** Label with the required marker; `spoken` adds "required" for controls that cannot carry aria-required. */
|
|
10
|
+
function ChoiceLabel({ label, required, spoken = false, }) {
|
|
11
|
+
return (_jsxs(Label, { children: [label, required && _jsx(RequiredMark, {}), required && spoken && _jsx("span", { className: "m-sr-only", children: " (required)" })] }));
|
|
12
|
+
}
|
|
13
|
+
/** The hint or error; the error is a separate alert so it is announced when it appears. */
|
|
14
|
+
function Message({ hint, error }) {
|
|
15
|
+
if (error)
|
|
16
|
+
return (_jsx(Description, { className: "m-error", role: "alert", children: error }, "error"));
|
|
17
|
+
return hint ? _jsx(Description, { children: hint }, "hint") : null;
|
|
18
|
+
}
|
|
19
|
+
/** Styled single select for rich options (avatars, descriptions). Use Select for plain native choices. */
|
|
20
|
+
export function Listbox({ label, options, value, onChange, placeholder = "Select…", hint, error, disabled = false, required, className, }) {
|
|
21
|
+
const selected = options.find((o) => o.value === value);
|
|
22
|
+
return (_jsxs(Field, { ...componentRoot("Listbox"), className: cx("m-field", className), disabled: disabled, children: [_jsx(ChoiceLabel, { label: label, required: required, spoken: true }), _jsxs(HeadlessListbox, { value: value, onChange: onChange, children: [_jsxs(ListboxButton, { className: "m-choice-control", "aria-invalid": error ? true : undefined, children: [selected ? (_jsxs("span", { className: "m-choice-value", children: [selected.leading && (_jsx("span", { className: "m-choice-leading", "aria-hidden": "true", children: selected.leading })), _jsx("span", { children: selected.label })] })) : (_jsx("span", { className: "m-placeholder", children: placeholder })), _jsx(ChevronDown, {})] }), _jsx(ListboxOptions, { anchor: "bottom start", className: "m-choice-options", children: options.map((option) => (_jsx(ListboxOption, { value: option.value, disabled: option.disabled, className: "m-choice-option", children: _jsx(OptionContent, { option: option }) }, option.value))) })] }), _jsx(Message, { hint: hint, error: error })] }));
|
|
23
|
+
}
|
|
24
|
+
/** Searchable single select; filters options by label as the person types. */
|
|
25
|
+
export function Combobox({ label, options, value, onChange, placeholder, hint, error, disabled = false, required, className, empty = { title: "No matches" }, }) {
|
|
26
|
+
const [query, setQuery] = useState("");
|
|
27
|
+
const needle = query.trim().toLowerCase();
|
|
28
|
+
const filtered = needle
|
|
29
|
+
? options.filter((o) => o.label.toLowerCase().includes(needle))
|
|
30
|
+
: options;
|
|
31
|
+
const leading = query
|
|
32
|
+
? undefined
|
|
33
|
+
: options.find((o) => o.value === value)?.leading;
|
|
34
|
+
const nothing = !!needle && !filtered.length;
|
|
35
|
+
return (_jsxs(Field, { ...componentRoot("Combobox"), className: cx("m-field", className), disabled: disabled, children: [_jsx(ChoiceLabel, { label: label, required: required }), _jsxs(HeadlessCombobox, { value: value, onChange: onChange, onClose: () => setQuery(""), children: [_jsxs("div", { className: "m-combobox", "data-leading": leading ? true : undefined, children: [leading && (_jsx("span", { className: "m-choice-leading", "aria-hidden": "true", children: leading })), _jsx(ComboboxInput, { className: "m-choice-control", placeholder: placeholder, "aria-invalid": error ? true : undefined, "aria-required": required || undefined, displayValue: (v) => options.find((o) => o.value === v)?.label ?? "", onChange: (event) => setQuery(event.target.value) }), _jsx(ComboboxButton, { className: "m-combobox-toggle", "aria-label": `Show ${label} options`, children: _jsx(ChevronDown, {}) })] }), filtered.length > 0 && (_jsx(ComboboxOptions, { anchor: "bottom start", className: "m-choice-options m-combobox-options", children: filtered.map((option) => (_jsx(ComboboxOption, { value: option.value, disabled: option.disabled, className: "m-choice-option", children: _jsx(OptionContent, { option: option }) }, option.value))) }))] }), _jsx("p", { role: "status", className: nothing ? undefined : "m-sr-only", children: nothing && (_jsxs(_Fragment, { children: [empty.title, empty.description && _jsxs("span", { children: [" ", empty.description] })] })) }), nothing && empty.action, _jsx(Message, { hint: hint, error: error })] }));
|
|
36
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Comment box with optional avatar, toolbar, Write/Preview tabs and a character count. */
|
|
3
|
+
export declare function CommentComposer({ onSubmit, value: controlled, onChange, label, labelVisible, placeholder, submitLabel, loading, avatar, actions, preview, maxLength, hint, error, rows, className, }: {
|
|
4
|
+
onSubmit: (value: string) => void;
|
|
5
|
+
/** Controlled text; omit to let the composer manage and clear it. */
|
|
6
|
+
value?: string;
|
|
7
|
+
/** Called with the text on every edit. */
|
|
8
|
+
onChange?: (value: string) => void;
|
|
9
|
+
label?: string;
|
|
10
|
+
labelVisible?: boolean;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
submitLabel?: string;
|
|
13
|
+
/** Busy submitting: the submit button keeps focus but ignores activation. */
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
/** Author avatar shown beside the box (decorative). */
|
|
16
|
+
avatar?: ReactNode;
|
|
17
|
+
/** Toolbar controls such as attach or emoji buttons. */
|
|
18
|
+
actions?: ReactNode;
|
|
19
|
+
/** Renders a preview of the text and enables Write/Preview tabs. */
|
|
20
|
+
preview?: (value: string) => ReactNode;
|
|
21
|
+
/** Shows a visible count; the remaining characters are announced only near the limit. */
|
|
22
|
+
maxLength?: number;
|
|
23
|
+
hint?: string;
|
|
24
|
+
/** Marks the text invalid; the message is announced when it appears. */
|
|
25
|
+
error?: string;
|
|
26
|
+
rows?: number;
|
|
27
|
+
className?: string;
|
|
28
|
+
}): import("react").JSX.Element;
|
package/dist/composer.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Button, Tabs } from "./primitives.js";
|
|
4
|
+
import { FieldFeedback, componentRoot, cx, useField } from "./internal.js";
|
|
5
|
+
/** Characters left at which the count starts being announced. */
|
|
6
|
+
const nearLimit = (maxLength) => Math.max(10, Math.ceil(maxLength * 0.1));
|
|
7
|
+
/** Comment box with optional avatar, toolbar, Write/Preview tabs and a character count. */
|
|
8
|
+
export function CommentComposer({ onSubmit, value: controlled, onChange, label = "Add your comment", labelVisible = false, placeholder = "Add your comment…", submitLabel = "Comment", loading = false, avatar, actions, preview, maxLength, hint, error, rows = 3, className, }) {
|
|
9
|
+
const { fieldId: id, messageId, describedBy } = useField({ hint, error });
|
|
10
|
+
const [own, setOwn] = useState("");
|
|
11
|
+
const [mode, setMode] = useState("write");
|
|
12
|
+
const value = controlled ?? own;
|
|
13
|
+
const setValue = (next) => {
|
|
14
|
+
if (controlled === undefined)
|
|
15
|
+
setOwn(next);
|
|
16
|
+
onChange?.(next);
|
|
17
|
+
};
|
|
18
|
+
const empty = !value.trim();
|
|
19
|
+
const submit = () => {
|
|
20
|
+
if (empty || loading)
|
|
21
|
+
return;
|
|
22
|
+
onSubmit(value);
|
|
23
|
+
if (controlled === undefined)
|
|
24
|
+
setOwn("");
|
|
25
|
+
};
|
|
26
|
+
const remaining = maxLength === undefined ? 0 : maxLength - value.length;
|
|
27
|
+
const field = (_jsx("textarea", { id: id, rows: rows, value: value, placeholder: placeholder, maxLength: maxLength, "aria-invalid": error ? true : undefined, "aria-describedby": describedBy, "aria-keyshortcuts": "Control+Enter Meta+Enter", onChange: (event) => setValue(event.target.value), onKeyDown: (event) => {
|
|
28
|
+
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
submit();
|
|
31
|
+
}
|
|
32
|
+
} }));
|
|
33
|
+
return (_jsxs("form", { ...componentRoot("CommentComposer"), className: cx("m-composer", className), onSubmit: (event) => {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
submit();
|
|
36
|
+
}, children: [avatar && (_jsx("span", { className: "m-composer-avatar", "aria-hidden": "true", children: avatar })), _jsxs("div", { className: "m-composer-main", children: [_jsx("label", { className: labelVisible ? "m-composer-label" : "m-sr-only", htmlFor: id, children: label }), _jsxs("div", { className: "m-composer-box", "data-invalid": error ? true : undefined, children: [preview ? (_jsx(Tabs, { label: "Comment mode", value: mode, onChange: setMode, items: [
|
|
37
|
+
{ id: "write", label: "Write", content: field },
|
|
38
|
+
{
|
|
39
|
+
id: "preview",
|
|
40
|
+
label: "Preview",
|
|
41
|
+
content: (_jsx("div", { className: "m-composer-preview", children: empty ? (_jsx("p", { className: "m-composer-empty", children: "Nothing to preview" })) : (preview(value)) })),
|
|
42
|
+
},
|
|
43
|
+
] })) : (field), _jsxs("div", { className: "m-composer-bar", children: [actions && _jsx("div", { className: "m-composer-actions", children: actions }), maxLength !== undefined && (_jsxs("span", { className: "m-composer-count", children: [value.length, " / ", maxLength] })), _jsx(Button, { type: "submit", disabled: empty, loading: loading, children: submitLabel }), maxLength !== undefined && (_jsx("span", { className: "m-sr-only", role: "status", children: remaining <= nearLimit(maxLength) &&
|
|
44
|
+
(remaining > 0
|
|
45
|
+
? `${remaining} ${remaining === 1 ? "character" : "characters"} left`
|
|
46
|
+
: "Character limit reached") }))] })] }), _jsx(FieldFeedback, { id: messageId, hint: hint, error: error })] })] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { Tone } from "./primitives.js";
|
|
3
|
+
import { type HeadingLevel } from "./heading.js";
|
|
4
|
+
export interface AvatarProps {
|
|
5
|
+
name: string;
|
|
6
|
+
src?: string;
|
|
7
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
8
|
+
shape?: "circle" | "square";
|
|
9
|
+
status?: "online" | "offline";
|
|
10
|
+
/** Corner of the status dot; `top` suits notification indicators. */
|
|
11
|
+
statusPosition?: "bottom" | "top";
|
|
12
|
+
/** Shown when there is no `src`: the name's initials or a generic person icon. */
|
|
13
|
+
placeholder?: "initials" | "icon";
|
|
14
|
+
/** Hide the avatar from assistive technology when the name is already visible beside it. */
|
|
15
|
+
decorative?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function Avatar({ name, src, size, shape, status, statusPosition, placeholder, decorative, className, }: AvatarProps): import("react").JSX.Element;
|
|
19
|
+
export interface AvatarGroupProps {
|
|
20
|
+
label: string;
|
|
21
|
+
people: {
|
|
22
|
+
name: string;
|
|
23
|
+
src?: string;
|
|
24
|
+
}[];
|
|
25
|
+
max?: number;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function AvatarGroup({ label, people, max, className, }: AvatarGroupProps): import("react").JSX.Element;
|
|
29
|
+
export interface KbdProps {
|
|
30
|
+
keys: string[];
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function Kbd({ keys, className }: KbdProps): import("react").JSX.Element;
|
|
34
|
+
export interface ProgressBarProps {
|
|
35
|
+
label: string;
|
|
36
|
+
value: number;
|
|
37
|
+
max?: number;
|
|
38
|
+
valueLabel?: string;
|
|
39
|
+
/** Status colour for the bar; omit for the accent colour. */
|
|
40
|
+
tone?: Tone;
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function ProgressBar({ label, value, max, valueLabel, tone, className, }: ProgressBarProps): import("react").JSX.Element;
|
|
44
|
+
export interface FilterChipProps {
|
|
45
|
+
label: ReactNode;
|
|
46
|
+
selected?: boolean;
|
|
47
|
+
onClick?: () => void;
|
|
48
|
+
/** Shows a remove button; focus moves to the next chip once the chip is gone. */
|
|
49
|
+
onRemove?: () => void;
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function FilterChip({ label, selected, onClick, onRemove, className, }: FilterChipProps): import("react").JSX.Element;
|
|
53
|
+
export interface CheckListProps {
|
|
54
|
+
items: ReactNode[];
|
|
55
|
+
label?: string;
|
|
56
|
+
columns?: 1 | 2;
|
|
57
|
+
size?: "sm" | "md";
|
|
58
|
+
className?: string;
|
|
59
|
+
}
|
|
60
|
+
/** Feature or inclusion list with a check marker; replaces ad-hoc list-disc styling. */
|
|
61
|
+
export declare function CheckList({ items, label, columns, size, className, }: CheckListProps): import("react").JSX.Element;
|
|
62
|
+
export interface DescriptionListProps {
|
|
63
|
+
title?: string;
|
|
64
|
+
description?: string;
|
|
65
|
+
headingLevel?: HeadingLevel;
|
|
66
|
+
items: {
|
|
67
|
+
term: string;
|
|
68
|
+
details: ReactNode;
|
|
69
|
+
action?: ReactNode;
|
|
70
|
+
}[];
|
|
71
|
+
layout?: "columns" | "stacked";
|
|
72
|
+
/**
|
|
73
|
+
* `striped` shades alternate rows instead of dividing them. To show the
|
|
74
|
+
* list in a card, place it in a Card with the title on the card.
|
|
75
|
+
*/
|
|
76
|
+
variant?: "plain" | "striped";
|
|
77
|
+
id?: string;
|
|
78
|
+
className?: string;
|
|
79
|
+
}
|
|
80
|
+
/** Label/value pairs for detail screens, stacked on narrow screens and in columns when wide. */
|
|
81
|
+
export declare function DescriptionList({ title, description, headingLevel, items, layout, variant, id, className, }: DescriptionListProps): import("react").JSX.Element;
|
|
82
|
+
export interface GridListItem {
|
|
83
|
+
id: string;
|
|
84
|
+
title: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
/** Destination for the card; its title link covers the whole card. */
|
|
87
|
+
href?: string;
|
|
88
|
+
/** Avatar or icon shown beside the title. */
|
|
89
|
+
media?: ReactNode;
|
|
90
|
+
/** Image across the top of the card. Use alt="" when the title already describes it. */
|
|
91
|
+
cover?: {
|
|
92
|
+
src: string;
|
|
93
|
+
alt: string;
|
|
94
|
+
};
|
|
95
|
+
/** Badges, dates or counts under the description. */
|
|
96
|
+
meta?: ReactNode;
|
|
97
|
+
/** Term/value pairs under the body, e.g. a client card's last invoice and amount. */
|
|
98
|
+
details?: {
|
|
99
|
+
term: string;
|
|
100
|
+
details: ReactNode;
|
|
101
|
+
}[];
|
|
102
|
+
actions?: ReactNode;
|
|
103
|
+
}
|
|
104
|
+
export interface GridListProps {
|
|
105
|
+
label: string;
|
|
106
|
+
items: GridListItem[];
|
|
107
|
+
columns?: 2 | 3 | 4;
|
|
108
|
+
/** Level of each card's title. */
|
|
109
|
+
itemHeadingLevel?: HeadingLevel;
|
|
110
|
+
/** `split` gives each action an equal cell divided by shared borders, e.g. Email | Call. */
|
|
111
|
+
actionsLayout?: "inline" | "split";
|
|
112
|
+
className?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Responsive grid of cards for people, projects or files. A card's title link
|
|
116
|
+
* covers the card; links and buttons in `media`, `meta`, `details` and
|
|
117
|
+
* `actions` sit above it and stay clickable.
|
|
118
|
+
*/
|
|
119
|
+
export declare function GridList({ label, items, columns, itemHeadingLevel, actionsLayout, className, }: GridListProps): import("react").JSX.Element;
|
|
120
|
+
export interface Attachment {
|
|
121
|
+
id: string;
|
|
122
|
+
name: string;
|
|
123
|
+
href: string;
|
|
124
|
+
/** Preformatted size, e.g. "2.4 MB". */
|
|
125
|
+
size?: string;
|
|
126
|
+
/** Link text; the file name is appended for screen readers. Defaults to "Download". */
|
|
127
|
+
actionLabel?: string;
|
|
128
|
+
icon?: ReactNode;
|
|
129
|
+
}
|
|
130
|
+
export interface AttachmentListProps {
|
|
131
|
+
files: Attachment[];
|
|
132
|
+
label?: string;
|
|
133
|
+
className?: string;
|
|
134
|
+
}
|
|
135
|
+
/** Files attached to a record, each with a size and a named download link. */
|
|
136
|
+
export declare function AttachmentList({ files, label, className, }: AttachmentListProps): import("react").JSX.Element;
|