@marianmeres/stuic 2.34.0 → 2.38.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.
@@ -0,0 +1,128 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import { twMerge } from "../../utils/tw-merge.js";
4
+ import { tooltip } from "../../actions/index.js";
5
+ import { isPlainObject } from "../../utils/is-plain-object.js";
6
+ import { replaceMap } from "../../utils/replace-map.js";
7
+ import type { TranslateFn } from "../../types.js";
8
+
9
+ export interface Props {
10
+ /** Content to display */
11
+ children: Snippet;
12
+ /** Number of lines to show when collapsed (default: 1) */
13
+ lines?: number;
14
+ /** Expanded state (bindable) */
15
+ expanded?: boolean;
16
+ /** Collapsed indicator character (default: "↓") */
17
+ collapsedIndicator?: string;
18
+ /** Expanded indicator character (default: "↑") */
19
+ expandedIndicator?: string;
20
+ /** Container class */
21
+ class?: string;
22
+ /** Content wrapper class */
23
+ classContent?: string;
24
+ /** Toggle button class */
25
+ classToggle?: string;
26
+ /** Opacity class for toggle button (default: "opacity-70") */
27
+ toggleOpacity?: string;
28
+ /** Bind reference to container element */
29
+ el?: HTMLDivElement;
30
+ /** Optional translate function */
31
+ t?: TranslateFn;
32
+ }
33
+
34
+ // i18n ready
35
+ function t_default(
36
+ k: string,
37
+ values: false | null | undefined | Record<string, string | number> = null,
38
+ fallback: string | boolean = "",
39
+ i18nSpanWrap: boolean = true
40
+ ) {
41
+ const m: Record<string, string> = {
42
+ more: "Show more...",
43
+ less: "Show less...",
44
+ };
45
+ let out = m[k] ?? fallback ?? k;
46
+
47
+ return isPlainObject(values) ? replaceMap(out, values as any) : out;
48
+ }
49
+ </script>
50
+
51
+ <script lang="ts">
52
+ let {
53
+ children,
54
+ lines = 1,
55
+ expanded = $bindable(false),
56
+ collapsedIndicator = "↓",
57
+ expandedIndicator = "↑",
58
+ class: classProp,
59
+ classContent,
60
+ classToggle,
61
+ toggleOpacity = "opacity-75",
62
+ el = $bindable(),
63
+ t = t_default,
64
+ }: Props = $props();
65
+
66
+ let contentEl: HTMLDivElement | undefined;
67
+ let containerWidth = $state(0);
68
+ let needsCollapse = $state(false);
69
+
70
+ $effect(() => {
71
+ // Only measure when collapsed (line-clamp applied) to detect if truncation is needed
72
+ // containerWidth dependency ensures re-measurement on resize
73
+ if (contentEl && !expanded && containerWidth) {
74
+ needsCollapse = contentEl.scrollHeight > contentEl.clientHeight;
75
+ }
76
+ });
77
+
78
+ // normalize, range validation
79
+ let _lines = $derived.by(() => {
80
+ const l = Math.abs(lines);
81
+ return l > 10 ? 10 : l;
82
+ });
83
+ </script>
84
+
85
+ <div
86
+ bind:this={el}
87
+ bind:clientWidth={containerWidth}
88
+ class={twMerge("stuic-collapsible", classProp)}
89
+ >
90
+ <div class="flex items-end">
91
+ <div
92
+ bind:this={contentEl}
93
+ class={twMerge("flex-1", !expanded && `line-clamp-${_lines}`, classContent)}
94
+ >
95
+ {@render children()}
96
+ </div>
97
+ {#if needsCollapse}
98
+ <button
99
+ type="button"
100
+ class={twMerge(
101
+ toggleOpacity,
102
+ "hover:opacity-100 cursor-pointer px-2 py-1 -my-1 -mr-2",
103
+ classToggle
104
+ )}
105
+ onclick={() => (expanded = !expanded)}
106
+ use:tooltip={() => ({
107
+ content: expanded ? t("less") : t("more"),
108
+ })}
109
+ >
110
+ {expanded ? expandedIndicator : collapsedIndicator}
111
+ </button>
112
+ {/if}
113
+ </div>
114
+ </div>
115
+
116
+ <!--
117
+ DO NOT REMOVE: Food for TW compiler
118
+ line-clamp-1
119
+ line-clamp-2
120
+ line-clamp-3
121
+ line-clamp-4
122
+ line-clamp-5
123
+ line-clamp-6
124
+ line-clamp-7
125
+ line-clamp-8
126
+ line-clamp-9
127
+ line-clamp-10
128
+ -->
@@ -0,0 +1,29 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { TranslateFn } from "../../types.js";
3
+ export interface Props {
4
+ /** Content to display */
5
+ children: Snippet;
6
+ /** Number of lines to show when collapsed (default: 1) */
7
+ lines?: number;
8
+ /** Expanded state (bindable) */
9
+ expanded?: boolean;
10
+ /** Collapsed indicator character (default: "↓") */
11
+ collapsedIndicator?: string;
12
+ /** Expanded indicator character (default: "↑") */
13
+ expandedIndicator?: string;
14
+ /** Container class */
15
+ class?: string;
16
+ /** Content wrapper class */
17
+ classContent?: string;
18
+ /** Toggle button class */
19
+ classToggle?: string;
20
+ /** Opacity class for toggle button (default: "opacity-70") */
21
+ toggleOpacity?: string;
22
+ /** Bind reference to container element */
23
+ el?: HTMLDivElement;
24
+ /** Optional translate function */
25
+ t?: TranslateFn;
26
+ }
27
+ declare const Collapsible: import("svelte").Component<Props, {}, "el" | "expanded">;
28
+ type Collapsible = ReturnType<typeof Collapsible>;
29
+ export default Collapsible;
@@ -0,0 +1,81 @@
1
+ # Collapsible
2
+
3
+ A component that truncates content to a specified number of lines with an expand/collapse toggle. Automatically detects if truncation is needed and only shows the toggle when content overflows.
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ | -------------------- | --------------- | ------------- | ------------------------------------------------ |
9
+ | `children` | `Snippet` | - | Content to display |
10
+ | `lines` | `number` | `1` | Number of lines to show when collapsed |
11
+ | `expanded` | `boolean` | `false` | Expanded state (bindable) |
12
+ | `collapsedIndicator` | `string` | `"↓"` | Character/text shown when collapsed |
13
+ | `expandedIndicator` | `string` | `"↑"` | Character/text shown when expanded |
14
+ | `class` | `string` | - | Container element class |
15
+ | `classContent` | `string` | - | Content wrapper class |
16
+ | `classToggle` | `string` | - | Toggle button class |
17
+ | `toggleOpacity` | `string` | `"opacity-70"`| Opacity class for toggle button |
18
+ | `el` | `HTMLDivElement`| - | Bind reference to container element |
19
+
20
+ ## Usage
21
+
22
+ ### Basic Usage
23
+
24
+ ```svelte
25
+ <script>
26
+ import { Collapsible } from 'stuic';
27
+ </script>
28
+
29
+ <Collapsible>
30
+ This is a long text that will be truncated to one line with an ellipsis
31
+ and a toggle button to expand it.
32
+ </Collapsible>
33
+ ```
34
+
35
+ ### Multiple Lines
36
+
37
+ ```svelte
38
+ <Collapsible lines={3}>
39
+ This content will be truncated to 3 lines before showing the expand toggle.
40
+ Add more content here to see the effect.
41
+ </Collapsible>
42
+ ```
43
+
44
+ ### Custom Indicators
45
+
46
+ ```svelte
47
+ <Collapsible collapsedIndicator="▼" expandedIndicator="▲">
48
+ Content with custom expand/collapse indicators.
49
+ </Collapsible>
50
+ ```
51
+
52
+ ### Controlled State
53
+
54
+ ```svelte
55
+ <script>
56
+ import { Collapsible } from 'stuic';
57
+
58
+ let expanded = $state(false);
59
+ </script>
60
+
61
+ <button onclick={() => expanded = !expanded}>
62
+ Toggle externally
63
+ </button>
64
+
65
+ <Collapsible bind:expanded>
66
+ This collapsible can be controlled from outside.
67
+ </Collapsible>
68
+ ```
69
+
70
+ ### Custom Styling
71
+
72
+ ```svelte
73
+ <Collapsible
74
+ class="bg-gray-100 p-4 rounded"
75
+ classContent="text-sm text-gray-600"
76
+ classToggle="text-blue-500 font-bold"
77
+ toggleOpacity="opacity-100"
78
+ >
79
+ Styled collapsible content.
80
+ </Collapsible>
81
+ ```
@@ -0,0 +1 @@
1
+ export { default as Collapsible, type Props as CollapsibleProps, } from "./Collapsible.svelte";
@@ -0,0 +1 @@
1
+ export { default as Collapsible, } from "./Collapsible.svelte";
@@ -36,6 +36,7 @@
36
36
  import { getId } from "../../utils/get-id.js";
37
37
  import { twMerge } from "../../utils/tw-merge.js";
38
38
  import { Thc, isTHCNotEmpty } from "../Thc/index.js";
39
+ import { Collapsible } from "../Collapsible/index.js";
39
40
  type THC = import("../Thc/index.js").THC;
40
41
 
41
42
  let {
@@ -183,7 +184,21 @@
183
184
  </div>
184
185
  {/if}
185
186
  {#if description}
186
- <div
187
+ <Collapsible>
188
+ <div
189
+ id={idDesc}
190
+ class={twMerge(
191
+ "desc-box",
192
+ _classCommon,
193
+ "text-sm opacity-50 cursor-pointer font-normal",
194
+ disabled && "cursor-not-allowed",
195
+ classDescBox
196
+ )}
197
+ >
198
+ {@render snippetOrThc({ id, value: description })}
199
+ </div>
200
+ </Collapsible>
201
+ <!-- <div
187
202
  id={idDesc}
188
203
  class={twMerge(
189
204
  "desc-box",
@@ -194,7 +209,7 @@
194
209
  )}
195
210
  >
196
211
  {@render snippetOrThc({ id, value: description })}
197
- </div>
212
+ </div> -->
198
213
  {/if}
199
214
  </div>
200
215
  </label>
@@ -3,6 +3,7 @@
3
3
  import { slide } from "svelte/transition";
4
4
  import type { ValidationResult } from "../../../actions/validate.svelte.js";
5
5
  import { twMerge } from "../../../utils/tw-merge.js";
6
+ import { Collapsible } from "../../Collapsible/index.js";
6
7
  import Thc, { isTHCNotEmpty, type THC } from "../../Thc/Thc.svelte";
7
8
 
8
9
  type SnippetWithId = Snippet<[{ id: string }]>;
@@ -78,9 +79,6 @@
78
79
  let invalid = $derived(validation && !validation?.valid);
79
80
 
80
81
  let width = $state<number>(0);
81
- let descExpanded = $state(descriptionDefaultExpanded);
82
- let descContentEl: HTMLDivElement | undefined;
83
- let descNeedsCollapse = $state(false);
84
82
 
85
83
  $effect(() => {
86
84
  // a non-zero breakpoint has priority
@@ -89,14 +87,6 @@
89
87
  }
90
88
  });
91
89
 
92
- $effect(() => {
93
- // only measure when collapsed (line-clamp applied) to detect if truncation is needed
94
- // width dependency ensures re-measurement on resize
95
- if (descContentEl && descriptionCollapsible && !descExpanded && width) {
96
- descNeedsCollapse = descContentEl.scrollHeight > descContentEl.clientHeight;
97
- }
98
- });
99
-
100
90
  let _classCommon = $derived(
101
91
  [invalid && "invalid", disabled && "disabled", required && "required", size]
102
92
  .filter(Boolean)
@@ -232,26 +222,12 @@
232
222
  )}
233
223
  >
234
224
  {#if descriptionCollapsible}
235
- <div class="flex items-start">
236
- <div
237
- bind:this={descContentEl}
238
- class={twMerge("flex-1", !descExpanded && "line-clamp-1")}
239
- >
240
- {@render snippetOrThc({ id, value: description })}
241
- </div>
242
- {#if descNeedsCollapse}
243
- <button
244
- type="button"
245
- class={twMerge(
246
- "opacity-70 hover:opacity-100 cursor-pointer px-2 py-1 -my-1 -mr-2",
247
- classDescBoxToggle
248
- )}
249
- onclick={() => (descExpanded = !descExpanded)}
250
- >
251
- {descExpanded ? "↑" : "↓"}
252
- </button>
253
- {/if}
254
- </div>
225
+ <Collapsible
226
+ expanded={descriptionDefaultExpanded}
227
+ classToggle={classDescBoxToggle}
228
+ >
229
+ {@render snippetOrThc({ id, value: description })}
230
+ </Collapsible>
255
231
  {:else}
256
232
  {@render snippetOrThc({ id, value: description })}
257
233
  {/if}
package/dist/index.d.ts CHANGED
@@ -28,6 +28,7 @@ export * from "./components/AvatarInitials/index.js";
28
28
  export * from "./components/Backdrop/index.js";
29
29
  export * from "./components/Button/index.js";
30
30
  export * from "./components/ButtonGroupRadio/index.js";
31
+ export * from "./components/Collapsible/index.js";
31
32
  export * from "./components/ColorScheme/index.js";
32
33
  export * from "./components/CommandMenu/index.js";
33
34
  export * from "./components/DismissibleMessage/index.js";
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ export * from "./components/AvatarInitials/index.js";
29
29
  export * from "./components/Backdrop/index.js";
30
30
  export * from "./components/Button/index.js";
31
31
  export * from "./components/ButtonGroupRadio/index.js";
32
+ export * from "./components/Collapsible/index.js";
32
33
  export * from "./components/ColorScheme/index.js";
33
34
  export * from "./components/CommandMenu/index.js";
34
35
  export * from "./components/DismissibleMessage/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "2.34.0",
3
+ "version": "2.38.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",