@viniaraujo68/plinth 0.0.0-stage → 0.2.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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +361 -2
  3. package/dist/anchoring.d.ts +61 -0
  4. package/dist/anchoring.js +151 -0
  5. package/dist/async-action.svelte.d.ts +37 -0
  6. package/dist/async-action.svelte.js +64 -0
  7. package/dist/attachments/index.d.ts +1 -0
  8. package/dist/attachments/index.js +1 -0
  9. package/dist/attachments/tooltip.d.ts +37 -0
  10. package/dist/attachments/tooltip.js +166 -0
  11. package/dist/components/AsyncButton.svelte +49 -0
  12. package/dist/components/AsyncButton.svelte.d.ts +32 -0
  13. package/dist/components/Combobox.svelte +380 -0
  14. package/dist/components/Combobox.svelte.d.ts +74 -0
  15. package/dist/components/Copyable.svelte +116 -0
  16. package/dist/components/Copyable.svelte.d.ts +37 -0
  17. package/dist/components/DateRangePicker.svelte +266 -0
  18. package/dist/components/DateRangePicker.svelte.d.ts +66 -0
  19. package/dist/components/Dialog.svelte +102 -0
  20. package/dist/components/Dialog.svelte.d.ts +38 -0
  21. package/dist/components/Dropdown.svelte +204 -0
  22. package/dist/components/Dropdown.svelte.d.ts +49 -0
  23. package/dist/components/ErrorDisplay.svelte +59 -0
  24. package/dist/components/ErrorDisplay.svelte.d.ts +24 -0
  25. package/dist/components/ErrorPage.svelte +119 -0
  26. package/dist/components/ErrorPage.svelte.d.ts +52 -0
  27. package/dist/components/JsonInput.svelte +106 -0
  28. package/dist/components/JsonInput.svelte.d.ts +40 -0
  29. package/dist/components/Listbox.svelte +257 -0
  30. package/dist/components/Listbox.svelte.d.ts +78 -0
  31. package/dist/components/LoadingButton.svelte +36 -0
  32. package/dist/components/LoadingButton.svelte.d.ts +22 -0
  33. package/dist/components/Logo.svelte +112 -0
  34. package/dist/components/Logo.svelte.d.ts +29 -0
  35. package/dist/components/Modal.svelte +187 -0
  36. package/dist/components/Modal.svelte.d.ts +62 -0
  37. package/dist/components/MultiSelect.svelte +256 -0
  38. package/dist/components/MultiSelect.svelte.d.ts +89 -0
  39. package/dist/components/OptionPanel.svelte +241 -0
  40. package/dist/components/OptionPanel.svelte.d.ts +51 -0
  41. package/dist/components/Paginator.svelte +205 -0
  42. package/dist/components/Paginator.svelte.d.ts +48 -0
  43. package/dist/components/RefreshButton.svelte +246 -0
  44. package/dist/components/RefreshButton.svelte.d.ts +46 -0
  45. package/dist/components/SegmentedControl.svelte +147 -0
  46. package/dist/components/SegmentedControl.svelte.d.ts +71 -0
  47. package/dist/components/Select.svelte +312 -0
  48. package/dist/components/Select.svelte.d.ts +68 -0
  49. package/dist/components/Skeleton.svelte +97 -0
  50. package/dist/components/Skeleton.svelte.d.ts +32 -0
  51. package/dist/components/Tooltip.svelte +60 -0
  52. package/dist/components/Tooltip.svelte.d.ts +58 -0
  53. package/dist/components/date-range.d.ts +101 -0
  54. package/dist/components/date-range.js +144 -0
  55. package/dist/components/index.d.ts +25 -0
  56. package/dist/components/index.js +37 -0
  57. package/dist/components/json-text.d.ts +22 -0
  58. package/dist/components/json-text.js +22 -0
  59. package/dist/components/pick.d.ts +18 -0
  60. package/dist/components/pick.js +18 -0
  61. package/dist/components/segmented.d.ts +7 -0
  62. package/dist/components/segmented.js +1 -0
  63. package/dist/components/select.d.ts +66 -0
  64. package/dist/components/select.js +90 -0
  65. package/dist/confirm/Confirmer.svelte +194 -0
  66. package/dist/confirm/Confirmer.svelte.d.ts +45 -0
  67. package/dist/confirm/challenge.d.ts +16 -0
  68. package/dist/confirm/challenge.js +16 -0
  69. package/dist/confirm/confirm-manager.svelte.d.ts +91 -0
  70. package/dist/confirm/confirm-manager.svelte.js +149 -0
  71. package/dist/confirm/index.d.ts +3 -0
  72. package/dist/confirm/index.js +3 -0
  73. package/dist/error.d.ts +25 -0
  74. package/dist/error.js +39 -0
  75. package/dist/formatters.d.ts +95 -0
  76. package/dist/formatters.js +146 -0
  77. package/dist/http.d.ts +68 -0
  78. package/dist/http.js +138 -0
  79. package/dist/index.d.ts +10 -0
  80. package/dist/index.js +14 -0
  81. package/dist/routing.svelte.d.ts +169 -0
  82. package/dist/routing.svelte.js +208 -0
  83. package/dist/shell/AppShell.svelte +874 -0
  84. package/dist/shell/AppShell.svelte.d.ts +93 -0
  85. package/dist/shell/Breadcrumbs.svelte +78 -0
  86. package/dist/shell/Breadcrumbs.svelte.d.ts +25 -0
  87. package/dist/shell/crumbs.d.ts +23 -0
  88. package/dist/shell/crumbs.js +1 -0
  89. package/dist/shell/index.d.ts +3 -0
  90. package/dist/shell/index.js +6 -0
  91. package/dist/table/ColumnMenu.svelte +249 -0
  92. package/dist/table/ColumnMenu.svelte.d.ts +47 -0
  93. package/dist/table/DataTable.svelte +643 -0
  94. package/dist/table/DataTable.svelte.d.ts +153 -0
  95. package/dist/table/columns.d.ts +55 -0
  96. package/dist/table/columns.js +153 -0
  97. package/dist/table/index.d.ts +4 -0
  98. package/dist/table/index.js +8 -0
  99. package/dist/table/sort.d.ts +48 -0
  100. package/dist/table/sort.js +139 -0
  101. package/dist/table/types.d.ts +93 -0
  102. package/dist/table/types.js +1 -0
  103. package/dist/theme/ThemeController.svelte +54 -0
  104. package/dist/theme/ThemeController.svelte.d.ts +27 -0
  105. package/dist/theme/ThemeToggle.svelte +119 -0
  106. package/dist/theme/ThemeToggle.svelte.d.ts +52 -0
  107. package/dist/theme/css-color.d.ts +23 -0
  108. package/dist/theme/css-color.js +31 -0
  109. package/dist/theme/index.d.ts +4 -0
  110. package/dist/theme/index.js +5 -0
  111. package/dist/theme/theme.css +410 -0
  112. package/dist/theme/theme.svelte.d.ts +54 -0
  113. package/dist/theme/theme.svelte.js +58 -0
  114. package/dist/toast/Toaster.svelte +257 -0
  115. package/dist/toast/Toaster.svelte.d.ts +35 -0
  116. package/dist/toast/index.d.ts +2 -0
  117. package/dist/toast/index.js +2 -0
  118. package/dist/toast/toast-manager.svelte.d.ts +99 -0
  119. package/dist/toast/toast-manager.svelte.js +176 -0
  120. package/dist/user/ScopedComponent.svelte +47 -0
  121. package/dist/user/ScopedComponent.svelte.d.ts +29 -0
  122. package/dist/user/anonymous.d.ts +21 -0
  123. package/dist/user/anonymous.js +26 -0
  124. package/dist/user/context.d.ts +55 -0
  125. package/dist/user/context.js +13 -0
  126. package/dist/user/index.d.ts +3 -0
  127. package/dist/user/index.js +6 -0
  128. package/package.json +133 -4
@@ -0,0 +1,49 @@
1
+ import { type Snippet } from "svelte";
2
+ import type { ClassValue, HTMLButtonAttributes } from "svelte/elements";
3
+ type Props = HTMLButtonAttributes & {
4
+ children: Snippet;
5
+ content: Snippet;
6
+ /** Classes for the panel — the only part of this component the trigger's `class` cannot
7
+ reach, since the trigger is the root. */
8
+ panelClass?: ClassValue | null;
9
+ /** Whether the panel is open, however it got there. Bind to read it; the popover decides it. */
10
+ expanded?: boolean;
11
+ /** Hangs the panel just under the trigger at the trigger's width, as a field's list does. */
12
+ matchWidth?: boolean;
13
+ };
14
+ /**
15
+ * A button that toggles a panel through the native popover API, so clicking the trigger again,
16
+ * clicking outside, and pressing Escape all dismiss it without a single listener of our own. The
17
+ * button is the root: class, handlers and aria go straight onto it, and `panelClass` styles the
18
+ * panel.
19
+ *
20
+ * The popover itself — not a local flag — is the source of truth for open/closed; `aria-expanded`
21
+ * and the bindable `expanded` are read back from its `toggle` event. Content mounts just before the
22
+ * panel shows, however it was opened, and unmounts once the panel is really hidden, so an expensive
23
+ * menu costs nothing until it is used. An element inside it marked `autofocus` takes the focus as the
24
+ * panel shows, and closing the panel with the focus inside it hands the focus back to the trigger.
25
+ *
26
+ * `open()` and `close()` are exposed for `bind:this`, which is what a form inside the panel needs to
27
+ * dismiss itself after submitting. `open()` on a panel that is already open moves the focus back to
28
+ * its `autofocus` element.
29
+ *
30
+ * A panel that would run off the side of the screen is pulled back onto it and keeps half a rem from
31
+ * the edge, in both the anchored and the scripted placement; `--dropdown-edge` changes the distance.
32
+ * `matchWidth` hangs it instead just under the trigger and exactly as wide, for a trigger that is a
33
+ * field, the way the `Select` panel hangs under its control.
34
+ *
35
+ * ```svelte
36
+ * <Dropdown bind:this={menu} class="btn" panelClass="dropdown menu w-56 rounded-box bg-base-200 p-2">
37
+ * Actions
38
+ * {#snippet content()}
39
+ * <li><button onclick={() => menu.close()}>Archive</button></li>
40
+ * {/snippet}
41
+ * </Dropdown>
42
+ * ```
43
+ */
44
+ declare const Dropdown: import("svelte").Component<Props, {
45
+ open: () => void;
46
+ close: () => void;
47
+ }, "expanded">;
48
+ type Dropdown = ReturnType<typeof Dropdown>;
49
+ export default Dropdown;
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ /** Wire the `reset` a `svelte:boundary` hands to its `failed` snippet. Omitted, no button. */
4
+ reset?: () => void;
5
+ title?: string;
6
+ message?: string;
7
+ }
8
+
9
+ let {
10
+ reset,
11
+ title = "Something went wrong",
12
+ message = "The application ran into an unexpected problem.",
13
+ }: Props = $props();
14
+ </script>
15
+
16
+ <!--
17
+ @component
18
+ Calm in-place fallback for a crashed subtree: it says what happened and offers the way out.
19
+
20
+ It shows no technical detail on purpose — a stack is not source-mapped in production and the
21
+ message was written for whoever wrote the code, not for whoever is reading the screen. Reporting
22
+ is not this component's job either: it renders on every retry, so a report from here would be
23
+ counted once per render. Wire `reportError` on the boundary's `onerror` instead.
24
+
25
+ ```svelte
26
+ <svelte:boundary onerror={reportError}>
27
+ {@render children()}
28
+ {#snippet failed(_error, reset)}<ErrorDisplay {reset} />{/snippet}
29
+ </svelte:boundary>
30
+ ```
31
+ -->
32
+
33
+ <div
34
+ class="flex h-full w-full flex-col items-center justify-center gap-2 p-8 text-center"
35
+ role="alert"
36
+ >
37
+ <svg
38
+ class="size-12 text-base-content/40"
39
+ viewBox="0 0 24 24"
40
+ fill="none"
41
+ stroke="currentColor"
42
+ stroke-width="1.5"
43
+ stroke-linecap="round"
44
+ stroke-linejoin="round"
45
+ aria-hidden="true"
46
+ >
47
+ <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
48
+ <path d="M12 9v4" />
49
+ <path d="M12 17h.01" />
50
+ </svg>
51
+ <p class="text-xs font-medium tracking-widest text-base-content/55 uppercase">Error</p>
52
+ <p class="text-xl font-semibold">{title}</p>
53
+ <p class="max-w-md text-sm text-base-content/60">{message}</p>
54
+ {#if reset}
55
+ <div class="mt-4">
56
+ <button type="button" class="btn btn-primary" onclick={reset}>Try again</button>
57
+ </div>
58
+ {/if}
59
+ </div>
@@ -0,0 +1,24 @@
1
+ interface Props {
2
+ /** Wire the `reset` a `svelte:boundary` hands to its `failed` snippet. Omitted, no button. */
3
+ reset?: () => void;
4
+ title?: string;
5
+ message?: string;
6
+ }
7
+ /**
8
+ * Calm in-place fallback for a crashed subtree: it says what happened and offers the way out.
9
+ *
10
+ * It shows no technical detail on purpose — a stack is not source-mapped in production and the
11
+ * message was written for whoever wrote the code, not for whoever is reading the screen. Reporting
12
+ * is not this component's job either: it renders on every retry, so a report from here would be
13
+ * counted once per render. Wire `reportError` on the boundary's `onerror` instead.
14
+ *
15
+ * ```svelte
16
+ * <svelte:boundary onerror={reportError}>
17
+ * {@render children()}
18
+ * {#snippet failed(_error, reset)}<ErrorDisplay {reset} />{/snippet}
19
+ * </svelte:boundary>
20
+ * ```
21
+ */
22
+ declare const ErrorDisplay: import("svelte").Component<Props, {}, "">;
23
+ type ErrorDisplay = ReturnType<typeof ErrorDisplay>;
24
+ export default ErrorDisplay;
@@ -0,0 +1,119 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ /** HTTP status behind the failure. 404 gets its own wording and icon. */
4
+ status: number;
5
+ /** Message from `error(status, message)`. Ignored for a 404, where the path says more. */
6
+ message?: string;
7
+ /** Path that was asked for. Shown only on a 404. */
8
+ pathname?: string;
9
+ /** Where "back to start" goes. Pass `resolve("/")` from an app with a base path. */
10
+ homeHref?: string;
11
+ /** The chip above the heading, given the status. */
12
+ statusLabel?: (status: number) => string;
13
+ /** Heading of a 404. */
14
+ notFoundTitle?: string;
15
+ /**
16
+ * Body of a 404, given the path when one was passed. Left out, the English sentence renders
17
+ * with the address in a monospace run of its own, which a returned string cannot carry.
18
+ */
19
+ notFoundBody?: (pathname: string | undefined) => string;
20
+ /** Heading of every other status. */
21
+ errorTitle?: string;
22
+ /** Body of every other status, used when no `message` was passed. */
23
+ errorBody?: string;
24
+ /** Label of the link home. */
25
+ homeLabel?: string;
26
+ /** Label of the reload button, which a 404 does not offer. */
27
+ reloadLabel?: string;
28
+ }
29
+
30
+ let {
31
+ status,
32
+ message,
33
+ pathname,
34
+ homeHref = "/",
35
+ statusLabel = (status: number) => `Error ${status}`,
36
+ notFoundTitle = "Page not found",
37
+ notFoundBody,
38
+ errorTitle = "Something went wrong",
39
+ errorBody = "The application ran into an unexpected problem.",
40
+ homeLabel = "Back to start",
41
+ reloadLabel = "Reload",
42
+ }: Props = $props();
43
+
44
+ const notFound = $derived(status === 404);
45
+ </script>
46
+
47
+ <!--
48
+ @component
49
+ Route-level error page: status-aware, human-facing, rendered from an app's `+error.svelte`.
50
+
51
+ Everything it shows arrives as a prop rather than being read from `$app/state` inside the
52
+ component. That keeps it renderable — and testable — outside a SvelteKit navigation, and it keeps
53
+ the choice of what to reveal at the app's own `+error.svelte`, which is where the app can tell an
54
+ intentional `error(403, "...")` message apart from one it would rather not print.
55
+
56
+ Every word of the copy is a prop with an English default, because the library ships no
57
+ translations: `statusLabel`, `notFoundTitle`, `notFoundBody`, `errorTitle`, `errorBody`,
58
+ `homeLabel` and `reloadLabel`. `message` still wins over `errorBody` whenever the app passed one.
59
+
60
+ ```svelte
61
+ <script lang="ts">
62
+ import { page } from "$app/state";
63
+ import { resolve } from "$app/paths";
64
+ </script>
65
+
66
+ <ErrorPage status={page.status} message={page.error?.message} pathname={page.url.pathname}
67
+ homeHref={resolve("/")} />
68
+ ```
69
+ -->
70
+
71
+ <div class="flex h-full w-full flex-col items-center justify-center gap-2 p-8 text-center">
72
+ <svg
73
+ class="size-12 text-base-content/40"
74
+ viewBox="0 0 24 24"
75
+ fill="none"
76
+ stroke="currentColor"
77
+ stroke-width="1.5"
78
+ stroke-linecap="round"
79
+ stroke-linejoin="round"
80
+ aria-hidden="true"
81
+ >
82
+ {#if notFound}
83
+ <path d="m16.24 7.76-1.8 5.41a2 2 0 0 1-1.27 1.27l-5.41 1.8 1.8-5.41a2 2 0 0 1 1.27-1.27z" />
84
+ <circle cx="12" cy="12" r="10" />
85
+ {:else}
86
+ <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
87
+ <path d="M12 9v4" />
88
+ <path d="M12 17h.01" />
89
+ {/if}
90
+ </svg>
91
+ <p class="text-xs font-medium tracking-widest text-base-content/55 uppercase">
92
+ {statusLabel(status)}
93
+ </p>
94
+ <h1 class="text-xl font-semibold">
95
+ {notFound ? notFoundTitle : errorTitle}
96
+ </h1>
97
+ <p class="max-w-md text-sm break-words text-base-content/60">
98
+ {#if notFound}
99
+ {#if notFoundBody}
100
+ {notFoundBody(pathname)}
101
+ {:else}
102
+ The address {#if pathname}<span class="font-mono break-all">{pathname}</span>{:else}you
103
+ asked for{/if} does not exist, or has moved.
104
+ {/if}
105
+ {:else}
106
+ {message ?? errorBody}
107
+ {/if}
108
+ </p>
109
+ <div class="mt-4 flex gap-2">
110
+ <!-- The link is resolved by the app, which is the side that knows its route ids and its base
111
+ path; the library only renders what it was handed. -->
112
+ <!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
113
+ <a class="btn btn-primary" href={homeHref}>{homeLabel}</a>
114
+ {#if !notFound}
115
+ <!-- A reload is worth offering only for a failure that might not repeat; a 404 will. -->
116
+ <button type="button" class="btn" onclick={() => location.reload()}>{reloadLabel}</button>
117
+ {/if}
118
+ </div>
119
+ </div>
@@ -0,0 +1,52 @@
1
+ interface Props {
2
+ /** HTTP status behind the failure. 404 gets its own wording and icon. */
3
+ status: number;
4
+ /** Message from `error(status, message)`. Ignored for a 404, where the path says more. */
5
+ message?: string;
6
+ /** Path that was asked for. Shown only on a 404. */
7
+ pathname?: string;
8
+ /** Where "back to start" goes. Pass `resolve("/")` from an app with a base path. */
9
+ homeHref?: string;
10
+ /** The chip above the heading, given the status. */
11
+ statusLabel?: (status: number) => string;
12
+ /** Heading of a 404. */
13
+ notFoundTitle?: string;
14
+ /**
15
+ * Body of a 404, given the path when one was passed. Left out, the English sentence renders
16
+ * with the address in a monospace run of its own, which a returned string cannot carry.
17
+ */
18
+ notFoundBody?: (pathname: string | undefined) => string;
19
+ /** Heading of every other status. */
20
+ errorTitle?: string;
21
+ /** Body of every other status, used when no `message` was passed. */
22
+ errorBody?: string;
23
+ /** Label of the link home. */
24
+ homeLabel?: string;
25
+ /** Label of the reload button, which a 404 does not offer. */
26
+ reloadLabel?: string;
27
+ }
28
+ /**
29
+ * Route-level error page: status-aware, human-facing, rendered from an app's `+error.svelte`.
30
+ *
31
+ * Everything it shows arrives as a prop rather than being read from `$app/state` inside the
32
+ * component. That keeps it renderable — and testable — outside a SvelteKit navigation, and it keeps
33
+ * the choice of what to reveal at the app's own `+error.svelte`, which is where the app can tell an
34
+ * intentional `error(403, "...")` message apart from one it would rather not print.
35
+ *
36
+ * Every word of the copy is a prop with an English default, because the library ships no
37
+ * translations: `statusLabel`, `notFoundTitle`, `notFoundBody`, `errorTitle`, `errorBody`,
38
+ * `homeLabel` and `reloadLabel`. `message` still wins over `errorBody` whenever the app passed one.
39
+ *
40
+ * ```svelte
41
+ * <script lang="ts">
42
+ * import { page } from "$app/state";
43
+ * import { resolve } from "$app/paths";
44
+ * </script>
45
+ *
46
+ * <ErrorPage status={page.status} message={page.error?.message} pathname={page.url.pathname}
47
+ * homeHref={resolve("/")} />
48
+ * ```
49
+ */
50
+ declare const ErrorPage: import("svelte").Component<Props, {}, "">;
51
+ type ErrorPage = ReturnType<typeof ErrorPage>;
52
+ export default ErrorPage;
@@ -0,0 +1,106 @@
1
+ <script lang="ts">
2
+ import type { Attachment } from "svelte/attachments";
3
+ import type { ClassValue, HTMLTextareaAttributes } from "svelte/elements";
4
+ import { formatJsonText, parseJsonText } from "./json-text.js";
5
+
6
+ type Props = Omit<
7
+ HTMLTextareaAttributes,
8
+ "bind:value" | "children" | "class" | "onblur" | "required" | "value"
9
+ > & {
10
+ /** The text being edited. Bindable: the field holds text, never a parsed object. */
11
+ value?: string;
12
+ /** `true` while the text does not parse. Bind it to gate a submit. */
13
+ invalid?: boolean;
14
+ /** The field's visible label. */
15
+ label?: string;
16
+ /** Reformats the text on blur when it parses. Text that does not parse is left as typed. */
17
+ formatOnBlur?: boolean;
18
+ /** Rejects blank text, in `invalid` and in the surrounding form's validation. */
19
+ required?: boolean;
20
+ /** Leads the error shown under the field; the parser's own message follows it. */
21
+ invalidLabel?: string;
22
+ /** The error for blank text when `required`. */
23
+ requiredLabel?: string;
24
+ /** Classes for the whole field. The textarea is where every other attribute goes. */
25
+ class?: ClassValue;
26
+ };
27
+
28
+ let {
29
+ value = $bindable(""),
30
+ invalid = $bindable(false),
31
+ label,
32
+ formatOnBlur = false,
33
+ required = false,
34
+ invalidLabel = "Invalid JSON",
35
+ requiredLabel = "Required",
36
+ rows = 12,
37
+ class: className,
38
+ ...rest
39
+ }: Props = $props();
40
+
41
+ const uid = $props.id();
42
+ const fieldId = $derived(rest.id ?? `plinth-json-input-${uid}`);
43
+ const errorId = `plinth-json-input-${uid}-error`;
44
+
45
+ let touched = $state(false);
46
+
47
+ const result = $derived(parseJsonText(value, { required, requiredMessage: requiredLabel }));
48
+ const message = $derived(result.valid ? "" : `${invalidLabel}: ${result.error}`);
49
+ const showError = $derived(touched && !result.valid);
50
+
51
+ $effect(() => {
52
+ const next = !result.valid;
53
+ if (invalid !== next) invalid = next;
54
+ });
55
+
56
+ const validity: Attachment<HTMLTextAreaElement> = (field) => {
57
+ field.setCustomValidity(message);
58
+ };
59
+
60
+ const onblur = () => {
61
+ touched = true;
62
+ if (formatOnBlur) value = formatJsonText(value);
63
+ };
64
+ </script>
65
+
66
+ <!--
67
+ @component
68
+ A JSON text field: it parses as you type, reports whether the text parses through a bindable
69
+ `invalid`, and can reformat the document on blur.
70
+
71
+ It holds text, never a parsed object. A document is invalid for most of the time it is being
72
+ edited, and holding the object would throw away what was typed. Bind `invalid` to gate a submit,
73
+ then parse the text once when sending, with `parseJsonText` or `JSON.parse`.
74
+
75
+ The error waits for the first blur, since every document is invalid while its first brace is being
76
+ typed, and it carries the parser's own message, which names where the document broke. The same
77
+ message is the textarea's custom validity, so a surrounding form will not submit text that does not
78
+ parse.
79
+
80
+ ```svelte
81
+ <JsonInput bind:value={text} bind:invalid label="Settings" formatOnBlur required />
82
+ <button disabled={invalid} onclick={() => save(JSON.parse(text))}>Save</button>
83
+ ```
84
+ -->
85
+
86
+ <div class={["plinth-json-input fieldset w-full", className]}>
87
+ {#if label}
88
+ <label class="fieldset-legend" for={fieldId}>{label}</label>
89
+ {/if}
90
+ <textarea
91
+ {...rest}
92
+ {@attach validity}
93
+ id={fieldId}
94
+ class={["textarea w-full font-mono text-xs", showError && "textarea-error"]}
95
+ {rows}
96
+ {required}
97
+ spellcheck="false"
98
+ aria-invalid={showError}
99
+ aria-describedby={[rest["aria-describedby"], showError && errorId].filter(Boolean).join(" ") ||
100
+ undefined}
101
+ bind:value
102
+ {onblur}></textarea>
103
+ {#if showError}
104
+ <p id={errorId} class="label whitespace-normal text-error">{message}</p>
105
+ {/if}
106
+ </div>
@@ -0,0 +1,40 @@
1
+ import type { ClassValue, HTMLTextareaAttributes } from "svelte/elements";
2
+ type Props = Omit<HTMLTextareaAttributes, "bind:value" | "children" | "class" | "onblur" | "required" | "value"> & {
3
+ /** The text being edited. Bindable: the field holds text, never a parsed object. */
4
+ value?: string;
5
+ /** `true` while the text does not parse. Bind it to gate a submit. */
6
+ invalid?: boolean;
7
+ /** The field's visible label. */
8
+ label?: string;
9
+ /** Reformats the text on blur when it parses. Text that does not parse is left as typed. */
10
+ formatOnBlur?: boolean;
11
+ /** Rejects blank text, in `invalid` and in the surrounding form's validation. */
12
+ required?: boolean;
13
+ /** Leads the error shown under the field; the parser's own message follows it. */
14
+ invalidLabel?: string;
15
+ /** The error for blank text when `required`. */
16
+ requiredLabel?: string;
17
+ /** Classes for the whole field. The textarea is where every other attribute goes. */
18
+ class?: ClassValue;
19
+ };
20
+ /**
21
+ * A JSON text field: it parses as you type, reports whether the text parses through a bindable
22
+ * `invalid`, and can reformat the document on blur.
23
+ *
24
+ * It holds text, never a parsed object. A document is invalid for most of the time it is being
25
+ * edited, and holding the object would throw away what was typed. Bind `invalid` to gate a submit,
26
+ * then parse the text once when sending, with `parseJsonText` or `JSON.parse`.
27
+ *
28
+ * The error waits for the first blur, since every document is invalid while its first brace is being
29
+ * typed, and it carries the parser's own message, which names where the document broke. The same
30
+ * message is the textarea's custom validity, so a surrounding form will not submit text that does not
31
+ * parse.
32
+ *
33
+ * ```svelte
34
+ * <JsonInput bind:value={text} bind:invalid label="Settings" formatOnBlur required />
35
+ * <button disabled={invalid} onclick={() => save(JSON.parse(text))}>Save</button>
36
+ * ```
37
+ */
38
+ declare const JsonInput: import("svelte").Component<Props, {}, "invalid" | "value">;
39
+ type JsonInput = ReturnType<typeof JsonInput>;
40
+ export default JsonInput;