@nxgt/material 3.60.0 → 3.63.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/CHANGELOG.md +44 -0
- package/README.md +3 -2
- package/dist/alert-dialog-RHm6WtQj.js +189 -0
- package/dist/{async-slice-HJY_VJjD.js → async-slice-DmX59NyA.js} +3 -4
- package/dist/{button-7BuUCGRm.js → button-DfS_amfQ.js} +1 -1
- package/dist/charts.js +3 -3
- package/dist/{chip-kQOrHlDX.js → chip-Cbndukko.js} +2 -2
- package/dist/{code-editor-Ckc9TTCM.js → code-editor-B0tN8Ha_.js} +11 -11
- package/dist/code-editor.js +54 -37
- package/dist/{code-language-items-rCPB0Xee.js → code-language-items-andgHKrM.js} +2 -2
- package/dist/components/ui/layout/sidebar/sidebar-tokens.css +13 -1
- package/dist/{components-BSM-h5IQ.js → components-Cm2doOJS.js} +13090 -12957
- package/dist/components.js +28 -26
- package/dist/dialog-Bd7Hid2L.js +219 -0
- package/dist/dnd.js +1 -1
- package/dist/{dropdown-menu-CiobN1wi.js → dropdown-menu-_SyVmSuy.js} +1 -1
- package/dist/editor.js +707 -706
- package/dist/{empty-state-DA517HAb.js → empty-state-D-2ZMXXJ.js} +2 -2
- package/dist/escape-owner-DpaP0lm1.js +19 -0
- package/dist/{field-blur-DdvY0NQQ.js → field-blur-DyOByjzw.js} +1 -1
- package/dist/{field-blur-context-DbJQZkEB.js → field-blur-context-DPe5_Nv5.js} +1 -1
- package/dist/hooks-a7_exKWS.js +329 -0
- package/dist/hooks.js +8 -102
- package/dist/{hover-card-DPs9huSs.js → hover-card-D_0smKBQ.js} +2 -2
- package/dist/i18n.js +8 -10
- package/dist/{icon-badge-C646JTOI.js → icon-badge-CBCU3ySF.js} +14 -14
- package/dist/{icon-button-DvP0Gy3j.js → icon-button-hKHsWTAt.js} +1 -1
- package/dist/illustrations-game.js +2 -0
- package/dist/lib/components/ui/display/list-tile/list-tile.d.ts +6 -0
- package/dist/lib/components/ui/display/list-tile/partials/list-tile-classes.d.ts +3 -1
- package/dist/lib/components/ui/display/list-tile/partials/list-tile-line.d.ts +1 -1
- package/dist/lib/components/ui/display/list-tile/partials/list-tile-toggle.d.ts +1 -1
- package/dist/lib/components/ui/feedback/index.d.ts +1 -0
- package/dist/lib/components/ui/feedback/unsaved-dialog/index.d.ts +1 -0
- package/dist/lib/components/ui/feedback/unsaved-dialog/unsaved-dialog.d.ts +20 -0
- package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
- package/dist/lib/components/ui/fields/textarea-field/textarea-footer.d.ts +2 -1
- package/dist/lib/components/ui/layout/app-shell/activity-layout.types.d.ts +6 -3
- package/dist/lib/components/ui/layout/app-shell/index.d.ts +1 -1
- package/dist/lib/components/ui/layout/app-shell/route-transition.d.ts +20 -2
- package/dist/lib/components/ui/layout/app-shell/shapes-clear-content.d.ts +2 -1
- package/dist/lib/components/ui/widgets/code-editor/configure-monaco.d.ts +2 -0
- package/dist/lib/components/ui/widgets/education/questions/index.d.ts +3 -1
- package/dist/lib/components/ui/widgets/education/questions/partials/question-content.d.ts +1 -1
- package/dist/lib/components/ui/widgets/education/questions/partials/question-top.d.ts +6 -0
- package/dist/lib/components/ui/widgets/education/questions/question-long-text.d.ts +5 -0
- package/dist/lib/components/ui/widgets/education/questions/question-media-content.d.ts +24 -0
- package/dist/lib/components/ui/widgets/education/questions/question-media-helpers.d.ts +11 -0
- package/dist/lib/components/ui/widgets/education/questions/question-media-view.d.ts +18 -0
- package/dist/lib/components/ui/widgets/education/questions/question-media-view.types.d.ts +32 -0
- package/dist/lib/components/ui/widgets/education/questions/question-options.d.ts +2 -2
- package/dist/lib/components/ui/widgets/education/questions/question-text.d.ts +1 -1
- package/dist/lib/components/ui/widgets/education/questions/question.d.ts +1 -1
- package/dist/lib/components/ui/widgets/education/questions/types.d.ts +38 -1
- package/dist/lib/components/ui/widgets/education/questions/use-reload-focus.d.ts +13 -0
- package/dist/lib/hooks/drafts-store.d.ts +4 -0
- package/dist/lib/hooks/index.d.ts +4 -0
- package/dist/lib/hooks/unsaved-guard-rules.d.ts +30 -0
- package/dist/lib/hooks/use-drafts.d.ts +75 -0
- package/dist/lib/hooks/use-react-router-block.d.ts +11 -0
- package/dist/lib/hooks/use-unsaved-guard/guard-core.d.ts +30 -0
- package/dist/lib/hooks/use-unsaved-guard/index.d.ts +3 -0
- package/dist/lib/hooks/use-unsaved-guard/types.d.ts +63 -0
- package/dist/lib/hooks/use-unsaved-guard/use-discard-guard.d.ts +15 -0
- package/dist/lib/hooks/use-unsaved-guard/use-unsaved-guard.d.ts +19 -0
- package/dist/lib/i18n/resources/en/en.d.ts +20 -0
- package/dist/lib/i18n/resources/en/material.json.d.ts +21 -1
- package/dist/lib/i18n/resources/fr/fr.d.ts +20 -0
- package/dist/lib/i18n/resources/fr/material.json.d.ts +21 -1
- package/dist/lib/i18n/resources/resources.d.ts +40 -0
- package/dist/lib/i18n/translator.d.ts +1 -1
- package/dist/lib/illustrations-game/index.d.ts +17 -0
- package/dist/lib.js +1 -1
- package/dist/main.js +42 -43
- package/dist/map.js +63 -63
- package/dist/material.css +1 -1
- package/dist/pdf-reader.js +1 -1
- package/dist/print.js +1 -2
- package/dist/{public-BYsrhyPW.js → public-D8JGp3PH.js} +823 -822
- package/dist/{rich-code-block-WJxDWPPY.js → rich-code-block-0R3bidsM.js} +5 -5
- package/dist/{sortable-provider-oPjlhlZo.js → sortable-provider-B75v53sB.js} +2 -2
- package/dist/{spinner-C5lObWXT.js → spinner-CAnpF3RK.js} +1 -1
- package/dist/theme-picker.js +2 -2
- package/dist/{theme-toggle-CPEq5JG-.js → theme-toggle-pQ5F602V.js} +1 -1
- package/dist/{toolbar-items-ByvXcMHm.js → toolbar-items-_ezD5B0r.js} +4 -4
- package/dist/translate-material-CvZK8_S6.js +96 -0
- package/dist/{stored-language-B-tiWZU2.js → translator-Caf-d96e.js} +2497 -8
- package/dist/{escape-owner-r57kuPjn.js → typography-CEK19bj8.js} +17 -34
- package/dist/{use-chat-composer-CzOVaIed.js → use-chat-composer-Dpvi_-CR.js} +5164 -5338
- package/dist/{use-heading-spy-BOtygPFu.js → use-heading-spy-mmcyEGmm.js} +186 -71
- package/dist/{use-material-translation-B5j3jZs7.js → use-material-translation-C_1cWRzW.js} +1 -2
- package/dist/use-return-focus-1ZIchrBM.js +269 -0
- package/docs/components/data-display.md +1 -0
- package/docs/components/feedback-and-status.md +23 -0
- package/docs/components/navigation-and-layout.md +5 -4
- package/docs/components/rich-widgets.md +73 -1
- package/docs/guides/app-shell.md +17 -0
- package/docs/guides/code-editor.md +8 -0
- package/docs/guides/csp.md +4 -0
- package/docs/guides/helpers.md +120 -0
- package/docs/guides/icon-motifs.md +3 -1
- package/docs/guides/illustrations.md +33 -1
- package/docs/guides/migrating-to-3.md +4 -2
- package/docs/guides/theming.md +13 -3
- package/docs/previews/feedback-status-illustration-game.webp +0 -0
- package/docs/previews/feedback-status-unsaveddialog.webp +0 -0
- package/docs/roadmap.md +1 -1
- package/docs/troubleshooting.md +9 -6
- package/lib/components/ui/layout/sidebar/sidebar-tokens.css +13 -1
- package/package.json +7 -2
- package/dist/dialog-yfPkLqUQ.js +0 -482
- package/dist/i18n-provider-BehR6cU7.js +0 -79
- package/dist/translate-material-tUaLZUmT.js +0 -20
- package/dist/translator-B9XjUDRu.js +0 -2451
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { ReactElement, ViewTransitionClass, ViewTransitionProps } from 'react';
|
|
2
2
|
/**
|
|
3
3
|
* `ActivityLayout`'s `routeTransition`: `false` for none, `true` for the
|
|
4
|
-
* default
|
|
4
|
+
* default (the region fades only when the pathname changes, not on a search
|
|
5
|
+
* or hash change, a fetcher or a revalidation), or the props of React's `<ViewTransition>` (minus `children` and
|
|
5
6
|
* `ref`) to tune it: the app's classes (a class, or a map by transition
|
|
6
7
|
* type), a `name`, callbacks.
|
|
7
8
|
*/
|
|
8
9
|
export type RouteTransition = boolean | Omit<ViewTransitionProps, 'children' | 'ref'>;
|
|
9
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* The class the library's reduced-motion rule and cross-fade target. Put it
|
|
12
|
+
* on a nested `<ViewTransition>` to fade with the same timing as the shell.
|
|
13
|
+
*/
|
|
10
14
|
export declare const ROUTE_TRANSITION_CLASS = "nxgt-route";
|
|
11
15
|
/**
|
|
12
16
|
* The attribute `<html>` carries while an `ActivityLayout` with the
|
|
@@ -44,6 +48,20 @@ export declare function routeTransitionProps(transition: Exclude<RouteTransition
|
|
|
44
48
|
onExit?: ((instance: import('react').ViewTransitionInstance, types: Array<string>) => void | (() => void)) | undefined;
|
|
45
49
|
onShare?: ((instance: import('react').ViewTransitionInstance, types: Array<string>) => void | (() => void)) | undefined;
|
|
46
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* `transition`, with the default (`true`) gated on the pathname: its update
|
|
53
|
+
* animation runs only for a render whose pathname differs from the last
|
|
54
|
+
* committed one. React Router runs search-param navigations, fetchers and
|
|
55
|
+
* revalidations as untyped transitions too, and those would fade the whole
|
|
56
|
+
* region (and drop keystrokes typed into a search inside it). An explicit
|
|
57
|
+
* value is returned as is, so the app keeps its own control.
|
|
58
|
+
*/
|
|
59
|
+
export declare function useGatedRouteTransition(transition: RouteTransition): RouteTransition;
|
|
60
|
+
/**
|
|
61
|
+
* `transition` for a render at `pathname`, the last commit being at
|
|
62
|
+
* `committed`: only `true` is gated (see {@link useGatedRouteTransition}).
|
|
63
|
+
*/
|
|
64
|
+
export declare function gateRouteTransition(transition: RouteTransition, committed: string, pathname: string): RouteTransition;
|
|
47
65
|
/**
|
|
48
66
|
* The app's one persistent route boundary, around the content region.
|
|
49
67
|
* Never keyed by location, so a navigation updates it instead of remounting.
|
|
@@ -9,8 +9,9 @@ export declare const CONTROLS = "input, textarea, select, button, a[href], img,
|
|
|
9
9
|
* The surfaces of the page: a card, a panel, a table, a list. A glyph
|
|
10
10
|
* is dropped when it meets one of these whole boxes, not only the text inside
|
|
11
11
|
* (which left glyphs half hidden at a card's edge, and between its lines).
|
|
12
|
+
* `.bg-card` is a plain element drawn as a card.
|
|
12
13
|
*/
|
|
13
|
-
export declare const SURFACES = "[data-slot=\"card\"], [data-slot=\"stat-card\"], [data-slot=\"table-wrapper\"], [data-slot=\"data-table-items\"], [data-slot=\"data-table-cards\"], [data-slot=\"data-table-tiles\"], [data-slot=\"data-grid\"], [data-slot=\"alert\"], [data-slot=\"banner\"], [role=\"alert\"], [role=\"alertdialog\"], [role=\"dialog\"], table, ul, ol";
|
|
14
|
+
export declare const SURFACES = "[data-slot=\"card\"], [data-slot=\"stat-card\"], [data-slot=\"table-wrapper\"], [data-slot=\"data-table-items\"], [data-slot=\"data-table-cards\"], [data-slot=\"data-table-tiles\"], [data-slot=\"data-grid\"], [data-slot=\"alert\"], [data-slot=\"banner\"], [role=\"alert\"], [role=\"alertdialog\"], [role=\"dialog\"], table, ul, ol, .bg-card";
|
|
14
15
|
/**
|
|
15
16
|
* The drop rule: whether a glyph lies over (or within `margin` of) any
|
|
16
17
|
* occupied box of the content. A glyph that does is dropped whole, never
|
|
@@ -6,5 +6,7 @@ import { loader } from '@monaco-editor/react';
|
|
|
6
6
|
* CDN, nothing for a strict CSP to allow — or `paths.vs` to serve Monaco's
|
|
7
7
|
* files from your origin. Call it once, before the first editor mounts. The
|
|
8
8
|
* self-hosting guide (docs/guides/code-editor.md) has the workers' setup.
|
|
9
|
+
*
|
|
10
|
+
* It also gives the `<style>` elements Monaco injects the page's CSP nonce.
|
|
9
11
|
*/
|
|
10
12
|
export declare function configureMonaco(options: Parameters<typeof loader.config>[0]): void;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { QuestionCard } from './question.js';
|
|
2
2
|
export { answerParts, isAnswerCorrect, isAnswered, parseAnswerNumber, } from './question.utils.js';
|
|
3
|
-
export
|
|
3
|
+
export { QuestionMediaView } from './question-media-view.js';
|
|
4
|
+
export type { QuestionMediaFile, QuestionMediaViewProps, } from './question-media-view.types.js';
|
|
5
|
+
export type { Question, QuestionAnswer, QuestionCodeField, QuestionImage, QuestionKind, QuestionMedia, QuestionMediaKind, QuestionOption, QuestionPoints, QuestionProps, QuestionVerdict, } from './types.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { QuestionProps } from '../types.js';
|
|
2
2
|
/** A question's fieldset: prompt, header, media, hint, the answer body and the feedback. */
|
|
3
|
-
export declare function QuestionContent({ question, value, onValueChange, revealed: revealedProp, verdict, points, feedback, disabled, variant, density, showShortcuts, number, showFeedback, }: QuestionProps): import("react").JSX.Element;
|
|
3
|
+
export declare function QuestionContent({ question, value, onValueChange, revealed: revealedProp, verdict, points, feedback, disabled, readOnly, busy, renderCode, variant, density, showShortcuts, number, showFeedback, }: QuestionProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { QuestionProps } from '../types.js';
|
|
2
|
+
/** The prompt (the fieldset's legend), the header, the media and the hint. */
|
|
3
|
+
export declare function QuestionTop({ question, number, density, revealed, promptId, }: Pick<QuestionProps, 'question' | 'number' | 'density'> & {
|
|
4
|
+
revealed: boolean;
|
|
5
|
+
promptId: string;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { QuestionProps } from './types.js';
|
|
2
|
+
/** A multi-line answer: `open` (prose) or `code` (the `renderCode` field, else a monospace textarea). */
|
|
3
|
+
export declare function QuestionLongText({ question, value, onValueChange, revealed, verdict, disabled, readOnly, busy, renderCode, labelledBy, }: QuestionProps & {
|
|
4
|
+
labelledBy: string;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { MouseEvent, Ref } from 'react';
|
|
2
|
+
import { QuestionMediaCaption, QuestionMediaFile } from './question-media-view.types.js';
|
|
3
|
+
/** The alert of a file that did not load, with Reload when the host can. */
|
|
4
|
+
export declare function MediaErrorAlert({ onReload, onReloadBlur, reloading, }: {
|
|
5
|
+
onReload?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
6
|
+
/** Focus went to another control: Reload's focus intent is over. */
|
|
7
|
+
onReloadBlur?: () => void;
|
|
8
|
+
reloading: boolean;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
/** A document as a plain link: it fetches nothing, so no `connect-src` or CORS. */
|
|
11
|
+
export declare function DocumentLink({ media }: {
|
|
12
|
+
media: QuestionMediaFile;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
type ContentProps = {
|
|
15
|
+
media: QuestionMediaFile;
|
|
16
|
+
family: 'image' | 'audio' | 'video' | 'document';
|
|
17
|
+
documents: 'link' | 'preview';
|
|
18
|
+
captions?: QuestionMediaCaption[];
|
|
19
|
+
onFail: () => void;
|
|
20
|
+
imageRef: Ref<HTMLImageElement>;
|
|
21
|
+
};
|
|
22
|
+
/** The file itself, by family. */
|
|
23
|
+
export declare function MediaContent({ media, family, documents, captions, onFail, imageRef, }: ContentProps): import("react").JSX.Element;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The file's family: `kind` when the host gave one, else the MIME `type`
|
|
3
|
+
* (case-insensitive), else an image (as `QuestionCard` reads `Question.media`).
|
|
4
|
+
* There is no `kind: 'video'`: a video is picked by its `type`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function mediaFamily(media: {
|
|
7
|
+
type?: string;
|
|
8
|
+
kind?: string;
|
|
9
|
+
}): "document" | "image" | "video" | "audio";
|
|
10
|
+
/** The short name of a MIME type: "PDF" for `application/pdf`. */
|
|
11
|
+
export declare const contentTypeLabel: (contentType: string) => string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { QuestionMediaViewProps } from './question-media-view.types.js';
|
|
2
|
+
export { contentTypeLabel, mediaFamily } from './question-media-helpers.js';
|
|
3
|
+
/**
|
|
4
|
+
* A question's file, picked by its type: an image, an audio clip, a video, or
|
|
5
|
+
* a document (a link that opens in a new tab and fetches nothing, or
|
|
6
|
+
* `FilePreview` with `documents="preview"`). It takes the shape of
|
|
7
|
+
* `Question.media` (plus `size`), so a question's `media` goes straight in. The
|
|
8
|
+
* file is named for assistive technology (`name`, else `alt`, else a generic
|
|
9
|
+
* name). When it fails to load (a presigned link that expired) an alert says
|
|
10
|
+
* so and, with `onReload`, offers a Reload button; a new `src` is a new
|
|
11
|
+
* chance, and focus then moves to the file if it was on Reload. A reload that
|
|
12
|
+
* returns the same `src` keeps the error. A document link cannot report that
|
|
13
|
+
* it expired.
|
|
14
|
+
*
|
|
15
|
+
* `QuestionCard` draws its own `media` without an error state; it should reuse
|
|
16
|
+
* this view (follow-up #2021). Vue twin: `vue` issue #2020.
|
|
17
|
+
*/
|
|
18
|
+
export declare function QuestionMediaView({ media, onReload, reloading, documents, captions, className, }: QuestionMediaViewProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { QuestionMedia } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A file attached to a question: the shape of `Question.media` (`src`, `alt`,
|
|
4
|
+
* `type`, `kind`, `name`) plus its size, so `question.media` can be passed
|
|
5
|
+
* straight in. `type` (a MIME type: `image/png`, `audio/mpeg`, `video/mp4`,
|
|
6
|
+
* `application/pdf`) picks the rendering; without it the file is an image.
|
|
7
|
+
*/
|
|
8
|
+
export type QuestionMediaFile = QuestionMedia & {
|
|
9
|
+
/** Size in bytes, shown in a document link. */
|
|
10
|
+
size?: number | null;
|
|
11
|
+
};
|
|
12
|
+
/** A caption track of a video. */
|
|
13
|
+
export type QuestionMediaCaption = {
|
|
14
|
+
src: string;
|
|
15
|
+
/** BCP 47 tag of the track's language (`en`, `fr`). */
|
|
16
|
+
srclang: string;
|
|
17
|
+
/** The name shown in the player's captions menu. */
|
|
18
|
+
label: string;
|
|
19
|
+
default?: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type QuestionMediaViewProps = {
|
|
22
|
+
media: QuestionMediaFile;
|
|
23
|
+
/** Called by the Reload button of the error alert; without it the alert has no button. */
|
|
24
|
+
onReload?: () => void;
|
|
25
|
+
/** The Reload button is busy: the new link is being fetched. */
|
|
26
|
+
reloading?: boolean;
|
|
27
|
+
/** A document is a link to a new tab (default, fetches nothing) or a `FilePreview`. */
|
|
28
|
+
documents?: 'link' | 'preview';
|
|
29
|
+
/** Caption tracks of a video; a clip with speech needs them (or a transcript next to it). */
|
|
30
|
+
captions?: QuestionMediaCaption[];
|
|
31
|
+
className?: string;
|
|
32
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Question, QuestionOption, QuestionProps } from './types.js';
|
|
2
|
-
type QuestionOptionsProps = Pick<QuestionProps, 'revealed' | 'verdict' | 'disabled' | 'density' | 'showShortcuts'> & {
|
|
2
|
+
type QuestionOptionsProps = Pick<QuestionProps, 'revealed' | 'verdict' | 'disabled' | 'readOnly' | 'busy' | 'density' | 'showShortcuts'> & {
|
|
3
3
|
question: Question;
|
|
4
4
|
options: QuestionOption[];
|
|
5
5
|
value: string[];
|
|
@@ -7,5 +7,5 @@ type QuestionOptionsProps = Pick<QuestionProps, 'revealed' | 'verdict' | 'disabl
|
|
|
7
7
|
labelledBy?: string;
|
|
8
8
|
onToggle: (id: string) => void;
|
|
9
9
|
};
|
|
10
|
-
export declare function QuestionOptions({ question, options, value, multiple, revealed, verdict, disabled, density, showShortcuts, labelledBy, onToggle, }: QuestionOptionsProps): import("react").JSX.Element;
|
|
10
|
+
export declare function QuestionOptions({ question, options, value, multiple, revealed, verdict, disabled, readOnly, busy, density, showShortcuts, labelledBy, onToggle, }: QuestionOptionsProps): import("react").JSX.Element;
|
|
11
11
|
export {};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { QuestionProps } from './types.js';
|
|
2
2
|
/** A typed answer: `text`, or `number` (a decimal keypad, an optional unit). */
|
|
3
|
-
export declare function QuestionText({ question, value, onValueChange, revealed, verdict, disabled, showFeedback, }: QuestionProps): import("react").JSX.Element;
|
|
3
|
+
export declare function QuestionText({ question, value, onValueChange, revealed, verdict, disabled, readOnly, busy, showFeedback, }: QuestionProps): import("react").JSX.Element;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { QuestionProps } from './types.js';
|
|
2
|
-
export declare function QuestionCard({ question, value, onValueChange, revealed, verdict, points, feedback, disabled, className, variant, density, showShortcuts, number, showFeedback, }: QuestionProps): import("react").JSX.Element;
|
|
2
|
+
export declare function QuestionCard({ question, value, onValueChange, revealed, verdict, points, feedback, disabled, readOnly, busy, renderCode, className, variant, density, showShortcuts, number, showFeedback, }: QuestionProps): import("react").JSX.Element;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* `text` is a one-line answer; `open` a multi-line one (a textarea with a
|
|
4
|
+
* counter); `code` source code (a code editor from `renderCode`, else a
|
|
5
|
+
* monospace textarea). `open` and `code` have no `correct`: a grader gives the
|
|
6
|
+
* `verdict`.
|
|
7
|
+
*/
|
|
8
|
+
export type QuestionKind = 'choice' | 'multi' | 'boolean' | 'text' | 'number' | 'open' | 'code';
|
|
3
9
|
export type QuestionImage = {
|
|
4
10
|
src: string;
|
|
5
11
|
alt?: string;
|
|
@@ -53,6 +59,27 @@ export type Question = {
|
|
|
53
59
|
tolerance?: number;
|
|
54
60
|
/** `number`: the unit shown after the field ("km", "%"). */
|
|
55
61
|
unit?: string;
|
|
62
|
+
/** `open`, `code`: the most characters accepted; a counter shows while editable. */
|
|
63
|
+
maxLength?: number;
|
|
64
|
+
/** `code`: the language of the code ("python"), given to `renderCode`. */
|
|
65
|
+
language?: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* What `renderCode` gets to draw the code field of a `code` question. The
|
|
69
|
+
* editor must be named by the prompt (`label`, or `aria-labelledby={labelledBy}`)
|
|
70
|
+
* and honour `readOnly` and `disabled`.
|
|
71
|
+
*/
|
|
72
|
+
export type QuestionCodeField = {
|
|
73
|
+
value: string;
|
|
74
|
+
onChange: (value: string) => void;
|
|
75
|
+
/** The prompt's id: name the field with `aria-labelledby`. */
|
|
76
|
+
labelledBy: string;
|
|
77
|
+
/** The prompt, as a text label for editors that take one. */
|
|
78
|
+
label: string;
|
|
79
|
+
language?: string;
|
|
80
|
+
maxLength?: number;
|
|
81
|
+
readOnly: boolean;
|
|
82
|
+
disabled: boolean;
|
|
56
83
|
};
|
|
57
84
|
export type QuestionProps = {
|
|
58
85
|
question: Question;
|
|
@@ -72,6 +99,16 @@ export type QuestionProps = {
|
|
|
72
99
|
/** A grader's remark, shown in the verdict banner (needs `verdict`). */
|
|
73
100
|
feedback?: string;
|
|
74
101
|
disabled?: boolean;
|
|
102
|
+
/** The answer is shown but not editable (already submitted), without the revealed marks. Every kind honours it. */
|
|
103
|
+
readOnly?: boolean;
|
|
104
|
+
/** An answer is being saved: the field is locked like `readOnly` and `aria-busy`. Every kind honours it. */
|
|
105
|
+
busy?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* The code field of a `code` question, e.g. `@nxgt/material/code-editor`'s
|
|
108
|
+
* `CodeEditor` (kept out of this bundle: Monaco is heavy). Without it the
|
|
109
|
+
* field is a monospace textarea.
|
|
110
|
+
*/
|
|
111
|
+
renderCode?: (field: QuestionCodeField) => ReactNode;
|
|
75
112
|
className?: string;
|
|
76
113
|
/** `card` (the default) is framed; `plain` sits inside another surface. */
|
|
77
114
|
variant?: 'card' | 'plain';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MouseEvent } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Where the focus goes when Reload works. The intent is set when Reload is
|
|
4
|
+
* pressed with the focus on its button, and honoured only for the `src` change
|
|
5
|
+
* that answers it. It ends when a new failure is recorded, when the reload
|
|
6
|
+
* ends with the same `src` (`reloading` back to false), or when the focus
|
|
7
|
+
* moves to another control.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useReloadFocus(src: string, reloading: boolean, onReload: (() => void) | undefined): {
|
|
10
|
+
frame: import('react').RefObject<HTMLDivElement | null>;
|
|
11
|
+
end: () => void;
|
|
12
|
+
reload: ((event: MouseEvent<HTMLButtonElement>) => void) | undefined;
|
|
13
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Reads and writes the drafts of one store key; every error reads as no drafts. */
|
|
2
|
+
export type DraftsStore = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'> | undefined;
|
|
3
|
+
export declare function readDrafts<T>(storage: DraftsStore, storageKey: string, parse: (raw: string) => T | undefined): Record<string, T>;
|
|
4
|
+
export declare function writeDrafts<T>(storage: DraftsStore, storageKey: string, drafts: Record<string, T>, serialize: (value: T) => string): void;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export * from './resolve-theme.js';
|
|
2
|
+
export * from './unsaved-guard-rules.js';
|
|
2
3
|
export * from './use-allowed-transitions.js';
|
|
3
4
|
export * from './use-countdown.js';
|
|
4
5
|
export { useCspNonce } from './use-csp-nonce.js';
|
|
6
|
+
export * from './use-drafts.js';
|
|
5
7
|
export * from './use-focus-on-mount.js';
|
|
6
8
|
export * from './use-formatters.js';
|
|
7
9
|
export * from './use-heading-spy.js';
|
|
@@ -11,7 +13,9 @@ export * from './use-mobile.js';
|
|
|
11
13
|
export * from './use-navigation.js';
|
|
12
14
|
export * from './use-number-formatter.js';
|
|
13
15
|
export * from './use-options.js';
|
|
16
|
+
export * from './use-react-router-block.js';
|
|
14
17
|
export * from './use-scroll-away.js';
|
|
15
18
|
export * from './use-search-param.js';
|
|
16
19
|
export * from './use-submit-once.js';
|
|
17
20
|
export * from './use-theme.js';
|
|
21
|
+
export * from './use-unsaved-guard/index.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** The part of a location a guard compares. */
|
|
2
|
+
export interface GuardLocation {
|
|
3
|
+
pathname: string;
|
|
4
|
+
search: string;
|
|
5
|
+
hash: string;
|
|
6
|
+
}
|
|
7
|
+
/** What a router tells its blocker: where the page is, and where it goes. */
|
|
8
|
+
export interface BlockContext {
|
|
9
|
+
currentLocation: GuardLocation;
|
|
10
|
+
nextLocation: GuardLocation;
|
|
11
|
+
}
|
|
12
|
+
/** Decides whether going from `current` to `next` counts as leaving. */
|
|
13
|
+
export type LeavePredicate = (current: GuardLocation, next: GuardLocation) => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The default notion of leaving: the pathname or the query string differs (a
|
|
16
|
+
* filter or period param remounts most pages). A hash change alone is not a
|
|
17
|
+
* leave. With `ignoreSearch`, only the pathname counts, for a page whose state
|
|
18
|
+
* lives in a step that a query change keeps mounted.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isLeaving(current: GuardLocation, next: GuardLocation, options?: {
|
|
21
|
+
ignoreSearch?: boolean;
|
|
22
|
+
}): boolean;
|
|
23
|
+
/** Whether the guard blocks this navigation right now. Pure, for tests. */
|
|
24
|
+
export declare function shouldBlockNavigation({ dirty, released, context, leaves, ignoreSearch, }: {
|
|
25
|
+
dirty: boolean;
|
|
26
|
+
released: boolean;
|
|
27
|
+
context: BlockContext;
|
|
28
|
+
leaves?: LeavePredicate;
|
|
29
|
+
ignoreSearch?: boolean;
|
|
30
|
+
}): boolean;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { DraftsStore } from './drafts-store.js';
|
|
2
|
+
/** A store, or a function that returns it (preferred: it is called only on the client). */
|
|
3
|
+
export type DraftsStorage = DraftsStore | (() => DraftsStore);
|
|
4
|
+
export interface UseDraftsOptions<T> {
|
|
5
|
+
/** Turns a value into a string for the store. Default: `JSON.stringify`. */
|
|
6
|
+
serialize?: (value: T) => string;
|
|
7
|
+
/**
|
|
8
|
+
* Turns a stored string back into a value; throw or return `undefined` to
|
|
9
|
+
* ignore a damaged entry. Default: `JSON.parse`.
|
|
10
|
+
*/
|
|
11
|
+
parse?: (raw: string) => T | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The store, or a function that returns it (preferred, so nothing touches
|
|
14
|
+
* `window` during render): `() => localStorage`. Default: the tab's
|
|
15
|
+
* `sessionStorage`.
|
|
16
|
+
*/
|
|
17
|
+
storage?: DraftsStorage;
|
|
18
|
+
}
|
|
19
|
+
export interface UseDraftsResult<T> {
|
|
20
|
+
/** The draft of `key`, or `undefined`. */
|
|
21
|
+
get: (key: string) => T | undefined;
|
|
22
|
+
/** Keeps `value` as the draft of `key`. */
|
|
23
|
+
set: (key: string, value: T) => void;
|
|
24
|
+
/** Drops the draft of `key`: call it when its value is saved. */
|
|
25
|
+
drop: (key: string) => void;
|
|
26
|
+
/** Drops every draft of this store. */
|
|
27
|
+
clear: () => void;
|
|
28
|
+
/** The keys that hold an unsaved draft, in the order they were first set. */
|
|
29
|
+
keys: readonly string[];
|
|
30
|
+
/** False until the stored drafts were read, after the first render. */
|
|
31
|
+
loaded: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Drops every draft of a store key without a mounted hook: call it at
|
|
35
|
+
* sign-out so the next person on the tab sees none. A mounted `useDrafts` of
|
|
36
|
+
* the same key keeps what it holds until it is remounted or calls `clear()`.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* clearDrafts('quiz-1');
|
|
41
|
+
* clearDrafts('quiz-1', () => localStorage);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function clearDrafts(storageKey: string, storage?: DraftsStorage): void;
|
|
45
|
+
/**
|
|
46
|
+
* Unsent values kept by key in the tab's `sessionStorage`, so a reload, the
|
|
47
|
+
* back button or a jump to another step of a form loses no typed text.
|
|
48
|
+
*
|
|
49
|
+
* - **SSR-safe**: the first render, on the server and on the client, has no
|
|
50
|
+
* drafts; they are read in an effect (`loaded` turns true), so hydration
|
|
51
|
+
* never mismatches.
|
|
52
|
+
* - **Storage errors are swallowed**: a private window, a blocked or full
|
|
53
|
+
* store, or a damaged entry reads and writes as no draft.
|
|
54
|
+
* - **Written at once**: `set`, `drop` and `clear` write the store in the
|
|
55
|
+
* call, so a `drop` or `clear` before `loaded` wins over the stored copy.
|
|
56
|
+
* - **Cleared on submit**: call `drop(key)` (or `clear()`) once the value is
|
|
57
|
+
* saved; an empty set removes the store key.
|
|
58
|
+
* - **A new `storageKey`** starts from that key's stored drafts, with
|
|
59
|
+
* `loaded` false until they are read; the old key's drafts stay in the
|
|
60
|
+
* store under the old key.
|
|
61
|
+
* - **Two instances on one key** each read at mount and merge on write;
|
|
62
|
+
* they do not update each other live.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* const drafts = useDrafts<string>('quiz-1');
|
|
67
|
+
* <Textarea
|
|
68
|
+
* value={drafts.get('q3') ?? ''}
|
|
69
|
+
* onChange={(e) => drafts.set('q3', e.target.value)}
|
|
70
|
+
* />
|
|
71
|
+
* // after the answer is saved:
|
|
72
|
+
* drafts.drop('q3');
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare function useDrafts<T = string>(storageKey: string, options?: UseDraftsOptions<T>): UseDraftsResult<T>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BlockAdapter } from './use-unsaved-guard/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The React Router adapter for `useUnsavedGuard`: needs a data router
|
|
4
|
+
* (`createBrowserRouter`, or `createMemoryRouter` in a story or test).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* const { release, dialog } = useUnsavedGuard(isDirty, { block: useReactRouterBlock });
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare const useReactRouterBlock: BlockAdapter;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { BlockContext, LeavePredicate } from '../unsaved-guard-rules.js';
|
|
2
|
+
import { Dirty } from './types.js';
|
|
3
|
+
export declare const readDirty: (dirty: Dirty) => boolean;
|
|
4
|
+
/**
|
|
5
|
+
* The framework-free state of a guard: a one-shot release and the two checks
|
|
6
|
+
* (a navigation, a tab unload). Plain functions so a node test can drive them.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createGuardCore(getDirty: () => Dirty): {
|
|
9
|
+
/**
|
|
10
|
+
* The next leaving navigation check passes, then the guard re-arms. A
|
|
11
|
+
* release no navigation consumed expires when the current task ends, so a
|
|
12
|
+
* later edit is guarded again.
|
|
13
|
+
*/
|
|
14
|
+
release(): void;
|
|
15
|
+
rearm: () => void;
|
|
16
|
+
/** Drops a pending expiry timer (unmount). */
|
|
17
|
+
dispose: () => void;
|
|
18
|
+
/** A navigation check: only an actual leave consumes a release. */
|
|
19
|
+
shouldBlock(context: BlockContext, rules: {
|
|
20
|
+
leaves?: LeavePredicate;
|
|
21
|
+
ignoreSearch?: boolean;
|
|
22
|
+
}): boolean;
|
|
23
|
+
/** A tab unload check: a release stays until the navigation consumes it. */
|
|
24
|
+
shouldWarnUnload(): boolean;
|
|
25
|
+
};
|
|
26
|
+
export type GuardCore = ReturnType<typeof createGuardCore>;
|
|
27
|
+
/** Warns the browser on unload while the core says so; returns the cleanup. */
|
|
28
|
+
export declare function listenBeforeUnload(target: Pick<Window, 'addEventListener' | 'removeEventListener'>, core: Pick<GuardCore, 'shouldWarnUnload'>): () => void;
|
|
29
|
+
/** Runs `onBlock`: true when nothing is left to lose; a throw or no hook is false. */
|
|
30
|
+
export declare function flushBeforeAsking(onBlock: (() => boolean | Promise<boolean>) | undefined): Promise<boolean>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { BlockContext, LeavePredicate } from '../unsaved-guard-rules.js';
|
|
3
|
+
export type BlockState = 'unblocked' | 'blocked' | 'proceeding';
|
|
4
|
+
/** What a router's blocker gives back. */
|
|
5
|
+
export interface BlockResult {
|
|
6
|
+
state: BlockState;
|
|
7
|
+
/** Lets the blocked navigation go through. */
|
|
8
|
+
proceed: () => void;
|
|
9
|
+
/** Cancels the blocked navigation: the user stays. */
|
|
10
|
+
reset: () => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A router adapter: a hook that registers `shouldBlock` with the router and
|
|
14
|
+
* returns its blocker. `useReactRouterBlock` is the React Router one; any
|
|
15
|
+
* router with a blocker (TanStack Router's `useBlocker`, …) fits in a few lines.
|
|
16
|
+
*/
|
|
17
|
+
export type BlockAdapter = (shouldBlock: (context: BlockContext) => boolean) => BlockResult;
|
|
18
|
+
/** A boolean, or a function read when the navigation happens (never stale). */
|
|
19
|
+
export type Dirty = boolean | (() => boolean);
|
|
20
|
+
export interface UnsavedGuardLabels {
|
|
21
|
+
title?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
stayLabel?: string;
|
|
24
|
+
leaveLabel?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface UseUnsavedGuardOptions extends UnsavedGuardLabels {
|
|
27
|
+
/** The router adapter, for example `useReactRouterBlock`. */
|
|
28
|
+
block: BlockAdapter;
|
|
29
|
+
/**
|
|
30
|
+
* What counts as leaving. Default: the pathname or the query string
|
|
31
|
+
* differs. Overrides `ignoreSearch`.
|
|
32
|
+
*/
|
|
33
|
+
leaves?: LeavePredicate;
|
|
34
|
+
/** Only a pathname change counts (a query change between steps is free). */
|
|
35
|
+
ignoreSearch?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Runs when a navigation is blocked, before the dialog. Resolve `true` when
|
|
38
|
+
* nothing is left to lose (pending saves flushed): the navigation goes
|
|
39
|
+
* through silently. Resolve `false` (or throw) to ask.
|
|
40
|
+
*/
|
|
41
|
+
onBlock?: () => boolean | Promise<boolean>;
|
|
42
|
+
}
|
|
43
|
+
export interface UseUnsavedGuardResult {
|
|
44
|
+
/**
|
|
45
|
+
* Synchronous, one-shot: the next navigation that really leaves (not a hash
|
|
46
|
+
* change, nor a query change under `ignoreSearch`) passes, and a tab unload
|
|
47
|
+
* prompt stays quiet until then; the guard then re-arms by itself. A release
|
|
48
|
+
* no navigation consumed expires when the current task ends, so call it right
|
|
49
|
+
* before `navigate()`. A boolean `dirty` turning true again re-arms it too.
|
|
50
|
+
*/
|
|
51
|
+
release: () => void;
|
|
52
|
+
/** Render it once, anywhere in the page. */
|
|
53
|
+
dialog: ReactNode;
|
|
54
|
+
}
|
|
55
|
+
export interface UseDiscardGuardResult {
|
|
56
|
+
/**
|
|
57
|
+
* Runs `action` (close the dialog or sheet) at once when the form is clean,
|
|
58
|
+
* and after the user chose "Leave" when it is dirty.
|
|
59
|
+
*/
|
|
60
|
+
close: (action: () => void) => void;
|
|
61
|
+
/** Render it once, beside the form. */
|
|
62
|
+
dialog: ReactNode;
|
|
63
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Dirty, UnsavedGuardLabels, UseDiscardGuardResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The guard of a form inside a dialog or a sheet: closing asks first, only
|
|
4
|
+
* when the form is dirty. Route its close button, Escape and overlay click
|
|
5
|
+
* through `close(action)`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const { close, dialog } = useDiscardGuard(form.formState.isDirty);
|
|
10
|
+
* <Sheet open={open} onOpenChange={(next) => (next ? setOpen(true) : close(() => setOpen(false)))}>
|
|
11
|
+
* …{dialog}
|
|
12
|
+
* </Sheet>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function useDiscardGuard(dirty: Dirty, labels?: UnsavedGuardLabels): UseDiscardGuardResult;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Dirty, UseUnsavedGuardOptions, UseUnsavedGuardResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Blocks in-app navigation while a form is dirty (a "Leave without saving?"
|
|
4
|
+
* dialog) and asks the browser to confirm closing or reloading the tab.
|
|
5
|
+
* Router-agnostic: the router comes in through `block`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const { release, dialog } = useUnsavedGuard(form.formState.isDirty, {
|
|
10
|
+
* block: useReactRouterBlock,
|
|
11
|
+
* });
|
|
12
|
+
* async function onSubmit(values) {
|
|
13
|
+
* await save(values);
|
|
14
|
+
* release();
|
|
15
|
+
* navigate('/items');
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function useUnsavedGuard(dirty: Dirty, options: UseUnsavedGuardOptions): UseUnsavedGuardResult;
|
|
@@ -303,6 +303,7 @@ export declare const en: {
|
|
|
303
303
|
true: string;
|
|
304
304
|
false: string;
|
|
305
305
|
"answer-placeholder": string;
|
|
306
|
+
"code-placeholder": string;
|
|
306
307
|
expected: string;
|
|
307
308
|
next: string;
|
|
308
309
|
previous: string;
|
|
@@ -319,6 +320,19 @@ export declare const en: {
|
|
|
319
320
|
"kind-multi": string;
|
|
320
321
|
"kind-boolean": string;
|
|
321
322
|
"kind-text": string;
|
|
323
|
+
"kind-open": string;
|
|
324
|
+
"kind-code": string;
|
|
325
|
+
};
|
|
326
|
+
"question-media": {
|
|
327
|
+
image: string;
|
|
328
|
+
audio: string;
|
|
329
|
+
video: string;
|
|
330
|
+
document: string;
|
|
331
|
+
"error-title": string;
|
|
332
|
+
"error-description": string;
|
|
333
|
+
reload: string;
|
|
334
|
+
open: string;
|
|
335
|
+
"new-tab": string;
|
|
322
336
|
};
|
|
323
337
|
quiz: {
|
|
324
338
|
count: string;
|
|
@@ -1339,6 +1353,12 @@ export declare const en: {
|
|
|
1339
1353
|
dark: string;
|
|
1340
1354
|
system: string;
|
|
1341
1355
|
};
|
|
1356
|
+
unsaved: {
|
|
1357
|
+
title: string;
|
|
1358
|
+
description: string;
|
|
1359
|
+
stay: string;
|
|
1360
|
+
leave: string;
|
|
1361
|
+
};
|
|
1342
1362
|
"load-more": {
|
|
1343
1363
|
shown: string;
|
|
1344
1364
|
"shown-count": string;
|