@nxgt/material 3.62.0 → 3.64.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 (103) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  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 +5 -5
  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-TDDUQ1Y0.js → components-2kTVPsMg.js} +15801 -15541
  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 +3 -3
  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/lib/components/ui/actions/load-more-footer/load-more-footer.d.ts +11 -2
  29. package/dist/lib/components/ui/actions/load-more-footer/use-footer-focus.d.ts +15 -0
  30. package/dist/lib/components/ui/actions/load-more-footer/use-last-page.d.ts +9 -0
  31. package/dist/lib/components/ui/collections/filter/bar/partials/filter-inline-chips.d.ts +11 -0
  32. package/dist/lib/components/ui/collections/filter/bar/partials/filter-inline-list.d.ts +6 -0
  33. package/dist/lib/components/ui/collections/server-table/partials/server-table-grid-paging.d.ts +9 -0
  34. package/dist/lib/components/ui/collections/server-table/partials/server-table-load-more.d.ts +17 -0
  35. package/dist/lib/components/ui/collections/server-table/types-paging.d.ts +10 -0
  36. package/dist/lib/components/ui/feedback/index.d.ts +1 -0
  37. package/dist/lib/components/ui/feedback/unsaved-dialog/index.d.ts +1 -0
  38. package/dist/lib/components/ui/feedback/unsaved-dialog/unsaved-dialog.d.ts +20 -0
  39. package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
  40. package/dist/lib/components/ui/fields/textarea-field/textarea-footer.d.ts +2 -1
  41. package/dist/lib/components/ui/widgets/education/questions/index.d.ts +3 -1
  42. package/dist/lib/components/ui/widgets/education/questions/partials/question-content.d.ts +1 -1
  43. package/dist/lib/components/ui/widgets/education/questions/partials/question-top.d.ts +6 -0
  44. package/dist/lib/components/ui/widgets/education/questions/question-long-text.d.ts +5 -0
  45. package/dist/lib/components/ui/widgets/education/questions/question-media-content.d.ts +24 -0
  46. package/dist/lib/components/ui/widgets/education/questions/question-media-helpers.d.ts +11 -0
  47. package/dist/lib/components/ui/widgets/education/questions/question-media-view.d.ts +18 -0
  48. package/dist/lib/components/ui/widgets/education/questions/question-media-view.types.d.ts +32 -0
  49. package/dist/lib/components/ui/widgets/education/questions/question-options.d.ts +2 -2
  50. package/dist/lib/components/ui/widgets/education/questions/question-text.d.ts +1 -1
  51. package/dist/lib/components/ui/widgets/education/questions/question.d.ts +1 -1
  52. package/dist/lib/components/ui/widgets/education/questions/types.d.ts +52 -1
  53. package/dist/lib/components/ui/widgets/education/questions/use-reload-focus.d.ts +13 -0
  54. package/dist/lib/hooks/index.d.ts +3 -0
  55. package/dist/lib/hooks/unsaved-guard-rules.d.ts +30 -0
  56. package/dist/lib/hooks/use-react-router-block.d.ts +11 -0
  57. package/dist/lib/hooks/use-unsaved-guard/guard-core.d.ts +30 -0
  58. package/dist/lib/hooks/use-unsaved-guard/index.d.ts +3 -0
  59. package/dist/lib/hooks/use-unsaved-guard/types.d.ts +63 -0
  60. package/dist/lib/hooks/use-unsaved-guard/use-discard-guard.d.ts +15 -0
  61. package/dist/lib/hooks/use-unsaved-guard/use-unsaved-guard.d.ts +19 -0
  62. package/dist/lib/i18n/resources/en/en.d.ts +20 -0
  63. package/dist/lib/i18n/resources/en/material.json.d.ts +21 -1
  64. package/dist/lib/i18n/resources/fr/fr.d.ts +20 -0
  65. package/dist/lib/i18n/resources/fr/material.json.d.ts +21 -1
  66. package/dist/lib/i18n/resources/resources.d.ts +40 -0
  67. package/dist/lib/i18n/translator.d.ts +1 -1
  68. package/dist/lib.js +1 -1
  69. package/dist/main.js +42 -43
  70. package/dist/map.js +63 -63
  71. package/dist/material.css +1 -1
  72. package/dist/pdf-reader.js +1 -1
  73. package/dist/print.js +1 -2
  74. package/dist/{public-BYsrhyPW.js → public-D8JGp3PH.js} +823 -822
  75. package/dist/{rich-code-block-WJxDWPPY.js → rich-code-block-0R3bidsM.js} +5 -5
  76. package/dist/{sortable-provider-oPjlhlZo.js → sortable-provider-B75v53sB.js} +2 -2
  77. package/dist/{spinner-C5lObWXT.js → spinner-CAnpF3RK.js} +1 -1
  78. package/dist/theme-picker.js +2 -2
  79. package/dist/{theme-toggle-CPEq5JG-.js → theme-toggle-pQ5F602V.js} +1 -1
  80. package/dist/{toolbar-items-ByvXcMHm.js → toolbar-items-_ezD5B0r.js} +4 -4
  81. package/dist/translate-material-CvZK8_S6.js +96 -0
  82. package/dist/{stored-language-B-tiWZU2.js → translator-Caf-d96e.js} +2497 -8
  83. package/dist/{escape-owner-r57kuPjn.js → typography-CEK19bj8.js} +17 -34
  84. package/dist/{use-chat-composer-CzOVaIed.js → use-chat-composer-Dpvi_-CR.js} +5164 -5338
  85. package/dist/{use-heading-spy-BOtygPFu.js → use-heading-spy-mmcyEGmm.js} +186 -71
  86. package/dist/{use-material-translation-B5j3jZs7.js → use-material-translation-C_1cWRzW.js} +1 -2
  87. package/dist/use-return-focus-1ZIchrBM.js +269 -0
  88. package/docs/components/buttons-and-actions.md +2 -2
  89. package/docs/components/data-table-filter-and-lists.md +96 -0
  90. package/docs/components/feedback-and-status.md +23 -0
  91. package/docs/components/navigation-and-layout.md +4 -3
  92. package/docs/components/rich-widgets.md +75 -1
  93. package/docs/guides/helpers.md +63 -0
  94. package/docs/guides/theming.md +13 -3
  95. package/docs/previews/feedback-status-unsaveddialog.webp +0 -0
  96. package/docs/roadmap.md +4 -0
  97. package/lib/components/ui/layout/sidebar/sidebar-tokens.css +13 -1
  98. package/package.json +2 -2
  99. package/dist/dialog-yfPkLqUQ.js +0 -482
  100. package/dist/hooks-CGnqFngB.js +0 -187
  101. package/dist/i18n-provider-BehR6cU7.js +0 -79
  102. package/dist/translate-material-tUaLZUmT.js +0 -20
  103. package/dist/translator-B9XjUDRu.js +0 -2451
@@ -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, promptId, 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,12 +59,47 @@ export type Question = {
53
59
  tolerance?: number;
54
60
  /** `number`: the unit shown after the field ("km", "%"). */
55
61
  unit?: string;
62
+ /**
63
+ * `open`, `code`: a persistent helper line under the answer field ("A person
64
+ * will mark this"), linked to it by `aria-describedby`. Its id is
65
+ * `${promptId}-description`. Unlike `hint`, it is always shown.
66
+ */
67
+ description?: string;
68
+ /** `open`, `code`: the most characters accepted; a counter shows while editable. */
69
+ maxLength?: number;
70
+ /** `code`: the language of the code ("python"), given to `renderCode`. */
71
+ language?: string;
72
+ };
73
+ /**
74
+ * What `renderCode` gets to draw the code field of a `code` question. The
75
+ * editor must be named by the prompt (`label`, or `aria-labelledby={labelledBy}`)
76
+ * and honour `readOnly` and `disabled`.
77
+ */
78
+ export type QuestionCodeField = {
79
+ value: string;
80
+ onChange: (value: string) => void;
81
+ /** The prompt's id: name the field with `aria-labelledby`. */
82
+ labelledBy: string;
83
+ /** The id of the `description` line, when there is one: add it to `aria-describedby`. */
84
+ describedBy?: string;
85
+ /** The prompt, as a text label for editors that take one. */
86
+ label: string;
87
+ language?: string;
88
+ maxLength?: number;
89
+ readOnly: boolean;
90
+ disabled: boolean;
56
91
  };
57
92
  export type QuestionProps = {
58
93
  question: Question;
59
94
  value?: QuestionAnswer;
60
95
  onValueChange?: (value: QuestionAnswer) => void;
61
96
  revealed?: boolean;
97
+ /**
98
+ * The id of the prompt (the fieldset's legend text), so a wrapper can use
99
+ * `aria-labelledby`. Default: a generated id. The `description` line is
100
+ * `${promptId}-description`.
101
+ */
102
+ promptId?: string;
62
103
  /**
63
104
  * The verdict the server gave, shown as given (no comparison with
64
105
  * `question.correct` in the browser). It reveals the card; `pending` is a
@@ -72,6 +113,16 @@ export type QuestionProps = {
72
113
  /** A grader's remark, shown in the verdict banner (needs `verdict`). */
73
114
  feedback?: string;
74
115
  disabled?: boolean;
116
+ /** The answer is shown but not editable (already submitted), without the revealed marks. Every kind honours it. */
117
+ readOnly?: boolean;
118
+ /** An answer is being saved: the field is locked like `readOnly` and `aria-busy`. Every kind honours it. */
119
+ busy?: boolean;
120
+ /**
121
+ * The code field of a `code` question, e.g. `@nxgt/material/code-editor`'s
122
+ * `CodeEditor` (kept out of this bundle: Monaco is heavy). Without it the
123
+ * field is a monospace textarea.
124
+ */
125
+ renderCode?: (field: QuestionCodeField) => ReactNode;
75
126
  className?: string;
76
127
  /** `card` (the default) is framed; `plain` sits inside another surface. */
77
128
  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
+ };
@@ -1,4 +1,5 @@
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';
@@ -12,7 +13,9 @@ export * from './use-mobile.js';
12
13
  export * from './use-navigation.js';
13
14
  export * from './use-number-formatter.js';
14
15
  export * from './use-options.js';
16
+ export * from './use-react-router-block.js';
15
17
  export * from './use-scroll-away.js';
16
18
  export * from './use-search-param.js';
17
19
  export * from './use-submit-once.js';
18
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,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;
@@ -293,6 +293,7 @@ declare const _default: {
293
293
  "true": "True",
294
294
  "false": "False",
295
295
  "answer-placeholder": "Your answer",
296
+ "code-placeholder": "Write your code",
296
297
  "expected": "Expected: {answer}",
297
298
  "next": "Next",
298
299
  "previous": "Previous",
@@ -308,7 +309,20 @@ declare const _default: {
308
309
  "kind-choice": "Multiple choice",
309
310
  "kind-multi": "Select all that apply",
310
311
  "kind-boolean": "True or false",
311
- "kind-text": "Short answer"
312
+ "kind-text": "Short answer",
313
+ "kind-open": "Open answer",
314
+ "kind-code": "Code answer"
315
+ },
316
+ "question-media": {
317
+ "image": "Image for this question",
318
+ "audio": "Audio for this question",
319
+ "video": "Video for this question",
320
+ "document": "Document for this question",
321
+ "error-title": "The file did not load",
322
+ "error-description": "The link may have expired.",
323
+ "reload": "Reload",
324
+ "open": "Open the file",
325
+ "new-tab": "opens in a new tab"
312
326
  },
313
327
  "quiz": {
314
328
  "count": "{count, plural, one {# question} other {# questions}}",
@@ -1329,6 +1343,12 @@ declare const _default: {
1329
1343
  "dark": "Dark",
1330
1344
  "system": "System"
1331
1345
  },
1346
+ "unsaved": {
1347
+ "title": "Leave without saving?",
1348
+ "description": "Your changes have not been saved and will be lost.",
1349
+ "stay": "Stay",
1350
+ "leave": "Leave"
1351
+ },
1332
1352
  "load-more": {
1333
1353
  "shown": "Showing {shown} of {total}",
1334
1354
  "shown-count": "{shown, plural, one {# shown} other {# shown}}",
@@ -303,6 +303,7 @@ export declare const fr: {
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 fr: {
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 fr: {
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;
@@ -293,6 +293,7 @@ declare const _default: {
293
293
  "true": "Vrai",
294
294
  "false": "Faux",
295
295
  "answer-placeholder": "Votre réponse",
296
+ "code-placeholder": "Écrivez votre code",
296
297
  "expected": "Attendu : {answer}",
297
298
  "next": "Suivant",
298
299
  "previous": "Précédent",
@@ -308,7 +309,20 @@ declare const _default: {
308
309
  "kind-choice": "Choix unique",
309
310
  "kind-multi": "Plusieurs réponses",
310
311
  "kind-boolean": "Vrai ou faux",
311
- "kind-text": "Réponse courte"
312
+ "kind-text": "Réponse courte",
313
+ "kind-open": "Réponse ouverte",
314
+ "kind-code": "Réponse en code"
315
+ },
316
+ "question-media": {
317
+ "image": "Image de cette question",
318
+ "audio": "Audio de cette question",
319
+ "video": "Vidéo de cette question",
320
+ "document": "Document de cette question",
321
+ "error-title": "Le fichier ne s’est pas chargé",
322
+ "error-description": "Le lien a peut-être expiré.",
323
+ "reload": "Recharger",
324
+ "open": "Ouvrir le fichier",
325
+ "new-tab": "s’ouvre dans un nouvel onglet"
312
326
  },
313
327
  "quiz": {
314
328
  "count": "{count, plural, one {# question} other {# questions}}",
@@ -1329,6 +1343,12 @@ declare const _default: {
1329
1343
  "dark": "Sombre",
1330
1344
  "system": "Système"
1331
1345
  },
1346
+ "unsaved": {
1347
+ "title": "Quitter sans enregistrer ?",
1348
+ "description": "Vos modifications n’ont pas été enregistrées et seront perdues.",
1349
+ "stay": "Rester",
1350
+ "leave": "Quitter"
1351
+ },
1332
1352
  "load-more": {
1333
1353
  "shown": "Affichage de {shown} sur {total}",
1334
1354
  "shown-count": "{shown, plural, one {# affiché} other {# affichés}}",
@@ -301,6 +301,7 @@ export declare const resources: {
301
301
  true: string;
302
302
  false: string;
303
303
  "answer-placeholder": string;
304
+ "code-placeholder": string;
304
305
  expected: string;
305
306
  next: string;
306
307
  previous: string;
@@ -317,6 +318,19 @@ export declare const resources: {
317
318
  "kind-multi": string;
318
319
  "kind-boolean": string;
319
320
  "kind-text": string;
321
+ "kind-open": string;
322
+ "kind-code": string;
323
+ };
324
+ "question-media": {
325
+ image: string;
326
+ audio: string;
327
+ video: string;
328
+ document: string;
329
+ "error-title": string;
330
+ "error-description": string;
331
+ reload: string;
332
+ open: string;
333
+ "new-tab": string;
320
334
  };
321
335
  quiz: {
322
336
  count: string;
@@ -1337,6 +1351,12 @@ export declare const resources: {
1337
1351
  dark: string;
1338
1352
  system: string;
1339
1353
  };
1354
+ unsaved: {
1355
+ title: string;
1356
+ description: string;
1357
+ stay: string;
1358
+ leave: string;
1359
+ };
1340
1360
  "load-more": {
1341
1361
  shown: string;
1342
1362
  "shown-count": string;
@@ -3256,6 +3276,7 @@ export declare const resources: {
3256
3276
  true: string;
3257
3277
  false: string;
3258
3278
  "answer-placeholder": string;
3279
+ "code-placeholder": string;
3259
3280
  expected: string;
3260
3281
  next: string;
3261
3282
  previous: string;
@@ -3272,6 +3293,19 @@ export declare const resources: {
3272
3293
  "kind-multi": string;
3273
3294
  "kind-boolean": string;
3274
3295
  "kind-text": string;
3296
+ "kind-open": string;
3297
+ "kind-code": string;
3298
+ };
3299
+ "question-media": {
3300
+ image: string;
3301
+ audio: string;
3302
+ video: string;
3303
+ document: string;
3304
+ "error-title": string;
3305
+ "error-description": string;
3306
+ reload: string;
3307
+ open: string;
3308
+ "new-tab": string;
3275
3309
  };
3276
3310
  quiz: {
3277
3311
  count: string;
@@ -4292,6 +4326,12 @@ export declare const resources: {
4292
4326
  dark: string;
4293
4327
  system: string;
4294
4328
  };
4329
+ unsaved: {
4330
+ title: string;
4331
+ description: string;
4332
+ stay: string;
4333
+ leave: string;
4334
+ };
4295
4335
  "load-more": {
4296
4336
  shown: string;
4297
4337
  "shown-count": string;