@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.
Files changed (114) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +3 -2
  3. package/dist/alert-dialog-RHm6WtQj.js +189 -0
  4. package/dist/{async-slice-HJY_VJjD.js → async-slice-DmX59NyA.js} +3 -4
  5. package/dist/{button-7BuUCGRm.js → button-DfS_amfQ.js} +1 -1
  6. package/dist/charts.js +3 -3
  7. package/dist/{chip-kQOrHlDX.js → chip-Cbndukko.js} +2 -2
  8. package/dist/{code-editor-Ckc9TTCM.js → code-editor-B0tN8Ha_.js} +11 -11
  9. package/dist/code-editor.js +54 -37
  10. package/dist/{code-language-items-rCPB0Xee.js → code-language-items-andgHKrM.js} +2 -2
  11. package/dist/components/ui/layout/sidebar/sidebar-tokens.css +13 -1
  12. package/dist/{components-BSM-h5IQ.js → components-Cm2doOJS.js} +13090 -12957
  13. package/dist/components.js +28 -26
  14. package/dist/dialog-Bd7Hid2L.js +219 -0
  15. package/dist/dnd.js +1 -1
  16. package/dist/{dropdown-menu-CiobN1wi.js → dropdown-menu-_SyVmSuy.js} +1 -1
  17. package/dist/editor.js +707 -706
  18. package/dist/{empty-state-DA517HAb.js → empty-state-D-2ZMXXJ.js} +2 -2
  19. package/dist/escape-owner-DpaP0lm1.js +19 -0
  20. package/dist/{field-blur-DdvY0NQQ.js → field-blur-DyOByjzw.js} +1 -1
  21. package/dist/{field-blur-context-DbJQZkEB.js → field-blur-context-DPe5_Nv5.js} +1 -1
  22. package/dist/hooks-a7_exKWS.js +329 -0
  23. package/dist/hooks.js +8 -102
  24. package/dist/{hover-card-DPs9huSs.js → hover-card-D_0smKBQ.js} +2 -2
  25. package/dist/i18n.js +8 -10
  26. package/dist/{icon-badge-C646JTOI.js → icon-badge-CBCU3ySF.js} +14 -14
  27. package/dist/{icon-button-DvP0Gy3j.js → icon-button-hKHsWTAt.js} +1 -1
  28. package/dist/illustrations-game.js +2 -0
  29. package/dist/lib/components/ui/display/list-tile/list-tile.d.ts +6 -0
  30. package/dist/lib/components/ui/display/list-tile/partials/list-tile-classes.d.ts +3 -1
  31. package/dist/lib/components/ui/display/list-tile/partials/list-tile-line.d.ts +1 -1
  32. package/dist/lib/components/ui/display/list-tile/partials/list-tile-toggle.d.ts +1 -1
  33. package/dist/lib/components/ui/feedback/index.d.ts +1 -0
  34. package/dist/lib/components/ui/feedback/unsaved-dialog/index.d.ts +1 -0
  35. package/dist/lib/components/ui/feedback/unsaved-dialog/unsaved-dialog.d.ts +20 -0
  36. package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
  37. package/dist/lib/components/ui/fields/textarea-field/textarea-footer.d.ts +2 -1
  38. package/dist/lib/components/ui/layout/app-shell/activity-layout.types.d.ts +6 -3
  39. package/dist/lib/components/ui/layout/app-shell/index.d.ts +1 -1
  40. package/dist/lib/components/ui/layout/app-shell/route-transition.d.ts +20 -2
  41. package/dist/lib/components/ui/layout/app-shell/shapes-clear-content.d.ts +2 -1
  42. package/dist/lib/components/ui/widgets/code-editor/configure-monaco.d.ts +2 -0
  43. package/dist/lib/components/ui/widgets/education/questions/index.d.ts +3 -1
  44. package/dist/lib/components/ui/widgets/education/questions/partials/question-content.d.ts +1 -1
  45. package/dist/lib/components/ui/widgets/education/questions/partials/question-top.d.ts +6 -0
  46. package/dist/lib/components/ui/widgets/education/questions/question-long-text.d.ts +5 -0
  47. package/dist/lib/components/ui/widgets/education/questions/question-media-content.d.ts +24 -0
  48. package/dist/lib/components/ui/widgets/education/questions/question-media-helpers.d.ts +11 -0
  49. package/dist/lib/components/ui/widgets/education/questions/question-media-view.d.ts +18 -0
  50. package/dist/lib/components/ui/widgets/education/questions/question-media-view.types.d.ts +32 -0
  51. package/dist/lib/components/ui/widgets/education/questions/question-options.d.ts +2 -2
  52. package/dist/lib/components/ui/widgets/education/questions/question-text.d.ts +1 -1
  53. package/dist/lib/components/ui/widgets/education/questions/question.d.ts +1 -1
  54. package/dist/lib/components/ui/widgets/education/questions/types.d.ts +38 -1
  55. package/dist/lib/components/ui/widgets/education/questions/use-reload-focus.d.ts +13 -0
  56. package/dist/lib/hooks/drafts-store.d.ts +4 -0
  57. package/dist/lib/hooks/index.d.ts +4 -0
  58. package/dist/lib/hooks/unsaved-guard-rules.d.ts +30 -0
  59. package/dist/lib/hooks/use-drafts.d.ts +75 -0
  60. package/dist/lib/hooks/use-react-router-block.d.ts +11 -0
  61. package/dist/lib/hooks/use-unsaved-guard/guard-core.d.ts +30 -0
  62. package/dist/lib/hooks/use-unsaved-guard/index.d.ts +3 -0
  63. package/dist/lib/hooks/use-unsaved-guard/types.d.ts +63 -0
  64. package/dist/lib/hooks/use-unsaved-guard/use-discard-guard.d.ts +15 -0
  65. package/dist/lib/hooks/use-unsaved-guard/use-unsaved-guard.d.ts +19 -0
  66. package/dist/lib/i18n/resources/en/en.d.ts +20 -0
  67. package/dist/lib/i18n/resources/en/material.json.d.ts +21 -1
  68. package/dist/lib/i18n/resources/fr/fr.d.ts +20 -0
  69. package/dist/lib/i18n/resources/fr/material.json.d.ts +21 -1
  70. package/dist/lib/i18n/resources/resources.d.ts +40 -0
  71. package/dist/lib/i18n/translator.d.ts +1 -1
  72. package/dist/lib/illustrations-game/index.d.ts +17 -0
  73. package/dist/lib.js +1 -1
  74. package/dist/main.js +42 -43
  75. package/dist/map.js +63 -63
  76. package/dist/material.css +1 -1
  77. package/dist/pdf-reader.js +1 -1
  78. package/dist/print.js +1 -2
  79. package/dist/{public-BYsrhyPW.js → public-D8JGp3PH.js} +823 -822
  80. package/dist/{rich-code-block-WJxDWPPY.js → rich-code-block-0R3bidsM.js} +5 -5
  81. package/dist/{sortable-provider-oPjlhlZo.js → sortable-provider-B75v53sB.js} +2 -2
  82. package/dist/{spinner-C5lObWXT.js → spinner-CAnpF3RK.js} +1 -1
  83. package/dist/theme-picker.js +2 -2
  84. package/dist/{theme-toggle-CPEq5JG-.js → theme-toggle-pQ5F602V.js} +1 -1
  85. package/dist/{toolbar-items-ByvXcMHm.js → toolbar-items-_ezD5B0r.js} +4 -4
  86. package/dist/translate-material-CvZK8_S6.js +96 -0
  87. package/dist/{stored-language-B-tiWZU2.js → translator-Caf-d96e.js} +2497 -8
  88. package/dist/{escape-owner-r57kuPjn.js → typography-CEK19bj8.js} +17 -34
  89. package/dist/{use-chat-composer-CzOVaIed.js → use-chat-composer-Dpvi_-CR.js} +5164 -5338
  90. package/dist/{use-heading-spy-BOtygPFu.js → use-heading-spy-mmcyEGmm.js} +186 -71
  91. package/dist/{use-material-translation-B5j3jZs7.js → use-material-translation-C_1cWRzW.js} +1 -2
  92. package/dist/use-return-focus-1ZIchrBM.js +269 -0
  93. package/docs/components/data-display.md +1 -0
  94. package/docs/components/feedback-and-status.md +23 -0
  95. package/docs/components/navigation-and-layout.md +5 -4
  96. package/docs/components/rich-widgets.md +73 -1
  97. package/docs/guides/app-shell.md +17 -0
  98. package/docs/guides/code-editor.md +8 -0
  99. package/docs/guides/csp.md +4 -0
  100. package/docs/guides/helpers.md +120 -0
  101. package/docs/guides/icon-motifs.md +3 -1
  102. package/docs/guides/illustrations.md +33 -1
  103. package/docs/guides/migrating-to-3.md +4 -2
  104. package/docs/guides/theming.md +13 -3
  105. package/docs/previews/feedback-status-illustration-game.webp +0 -0
  106. package/docs/previews/feedback-status-unsaveddialog.webp +0 -0
  107. package/docs/roadmap.md +1 -1
  108. package/docs/troubleshooting.md +9 -6
  109. package/lib/components/ui/layout/sidebar/sidebar-tokens.css +13 -1
  110. package/package.json +7 -2
  111. package/dist/dialog-yfPkLqUQ.js +0 -482
  112. package/dist/i18n-provider-BehR6cU7.js +0 -79
  113. package/dist/translate-material-tUaLZUmT.js +0 -20
  114. 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, or the props of React's `<ViewTransition>` (minus `children` and
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
- /** The class the library's reduced-motion rule and cross-fade target. */
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 type { Question, QuestionAnswer, QuestionImage, QuestionKind, QuestionMedia, QuestionMediaKind, QuestionOption, QuestionPoints, QuestionProps, QuestionVerdict, } from './types.js';
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
- export type QuestionKind = 'choice' | 'multi' | 'boolean' | 'text' | 'number';
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,3 @@
1
+ export * from './types.js';
2
+ export * from './use-discard-guard.js';
3
+ export * from './use-unsaved-guard.js';
@@ -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;