@spaethtech/svelte-ui 0.1.10 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -42
- package/dist/components/Alert.svelte +119 -0
- package/dist/components/Alert.svelte.d.ts +29 -0
- package/dist/components/Badge/Badge.svelte +142 -69
- package/dist/components/Badge/Badge.svelte.d.ts +26 -6
- package/dist/components/Badge/index.d.ts +1 -1
- package/dist/components/Badge/index.js +1 -1
- package/dist/components/Banner.svelte +133 -0
- package/dist/components/Banner.svelte.d.ts +31 -0
- package/dist/components/Button.svelte +240 -0
- package/dist/components/Button.svelte.d.ts +33 -0
- package/dist/components/ButtonDropdown.svelte +145 -0
- package/dist/components/ButtonDropdown.svelte.d.ts +31 -0
- package/dist/components/Calendar.svelte +259 -0
- package/dist/components/Calendar.svelte.d.ts +27 -0
- package/dist/components/Card.svelte +78 -0
- package/dist/components/Card.svelte.d.ts +17 -0
- package/dist/components/CardBody.svelte +41 -0
- package/dist/components/CardBody.svelte.d.ts +17 -0
- package/dist/components/CardFooter.svelte +49 -0
- package/dist/components/CardFooter.svelte.d.ts +16 -0
- package/dist/components/CardHeader.svelte +49 -0
- package/dist/components/CardHeader.svelte.d.ts +16 -0
- package/dist/components/Checkbox.svelte +131 -0
- package/dist/components/Checkbox.svelte.d.ts +16 -0
- package/dist/components/DataTable.svelte +518 -0
- package/dist/components/DataTable.svelte.d.ts +74 -0
- package/dist/components/DatePicker.svelte +312 -0
- package/dist/components/DatePicker.svelte.d.ts +32 -0
- package/dist/components/DateTimeInput.svelte +93 -0
- package/dist/components/DateTimeInput.svelte.d.ts +20 -0
- package/dist/components/Dialog.svelte +132 -0
- package/dist/components/Dialog.svelte.d.ts +24 -0
- package/dist/components/{EmailInput/EmailInput.svelte → EmailInput.svelte} +17 -8
- package/dist/components/EmailInput.svelte.d.ts +21 -0
- package/dist/components/Input.svelte +369 -0
- package/dist/components/Input.svelte.d.ts +35 -0
- package/dist/components/{List/List.svelte → List.svelte} +194 -133
- package/dist/components/{List/List.svelte.d.ts → List.svelte.d.ts} +9 -1
- package/dist/components/Menu.svelte +117 -0
- package/dist/components/Menu.svelte.d.ts +20 -0
- package/dist/components/NotesEditor.svelte +127 -0
- package/dist/components/NotesEditor.svelte.d.ts +17 -0
- package/dist/components/{NumberInput/NumberInput.svelte → NumberInput.svelte} +146 -105
- package/dist/components/{NumberInput/NumberInput.svelte.d.ts → NumberInput.svelte.d.ts} +11 -4
- package/dist/components/PasswordInput.svelte +52 -0
- package/dist/components/PasswordInput.svelte.d.ts +20 -0
- package/dist/components/Popup.svelte +140 -0
- package/dist/components/Popup.svelte.d.ts +31 -0
- package/dist/components/Query.svelte +284 -0
- package/dist/components/Query.svelte.d.ts +15 -0
- package/dist/components/{Rating/Rating.svelte → Rating.svelte} +19 -10
- package/dist/components/Rating.svelte.d.ts +13 -0
- package/dist/components/{SearchInput/SearchInput.svelte → SearchInput.svelte} +19 -8
- package/dist/components/{SearchInput/SearchInput.svelte.d.ts → SearchInput.svelte.d.ts} +9 -2
- package/dist/components/{Select/Select.svelte → Select.svelte} +48 -76
- package/dist/components/{Select/Select.svelte.d.ts → Select.svelte.d.ts} +11 -1
- package/dist/components/SideBarMenu/SideBarMenu.svelte +724 -0
- package/dist/components/SideBarMenu/SideBarMenu.svelte.d.ts +109 -0
- package/dist/components/SideBarMenu/index.d.ts +2 -0
- package/dist/components/SideBarMenu/index.js +1 -0
- package/dist/components/TabStrip/TabStrip.svelte +384 -0
- package/dist/components/TabStrip/TabStrip.svelte.d.ts +50 -0
- package/dist/components/TabStrip/index.d.ts +3 -0
- package/dist/components/TabStrip/index.js +2 -0
- package/dist/components/{TextArea/TextArea.svelte → TextArea.svelte} +143 -112
- package/dist/components/TextArea.svelte.d.ts +38 -0
- package/dist/components/ThemeSelector.svelte +81 -0
- package/dist/components/ThemeSelector.svelte.d.ts +10 -0
- package/dist/components/TimePicker.svelte +148 -0
- package/dist/components/TimePicker.svelte.d.ts +28 -0
- package/dist/components/TimeRangeInput.svelte +203 -0
- package/dist/components/TimeRangeInput.svelte.d.ts +29 -0
- package/dist/components/TimeSpinner.svelte +202 -0
- package/dist/components/TimeSpinner.svelte.d.ts +26 -0
- package/dist/components/Toast/Toaster.svelte +51 -0
- package/dist/components/Toast/Toaster.svelte.d.ts +12 -0
- package/dist/components/Toast/toast.svelte.d.ts +29 -0
- package/dist/components/Toast/toast.svelte.js +27 -0
- package/dist/components/Toggle.svelte +93 -0
- package/dist/components/Toggle.svelte.d.ts +15 -0
- package/dist/data/dataset.d.ts +42 -0
- package/dist/data/dataset.js +3 -0
- package/dist/data/index.d.ts +3 -0
- package/dist/data/index.js +3 -0
- package/dist/data/query/ast.d.ts +62 -0
- package/dist/data/query/ast.js +12 -0
- package/dist/data/query/index.d.ts +3 -0
- package/dist/data/query/index.js +3 -0
- package/dist/data/query/lexer.d.ts +33 -0
- package/dist/data/query/lexer.js +225 -0
- package/dist/data/query/parser.d.ts +11 -0
- package/dist/data/query/parser.js +252 -0
- package/dist/data/table/grid.svelte.d.ts +57 -0
- package/dist/data/table/grid.svelte.js +139 -0
- package/dist/data/table/index.d.ts +2 -0
- package/dist/data/table/index.js +2 -0
- package/dist/data/table/types.d.ts +79 -0
- package/dist/index.d.ts +48 -22
- package/dist/index.js +45 -21
- package/dist/positioning/anchored.d.ts +61 -0
- package/dist/positioning/anchored.js +122 -0
- package/dist/positioning/tooltip.d.ts +41 -0
- package/dist/positioning/tooltip.js +147 -0
- package/dist/theme.css +205 -0
- package/dist/types/breakpoints.d.ts +24 -0
- package/dist/types/breakpoints.js +13 -0
- package/dist/types/responsive.d.ts +32 -0
- package/dist/types/responsive.js +54 -0
- package/dist/types/sizes.d.ts +10 -3
- package/dist/types/time.d.ts +19 -0
- package/dist/types/time.js +10 -0
- package/dist/types/variants.d.ts +8 -1
- package/dist/types/variants.js +16 -1
- package/package.json +93 -63
- package/dist/components/Button/Button.svelte +0 -172
- package/dist/components/Button/Button.svelte.d.ts +0 -32
- package/dist/components/Button/index.d.ts +0 -1
- package/dist/components/Button/index.js +0 -1
- package/dist/components/Dialog/Dialog.svelte +0 -101
- package/dist/components/Dialog/Dialog.svelte.d.ts +0 -18
- package/dist/components/Dialog/index.d.ts +0 -1
- package/dist/components/Dialog/index.js +0 -1
- package/dist/components/ElementManager/ElementManager.svelte +0 -397
- package/dist/components/ElementManager/ElementManager.svelte.d.ts +0 -43
- package/dist/components/ElementManager/index.d.ts +0 -1
- package/dist/components/ElementManager/index.js +0 -1
- package/dist/components/EmailInput/EmailInput.svelte.d.ts +0 -14
- package/dist/components/EmailInput/index.d.ts +0 -1
- package/dist/components/EmailInput/index.js +0 -1
- package/dist/components/Icon/Icon.svelte +0 -74
- package/dist/components/Icon/Icon.svelte.d.ts +0 -13
- package/dist/components/Icon/index.d.ts +0 -1
- package/dist/components/Icon/index.js +0 -1
- package/dist/components/Input/Input.svelte +0 -268
- package/dist/components/Input/Input.svelte.d.ts +0 -18
- package/dist/components/Input/index.d.ts +0 -1
- package/dist/components/Input/index.js +0 -1
- package/dist/components/List/index.d.ts +0 -1
- package/dist/components/List/index.js +0 -1
- package/dist/components/ListItem/ListItem.svelte +0 -175
- package/dist/components/ListItem/ListItem.svelte.d.ts +0 -24
- package/dist/components/ListItem/index.d.ts +0 -2
- package/dist/components/ListItem/index.js +0 -2
- package/dist/components/ListView/ListView.svelte +0 -463
- package/dist/components/ListView/ListView.svelte.d.ts +0 -37
- package/dist/components/ListView/index.d.ts +0 -2
- package/dist/components/ListView/index.js +0 -2
- package/dist/components/NodeGraph/BaseNode.d.ts +0 -57
- package/dist/components/NodeGraph/BaseNode.js +0 -30
- package/dist/components/NodeGraph/Edge.svelte +0 -60
- package/dist/components/NodeGraph/Edge.svelte.d.ts +0 -16
- package/dist/components/NodeGraph/ExpressionEvaluator.d.ts +0 -82
- package/dist/components/NodeGraph/ExpressionEvaluator.js +0 -152
- package/dist/components/NodeGraph/Node.svelte +0 -100
- package/dist/components/NodeGraph/Node.svelte.d.ts +0 -10
- package/dist/components/NodeGraph/NodeGraph.svelte +0 -52
- package/dist/components/NodeGraph/NodeGraph.svelte.d.ts +0 -14
- package/dist/components/NodeGraph/NodeInstance.svelte.d.ts +0 -80
- package/dist/components/NodeGraph/NodeInstance.svelte.js +0 -241
- package/dist/components/NodeGraph/NodePort.svelte +0 -75
- package/dist/components/NodeGraph/NodePort.svelte.d.ts +0 -11
- package/dist/components/NodeGraph/decorators.d.ts +0 -81
- package/dist/components/NodeGraph/decorators.js +0 -148
- package/dist/components/NodeGraph/index.d.ts +0 -9
- package/dist/components/NodeGraph/index.js +0 -9
- package/dist/components/NodeGraph/types.d.ts +0 -94
- package/dist/components/NodeGraph/types.js +0 -33
- package/dist/components/NotesEditor/NotesEditor.svelte +0 -203
- package/dist/components/NotesEditor/NotesEditor.svelte.d.ts +0 -9
- package/dist/components/NotesEditor/index.d.ts +0 -1
- package/dist/components/NotesEditor/index.js +0 -1
- package/dist/components/NumberInput/index.d.ts +0 -1
- package/dist/components/NumberInput/index.js +0 -1
- package/dist/components/PasswordInput/PasswordInput.svelte +0 -41
- package/dist/components/PasswordInput/PasswordInput.svelte.d.ts +0 -13
- package/dist/components/PasswordInput/index.d.ts +0 -1
- package/dist/components/PasswordInput/index.js +0 -1
- package/dist/components/Popup/index.d.ts +0 -1
- package/dist/components/Popup/index.js +0 -1
- package/dist/components/Rating/Rating.svelte.d.ts +0 -7
- package/dist/components/Rating/index.d.ts +0 -1
- package/dist/components/Rating/index.js +0 -1
- package/dist/components/ScrollView/ScrollView.svelte +0 -285
- package/dist/components/ScrollView/ScrollView.svelte.d.ts +0 -19
- package/dist/components/ScrollView/index.d.ts +0 -1
- package/dist/components/ScrollView/index.js +0 -1
- package/dist/components/SearchInput/index.d.ts +0 -1
- package/dist/components/SearchInput/index.js +0 -1
- package/dist/components/Select/index.d.ts +0 -1
- package/dist/components/Select/index.js +0 -1
- package/dist/components/TextArea/TextArea.svelte.d.ts +0 -19
- package/dist/components/TextArea/index.d.ts +0 -1
- package/dist/components/TextArea/index.js +0 -1
- package/dist/components/ThemeSelector/ThemeSelector.svelte +0 -64
- package/dist/components/ThemeSelector/ThemeSelector.svelte.d.ts +0 -3
- package/dist/components/ThemeSelector/index.d.ts +0 -1
- package/dist/components/ThemeSelector/index.js +0 -1
- package/dist/components/TooltipHandler/TooltipHandler.svelte +0 -593
- package/dist/components/TooltipHandler/TooltipHandler.svelte.d.ts +0 -10
- package/dist/components/TooltipHandler/index.d.ts +0 -1
- package/dist/components/TooltipHandler/index.js +0 -1
- package/dist/styles/sizes.d.ts +0 -78
- package/dist/styles/sizes.js +0 -120
- package/dist/styles/variants.d.ts +0 -106
- package/dist/styles/variants.js +0 -194
- package/dist/types/ManagerTypes.d.ts +0 -42
- /package/dist/{types/ManagerTypes.js → data/table/types.js} +0 -0
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import { faCopy } from "@fortawesome/free-solid-svg-icons";
|
|
2
|
+
import ContentCopy from "~icons/mdi/content-copy";
|
|
4
3
|
import type { HTMLTextareaAttributes } from "svelte/elements";
|
|
4
|
+
import type { Size } from "../types/sizes.js";
|
|
5
|
+
import { responsiveClasses, resolveScalar, type Responsive } from "../types/responsive.js";
|
|
6
|
+
import type { Variant } from "../types/variants.js";
|
|
5
7
|
import { DEV } from "esm-env";
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
type TextAreaCopy = "none" | "selection" | "button";
|
|
10
|
+
|
|
11
|
+
// Omit the native `oncopy` DOM handler — we repurpose it as a copy-button callback (see below),
|
|
12
|
+
// the same way TabStrip omits the native `onselect`.
|
|
13
|
+
interface Props extends Omit<HTMLTextareaAttributes, "oncopy"> {
|
|
8
14
|
value?: string;
|
|
9
15
|
lines?: string[];
|
|
10
16
|
class?: string;
|
|
@@ -16,10 +22,62 @@
|
|
|
16
22
|
minRows?: number;
|
|
17
23
|
maxRows?: number;
|
|
18
24
|
lineHeightRem?: number;
|
|
25
|
+
size?: Responsive<Size>;
|
|
26
|
+
/** Colour axis — tints the border + placeholder with the SAME calc as Input (token @ 30% at rest,
|
|
27
|
+
* 55% on hover/focus, mixed toward transparent). No `secondary`/`-strong` special case. */
|
|
28
|
+
variant?: Variant;
|
|
29
|
+
/** Drop the visible frame while keeping the 1px box + focus outline (border → transparent). */
|
|
30
|
+
borderless?: boolean;
|
|
19
31
|
required?: boolean;
|
|
20
|
-
|
|
32
|
+
/** How the text can be copied — one exclusive mode:
|
|
33
|
+
* `'selection'` (default) — copy via native text selection only, no button;
|
|
34
|
+
* `'button'` — also show the copy-to-clipboard button;
|
|
35
|
+
* `'none'` — copying disabled: no button and the text is not selectable.
|
|
36
|
+
* Orthogonal to `readonly`/`disabled` (all other behaviour unchanged). */
|
|
37
|
+
copy?: TextAreaCopy;
|
|
38
|
+
/** Fires after the copy button successfully writes to the clipboard, with the copied text.
|
|
39
|
+
* Lets a consumer confirm the copy (e.g. `oncopy={() => toast.success('Copied to clipboard')}`).
|
|
40
|
+
* Not called for native selection-copy, only the `copy="button"` action. */
|
|
41
|
+
oncopy?: (text: string) => void;
|
|
21
42
|
}
|
|
22
43
|
|
|
44
|
+
// Border + placeholder colour by variant — IDENTICAL formula to Input (one calc for every variant):
|
|
45
|
+
// token @ --ui-tint-border (30%) at rest, token @ 55% on hover/focus, mixed toward transparent.
|
|
46
|
+
const borderVariantClass: Record<Variant, string> = {
|
|
47
|
+
secondary:
|
|
48
|
+
"[border-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border-hover),transparent)] focus-within:[border-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border-hover),transparent)]",
|
|
49
|
+
primary:
|
|
50
|
+
"[border-color:color-mix(in_srgb,var(--ui-color-primary)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-primary)_var(--ui-tint-border-hover),transparent)] focus-within:[border-color:color-mix(in_srgb,var(--ui-color-primary)_var(--ui-tint-border-hover),transparent)]",
|
|
51
|
+
danger:
|
|
52
|
+
"[border-color:color-mix(in_srgb,var(--ui-color-error)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-error)_var(--ui-tint-border-hover),transparent)] focus-within:[border-color:color-mix(in_srgb,var(--ui-color-error)_var(--ui-tint-border-hover),transparent)]",
|
|
53
|
+
info: "[border-color:color-mix(in_srgb,var(--ui-color-info)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-info)_var(--ui-tint-border-hover),transparent)] focus-within:[border-color:color-mix(in_srgb,var(--ui-color-info)_var(--ui-tint-border-hover),transparent)]",
|
|
54
|
+
success:
|
|
55
|
+
"[border-color:color-mix(in_srgb,var(--ui-color-success)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-success)_var(--ui-tint-border-hover),transparent)] focus-within:[border-color:color-mix(in_srgb,var(--ui-color-success)_var(--ui-tint-border-hover),transparent)]",
|
|
56
|
+
warning:
|
|
57
|
+
"[border-color:color-mix(in_srgb,var(--ui-color-warning)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-warning)_var(--ui-tint-border-hover),transparent)] focus-within:[border-color:color-mix(in_srgb,var(--ui-color-warning)_var(--ui-tint-border-hover),transparent)]",
|
|
58
|
+
neutral:
|
|
59
|
+
"[border-color:color-mix(in_srgb,var(--ui-color-text)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-text)_var(--ui-tint-border-hover),transparent)] focus-within:[border-color:color-mix(in_srgb,var(--ui-color-text)_var(--ui-tint-border-hover),transparent)]",
|
|
60
|
+
ghost:
|
|
61
|
+
"[border-color:color-mix(in_srgb,var(--ui-color-text)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-text)_var(--ui-tint-border-hover),transparent)] focus-within:[border-color:color-mix(in_srgb,var(--ui-color-text)_var(--ui-tint-border-hover),transparent)]",
|
|
62
|
+
};
|
|
63
|
+
const placeholderVariantClass: Record<Variant, string> = {
|
|
64
|
+
secondary:
|
|
65
|
+
"placeholder:[color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border),transparent)]",
|
|
66
|
+
primary:
|
|
67
|
+
"placeholder:[color:color-mix(in_srgb,var(--ui-color-primary)_var(--ui-tint-border),transparent)]",
|
|
68
|
+
danger:
|
|
69
|
+
"placeholder:[color:color-mix(in_srgb,var(--ui-color-error)_var(--ui-tint-border),transparent)]",
|
|
70
|
+
info: "placeholder:[color:color-mix(in_srgb,var(--ui-color-info)_var(--ui-tint-border),transparent)]",
|
|
71
|
+
success:
|
|
72
|
+
"placeholder:[color:color-mix(in_srgb,var(--ui-color-success)_var(--ui-tint-border),transparent)]",
|
|
73
|
+
warning:
|
|
74
|
+
"placeholder:[color:color-mix(in_srgb,var(--ui-color-warning)_var(--ui-tint-border),transparent)]",
|
|
75
|
+
neutral:
|
|
76
|
+
"placeholder:[color:color-mix(in_srgb,var(--ui-color-text)_var(--ui-tint-border),transparent)]",
|
|
77
|
+
ghost:
|
|
78
|
+
"placeholder:[color:color-mix(in_srgb,var(--ui-color-text)_var(--ui-tint-border),transparent)]",
|
|
79
|
+
};
|
|
80
|
+
|
|
23
81
|
let {
|
|
24
82
|
value = $bindable(),
|
|
25
83
|
lines = $bindable(),
|
|
@@ -32,13 +90,27 @@
|
|
|
32
90
|
disabled = false,
|
|
33
91
|
minRows = 3,
|
|
34
92
|
maxRows,
|
|
35
|
-
lineHeightRem
|
|
93
|
+
lineHeightRem,
|
|
94
|
+
size = "md",
|
|
95
|
+
variant = "secondary",
|
|
96
|
+
borderless = false,
|
|
36
97
|
required = false,
|
|
37
|
-
|
|
98
|
+
copy = "selection",
|
|
99
|
+
oncopy,
|
|
38
100
|
readonly = false,
|
|
39
101
|
...restProps
|
|
40
102
|
}: Props = $props();
|
|
41
103
|
|
|
104
|
+
// Size axis (see types/sizes.ts): text size, wrapper padding, and the line-height the autosize
|
|
105
|
+
// math keys off (so smaller sizes aren't airy). An explicit lineHeightRem still wins.
|
|
106
|
+
const textMap: Record<Size, string> = { sm: "text-xs", md: "text-sm", lg: "text-base" };
|
|
107
|
+
const padMap: Record<Size, string> = { sm: "py-0.5 px-2", md: "py-1 px-2", lg: "py-2 px-3" };
|
|
108
|
+
const textClass = $derived(responsiveClasses(size, textMap));
|
|
109
|
+
const padClass = $derived(responsiveClasses(size, padMap));
|
|
110
|
+
// Autosize math is JS-numeric (not a class), so it keys off the base tier of a responsive size.
|
|
111
|
+
const lhMap: Record<Size, number> = { sm: 1.25, md: 1.5, lg: 1.75 };
|
|
112
|
+
const effLineHeightRem = $derived(lineHeightRem ?? lhMap[resolveScalar(size, "md")]);
|
|
113
|
+
|
|
42
114
|
let textareaElement = $state<HTMLTextAreaElement>();
|
|
43
115
|
let containerElement = $state<HTMLDivElement>();
|
|
44
116
|
let isDragging = $state(false);
|
|
@@ -50,14 +122,10 @@
|
|
|
50
122
|
// Development warnings for prop conflicts and empty content
|
|
51
123
|
if (DEV) {
|
|
52
124
|
if (value !== undefined && lines !== undefined) {
|
|
53
|
-
console.warn(
|
|
54
|
-
"TextArea: Both value and lines provided. Using value, ignoring lines."
|
|
55
|
-
);
|
|
125
|
+
console.warn("TextArea: Both value and lines provided. Using value, ignoring lines.");
|
|
56
126
|
}
|
|
57
127
|
if (value === undefined && !lines) {
|
|
58
|
-
console.warn(
|
|
59
|
-
"TextArea: No content provided (value or lines). TextArea will be empty."
|
|
60
|
-
);
|
|
128
|
+
console.warn("TextArea: No content provided (value or lines). TextArea will be empty.");
|
|
61
129
|
}
|
|
62
130
|
}
|
|
63
131
|
|
|
@@ -67,7 +135,7 @@
|
|
|
67
135
|
|
|
68
136
|
// Content precedence: value > lines
|
|
69
137
|
let effectiveContent = $derived(
|
|
70
|
-
hasValueProp ? value : hasLinesProp ? lines?.join("\n") || "" : ""
|
|
138
|
+
hasValueProp ? value : hasLinesProp ? lines?.join("\n") || "" : "",
|
|
71
139
|
);
|
|
72
140
|
|
|
73
141
|
// Handle bidirectional binding for lines
|
|
@@ -81,11 +149,12 @@
|
|
|
81
149
|
});
|
|
82
150
|
|
|
83
151
|
// Enforce minimum rows and handle maxRows auto-sizing
|
|
84
|
-
const effectiveMinRows = Math.max(minRows, 2);
|
|
85
|
-
const effectiveMaxRows = maxRows ?? Infinity;
|
|
86
|
-
// disabled overrides
|
|
87
|
-
const effectiveReadonly = disabled ? true : readonly;
|
|
88
|
-
const
|
|
152
|
+
const effectiveMinRows = $derived(Math.max(minRows, 2));
|
|
153
|
+
const effectiveMaxRows = $derived(maxRows ?? Infinity);
|
|
154
|
+
// disabled overrides readonly + the copy mode. `button` shows the button; `none` also blocks selection.
|
|
155
|
+
const effectiveReadonly = $derived(disabled ? true : readonly);
|
|
156
|
+
const effectiveCopyable = $derived(disabled ? false : copy === "button");
|
|
157
|
+
const effectiveSelectable = $derived(disabled ? false : copy !== "none");
|
|
89
158
|
|
|
90
159
|
// Calculate initial rows based on content
|
|
91
160
|
// NOTE: This only counts explicit line breaks, not wrapped lines.
|
|
@@ -96,10 +165,7 @@
|
|
|
96
165
|
// Count actual lines in the content (newline characters only)
|
|
97
166
|
const lineCount = effectiveContent.split("\n").length;
|
|
98
167
|
// Clamp between min and max
|
|
99
|
-
return Math.min(
|
|
100
|
-
Math.max(lineCount, effectiveMinRows, minRows),
|
|
101
|
-
effectiveMaxRows
|
|
102
|
-
);
|
|
168
|
+
return Math.min(Math.max(lineCount, effectiveMinRows, minRows), effectiveMaxRows);
|
|
103
169
|
}
|
|
104
170
|
// Default to minRows if no content
|
|
105
171
|
return Math.max(minRows, effectiveMinRows);
|
|
@@ -137,18 +203,13 @@
|
|
|
137
203
|
// Auto-resize functionality
|
|
138
204
|
function updateHeight() {
|
|
139
205
|
if (!textareaElement) return;
|
|
140
|
-
|
|
141
|
-
|
|
142
206
|
|
|
143
207
|
// Get line height - calculate from the prop value, ensuring whole pixels
|
|
144
208
|
if (lineHeight === 0) {
|
|
145
|
-
const rootFontSize = parseFloat(
|
|
146
|
-
getComputedStyle(document.documentElement).fontSize
|
|
147
|
-
);
|
|
209
|
+
const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
148
210
|
// Round to nearest whole pixel, then convert back to rem for consistency
|
|
149
|
-
lineHeight = Math.round(
|
|
211
|
+
lineHeight = Math.round(effLineHeightRem * rootFontSize);
|
|
150
212
|
actualLineHeightRem = lineHeight / rootFontSize;
|
|
151
|
-
|
|
152
213
|
}
|
|
153
214
|
|
|
154
215
|
// Store current scroll position
|
|
@@ -157,24 +218,21 @@
|
|
|
157
218
|
// Force browser reflow by completely resetting styles
|
|
158
219
|
textareaElement.style.height = "";
|
|
159
220
|
textareaElement.style.overflow = "";
|
|
160
|
-
|
|
221
|
+
|
|
161
222
|
// Force a layout reflow
|
|
162
223
|
textareaElement.offsetHeight;
|
|
163
|
-
|
|
224
|
+
|
|
164
225
|
// Now set to auto for measurement
|
|
165
226
|
textareaElement.style.height = "auto";
|
|
166
227
|
textareaElement.style.overflow = "hidden";
|
|
167
|
-
|
|
228
|
+
|
|
168
229
|
// Force another reflow to ensure measurement is accurate
|
|
169
230
|
const scrollHeight = textareaElement.scrollHeight;
|
|
170
231
|
|
|
171
232
|
// Calculate rows needed
|
|
172
233
|
const neededRows = Math.ceil(scrollHeight / lineHeight);
|
|
173
|
-
const actualRows = Math.min(
|
|
174
|
-
|
|
175
|
-
effectiveMaxRows
|
|
176
|
-
);
|
|
177
|
-
|
|
234
|
+
const actualRows = Math.min(Math.max(neededRows, effectiveMinRows), effectiveMaxRows);
|
|
235
|
+
|
|
178
236
|
// Update maxRows exceeded state
|
|
179
237
|
maxRowsExceeded = neededRows > effectiveMaxRows;
|
|
180
238
|
|
|
@@ -195,8 +253,7 @@
|
|
|
195
253
|
isScrollable = false;
|
|
196
254
|
} else {
|
|
197
255
|
// Only scrollable if content actually overflows the set height
|
|
198
|
-
isScrollable =
|
|
199
|
-
textareaElement.scrollHeight > textareaElement.clientHeight;
|
|
256
|
+
isScrollable = textareaElement.scrollHeight > textareaElement.clientHeight;
|
|
200
257
|
}
|
|
201
258
|
} else if (effectiveReadonly) {
|
|
202
259
|
// Readonly textareas are never scrollable in our context
|
|
@@ -229,10 +286,7 @@
|
|
|
229
286
|
setTimeout(() => updateHeight(), 0);
|
|
230
287
|
|
|
231
288
|
// Check if mouse is already over the element on mount (scrollbar for readonly too)
|
|
232
|
-
if (
|
|
233
|
-
containerElement?.matches(":hover") ||
|
|
234
|
-
textareaElement?.matches(":hover")
|
|
235
|
-
) {
|
|
289
|
+
if (containerElement?.matches(":hover") || textareaElement?.matches(":hover")) {
|
|
236
290
|
hovered = true;
|
|
237
291
|
updateScrollbar();
|
|
238
292
|
}
|
|
@@ -246,14 +300,14 @@
|
|
|
246
300
|
});
|
|
247
301
|
|
|
248
302
|
let wrapperClasses = $derived.by(() => {
|
|
249
|
-
const base =
|
|
303
|
+
const base = `relative w-full [border-radius:var(--ui-border-radius)] border ${padClass}`;
|
|
250
304
|
|
|
251
|
-
//
|
|
305
|
+
// Border colour from the variant map (same calc as Input); `borderless` → transparent (its own
|
|
306
|
+
// background), keeping the 1px box + focus outline.
|
|
307
|
+
const frameStyles = borderless ? "[border-color:transparent]" : borderVariantClass[variant];
|
|
252
308
|
const themeStyles = [
|
|
253
309
|
"bg-transparent",
|
|
254
|
-
|
|
255
|
-
"hover:[border-color:color-mix(in_srgb,var(--color-secondary)_80%,black)]",
|
|
256
|
-
"focus-within:[border-color:color-mix(in_srgb,var(--color-secondary)_90%,black)]",
|
|
310
|
+
frameStyles,
|
|
257
311
|
"focus-within:[outline:2px_solid_color-mix(in_srgb,currentColor_70%,transparent)]",
|
|
258
312
|
"focus-within:[outline-offset:0px]",
|
|
259
313
|
].join(" ");
|
|
@@ -262,37 +316,34 @@
|
|
|
262
316
|
let validationStyles = "";
|
|
263
317
|
if (touched && !valid) {
|
|
264
318
|
validationStyles =
|
|
265
|
-
"[border-color:var(--color-
|
|
319
|
+
"[border-color:var(--ui-color-error)] focus-within:[outline:2px_solid_color-mix(in_srgb,var(--ui-color-error)_70%,transparent)]";
|
|
266
320
|
}
|
|
267
321
|
|
|
268
|
-
// Disabled
|
|
322
|
+
// Disabled state — borders are uniformly 1px, so no padding compensation is needed.
|
|
269
323
|
const disabledStyles = disabled
|
|
270
|
-
? "![background-color:color-mix(in_srgb,var(--color-text)_5%,transparent)] [border-color:color-mix(in_srgb,var(--color-secondary)_50%,transparent)]
|
|
271
|
-
:
|
|
272
|
-
? "[border-color:var(--color-secondary)] !border !py-[calc(0.25rem+1px)] !px-[calc(0.5rem+1px)]"
|
|
273
|
-
: "";
|
|
324
|
+
? "![background-color:color-mix(in_srgb,var(--ui-color-text)_5%,transparent)] [border-color:color-mix(in_srgb,var(--ui-color-secondary)_50%,transparent)] cursor-not-allowed"
|
|
325
|
+
: "";
|
|
274
326
|
|
|
275
327
|
return `${base} ${themeStyles} ${validationStyles} ${disabledStyles}`.trim();
|
|
276
328
|
});
|
|
277
329
|
|
|
278
330
|
let textareaClasses = $derived.by(() => {
|
|
279
|
-
const base =
|
|
280
|
-
"w-full bg-transparent border-none outline-none text-sm resize-none custom-textarea";
|
|
331
|
+
const base = `w-full bg-transparent border-none outline-none ${textClass} resize-none custom-textarea`;
|
|
281
332
|
|
|
282
333
|
// Use new semantic color system
|
|
283
334
|
const themeStyles = [
|
|
284
|
-
"[color:var(--color-text)]",
|
|
285
|
-
|
|
335
|
+
"[color:var(--ui-color-text)]",
|
|
336
|
+
placeholderVariantClass[variant],
|
|
286
337
|
"disabled:bg-transparent",
|
|
287
|
-
"disabled:[color:color-mix(in_srgb,var(--color-text)_40%,transparent)]",
|
|
338
|
+
"disabled:[color:color-mix(in_srgb,var(--ui-color-text)_40%,transparent)]",
|
|
288
339
|
"disabled:cursor-not-allowed",
|
|
289
340
|
].join(" ");
|
|
290
341
|
|
|
291
|
-
// Padding adjustment for copy icon
|
|
292
|
-
const padding =
|
|
342
|
+
// Padding adjustment for the copy icon (only when the button is shown)
|
|
343
|
+
const padding = effectiveCopyable ? "pr-8" : "";
|
|
293
344
|
|
|
294
345
|
// Cursor style based on allowCopy
|
|
295
|
-
const cursor =
|
|
346
|
+
const cursor = effectiveSelectable ? "" : "cursor-default";
|
|
296
347
|
|
|
297
348
|
return `${base} ${themeStyles} ${padding} ${cursor} ${textareaClass}`.trim();
|
|
298
349
|
});
|
|
@@ -300,8 +351,7 @@
|
|
|
300
351
|
let copyIconClasses = $derived.by(() => {
|
|
301
352
|
const base =
|
|
302
353
|
"absolute top-0 right-0 w-8 h-8 flex items-center justify-center flex-shrink-0 select-none z-20";
|
|
303
|
-
const interactive =
|
|
304
|
-
"cursor-pointer focus:outline-none transition-colors group";
|
|
354
|
+
const interactive = "cursor-pointer focus:outline-none transition-colors group";
|
|
305
355
|
|
|
306
356
|
return `${base} ${interactive}`.trim();
|
|
307
357
|
});
|
|
@@ -324,7 +374,7 @@
|
|
|
324
374
|
}
|
|
325
375
|
|
|
326
376
|
function handleFocus(event: FocusEvent) {
|
|
327
|
-
if (!
|
|
377
|
+
if (!effectiveSelectable) {
|
|
328
378
|
// Prevent focus when allowCopy is false
|
|
329
379
|
event.preventDefault();
|
|
330
380
|
if (textareaElement) {
|
|
@@ -341,7 +391,7 @@
|
|
|
341
391
|
if (!effectiveContent) return;
|
|
342
392
|
try {
|
|
343
393
|
await navigator.clipboard.writeText(effectiveContent);
|
|
344
|
-
|
|
394
|
+
oncopy?.(effectiveContent);
|
|
345
395
|
} catch (err) {
|
|
346
396
|
console.error("Failed to copy text: ", err);
|
|
347
397
|
}
|
|
@@ -372,7 +422,7 @@
|
|
|
372
422
|
|
|
373
423
|
// Prevent text selection when allowCopy is false
|
|
374
424
|
function handleSelectStart(event: Event) {
|
|
375
|
-
if (!
|
|
425
|
+
if (!effectiveSelectable) {
|
|
376
426
|
event.preventDefault();
|
|
377
427
|
return false;
|
|
378
428
|
}
|
|
@@ -380,14 +430,14 @@
|
|
|
380
430
|
|
|
381
431
|
// Additional handlers for comprehensive text selection prevention
|
|
382
432
|
function handleDragStart(event: DragEvent) {
|
|
383
|
-
if (!
|
|
433
|
+
if (!effectiveSelectable) {
|
|
384
434
|
event.preventDefault();
|
|
385
435
|
return false;
|
|
386
436
|
}
|
|
387
437
|
}
|
|
388
438
|
|
|
389
439
|
function handleContextMenu(event: MouseEvent) {
|
|
390
|
-
if (!
|
|
440
|
+
if (!effectiveSelectable) {
|
|
391
441
|
event.preventDefault();
|
|
392
442
|
return false;
|
|
393
443
|
}
|
|
@@ -397,7 +447,7 @@
|
|
|
397
447
|
// Handle restProps mousedown first
|
|
398
448
|
if (restProps.onmousedown) restProps.onmousedown(event as any);
|
|
399
449
|
|
|
400
|
-
if (!
|
|
450
|
+
if (!effectiveSelectable) {
|
|
401
451
|
// Prevent clicking inside entirely - no focus, no selection
|
|
402
452
|
event.preventDefault();
|
|
403
453
|
event.stopPropagation();
|
|
@@ -408,12 +458,9 @@
|
|
|
408
458
|
// Handle restProps keydown first
|
|
409
459
|
if (restProps.onkeydown) restProps.onkeydown(event as any);
|
|
410
460
|
|
|
411
|
-
if (!
|
|
461
|
+
if (!effectiveSelectable) {
|
|
412
462
|
// Prevent Ctrl+A (select all) and other selection shortcuts
|
|
413
|
-
if (
|
|
414
|
-
(event.ctrlKey || event.metaKey) &&
|
|
415
|
-
(event.key === "a" || event.key === "A")
|
|
416
|
-
) {
|
|
463
|
+
if ((event.ctrlKey || event.metaKey) && (event.key === "a" || event.key === "A")) {
|
|
417
464
|
event.preventDefault();
|
|
418
465
|
return false;
|
|
419
466
|
}
|
|
@@ -454,16 +501,11 @@
|
|
|
454
501
|
return;
|
|
455
502
|
}
|
|
456
503
|
|
|
457
|
-
const visibleRatio =
|
|
458
|
-
textareaElement.clientHeight / textareaElement.scrollHeight;
|
|
504
|
+
const visibleRatio = textareaElement.clientHeight / textareaElement.scrollHeight;
|
|
459
505
|
const trackHeight = textareaElement.clientHeight;
|
|
460
|
-
thumbHeight = Math.max(
|
|
461
|
-
20,
|
|
462
|
-
Math.min(trackHeight, visibleRatio * trackHeight)
|
|
463
|
-
);
|
|
506
|
+
thumbHeight = Math.max(20, Math.min(trackHeight, visibleRatio * trackHeight));
|
|
464
507
|
|
|
465
|
-
const maxScrollTop =
|
|
466
|
-
textareaElement.scrollHeight - textareaElement.clientHeight;
|
|
508
|
+
const maxScrollTop = textareaElement.scrollHeight - textareaElement.clientHeight;
|
|
467
509
|
if (maxScrollTop > 0) {
|
|
468
510
|
const scrollRatio = textareaElement.scrollTop / maxScrollTop;
|
|
469
511
|
const availableTrackHeight = trackHeight - thumbHeight;
|
|
@@ -485,9 +527,7 @@
|
|
|
485
527
|
const currentScroll = textareaElement.scrollTop;
|
|
486
528
|
|
|
487
529
|
// Calculate the nearest line boundary (ensure whole pixels)
|
|
488
|
-
const nearestLineScroll = Math.round(
|
|
489
|
-
Math.round(currentScroll / lineHeight) * lineHeight
|
|
490
|
-
);
|
|
530
|
+
const nearestLineScroll = Math.round(Math.round(currentScroll / lineHeight) * lineHeight);
|
|
491
531
|
|
|
492
532
|
// Only adjust if we're more than 0.5px away from a line boundary
|
|
493
533
|
// Use a smaller threshold to reduce bounce but still snap effectively
|
|
@@ -515,8 +555,7 @@
|
|
|
515
555
|
|
|
516
556
|
// Check if the scroll would stay within textarea bounds
|
|
517
557
|
const currentScroll = textareaElement.scrollTop;
|
|
518
|
-
const maxScrollTop =
|
|
519
|
-
textareaElement.scrollHeight - textareaElement.clientHeight;
|
|
558
|
+
const maxScrollTop = textareaElement.scrollHeight - textareaElement.clientHeight;
|
|
520
559
|
const scrollingDown = event.deltaY > 0;
|
|
521
560
|
const scrollingUp = event.deltaY < 0;
|
|
522
561
|
|
|
@@ -544,10 +583,7 @@
|
|
|
544
583
|
const currentLine = Math.round(currentScroll / lineHeight);
|
|
545
584
|
|
|
546
585
|
// Calculate new line position
|
|
547
|
-
const newLine = Math.max(
|
|
548
|
-
0,
|
|
549
|
-
currentLine + (scrollDelta > 0 ? linesPerStep : -linesPerStep)
|
|
550
|
-
);
|
|
586
|
+
const newLine = Math.max(0, currentLine + (scrollDelta > 0 ? linesPerStep : -linesPerStep));
|
|
551
587
|
const newScrollTop = newLine * lineHeight;
|
|
552
588
|
|
|
553
589
|
// Apply bounds checking
|
|
@@ -574,12 +610,11 @@
|
|
|
574
610
|
const deltaY = event.clientY - dragStartY;
|
|
575
611
|
const trackHeight = textareaElement.clientHeight - thumbHeight;
|
|
576
612
|
const scrollRatio = deltaY / trackHeight;
|
|
577
|
-
const maxScrollTop =
|
|
578
|
-
textareaElement.scrollHeight - textareaElement.clientHeight;
|
|
613
|
+
const maxScrollTop = textareaElement.scrollHeight - textareaElement.clientHeight;
|
|
579
614
|
|
|
580
615
|
textareaElement.scrollTop = Math.max(
|
|
581
616
|
0,
|
|
582
|
-
Math.min(maxScrollTop, dragStartScrollTop + scrollRatio * maxScrollTop)
|
|
617
|
+
Math.min(maxScrollTop, dragStartScrollTop + scrollRatio * maxScrollTop),
|
|
583
618
|
);
|
|
584
619
|
}
|
|
585
620
|
|
|
@@ -595,7 +630,7 @@
|
|
|
595
630
|
|
|
596
631
|
// Initialize window dimensions
|
|
597
632
|
$effect(() => {
|
|
598
|
-
if (typeof window !==
|
|
633
|
+
if (typeof window !== "undefined") {
|
|
599
634
|
windowWidth = window.innerWidth;
|
|
600
635
|
windowHeight = window.innerHeight;
|
|
601
636
|
|
|
@@ -604,8 +639,8 @@
|
|
|
604
639
|
windowHeight = window.innerHeight;
|
|
605
640
|
};
|
|
606
641
|
|
|
607
|
-
window.addEventListener(
|
|
608
|
-
return () => window.removeEventListener(
|
|
642
|
+
window.addEventListener("resize", handleResize);
|
|
643
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
609
644
|
}
|
|
610
645
|
});
|
|
611
646
|
|
|
@@ -658,25 +693,22 @@
|
|
|
658
693
|
{...restProps}
|
|
659
694
|
class={textareaClasses}
|
|
660
695
|
style="line-height: {actualLineHeightRem ||
|
|
661
|
-
|
|
696
|
+
effLineHeightRem}rem; margin: 0; padding: 0; vertical-align: top; {effectiveReadonly
|
|
662
697
|
? ''
|
|
663
|
-
: 'scroll-snap-type: y mandatory; scroll-padding: 0; overscroll-behavior: contain;'}{
|
|
698
|
+
: 'scroll-snap-type: y mandatory; scroll-padding: 0; overscroll-behavior: contain;'}{effectiveSelectable
|
|
664
699
|
? ''
|
|
665
700
|
: ' user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none;'}"
|
|
666
701
|
rows={initialRows}
|
|
667
702
|
{disabled}
|
|
668
703
|
readonly={effectiveReadonly}
|
|
669
704
|
{required}
|
|
670
|
-
tabindex={
|
|
705
|
+
tabindex={effectiveSelectable ? undefined : -1}
|
|
671
706
|
aria-invalid={touched && !valid ? "true" : "false"}
|
|
672
|
-
aria-describedby={touched &&
|
|
673
|
-
!valid &&
|
|
674
|
-
typeof validationResult === "string"
|
|
707
|
+
aria-describedby={touched && !valid && typeof validationResult === "string"
|
|
675
708
|
? "textarea-error"
|
|
676
|
-
: undefined}
|
|
677
|
-
></textarea>
|
|
709
|
+
: undefined}></textarea>
|
|
678
710
|
|
|
679
|
-
{#if
|
|
711
|
+
{#if effectiveCopyable && effectiveContent}
|
|
680
712
|
<div
|
|
681
713
|
class={copyIconClasses}
|
|
682
714
|
onclick={copyToClipboard}
|
|
@@ -689,9 +721,8 @@
|
|
|
689
721
|
style="user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none;"
|
|
690
722
|
title="Copy to clipboard"
|
|
691
723
|
>
|
|
692
|
-
<
|
|
693
|
-
|
|
694
|
-
class="w-4 h-4 [color:var(--color-secondary)!important] group-hover:[color:color-mix(in_srgb,var(--color-secondary)_80%,black)!important] group-focus:[color:color-mix(in_srgb,currentColor_70%,transparent)!important] transition-colors"
|
|
724
|
+
<ContentCopy
|
|
725
|
+
class="w-4 h-4 [color:var(--ui-color-secondary)!important] group-hover:[color:color-mix(in_srgb,var(--ui-color-secondary)_80%,black)!important] group-focus:[color:color-mix(in_srgb,currentColor_70%,transparent)!important] transition-colors"
|
|
695
726
|
/>
|
|
696
727
|
</div>
|
|
697
728
|
{/if}
|
|
@@ -699,7 +730,7 @@
|
|
|
699
730
|
<!-- Custom scrollbar thumb -->
|
|
700
731
|
{#if showScrollbar()}
|
|
701
732
|
<div
|
|
702
|
-
class="absolute right-1 [background-color:var(--color-secondary)] rounded cursor-pointer hover:[background-color:color-mix(in_srgb,var(--color-secondary)_90%,black)] transition-colors z-10"
|
|
733
|
+
class="absolute right-1 [background-color:var(--ui-color-secondary)] rounded cursor-pointer hover:[background-color:color-mix(in_srgb,var(--ui-color-secondary)_90%,black)] transition-colors z-10"
|
|
703
734
|
style="top: {thumbTop + 4}px; height: {thumbHeight}px; width: 6px;"
|
|
704
735
|
onmousedown={handleThumbMouseDown}
|
|
705
736
|
role="scrollbar"
|
|
@@ -709,7 +740,7 @@
|
|
|
709
740
|
? Math.round(
|
|
710
741
|
(textareaElement.scrollTop /
|
|
711
742
|
(textareaElement.scrollHeight - textareaElement.clientHeight)) *
|
|
712
|
-
100
|
|
743
|
+
100,
|
|
713
744
|
)
|
|
714
745
|
: 0}
|
|
715
746
|
aria-valuemin="0"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { HTMLTextareaAttributes } from "svelte/elements";
|
|
2
|
+
import type { Size } from "../types/sizes.js";
|
|
3
|
+
import { type Responsive } from "../types/responsive.js";
|
|
4
|
+
import type { Variant } from "../types/variants.js";
|
|
5
|
+
type TextAreaCopy = "none" | "selection" | "button";
|
|
6
|
+
interface Props extends Omit<HTMLTextareaAttributes, "oncopy"> {
|
|
7
|
+
value?: string;
|
|
8
|
+
lines?: string[];
|
|
9
|
+
class?: string;
|
|
10
|
+
textareaClass?: string;
|
|
11
|
+
validate?: (value: string) => boolean | string;
|
|
12
|
+
valid?: boolean;
|
|
13
|
+
touched?: boolean;
|
|
14
|
+
element?: HTMLTextAreaElement;
|
|
15
|
+
minRows?: number;
|
|
16
|
+
maxRows?: number;
|
|
17
|
+
lineHeightRem?: number;
|
|
18
|
+
size?: Responsive<Size>;
|
|
19
|
+
/** Colour axis — tints the border + placeholder with the SAME calc as Input (token @ 30% at rest,
|
|
20
|
+
* 55% on hover/focus, mixed toward transparent). No `secondary`/`-strong` special case. */
|
|
21
|
+
variant?: Variant;
|
|
22
|
+
/** Drop the visible frame while keeping the 1px box + focus outline (border → transparent). */
|
|
23
|
+
borderless?: boolean;
|
|
24
|
+
required?: boolean;
|
|
25
|
+
/** How the text can be copied — one exclusive mode:
|
|
26
|
+
* `'selection'` (default) — copy via native text selection only, no button;
|
|
27
|
+
* `'button'` — also show the copy-to-clipboard button;
|
|
28
|
+
* `'none'` — copying disabled: no button and the text is not selectable.
|
|
29
|
+
* Orthogonal to `readonly`/`disabled` (all other behaviour unchanged). */
|
|
30
|
+
copy?: TextAreaCopy;
|
|
31
|
+
/** Fires after the copy button successfully writes to the clipboard, with the copied text.
|
|
32
|
+
* Lets a consumer confirm the copy (e.g. `oncopy={() => toast.success('Copied to clipboard')}`).
|
|
33
|
+
* Not called for native selection-copy, only the `copy="button"` action. */
|
|
34
|
+
oncopy?: (text: string) => void;
|
|
35
|
+
}
|
|
36
|
+
declare const TextArea: import("svelte").Component<Props, {}, "element" | "value" | "valid" | "touched" | "lines">;
|
|
37
|
+
type TextArea = ReturnType<typeof TextArea>;
|
|
38
|
+
export default TextArea;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Select from "./Select.svelte";
|
|
3
|
+
import type { Size } from "../types/sizes.js";
|
|
4
|
+
import { responsiveClasses, type Responsive } from "../types/responsive.js";
|
|
5
|
+
import type { Variant } from "../types/variants.js";
|
|
6
|
+
|
|
7
|
+
// Shared axes, forwarded to the underlying Select (and the label text tracks size).
|
|
8
|
+
let { size = "md", variant }: { size?: Responsive<Size>; variant?: Variant } = $props();
|
|
9
|
+
const labelText: Record<Size, string> = { sm: "text-xs", md: "text-sm", lg: "text-base" };
|
|
10
|
+
const labelClass = $derived(responsiveClasses(size, labelText));
|
|
11
|
+
|
|
12
|
+
const options = [
|
|
13
|
+
{ label: "Auto (System)", value: "auto" },
|
|
14
|
+
{ label: "Light", value: "light" },
|
|
15
|
+
{ label: "Dark", value: "dark" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const themeClasses = {
|
|
19
|
+
light: "theme-light",
|
|
20
|
+
dark: "theme-dark",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let selectedTheme = $state("auto");
|
|
24
|
+
|
|
25
|
+
// Load saved theme on mount
|
|
26
|
+
$effect(() => {
|
|
27
|
+
if (typeof window !== "undefined") {
|
|
28
|
+
const savedTheme = localStorage.getItem("svelte-ui-theme");
|
|
29
|
+
if (savedTheme && options.some((opt) => opt.value === savedTheme)) {
|
|
30
|
+
selectedTheme = savedTheme;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Apply theme to <html> (documentElement) — NOT <body>.
|
|
36
|
+
// The theme tokens (incl. --ui-color-hover/active) are defined at :root, and app.html's pre-hydration
|
|
37
|
+
// script forces the class onto <html>. If we applied it to <body> instead, <html> would stay on the
|
|
38
|
+
// system @media(prefers-color-scheme) path, so tokens that reference other tokens (--ui-color-hover =
|
|
39
|
+
// color-mix(var(--ui-color-text)…)) would resolve against the WRONG theme at :root and inherit down
|
|
40
|
+
// stale (e.g. a white hover-tint on a light page → invisible hover). Keep the class on :root.
|
|
41
|
+
function applyTheme(themeValue: string) {
|
|
42
|
+
const body = document.body;
|
|
43
|
+
const html = document.documentElement;
|
|
44
|
+
|
|
45
|
+
// Remove all existing theme classes from both body and html (clear any stray body class too)
|
|
46
|
+
Object.values(themeClasses).forEach((className) => {
|
|
47
|
+
body.classList.remove(className);
|
|
48
|
+
html.classList.remove(className);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Add the selected theme class to <html>
|
|
52
|
+
const themeClass = themeClasses[themeValue as keyof typeof themeClasses];
|
|
53
|
+
if (themeClass) {
|
|
54
|
+
html.classList.add(themeClass);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Save theme to localStorage
|
|
58
|
+
if (typeof window !== "undefined") {
|
|
59
|
+
localStorage.setItem("svelte-ui-theme", themeValue);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Apply theme when selection changes
|
|
64
|
+
$effect(() => {
|
|
65
|
+
applyTheme(selectedTheme);
|
|
66
|
+
});
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<div class="flex items-center gap-2">
|
|
70
|
+
<label for="theme-selector" class="{labelClass} font-medium text-[var(--ui-color-text)]"
|
|
71
|
+
>Theme:</label
|
|
72
|
+
>
|
|
73
|
+
<Select
|
|
74
|
+
bind:value={selectedTheme}
|
|
75
|
+
{options}
|
|
76
|
+
{size}
|
|
77
|
+
{variant}
|
|
78
|
+
placeholder="Select theme..."
|
|
79
|
+
class="w-40"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Size } from "../types/sizes.js";
|
|
2
|
+
import { type Responsive } from "../types/responsive.js";
|
|
3
|
+
import type { Variant } from "../types/variants.js";
|
|
4
|
+
type $$ComponentProps = {
|
|
5
|
+
size?: Responsive<Size>;
|
|
6
|
+
variant?: Variant;
|
|
7
|
+
};
|
|
8
|
+
declare const ThemeSelector: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type ThemeSelector = ReturnType<typeof ThemeSelector>;
|
|
10
|
+
export default ThemeSelector;
|