@blakesteve/roster 5.3.0 → 5.4.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 +84 -1
- package/dist/components/molecules/MediaPreview/MediaPreview.cjs +1 -0
- package/dist/components/molecules/MediaPreview/MediaPreview.d.ts +27 -0
- package/dist/components/molecules/MediaPreview/MediaPreview.js +55 -0
- package/dist/components/organisms/Dialog/Dialog.cjs +1 -1
- package/dist/components/organisms/Dialog/Dialog.d.ts +9 -1
- package/dist/components/organisms/Dialog/Dialog.js +26 -24
- package/dist/components/organisms/ModerationQueue/ModerationQueue.cjs +1 -0
- package/dist/components/organisms/ModerationQueue/ModerationQueue.d.ts +73 -0
- package/dist/components/organisms/ModerationQueue/ModerationQueue.js +430 -0
- package/dist/components/organisms/ModerationQueue/QueueEditDialog.cjs +1 -0
- package/dist/components/organisms/ModerationQueue/QueueEditDialog.d.ts +8 -0
- package/dist/components/organisms/ModerationQueue/QueueEditDialog.js +37 -0
- package/dist/components/organisms/ModerationQueue/QueueEditSheet.cjs +1 -0
- package/dist/components/organisms/ModerationQueue/QueueEditSheet.d.ts +12 -0
- package/dist/components/organisms/ModerationQueue/QueueEditSheet.js +30 -0
- package/dist/components/organisms/ModerationQueue/queue-actions.cjs +1 -0
- package/dist/components/organisms/ModerationQueue/queue-actions.d.ts +157 -0
- package/dist/components/organisms/ModerationQueue/queue-actions.js +6 -0
- package/dist/index.d.ts +5 -0
- package/dist/meta.json +3 -3
- package/dist/roster.cjs +1 -1
- package/dist/roster.css +1 -1
- package/dist/roster.js +118 -108
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1208,6 +1208,87 @@ escape the box. Lay the options out across with `orientation` rather than
|
|
|
1208
1208
|
through `optionsClassName`, for the same reason: the horizontal spacing that
|
|
1209
1209
|
keeps two radios 44px apart rides on that prop.
|
|
1210
1210
|
|
|
1211
|
+
### ModerationQueue
|
|
1212
|
+
|
|
1213
|
+
A queue of things waiting for a decision: submissions to approve, reports to
|
|
1214
|
+
resolve, claims to check before they publish. The app owns the data and every
|
|
1215
|
+
request; the queue owns the layout, the states, the steps and the keyboard.
|
|
1216
|
+
**It never fetches.** An action's `run` is the only path from a decision to
|
|
1217
|
+
the network, and the app says an item has left by removing it from `items`.
|
|
1218
|
+
|
|
1219
|
+
```tsx
|
|
1220
|
+
import { ModerationQueue, QueueEditDialog } from "@blakesteve/roster";
|
|
1221
|
+
|
|
1222
|
+
<ModerationQueue
|
|
1223
|
+
label="New recipes"
|
|
1224
|
+
items={recipes}
|
|
1225
|
+
getKey={(r) => r.id}
|
|
1226
|
+
itemLabel={(r) => `${r.title}, from ${r.by}`}
|
|
1227
|
+
renderMeta={(r) => <Badge size="xs">{r.category}</Badge>}
|
|
1228
|
+
renderContent={(r) => <p>{r.method}</p>}
|
|
1229
|
+
status={loading ? "loading" : failed ? "error" : "ready"}
|
|
1230
|
+
onRetry={reload}
|
|
1231
|
+
actions={(a) => [
|
|
1232
|
+
{ id: "list", label: "List it", done: "Listed", tone: "success",
|
|
1233
|
+
run: async (r) => { await api.list(r.id); await reload(); } },
|
|
1234
|
+
a.edit({ id: "edit", label: "Edit and list", done: "Listed with edits",
|
|
1235
|
+
edit: { title: "Tidy it up", submitLabel: "List it", surface: QueueEditDialog,
|
|
1236
|
+
initial: (r) => ({ title: r.title }), Form: TitleForm,
|
|
1237
|
+
validate: (d) => (d.title.trim() ? undefined : "Give it a title.") },
|
|
1238
|
+
run: async (r, { draft }) => { await api.list(r.id, draft); await reload(); } }),
|
|
1239
|
+
{ id: "send-back", label: "Send back", done: "Sent back", tone: "danger",
|
|
1240
|
+
reason: { prompt: "Why? The cook will see this.", required: true,
|
|
1241
|
+
options: [{ value: "duplicate", label: "We already have this one" }] },
|
|
1242
|
+
run: async (r, { reason }) => { await api.sendBack(r.id, reason); await reload(); } },
|
|
1243
|
+
]}
|
|
1244
|
+
/>;
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
**Four kinds of action.** One click (no step). `confirm` asks in place first.
|
|
1248
|
+
`reason` asks why in place: radios, a note, or both. `edit` opens a form the
|
|
1249
|
+
app supplies, inline by default or in a Dialog or Sheet (`QueueEditDialog`,
|
|
1250
|
+
`QueueEditSheet`, separate exports so a queue that doesn't use them doesn't
|
|
1251
|
+
ship them).
|
|
1252
|
+
|
|
1253
|
+
**Drafts are typed.** An edit's draft is whatever its `initial` returns, and
|
|
1254
|
+
`validate`, the form and `run` all get that type. Inside JSX, pass `actions` as
|
|
1255
|
+
a function and write edits with the `edit` it's handed, as above; outside it,
|
|
1256
|
+
`editAction(…)` does the same in a list typed `QueueAction<Item>[]`. A literal
|
|
1257
|
+
`{ edit: … }` doesn't compile, because its draft could only be `any`.
|
|
1258
|
+
|
|
1259
|
+
What it promises:
|
|
1260
|
+
|
|
1261
|
+
- **Only the clicked action is busy, and it keeps focus.** It shows a spinner
|
|
1262
|
+
and is `aria-disabled` rather than `disabled`, which would drop focus to the
|
|
1263
|
+
page. That item's other actions wait; every other item stays live.
|
|
1264
|
+
- **A decision that lands and removes the item** is announced in a polite live
|
|
1265
|
+
region ("Listed: Smoky red lentil soup, from Dana Okafor. 2 left."), and
|
|
1266
|
+
focus moves to the next item, the previous one, or the empty state. Never to
|
|
1267
|
+
the page. If the app keeps the item, the announcement drops the count and
|
|
1268
|
+
focus stays. `run` may resolve with `{ announce }` to say something else
|
|
1269
|
+
("Already decided by someone else.").
|
|
1270
|
+
- **A decision that fails** leaves the item where it was with an Alert saying
|
|
1271
|
+
why, and focus where it was. Inside a step, the step stays open with its
|
|
1272
|
+
draft. A reviewer who moved to another item meanwhile isn't pulled back.
|
|
1273
|
+
- **Several outcomes in one render are said in one sentence**: "Approved:
|
|
1274
|
+
Lentil soup. Approved: Rye crackers. 1 left."
|
|
1275
|
+
- **Steps are per item.** A reason half-written on one item survives another
|
|
1276
|
+
item's decision.
|
|
1277
|
+
- **Steps take focus and give it back.** Escape or Cancel returns focus to the
|
|
1278
|
+
button that opened the step.
|
|
1279
|
+
- **An item that disappears** (another reviewer, a reload) with its step open
|
|
1280
|
+
closes the step without deciding, and is announced once as decided
|
|
1281
|
+
elsewhere. Without a step, it moves focus only if focus was inside it.
|
|
1282
|
+
- **Headings follow the page.** `headingLevel` sets the queue's (2 by default),
|
|
1283
|
+
and items sit one below unless `itemHeadingLevel` says otherwise.
|
|
1284
|
+
- **Phones.** Every button is 44px tall at every width, and nothing scrolls
|
|
1285
|
+
sideways at 320px. Nothing animates as an item leaves.
|
|
1286
|
+
|
|
1287
|
+
Counts for a tab badge need no API: the app holds the items, so it's
|
|
1288
|
+
`items.length`. `MediaPreview` is the thumbnail to use in an item: when the
|
|
1289
|
+
image can't render (an expired link, a format the browser can't draw), it
|
|
1290
|
+
falls back to a link to the original in the same footprint.
|
|
1291
|
+
|
|
1211
1292
|
### Carousel
|
|
1212
1293
|
|
|
1213
1294
|
A sideways row: a row of cards, a selection strip, or a gallery of one per
|
|
@@ -1349,6 +1430,7 @@ function App() {
|
|
|
1349
1430
|
| `LiquidNav` | `LiquidTabs`' strip as a `nav` of links; takes a router's link component |
|
|
1350
1431
|
| `LiquidTabs` | Tab strip with a sliding pill; arrow keys, roving focus, 44px `lg` size |
|
|
1351
1432
|
| `MatchupCard` | Head-to-head comparison card |
|
|
1433
|
+
| `MediaPreview` | Thumbnail that falls back to "Preview unavailable, open original" when it can't render |
|
|
1352
1434
|
| `Pullquote` | A line lifted out of prose, as `<figure>` + `<blockquote>` + `<figcaption>` |
|
|
1353
1435
|
| `RadioGroup` | Radio set reporting one `string`; descriptions, error, 44px targets |
|
|
1354
1436
|
| `Toast` | Transient floating notice; `Toaster` + the imperative `toast` handle |
|
|
@@ -1360,8 +1442,9 @@ function App() {
|
|
|
1360
1442
|
| `ActionBar` | Sticky bottom action strip |
|
|
1361
1443
|
| `Countdown` | Live countdown timer |
|
|
1362
1444
|
| `DataTable` | Full-featured table with sorting and pagination via TanStack Table v9. Imported from `@blakesteve/roster/data-table` (see [DataTable columns](#datatable-columns)) |
|
|
1363
|
-
| `Dialog` | Modal dialog
|
|
1445
|
+
| `Dialog` | Modal dialog; `onAfterClose` runs once it has finished closing |
|
|
1364
1446
|
| `Footer` | Site footer |
|
|
1447
|
+
| `ModerationQueue` | Items waiting for a decision: busy per action, confirm, reason and edit steps, focus after an item leaves, a live region. Never fetches (see [ModerationQueue](#moderationqueue)) |
|
|
1365
1448
|
| `Navbar` | Responsive navigation bar with mobile slide-out |
|
|
1366
1449
|
| `Sheet` | Bottom sheet: drag, Escape or backdrop to dismiss; only its content scrolls; returns focus, even from a deep link; the title may be a node |
|
|
1367
1450
|
| `Table` | Static data table |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("react/jsx-runtime"),m=require("react"),i=require("../../../lib/utils.cjs");function h({src:e,href:t,alt:s,unavailableLabel:n="Preview unavailable",openLabel:a="Open original",onUnavailable:u,className:o,...g}){const[x,b]=m.useState(null),f=!e||x===e,c="rst:font-ui rst:h-28 rst:w-36 rst:shrink-0 rst:rounded-md rst:border rst:border-[var(--roster-card-border)]";if(f){const d=r.jsxs(r.Fragment,{children:[r.jsx("span",{children:n}),t?r.jsxs("span",{className:"rst:font-semibold rst:underline",children:[a,r.jsx("span",{"aria-hidden":"true",children:" ↗"})]}):null]}),l=i.cn(c,"rst:flex rst:flex-col rst:items-center rst:justify-center rst:gap-1 rst:border-dashed rst:px-2 rst:text-center rst:text-xs rst:text-gray-600 rst:dark:text-gray-300",o);return t?r.jsx("a",{href:t,target:"_blank",rel:"noreferrer",className:i.cn(l,"rst:hover:bg-gray-50 rst:dark:hover:bg-gray-800 rst:focus-visible:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring"),"aria-label":`${n}. ${a}, in a new tab: ${s}`,children:d}):r.jsx("span",{role:"img","aria-label":`${n}: ${s}`,className:l,children:d})}return r.jsx("img",{...g,src:e,alt:s,className:i.cn(c,"rst:object-cover",o),onError:()=>{b(e),u?.()}})}exports.MediaPreview=h;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ImgHTMLAttributes } from 'react';
|
|
2
|
+
export interface MediaPreviewProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "onError"> {
|
|
3
|
+
/** The thumbnail. Missing or failing to load, the preview falls back. */
|
|
4
|
+
src?: string | null;
|
|
5
|
+
/** The original, opened from the fallback. Without it the fallback says so. */
|
|
6
|
+
href?: string | null;
|
|
7
|
+
/** What the image shows. Required: a preview in a queue is content. */
|
|
8
|
+
alt: string;
|
|
9
|
+
/** What the fallback says before "Open original". */
|
|
10
|
+
unavailableLabel?: string;
|
|
11
|
+
/** The link's own words. */
|
|
12
|
+
openLabel?: string;
|
|
13
|
+
/** Called once when the thumbnail fails, for an app that keeps a count. */
|
|
14
|
+
onUnavailable?: () => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A thumbnail that admits when it can't render.
|
|
18
|
+
*
|
|
19
|
+
* Previews fail in ordinary ways: a short-lived signed link that expired, or a
|
|
20
|
+
* format the browser can't draw, such as a photo straight off a phone. The
|
|
21
|
+
* browser's answer is a broken-image glyph, which tells a reviewer nothing
|
|
22
|
+
* and hides the one thing they may need to see before deciding. This swaps it
|
|
23
|
+
* for a card that says the preview is unavailable and links to the original,
|
|
24
|
+
* in the same footprint, so a row of previews doesn't jump.
|
|
25
|
+
*/
|
|
26
|
+
declare function MediaPreview({ src, href, alt, unavailableLabel, openLabel, onUnavailable, className, ...props }: MediaPreviewProps): import("react/jsx-runtime").JSX.Element;
|
|
27
|
+
export { MediaPreview };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as r, jsxs as m, Fragment as x } from "react/jsx-runtime";
|
|
3
|
+
import { useState as p } from "react";
|
|
4
|
+
import { cn as i } from "../../../lib/utils.js";
|
|
5
|
+
function $({
|
|
6
|
+
src: e,
|
|
7
|
+
href: t,
|
|
8
|
+
alt: s,
|
|
9
|
+
unavailableLabel: n = "Preview unavailable",
|
|
10
|
+
openLabel: o = "Open original",
|
|
11
|
+
onUnavailable: g,
|
|
12
|
+
className: a,
|
|
13
|
+
...u
|
|
14
|
+
}) {
|
|
15
|
+
const [f, b] = p(null), h = !e || f === e, d = "rst:font-ui rst:h-28 rst:w-36 rst:shrink-0 rst:rounded-md rst:border rst:border-[var(--roster-card-border)]";
|
|
16
|
+
if (h) {
|
|
17
|
+
const l = /* @__PURE__ */ m(x, { children: [
|
|
18
|
+
/* @__PURE__ */ r("span", { children: n }),
|
|
19
|
+
t ? /* @__PURE__ */ m("span", { className: "rst:font-semibold rst:underline", children: [
|
|
20
|
+
o,
|
|
21
|
+
/* @__PURE__ */ r("span", { "aria-hidden": "true", children: " ↗" })
|
|
22
|
+
] }) : null
|
|
23
|
+
] }), c = i(
|
|
24
|
+
d,
|
|
25
|
+
"rst:flex rst:flex-col rst:items-center rst:justify-center rst:gap-1 rst:border-dashed rst:px-2 rst:text-center rst:text-xs rst:text-gray-600 rst:dark:text-gray-300",
|
|
26
|
+
a
|
|
27
|
+
);
|
|
28
|
+
return t ? /* @__PURE__ */ r(
|
|
29
|
+
"a",
|
|
30
|
+
{
|
|
31
|
+
href: t,
|
|
32
|
+
target: "_blank",
|
|
33
|
+
rel: "noreferrer",
|
|
34
|
+
className: i(c, "rst:hover:bg-gray-50 rst:dark:hover:bg-gray-800 rst:focus-visible:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring"),
|
|
35
|
+
"aria-label": `${n}. ${o}, in a new tab: ${s}`,
|
|
36
|
+
children: l
|
|
37
|
+
}
|
|
38
|
+
) : /* @__PURE__ */ r("span", { role: "img", "aria-label": `${n}: ${s}`, className: c, children: l });
|
|
39
|
+
}
|
|
40
|
+
return /* @__PURE__ */ r(
|
|
41
|
+
"img",
|
|
42
|
+
{
|
|
43
|
+
...u,
|
|
44
|
+
src: e,
|
|
45
|
+
alt: s,
|
|
46
|
+
className: i(d, "rst:object-cover", a),
|
|
47
|
+
onError: () => {
|
|
48
|
+
b(e), g?.();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
$ as MediaPreview
|
|
55
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("react/jsx-runtime"),t=require("react"),
|
|
1
|
+
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("react/jsx-runtime"),t=require("react"),h=require("../../../node_modules/@fortawesome/react-fontawesome/dist/index.cjs"),k=require("../../../node_modules/@fortawesome/free-solid-svg-icons/index.cjs"),o=require("../../../node_modules/class-variance-authority/dist/index.cjs"),s=require("../../../lib/utils.cjs"),j=require("../../../internal/page-inert.cjs"),a=require("../../../node_modules/@headlessui/react/dist/components/dialog/dialog.cjs"),d=require("../../../node_modules/@headlessui/react/dist/components/transition/transition.cjs"),w=o.cva("rst:relative rst:w-full rst:transform rst:overflow-hidden rst:rounded-2xl rst:p-6 rst:text-left rst:align-middle rst:elevation-overlay rst:transition-all rst:border",{variants:{size:{xs:"rst:max-w-xs",sm:"rst:max-w-sm",md:"rst:max-w-md",lg:"rst:max-w-lg",xl:"rst:max-w-xl","2xl":"rst:max-w-2xl","3xl":"rst:max-w-3xl",full:"rst:max-w-[95vw] rst:m-4"},variant:{white:"rst:bg-[var(--roster-popover-bg)] rst:border-[var(--roster-popover-border)] rst:text-[var(--roster-popover-text)]",slate:"rst:bg-gray-700 rst:border-gray-600 rst:text-gray-100 rst:dark:bg-gray-900 rst:dark:border-gray-800 rst:[--roster-ring:var(--roster-primary-400,#5ea3de)] rst:[--roster-ring-offset:var(--roster-gray-700,#44403c)] rst:dark:[--roster-ring-offset:var(--roster-gray-900,#1c1917)] rst:[--roster-control-text:var(--roster-gray-100,#f5f5f4)] rst:[--roster-control-border:var(--roster-gray-400,#a8a29e)] rst:[--roster-control-border-focus:var(--roster-primary-400,#5ea3de)]",primary:"rst:bg-primary-700 rst:border-primary-600 rst:text-primary-700-ink rst:dark:text-white rst:dark:bg-primary-950 rst:dark:border-primary-900 rst:[--roster-ring:var(--roster-primary-400,#5ea3de)] rst:[--roster-ring-offset:var(--roster-primary-700,#084063)] rst:dark:[--roster-ring-offset:var(--roster-primary-950,#021724)] rst:[--roster-control-text:var(--roster-gray-100,#f5f5f4)] rst:[--roster-control-border:var(--roster-gray-400,#a8a29e)] rst:[--roster-control-border-focus:var(--roster-primary-400,#5ea3de)]",glass:"rst:bg-white/80 rst:border-white/20 rst:backdrop-blur-xl rst:text-gray-900 rst:dark:bg-slate-900/80 rst:dark:border-slate-700/50 rst:dark:text-white"},status:{default:"",destructive:"rst:border-t-4 rst:border-t-error-500",success:"rst:border-t-4 rst:border-t-success-500"}},defaultVariants:{size:"md",variant:"white",status:"default"}}),N=o.cva("rst:text-xl rst:font-bold rst:leading-6 rst:text-inherit"),_=o.cva("rst:mt-1 rst:text-sm rst:text-inherit rst:opacity-75"),q=o.cva("rst:inline-flex rst:size-11 rst:shrink-0 rst:items-center rst:justify-center rst:-m-3 rst:cursor-pointer rst:rounded-md rst:bg-transparent rst:text-inherit rst:opacity-50 rst:hover:opacity-100 rst:focus:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring rst:focus-visible:ring-offset-2 rst:ring-offset-background rst:transition-opacity"),F=({isOpen:u,onClose:i,title:m,description:n,size:x,variant:l,status:g,children:f,className:v,onAfterClose:b})=>{const[p,y]=t.useState(!1);t.useLayoutEffect(()=>{y(!0)},[]);const e=u&&p,c=t.useRef(null);return t.useLayoutEffect(()=>{if(e)return j.lockPage(c.current)},[e]),r.jsxs(r.Fragment,{children:[e&&r.jsx("span",{ref:c,hidden:!0}),r.jsxs(a.Dialog,{open:e,as:"div",className:"rst:font-ui rst:relative rst:z-50",onClose:i,children:[r.jsx(d.TransitionChild,{as:t.Fragment,enter:"rst:ease-out rst:duration-300",enterFrom:"rst:opacity-0",enterTo:"rst:opacity-100",leave:"rst:ease-in rst:duration-200",leaveFrom:"rst:opacity-100",leaveTo:"rst:opacity-0",children:r.jsx(a.DialogBackdrop,{className:s.cn("rst:fixed rst:inset-0 rst:transition-opacity",l==="glass"?"rst:bg-[var(--roster-backdrop,color-mix(in_oklab,oklch(20.8%_0.042_265.755)_40%,transparent))] rst:dark:bg-[var(--roster-backdrop,color-mix(in_oklab,var(--roster-black,#1e1c1a)_60%,transparent))]":"rst:bg-[var(--roster-backdrop,color-mix(in_oklab,oklch(20.8%_0.042_265.755)_60%,transparent))] rst:dark:bg-[var(--roster-backdrop,color-mix(in_oklab,var(--roster-black,#1e1c1a)_80%,transparent))] rst:backdrop-blur-backdrop")})}),r.jsx("div",{className:"rst:fixed rst:inset-0 rst:overflow-y-auto",children:r.jsx("div",{className:"rst:flex rst:min-h-full rst:items-center rst:justify-center rst:p-4 rst:text-center",children:r.jsx(d.TransitionChild,{as:t.Fragment,enter:"rst:ease-out rst:duration-300",enterFrom:"rst:opacity-0 rst:scale-95",enterTo:"rst:opacity-100 rst:scale-100",leave:"rst:ease-in rst:duration-200",leaveFrom:"rst:opacity-100 rst:scale-100",leaveTo:"rst:opacity-0 rst:scale-95",afterLeave:b,children:r.jsxs(a.DialogPanel,{className:s.cn(w({size:x,variant:l,status:g}),v),children:[r.jsxs("div",{className:"rst:flex rst:items-start rst:justify-between",children:[r.jsxs("div",{children:[r.jsx(a.DialogTitle,{as:"h2",className:s.cn(N()),children:m}),n&&r.jsx("p",{className:s.cn(_()),children:n})]}),r.jsx("div",{className:"rst:ml-4 rst:flex rst:shrink-0",children:r.jsx("button",{type:"button",onClick:i,className:s.cn(q()),"aria-label":"Close dialog",children:r.jsx(h.FontAwesomeIcon,{icon:k.faXmark,className:"rst:h-5 rst:w-5"})})})]}),r.jsx("div",{className:"rst:mt-6",children:f})]})})})})]})]})};exports.Dialog=F;
|
|
@@ -12,6 +12,14 @@ export interface DialogProps extends VariantProps<typeof dialogVariants> {
|
|
|
12
12
|
description?: string;
|
|
13
13
|
children: ReactNode;
|
|
14
14
|
className?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Called once the dialog has finished closing, after its leave transition.
|
|
17
|
+
* The place to move focus somewhere other than where it was before the
|
|
18
|
+
* dialog opened, for instance when that element no longer exists. Not
|
|
19
|
+
* called for a close cut short by reopening, nor when the dialog unmounts
|
|
20
|
+
* while open.
|
|
21
|
+
*/
|
|
22
|
+
onAfterClose?: () => void;
|
|
15
23
|
}
|
|
16
|
-
declare const Dialog: ({ isOpen, onClose, title, description, size, variant, status, children, className, }: DialogProps) => import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
declare const Dialog: ({ isOpen, onClose, title, description, size, variant, status, children, className, onAfterClose, }: DialogProps) => import("react/jsx-runtime").JSX.Element;
|
|
17
25
|
export { Dialog };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsxs as t, Fragment as
|
|
3
|
-
import { useState as
|
|
4
|
-
import { FontAwesomeIcon as
|
|
5
|
-
import { faXmark as
|
|
2
|
+
import { jsxs as t, Fragment as k, jsx as r } from "react/jsx-runtime";
|
|
3
|
+
import { useState as w, useLayoutEffect as c, useRef as N, Fragment as d } from "react";
|
|
4
|
+
import { FontAwesomeIcon as _ } from "../../../node_modules/@fortawesome/react-fontawesome/dist/index.js";
|
|
5
|
+
import { faXmark as F } from "../../../node_modules/@fortawesome/free-solid-svg-icons/index.js";
|
|
6
6
|
import { cva as a } from "../../../node_modules/class-variance-authority/dist/index.js";
|
|
7
7
|
import { cn as s } from "../../../lib/utils.js";
|
|
8
|
-
import { lockPage as
|
|
9
|
-
import { Dialog as
|
|
8
|
+
import { lockPage as T } from "../../../internal/page-inert.js";
|
|
9
|
+
import { Dialog as j, DialogBackdrop as z, DialogPanel as D, DialogTitle as V } from "../../../node_modules/@headlessui/react/dist/components/dialog/dialog.js";
|
|
10
10
|
import { TransitionChild as m } from "../../../node_modules/@headlessui/react/dist/components/transition/transition.js";
|
|
11
|
-
const
|
|
11
|
+
const L = /* @__PURE__ */ a(
|
|
12
12
|
"rst:relative rst:w-full rst:transform rst:overflow-hidden rst:rounded-2xl rst:p-6 rst:text-left rst:align-middle rst:elevation-overlay rst:transition-all rst:border",
|
|
13
13
|
{
|
|
14
14
|
variants: {
|
|
@@ -77,9 +77,9 @@ const V = /* @__PURE__ */ a(
|
|
|
77
77
|
status: "default"
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
), C = /* @__PURE__ */ a("rst:text-xl rst:font-bold rst:leading-6 rst:text-inherit"),
|
|
80
|
+
), C = /* @__PURE__ */ a("rst:text-xl rst:font-bold rst:leading-6 rst:text-inherit"), P = /* @__PURE__ */ a("rst:mt-1 rst:text-sm rst:text-inherit rst:opacity-75"), R = /* @__PURE__ */ a(
|
|
81
81
|
"rst:inline-flex rst:size-11 rst:shrink-0 rst:items-center rst:justify-center rst:-m-3 rst:cursor-pointer rst:rounded-md rst:bg-transparent rst:text-inherit rst:opacity-50 rst:hover:opacity-100 rst:focus:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring rst:focus-visible:ring-offset-2 rst:ring-offset-background rst:transition-opacity"
|
|
82
|
-
),
|
|
82
|
+
), $ = ({
|
|
83
83
|
isOpen: f,
|
|
84
84
|
onClose: o,
|
|
85
85
|
title: p,
|
|
@@ -88,20 +88,21 @@ const V = /* @__PURE__ */ a(
|
|
|
88
88
|
variant: n,
|
|
89
89
|
status: g,
|
|
90
90
|
children: v,
|
|
91
|
-
className: u
|
|
91
|
+
className: u,
|
|
92
|
+
onAfterClose: x
|
|
92
93
|
}) => {
|
|
93
|
-
const [
|
|
94
|
+
const [y, h] = w(!1);
|
|
94
95
|
c(() => {
|
|
95
|
-
|
|
96
|
+
h(!0);
|
|
96
97
|
}, []);
|
|
97
|
-
const e = f &&
|
|
98
|
+
const e = f && y, l = N(null);
|
|
98
99
|
return c(() => {
|
|
99
100
|
if (e)
|
|
100
|
-
return
|
|
101
|
-
}, [e]), /* @__PURE__ */ t(
|
|
101
|
+
return T(l.current);
|
|
102
|
+
}, [e]), /* @__PURE__ */ t(k, { children: [
|
|
102
103
|
e && /* @__PURE__ */ r("span", { ref: l, hidden: !0 }),
|
|
103
104
|
/* @__PURE__ */ t(
|
|
104
|
-
|
|
105
|
+
j,
|
|
105
106
|
{
|
|
106
107
|
open: e,
|
|
107
108
|
as: "div",
|
|
@@ -119,7 +120,7 @@ const V = /* @__PURE__ */ a(
|
|
|
119
120
|
leaveFrom: "rst:opacity-100",
|
|
120
121
|
leaveTo: "rst:opacity-0",
|
|
121
122
|
children: /* @__PURE__ */ r(
|
|
122
|
-
|
|
123
|
+
z,
|
|
123
124
|
{
|
|
124
125
|
className: s(
|
|
125
126
|
"rst:fixed rst:inset-0 rst:transition-opacity",
|
|
@@ -139,27 +140,28 @@ const V = /* @__PURE__ */ a(
|
|
|
139
140
|
leave: "rst:ease-in rst:duration-200",
|
|
140
141
|
leaveFrom: "rst:opacity-100 rst:scale-100",
|
|
141
142
|
leaveTo: "rst:opacity-0 rst:scale-95",
|
|
143
|
+
afterLeave: x,
|
|
142
144
|
children: /* @__PURE__ */ t(
|
|
143
|
-
|
|
145
|
+
D,
|
|
144
146
|
{
|
|
145
147
|
className: s(
|
|
146
|
-
|
|
148
|
+
L({ size: b, variant: n, status: g }),
|
|
147
149
|
u
|
|
148
150
|
),
|
|
149
151
|
children: [
|
|
150
152
|
/* @__PURE__ */ t("div", { className: "rst:flex rst:items-start rst:justify-between", children: [
|
|
151
153
|
/* @__PURE__ */ t("div", { children: [
|
|
152
|
-
/* @__PURE__ */ r(
|
|
153
|
-
i && /* @__PURE__ */ r("p", { className: s(
|
|
154
|
+
/* @__PURE__ */ r(V, { as: "h2", className: s(C()), children: p }),
|
|
155
|
+
i && /* @__PURE__ */ r("p", { className: s(P()), children: i })
|
|
154
156
|
] }),
|
|
155
157
|
/* @__PURE__ */ r("div", { className: "rst:ml-4 rst:flex rst:shrink-0", children: /* @__PURE__ */ r(
|
|
156
158
|
"button",
|
|
157
159
|
{
|
|
158
160
|
type: "button",
|
|
159
161
|
onClick: o,
|
|
160
|
-
className: s(
|
|
162
|
+
className: s(R()),
|
|
161
163
|
"aria-label": "Close dialog",
|
|
162
|
-
children: /* @__PURE__ */ r(
|
|
164
|
+
children: /* @__PURE__ */ r(_, { icon: F, className: "rst:h-5 rst:w-5" })
|
|
163
165
|
}
|
|
164
166
|
) })
|
|
165
167
|
] }),
|
|
@@ -175,5 +177,5 @@ const V = /* @__PURE__ */ a(
|
|
|
175
177
|
] });
|
|
176
178
|
};
|
|
177
179
|
export {
|
|
178
|
-
|
|
180
|
+
$ as Dialog
|
|
179
181
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("react/jsx-runtime"),a=require("react"),z=require("../../../lib/utils.cjs"),xe=require("../../atoms/Button/Button.cjs"),Oe=require("../../atoms/Card/Card.cjs"),he=require("../../atoms/Spinner/Spinner.cjs"),Pe=require("../../atoms/Textarea/Textarea.cjs"),Z=require("../../molecules/Alert/Alert.cjs"),ze=require("../../molecules/EmptyState/EmptyState.cjs"),De=require("../../molecules/RadioGroup/RadioGroup.cjs"),He=require("./queue-actions.cjs"),pe={primary:{colorScheme:"primary",variant:"solid"},success:{colorScheme:"success",variant:"solid"},danger:{colorScheme:"error",variant:"soft"},neutral:{colorScheme:"neutral",variant:"outline"}},Ke={edit:He.editAction},R=o=>o.edit?o:null,Qe=o=>o===0?"None left.":`${o} left.`,Ge=o=>Math.min(6,Math.max(1,o)),We=o=>o instanceof Error&&o.message?o.message:"Something went wrong.";function B(o,f){const p={...o};return delete p[f],p}function _({busy:o=!1,blocked:f=!1,onClick:p,children:j,className:v,...y}){const g=o||f;return n.jsx(xe.Button,{size:"lg",...y,"aria-disabled":g||void 0,"aria-busy":o||void 0,startIcon:o?n.jsx(he.Spinner,{size:"sm",variant:"current"}):void 0,className:z.cn(f&&!o&&"rst:cursor-not-allowed rst:opacity-50",v),onClick:T=>{if(g){T.preventDefault();return}p?.(T)},children:j})}function Je({label:o,hideLabel:f=!1,headingLevel:p=2,itemHeadingLevel:j,items:v,getKey:y,itemLabel:g,renderMeta:T,renderContent:ve,actions:D,status:ee="ready",error:be,onRetry:te,empty:re={title:"Nothing waiting"},density:ne="comfortable",footer:ye,className:Se}){const je=typeof D=="function"?D(Ke):D,N=a.useId(),q=a.useRef(null),k=v.map(y),E=a.useRef(new Map);E.current=new Map(v.map((e,t)=>[k[t],e]));const[H,qe]=a.useState({}),$=a.useRef({}),se=e=>{$.current=e($.current),qe($.current)},[oe,F]=a.useState({}),[ce,ke]=a.useState({}),x=a.useRef({}),h=(e,t)=>{x.current=t?{...x.current,[e]:t}:B(x.current,e),ke(x.current)},[K,$e]=a.useState({text:"",n:0}),Q=a.useRef([]),w=a.useRef(null),ie=a.useCallback(e=>{Q.current.push(e),!w.current&&(w.current=setTimeout(()=>{w.current=null;const t=Q.current.join(" ");Q.current=[],$e(r=>({text:t,n:r.n+1}))},0))},[]);a.useLayoutEffect(()=>()=>{w.current&&clearTimeout(w.current)},[]);const[Re,Ne]=a.useState(0),G=a.useRef(null),S=a.useCallback(e=>{G.current=e,Ne(t=>t+1)},[]),ae=a.useCallback(e=>{const t=q.current;if(!t)return null;const r="key"in e?CSS.escape(e.key):"";switch(e.kind){case"item":return t.querySelector(`[data-queue-item="${r}"]`);case"empty":return t.querySelector("[data-queue-empty]");case"trigger":return t.querySelector(`[data-queue-item="${r}"] [data-queue-action="${CSS.escape(e.actionId)}"]`);case"step":return t.querySelector(`[data-queue-item="${r}"] [data-queue-step]`)}},[]);a.useLayoutEffect(()=>{const e=G.current;if(!e)return;G.current=null,(ae(e)??q.current?.querySelector("[data-queue-item]")??q.current?.querySelector("[data-queue-empty]"))?.focus()},[Re,ae]);const W=a.useRef(null),we=e=>()=>{W.current=e},Ce=e=>{e.relatedTarget&&!q.current?.contains(e.relatedTarget)&&(W.current=null)},A=e=>{if(W.current!==e)return!1;const t=document.activeElement;return!t||t===document.body?!0:q.current?.contains(t)?!!t.closest(`[data-queue-item="${CSS.escape(e)}"]`):!1},C=a.useRef(null),M=(e,t)=>{C.current=t,h(e.key,{...e,open:!1})},le=a.useRef(k),J=a.useRef(new Map),[,Ie]=a.useState(0),U=a.useRef(new Set),ue=(e,t,r)=>{const c=t.indexOf(e),d=t.slice(c+1).find(i=>r.has(i));if(d)return{kind:"item",key:d};const s=t.slice(0,c).reverse().find(i=>r.has(i));return s?{kind:"item",key:s}:{kind:"empty"}};a.useLayoutEffect(()=>{const e=le.current;le.current=k;const t=new Set(k),r=J.current;J.current=new Map;const c=[];let d=!1;for(const[s,i]of r){const l=t.has(s);c.push(i.announce??i.sentence),l||(d=!0),U.current.delete(s);const u=l?{kind:"trigger",key:s,actionId:i.actionId}:e.includes(s)?ue(s,e,t):null,m=x.current[s];if(m?.surface)M(m,u??{kind:"empty"});else{const I=A(s);m&&h(s,null),u&&I&&S(u)}}for(const s of e.filter(i=>!t.has(i))){if(r.has(s))continue;F(m=>s in m?B(m,s):m);const i=ue(s,e,t),l=C.current;if(l&&"key"in l&&l.key===s&&(C.current=i),$.current[s]){U.current.add(s),A(s)&&S(i);continue}const u=x.current[s];if(u&&!(u.surface&&!u.open))if(c.push(`${u.label} was decided elsewhere.`),d=!0,u.surface)M(u,i);else{const m=A(s);h(s,null),m&&S(i)}else!u&&A(s)&&S(i)}c.length&&ie(`${c.join(" ")}${d?` ${Qe(k.length)}`:""}`)});const L=async(e,t,r,c)=>{const d=E.current.get(e)??t,s=g(d),i=r.edit?R(r).run:r.run;se(l=>({...l,[e]:r.id})),F(l=>B(l,e));try{const l=await i(d,c);J.current.set(e,{actionId:r.id,sentence:`${r.done??r.label}: ${s}.`,announce:l?l.announce:void 0}),Ie(u=>u+1)}catch(l){const u=We(l);ie(`${r.label} didn't go through for ${s}. ${u}`);const m=x.current[e];U.current.delete(e)?m?.surface?M(m,{kind:"empty"}):m&&h(e,null):m?h(e,{...m,error:u}):F(I=>({...I,[e]:u}))}finally{se(l=>B(l,e))}},Be=(e,t)=>{const r=y(e),c=R(t);if(!t.confirm&&!t.reason&&!c){L(r,e,t,{});return}F(d=>B(d,r)),h(r,{key:r,label:g(e),item:e,action:t,draft:c?c.edit.initial(e):void 0,reason:{},surface:!!c?.edit.surface,open:!0}),c?.edit.surface||S({kind:"step",key:r})},V=e=>{const t=x.current[e];if(!t||$.current[e])return;const r={kind:"trigger",key:e,actionId:t.action.id};t.surface?M(t,r):(h(e,null),S(r))},Te=e=>{const t=x.current[e];!t||$.current[e]||ge(t,E.current.get(e)??t.item)||(t.action.edit?L(e,t.item,t.action,{draft:t.draft}):t.action.reason?L(e,t.item,t.action,{reason:Ue(t.reason)}):L(e,t.item,t.action,{}))},Ee=e=>{const t=C.current;C.current=null,h(e,null),t&&S(t)},Fe=`h${p}`,de=j??Ge(p+1),Ae=`h${de}`,X=`${N}-label`,fe=(e,t)=>{const{action:r}=e,c=R(r),d=E.current.get(e.key)??e.item,s=!!H[e.key],i=ge(e,d),l=`${N}-step-${t}-message`,u=n.jsx(_,{...pe[r.tone??"neutral"],busy:s,blocked:!!i,"aria-describedby":i?l:void 0,onClick:()=>Te(e.key),"data-queue-submit":"",children:c?c.edit.submitLabel:r.label}),m=n.jsx(_,{variant:"ghost",colorScheme:"neutral",blocked:s,onClick:()=>V(e.key),children:"Cancel"}),I=n.jsxs(n.Fragment,{children:[r.reason&&n.jsx(Ve,{id:`${N}-step-${t}`,step:r.reason,value:e.reason,disabled:s,onChange:Y=>h(e.key,{...x.current[e.key],reason:Y})}),c&&n.jsx(c.edit.Form,{item:d,value:e.draft,onChange:Y=>h(e.key,{...x.current[e.key],draft:Y})}),i&&n.jsx("p",{id:l,className:"rst:text-sm rst:text-error-600 rst:dark:text-error-400","data-queue-validation":"",children:i}),e.error&&n.jsx(Z.Alert,{colorScheme:"error",role:"group","aria-live":"off",children:e.error})]});return{submitButton:u,cancelButton:m,fields:I}},Me=(e,t)=>{const{submitButton:r,cancelButton:c,fields:d}=fe(e,t),s=`${N}-step-${t}-prompt`,i=R(e.action),l=e.action.confirm?.prompt??e.action.reason?.prompt??i?.edit.title;return n.jsxs("div",{role:"group","aria-labelledby":s,tabIndex:-1,"data-queue-step":"",onKeyDown:u=>{u.key==="Escape"&&(u.stopPropagation(),V(e.key))},className:"rst:flex rst:flex-col rst:gap-3 rst:rounded-lg rst:border rst:border-[var(--roster-card-border)] rst:p-4 rst:focus-visible:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring",children:[n.jsx("p",{id:s,className:"rst:text-sm rst:font-medium rst:text-[var(--roster-control-text)]",children:l}),i?.edit.description&&n.jsx("p",{className:"rst:text-sm rst:text-gray-600 rst:dark:text-gray-300",children:i.edit.description}),d,n.jsxs("div",{className:"rst:flex rst:flex-wrap rst:gap-2",children:[r,c]})]})},Le=(e,t)=>{const r=H[t];return n.jsx("div",{role:"group","aria-label":`Decide: ${g(e)}`,className:"rst:flex rst:flex-wrap rst:gap-2",children:je.filter(c=>!c.hidden?.(e)).map(c=>n.jsx(_,{...pe[c.tone??"neutral"],"data-queue-action":c.id,busy:r===c.id,blocked:!!r&&r!==c.id,onClick:()=>Be(e,c),children:c.label},c.id))})},b=Object.values(ce).find(e=>e.surface)??null,O=b?R(b.action):null,me=O?.edit.surface,P=b?fe(b,-1):null;return n.jsxs("section",{ref:q,"aria-labelledby":X,onBlur:Ce,className:z.cn("rst:font-ui rst:flex rst:flex-col rst:gap-4",Se),children:[n.jsx(Fe,{id:X,className:z.cn("rst:text-lg rst:font-semibold rst:text-[var(--roster-control-text)]",f&&"rst:sr-only"),children:o}),n.jsxs("div",{role:"status","aria-live":"polite","aria-atomic":"true",className:"rst:sr-only","data-queue-live":"","data-queue-live-n":K.n,children:[K.text,K.n%2?" ":""]}),ee==="loading"?n.jsx("div",{className:"rst:flex rst:justify-center rst:py-12","aria-busy":"true",children:n.jsx(he.Spinner,{size:"lg",label:`Loading ${o}`})}):ee==="error"?n.jsx(Z.Alert,{colorScheme:"error",children:n.jsxs("div",{className:"rst:flex rst:flex-col rst:items-start rst:gap-3",children:[n.jsx("div",{children:be??"The queue couldn't load."}),te&&n.jsx(xe.Button,{size:"lg",variant:"outline",colorScheme:"neutral",onClick:te,children:"Retry"})]})}):v.length===0?n.jsx(ze.EmptyState,{title:re.title,description:re.description,headingLevel:de,tabIndex:-1,"data-queue-empty":"",className:"rst:focus-visible:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring"}):n.jsx("ul",{role:"list","aria-labelledby":X,className:"rst:m-0 rst:flex rst:list-none rst:flex-col rst:gap-3 rst:p-0",children:v.map((e,t)=>{const r=k[t],c=`${N}-item-${t}`,d=T?.(e),s=ce[r];return n.jsx("li",{children:n.jsx("article",{"data-queue-item":r,tabIndex:-1,"aria-labelledby":c,onFocus:we(r),className:"rst:rounded-xl rst:focus-visible:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring rst:focus-visible:ring-offset-2 rst:ring-offset-background",children:n.jsx(Oe.Card,{padding:ne==="compact"?"sm":"md",children:n.jsxs("div",{className:z.cn("rst:flex rst:flex-col",ne==="compact"?"rst:gap-3":"rst:gap-4"),children:[n.jsxs("div",{className:"rst:flex rst:flex-wrap rst:items-baseline rst:gap-x-3 rst:gap-y-1",children:[n.jsx(Ae,{id:c,className:"rst:text-base rst:font-semibold",children:g(e)}),d&&n.jsx("div",{className:"rst:flex rst:flex-wrap rst:items-center rst:gap-2 rst:text-sm rst:text-gray-600 rst:dark:text-gray-300",children:d})]}),n.jsx("div",{className:"rst:min-w-0",children:ve(e)}),oe[r]&&n.jsx(Z.Alert,{colorScheme:"error",role:"group","aria-live":"off",children:oe[r]}),s&&!s.surface?Me(s,t):Le(e,r)]})})})},r)})}),ye,me&&b&&O&&P&&n.jsx(me,{open:b.open,title:O.edit.title,description:O.edit.description,busy:!!H[b.key],onCancel:()=>V(b.key),onAfterClose:()=>Ee(b.key),submitButton:P.submitButton,cancelButton:P.cancelButton,children:n.jsx("div",{className:"rst:flex rst:flex-col rst:gap-4",children:P.fields})})]})}function ge(o,f){const p=R(o.action);if(p)return p.edit.validate?.(o.draft,f);if(o.action.reason){if(o.action.reason.required&&!o.reason.value)return"Pick a reason.";if(o.action.reason.note==="required"&&!o.reason.note?.trim())return"Add a note."}}function Ue(o){const f=o.note?.trim();return{...o.value?{value:o.value}:{},...f?{note:f}:{}}}function Ve({id:o,step:f,value:p,disabled:j,onChange:v}){const y=f.note??"optional";return n.jsxs(n.Fragment,{children:[f.options&&f.options.length>0&&n.jsx(De.RadioGroup,{label:f.required?"Reason":"Reason (optional)",options:f.options,value:p.value??"",onChange:g=>v({...p,value:g}),disabled:j}),y!=="none"&&n.jsx(Pe.Textarea,{id:`${o}-note`,label:f.noteLabel??(y==="required"?"Note":"Note (optional)"),rows:3,value:p.note??"",onChange:g=>v({...p,note:g.target.value}),disabled:j})]})}exports.ModerationQueue=Je;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { QueueAction, QueueActionKit, QueueCheckedEditAction, QueueHeadingLevel } from './queue-actions';
|
|
3
|
+
export interface ModerationQueueProps<T> {
|
|
4
|
+
/** The queue's heading, and the list's accessible name. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Keep the heading for assistive technology only, when the page already titles the queue. */
|
|
7
|
+
hideLabel?: boolean;
|
|
8
|
+
/** The queue heading's level. `2` by default. */
|
|
9
|
+
headingLevel?: QueueHeadingLevel;
|
|
10
|
+
/** Each item's heading level. One below `headingLevel` by default. */
|
|
11
|
+
itemHeadingLevel?: QueueHeadingLevel;
|
|
12
|
+
/** What's waiting. Remove an item once its decision lands; the queue notices. */
|
|
13
|
+
items: readonly T[];
|
|
14
|
+
getKey: (item: T) => string;
|
|
15
|
+
/** The item's heading, and what announcements call it: "Recipe from Dana". */
|
|
16
|
+
itemLabel: (item: T) => string;
|
|
17
|
+
/** Beside the heading: badges, a time, who sent it. */
|
|
18
|
+
renderMeta?: (item: T) => ReactNode;
|
|
19
|
+
/** The item's body: its text, its previews, links out. */
|
|
20
|
+
renderContent: (item: T) => ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* What a reviewer can decide. A list, or a function returning one: the
|
|
23
|
+
* function form is handed `edit`, which types an edit action's draft, and
|
|
24
|
+
* is the one to use inline in JSX.
|
|
25
|
+
*
|
|
26
|
+
* actions={(a) => [
|
|
27
|
+
* { id: "approve", label: "Approve", run: (r) => approve(r.id) },
|
|
28
|
+
* a.edit({ id: "edit", label: "Edit", edit: { initial: (r) => ({ … }), … },
|
|
29
|
+
* run: (r, { draft }) => save(r.id, draft) }),
|
|
30
|
+
* ]}
|
|
31
|
+
*/
|
|
32
|
+
actions: readonly QueueAction<T>[] | ((kit: QueueActionKit<T>) => readonly QueueAction<T>[]);
|
|
33
|
+
/** `"loading"` before the first answer, `"error"` when the queue itself can't load. */
|
|
34
|
+
status?: "ready" | "loading" | "error";
|
|
35
|
+
/** The whole-queue error, shown with a Retry when `onRetry` is passed. */
|
|
36
|
+
error?: ReactNode;
|
|
37
|
+
onRetry?: () => void;
|
|
38
|
+
empty?: {
|
|
39
|
+
title: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
};
|
|
42
|
+
density?: "comfortable" | "compact";
|
|
43
|
+
/** After the list: "Load more", if the app pages. */
|
|
44
|
+
footer?: ReactNode;
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A queue of things waiting for a decision: submissions to approve, reports to
|
|
49
|
+
* resolve, claims to check before they publish.
|
|
50
|
+
*
|
|
51
|
+
* The app owns the data and every request; this owns the layout, the states,
|
|
52
|
+
* the steps and the keyboard. It never fetches. An action's `run` is the only
|
|
53
|
+
* path from a decision to the network, and the app says whether the item has
|
|
54
|
+
* left by removing it from `items`.
|
|
55
|
+
*
|
|
56
|
+
* What it promises, each pinned in ModerationQueue.checks.stories.tsx:
|
|
57
|
+
* - Only the clicked action is busy, and it keeps focus while it is. That
|
|
58
|
+
* item's other actions wait; every other item stays live.
|
|
59
|
+
* - A decision that lands and removes the item is announced with how many
|
|
60
|
+
* are left, and focus moves to the next item, the previous one, or the
|
|
61
|
+
* empty state. Never to the page.
|
|
62
|
+
* - A decision that fails leaves the item where it was, says why, and leaves
|
|
63
|
+
* focus on the button that tried. A step stays open with its draft.
|
|
64
|
+
* - Confirm, reason and edit steps open in place (an edit may open in a
|
|
65
|
+
* Dialog or Sheet instead), take focus, and give it back on Escape.
|
|
66
|
+
* - An item that disappears for any other reason, with its step open,
|
|
67
|
+
* closes the step without deciding and is announced once as decided
|
|
68
|
+
* elsewhere. Focus moves only if it was inside the item.
|
|
69
|
+
* - Several outcomes in one render are said in one sentence.
|
|
70
|
+
*/
|
|
71
|
+
declare function ModerationQueue<T>({ label, hideLabel, headingLevel, itemHeadingLevel, items, getKey, itemLabel, renderMeta, renderContent, actions: actionsProp, status, error, onRetry, empty, density, footer, className, }: ModerationQueueProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
72
|
+
export type { QueueCheckedEditAction };
|
|
73
|
+
export { ModerationQueue };
|