@uibit/text-clamp 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 UIBit Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # Text Clamp
2
+
3
+ [Interactive Demonstration](https://rawlings.github.io/uibit/components/text-clamp)
4
+
5
+ Text Clamp constrains long, multi-line text blocks to a designated maximum line count. When the text exceeds the boundary, the component inserts an inline ellipsis and an interactive toggle (e.g. "More" / "Less") to expand or collapse the content dynamically without page navigation.
6
+
7
+ ## Value Delivery
8
+
9
+ - **Clean Dashboard Density** – Keeps user interface cards, card feeds, or list view items uniform in size by hiding excessive copy by default.
10
+ - **Accurate Recalculation** – Uses a ResizeObserver behind the scenes to recalculate text constraints automatically when viewport widths or container dimensions change.
11
+ - **Smooth Layout Integration** – Respects nested HTML templates, allowing styled inline text (bolding, links, lists) to be truncated correctly without breaking markup structure.
12
+
13
+ ## Ideal Applications
14
+
15
+ - **Review Feeds** – Showing customer ratings or feedback remarks where some responses are extremely long.
16
+ - **Blog & Article Previews** – Displaying uniform description snippets in card lists or search results.
17
+ - **Biographies & Descriptions** – Managing profiles or catalog details on structured page grids.
18
+
19
+ ## Further Information
20
+
21
+ Detailed design guidelines, customizable attributes, and integration examples are available on our documentation site.
22
+
23
+ ## Changelog
24
+
25
+ Please see the [Changelog](CHANGELOG.md) for version history.
@@ -0,0 +1,239 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "readme": "",
4
+ "modules": [
5
+ {
6
+ "kind": "javascript-module",
7
+ "path": "src/index.ts",
8
+ "declarations": [],
9
+ "exports": [
10
+ {
11
+ "kind": "js",
12
+ "name": "default",
13
+ "declaration": {
14
+ "name": "default",
15
+ "module": "./text-clamp"
16
+ }
17
+ },
18
+ {
19
+ "kind": "js",
20
+ "name": "TextClamp",
21
+ "declaration": {
22
+ "name": "TextClamp",
23
+ "module": "./text-clamp"
24
+ }
25
+ },
26
+ {
27
+ "kind": "js",
28
+ "name": "*",
29
+ "declaration": {
30
+ "name": "*",
31
+ "module": "src/types"
32
+ }
33
+ }
34
+ ]
35
+ },
36
+ {
37
+ "kind": "javascript-module",
38
+ "path": "src/styles.ts",
39
+ "declarations": [
40
+ {
41
+ "kind": "variable",
42
+ "name": "styles",
43
+ "default": "css` :host { display: block; font-size: var(--uibit-text-clamp-font-size, inherit); font-weight: var(--uibit-text-clamp-font-weight, inherit); color: var(--uibit-text-clamp-color, inherit); font-family: var(--uibit-text-clamp-font-family, inherit); line-height: var(--uibit-text-clamp-line-height, var(--uibit-line-height-normal, 1.5)); } /* Hide the source slot — it's only used for slotchange detection */ .source { display: none; } .content { overflow: hidden; } .content.clamped { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; } .toggle { display: inline; background: none; border: none; padding: 0; margin: 0; font: inherit; color: var(--uibit-text-clamp-toggle-color, currentColor); font-weight: var(--uibit-text-clamp-toggle-font-weight, var(--uibit-font-weight-semibold, 600)); cursor: pointer; text-decoration: var(--uibit-text-clamp-toggle-decoration, underline); text-underline-offset: 0.15em; } .toggle:focus-visible { outline: 0.125rem solid var(--uibit-text-clamp-focus-color, #000000); outline-offset: 0.125rem; border-radius: 0.125rem; } `"
44
+ }
45
+ ],
46
+ "exports": [
47
+ {
48
+ "kind": "js",
49
+ "name": "styles",
50
+ "declaration": {
51
+ "name": "styles",
52
+ "module": "src/styles.ts"
53
+ }
54
+ }
55
+ ]
56
+ },
57
+ {
58
+ "kind": "javascript-module",
59
+ "path": "src/text-clamp.ts",
60
+ "declarations": [
61
+ {
62
+ "kind": "class",
63
+ "description": "Clamps slotted content to an exact line count. When content overflows the\nclamped height a \"More\" toggle appears inline at the truncation point.\nClicking it expands the block; clicking \"Less\" collapses it again.\n\nUses a ResizeObserver to recalculate on every container-width change.",
64
+ "name": "TextClamp",
65
+ "cssProperties": [
66
+ {
67
+ "description": "Font size",
68
+ "name": "--uibit-text-clamp-font-size",
69
+ "default": "inherit"
70
+ },
71
+ {
72
+ "description": "Font weight",
73
+ "name": "--uibit-text-clamp-font-weight",
74
+ "default": "inherit"
75
+ },
76
+ {
77
+ "description": "Text color",
78
+ "name": "--uibit-text-clamp-color",
79
+ "default": "inherit"
80
+ },
81
+ {
82
+ "description": "Font family",
83
+ "name": "--uibit-text-clamp-font-family",
84
+ "default": "inherit"
85
+ },
86
+ {
87
+ "description": "Line height (unitless recommended)",
88
+ "name": "--uibit-text-clamp-line-height",
89
+ "default": "1.5"
90
+ },
91
+ {
92
+ "description": "Toggle button color",
93
+ "name": "--uibit-text-clamp-toggle-color",
94
+ "default": "currentColor"
95
+ },
96
+ {
97
+ "description": "Toggle button font weight",
98
+ "name": "--uibit-text-clamp-toggle-font-weight",
99
+ "default": "600"
100
+ },
101
+ {
102
+ "description": "Toggle button text decoration",
103
+ "name": "--uibit-text-clamp-toggle-decoration",
104
+ "default": "underline"
105
+ }
106
+ ],
107
+ "slots": [
108
+ {
109
+ "description": "The text content to clamp (HTML is preserved)",
110
+ "name": ""
111
+ }
112
+ ],
113
+ "members": [
114
+ {
115
+ "kind": "field",
116
+ "name": "lines",
117
+ "type": {
118
+ "text": "number"
119
+ },
120
+ "default": "3",
121
+ "description": "Maximum number of lines to show when collapsed.",
122
+ "attribute": "lines"
123
+ },
124
+ {
125
+ "kind": "field",
126
+ "name": "_expanded",
127
+ "type": {
128
+ "text": "boolean"
129
+ },
130
+ "privacy": "private",
131
+ "default": "false"
132
+ },
133
+ {
134
+ "kind": "field",
135
+ "name": "_overflows",
136
+ "type": {
137
+ "text": "boolean"
138
+ },
139
+ "privacy": "private",
140
+ "default": "false"
141
+ },
142
+ {
143
+ "kind": "field",
144
+ "name": "_contentEl",
145
+ "type": {
146
+ "text": "HTMLElement"
147
+ },
148
+ "privacy": "private"
149
+ },
150
+ {
151
+ "kind": "field",
152
+ "name": "_resize",
153
+ "privacy": "protected",
154
+ "default": "new ResizeController(this, { target: () => this._contentEl, callback: () => this._measure(), })"
155
+ },
156
+ {
157
+ "kind": "method",
158
+ "name": "_onSlotChange",
159
+ "privacy": "private"
160
+ },
161
+ {
162
+ "kind": "method",
163
+ "name": "_measure",
164
+ "privacy": "private"
165
+ },
166
+ {
167
+ "kind": "method",
168
+ "name": "_applyClamp",
169
+ "privacy": "private"
170
+ },
171
+ {
172
+ "kind": "method",
173
+ "name": "_toggle",
174
+ "privacy": "private"
175
+ }
176
+ ],
177
+ "events": [
178
+ {
179
+ "type": {
180
+ "text": "{ expanded: boolean }"
181
+ },
182
+ "description": "Fired when expanded/collapsed state changes",
183
+ "name": "toggle"
184
+ }
185
+ ],
186
+ "attributes": [
187
+ {
188
+ "name": "lines",
189
+ "type": {
190
+ "text": "number"
191
+ },
192
+ "default": "3",
193
+ "description": "Maximum number of lines to show when collapsed.",
194
+ "fieldName": "lines"
195
+ }
196
+ ],
197
+ "superclass": {
198
+ "name": "UIBitElement",
199
+ "package": "@uibit/core"
200
+ },
201
+ "tagName": "uibit-text-clamp",
202
+ "customElement": true
203
+ }
204
+ ],
205
+ "exports": [
206
+ {
207
+ "kind": "js",
208
+ "name": "TextClamp",
209
+ "declaration": {
210
+ "name": "TextClamp",
211
+ "module": "src/text-clamp.ts"
212
+ }
213
+ },
214
+ {
215
+ "kind": "custom-element-definition",
216
+ "name": "uibit-text-clamp",
217
+ "declaration": {
218
+ "name": "TextClamp",
219
+ "module": "src/text-clamp.ts"
220
+ }
221
+ },
222
+ {
223
+ "kind": "js",
224
+ "name": "default",
225
+ "declaration": {
226
+ "name": "TextClamp",
227
+ "module": "src/text-clamp.ts"
228
+ }
229
+ }
230
+ ]
231
+ },
232
+ {
233
+ "kind": "javascript-module",
234
+ "path": "src/types.ts",
235
+ "declarations": [],
236
+ "exports": []
237
+ }
238
+ ]
239
+ }
@@ -0,0 +1,32 @@
1
+ import { Component, ChangeDetectionStrategy, ElementRef, input, effect, output, numberAttribute } from '@angular/core';
2
+ import '@uibit/text-clamp';
3
+ import type { TextClamp as HTMLElementClass } from '@uibit/text-clamp';
4
+
5
+ @Component({
6
+ selector: 'uibit-text-clamp',
7
+ template: '<ng-content></ng-content>',
8
+ changeDetection: ChangeDetectionStrategy.OnPush,
9
+ standalone: true,
10
+ host: {
11
+ '(toggle)': 'toggle.emit($event)'
12
+ }
13
+ })
14
+ export class NgxTextClamp {
15
+ constructor(private el: ElementRef<HTMLElementClass>) {
16
+ effect(() => {
17
+ if (this.el.nativeElement) {
18
+ this.el.nativeElement.lines = this.lines();
19
+ }
20
+ });
21
+ effect(() => {
22
+ if (this.el.nativeElement) {
23
+ this.el.nativeElement._resize = this._resize();
24
+ }
25
+ });
26
+ }
27
+
28
+ readonly lines = input<number, any>(3, { transform: numberAttribute });
29
+ readonly _resize = input<any, any>(new ResizeController(this, { target: () => this._contentEl, callback: () => this._measure(), }));
30
+
31
+ readonly toggle = output<CustomEvent<{ expanded: boolean }>>();
32
+ }
@@ -0,0 +1,10 @@
1
+ import type { TextClamp as HTMLElementClass } from '@uibit/text-clamp';
2
+ import '@uibit/text-clamp';
3
+
4
+ declare global {
5
+ namespace astroHTML.JSX {
6
+ interface IntrinsicElements {
7
+ 'uibit-text-clamp': Partial<HTMLElementClass> & astroHTML.JSX.HTMLAttributes;
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,7 @@
1
+ import { defineNuxtPlugin } from '#app';
2
+
3
+ export default defineNuxtPlugin(() => {
4
+ if (process.client) {
5
+ import('@uibit/text-clamp');
6
+ }
7
+ });
@@ -0,0 +1,14 @@
1
+ import type { JSX } from 'preact';
2
+ import type { TextClamp as HTMLElementClass } from '@uibit/text-clamp';
3
+ import '@uibit/text-clamp';
4
+
5
+ declare module 'preact' {
6
+ namespace JSX {
7
+ interface IntrinsicElements {
8
+ 'uibit-text-clamp': JSX.HTMLAttributes<HTMLElementClass> & {
9
+ lines?: number;
10
+ _resize?: any;
11
+ };
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,10 @@
1
+ import { component$, Slot } from '@builder.io/qwik';
2
+ import '@uibit/text-clamp';
3
+
4
+ export const TextClamp = component$<any>((props) => {
5
+ return (
6
+ <uibit-text-clamp {...props}>
7
+ <Slot />
8
+ </uibit-text-clamp>
9
+ );
10
+ });
@@ -0,0 +1,19 @@
1
+ import type { HTMLAttributes, ClassAttributes } from 'react';
2
+ import type { TextClamp as HTMLElementClass } from '@uibit/text-clamp';
3
+ import '@uibit/text-clamp';
4
+
5
+ declare global {
6
+ namespace React {
7
+ namespace JSX {
8
+ interface IntrinsicElements {
9
+ 'uibit-text-clamp': ClassAttributes<HTMLElementClass> & HTMLAttributes<HTMLElementClass> & {
10
+ children?: React.ReactNode;
11
+ class?: string;
12
+ lines?: number;
13
+ _resize?: any;
14
+ onToggle?: (event: any) => void;
15
+ };
16
+ }
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,15 @@
1
+ import type { JSX } from 'solid-js';
2
+ import type { TextClamp as HTMLElementClass } from '@uibit/text-clamp';
3
+ import '@uibit/text-clamp';
4
+
5
+ declare module 'solid-js' {
6
+ namespace JSX {
7
+ interface IntrinsicElements {
8
+ 'uibit-text-clamp': Partial<HTMLElementClass> & JSX.HTMLAttributes<HTMLElementClass> & {
9
+ lines?: number;
10
+ _resize?: any;
11
+ "on:toggle"?: (event: CustomEvent) => void;
12
+ };
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,13 @@
1
+ import type { TextClamp as HTMLElementClass } from '@uibit/text-clamp';
2
+ import '@uibit/text-clamp';
3
+
4
+ declare module '@stencil/core' {
5
+ export namespace JSX {
6
+ interface IntrinsicElements {
7
+ 'uibit-text-clamp': HTMLElementClass & {
8
+ lines?: number;
9
+ _resize?: any;
10
+ };
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import '@uibit/text-clamp';
3
+ import type { TextClamp as HTMLElementClass } from '@uibit/text-clamp';
4
+
5
+ let {
6
+ lines = undefined,
7
+ _resize = undefined,
8
+ children
9
+ } = $props<{
10
+ children?: any;
11
+ lines?: number;
12
+ _resize?: any;
13
+
14
+ }>();
15
+
16
+ let elementRef: HTMLElementClass | null = $state(null);
17
+
18
+ $effect(() => {
19
+ if (elementRef && lines !== undefined) {
20
+ elementRef.lines = lines;
21
+ }
22
+ if (elementRef && _resize !== undefined) {
23
+ elementRef._resize = _resize;
24
+ }
25
+ });
26
+
27
+ </script>
28
+
29
+ <uibit-text-clamp bind:this={elementRef} {...$$restProps}>
30
+
31
+ {#if children}
32
+ {@render children()}
33
+ {:else}
34
+ <slot />
35
+ {/if}
36
+ </uibit-text-clamp>
@@ -0,0 +1,25 @@
1
+ import { defineComponent, h } from 'vue';
2
+ import type { TextClamp as HTMLElementClass } from '@uibit/text-clamp';
3
+ import '@uibit/text-clamp';
4
+
5
+ export const TextClamp = defineComponent({
6
+ name: 'TextClamp',
7
+ props: {
8
+ lines: { type: [String, Number, Boolean, Array, Object] as any },
9
+ _resize: { type: [String, Number, Boolean, Array, Object] as any }
10
+ },
11
+ emits: ['toggle'],
12
+ setup(props, { slots, emit }) {
13
+ return () => {
14
+ const eventListeners: Record<string, any> = {};
15
+ eventListeners['onToggle'] = (event: Event) => {
16
+ emit('toggle', event);
17
+ };
18
+
19
+ return h('uibit-text-clamp', {
20
+ ...props,
21
+ ...eventListeners
22
+ }, slots.default?.());
23
+ };
24
+ }
25
+ });
@@ -0,0 +1,3 @@
1
+ export { default, TextClamp } from './text-clamp';
2
+ export * from './types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAClD,cAAc,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { default, TextClamp } from './text-clamp';
2
+ export * from './types';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAClD,cAAc,SAAS,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const styles: import("lit").CSSResult;
2
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM,yBA4ClB,CAAC"}
package/dist/styles.js ADDED
@@ -0,0 +1,47 @@
1
+ import { css } from 'lit';
2
+ export const styles = css `
3
+ :host {
4
+ display: block;
5
+ font-size: var(--uibit-text-clamp-font-size, inherit);
6
+ font-weight: var(--uibit-text-clamp-font-weight, inherit);
7
+ color: var(--uibit-text-clamp-color, inherit);
8
+ font-family: var(--uibit-text-clamp-font-family, inherit);
9
+ line-height: var(--uibit-text-clamp-line-height, var(--uibit-line-height-normal, 1.5));
10
+ }
11
+
12
+ /* Hide the source slot — it's only used for slotchange detection */
13
+ .source {
14
+ display: none;
15
+ }
16
+
17
+ .content {
18
+ overflow: hidden;
19
+ }
20
+
21
+ .content.clamped {
22
+ display: -webkit-box;
23
+ -webkit-box-orient: vertical;
24
+ overflow: hidden;
25
+ }
26
+
27
+ .toggle {
28
+ display: inline;
29
+ background: none;
30
+ border: none;
31
+ padding: 0;
32
+ margin: 0;
33
+ font: inherit;
34
+ color: var(--uibit-text-clamp-toggle-color, currentColor);
35
+ font-weight: var(--uibit-text-clamp-toggle-font-weight, var(--uibit-font-weight-semibold, 600));
36
+ cursor: pointer;
37
+ text-decoration: var(--uibit-text-clamp-toggle-decoration, underline);
38
+ text-underline-offset: 0.15em;
39
+ }
40
+
41
+ .toggle:focus-visible {
42
+ outline: 0.125rem solid var(--uibit-text-clamp-focus-color, #000000);
43
+ outline-offset: 0.125rem;
44
+ border-radius: 0.125rem;
45
+ }
46
+ `;
47
+ //# sourceMappingURL=styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.js","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B,MAAM,CAAC,MAAM,MAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4CxB,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { UIBitElement, ResizeController } from '@uibit/core';
2
+ /**
3
+ * Clamps slotted content to an exact line count. When content overflows the
4
+ * clamped height a "More" toggle appears inline at the truncation point.
5
+ * Clicking it expands the block; clicking "Less" collapses it again.
6
+ *
7
+ * Uses a ResizeObserver to recalculate on every container-width change.
8
+ *
9
+ * @slot - The text content to clamp (HTML is preserved)
10
+ *
11
+ * @fires {{ expanded: boolean }} toggle - Fired when expanded/collapsed state changes
12
+ *
13
+ * @cssprop [--uibit-text-clamp-font-size=inherit] - Font size
14
+ * @cssprop [--uibit-text-clamp-font-weight=inherit] - Font weight
15
+ * @cssprop [--uibit-text-clamp-color=inherit] - Text color
16
+ * @cssprop [--uibit-text-clamp-font-family=inherit] - Font family
17
+ * @cssprop [--uibit-text-clamp-line-height=1.5] - Line height (unitless recommended)
18
+ * @cssprop [--uibit-text-clamp-toggle-color=currentColor] - Toggle button color
19
+ * @cssprop [--uibit-text-clamp-toggle-font-weight=600] - Toggle button font weight
20
+ * @cssprop [--uibit-text-clamp-toggle-decoration=underline] - Toggle button text decoration
21
+ */
22
+ export declare class TextClamp extends UIBitElement {
23
+ static styles: import("lit").CSSResult;
24
+ /** Maximum number of lines to show when collapsed. */
25
+ lines: number;
26
+ private _expanded;
27
+ private _overflows;
28
+ private _contentEl;
29
+ protected _resize: ResizeController;
30
+ updated(changed: Map<string, unknown>): void;
31
+ private _onSlotChange;
32
+ private _measure;
33
+ private _applyClamp;
34
+ private _toggle;
35
+ render(): import("lit").TemplateResult<1>;
36
+ }
37
+ export default TextClamp;
38
+ //# sourceMappingURL=text-clamp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-clamp.d.ts","sourceRoot":"","sources":["../src/text-clamp.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAI5E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBACa,SAAU,SAAQ,YAAY;IACzC,MAAM,CAAC,MAAM,0BAAU;IAEvB,sDAAsD;IAC1B,KAAK,SAAK;IAE7B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;IAEjB,OAAO,CAAC,UAAU,CAAe;IAEpD,SAAS,CAAC,OAAO,mBAGd;IAEH,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,QAGpC;IAED,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,QAAQ;IA4BhB,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,OAAO;IAKf,MAAM,oCA2BL;CACF;eAEc,SAAS"}
@@ -0,0 +1,138 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { html } from 'lit';
8
+ import { customElement, UIBitElement, ResizeController } from '@uibit/core';
9
+ import { property, query, state } from 'lit/decorators.js';
10
+ import { styles } from './styles';
11
+ /**
12
+ * Clamps slotted content to an exact line count. When content overflows the
13
+ * clamped height a "More" toggle appears inline at the truncation point.
14
+ * Clicking it expands the block; clicking "Less" collapses it again.
15
+ *
16
+ * Uses a ResizeObserver to recalculate on every container-width change.
17
+ *
18
+ * @slot - The text content to clamp (HTML is preserved)
19
+ *
20
+ * @fires {{ expanded: boolean }} toggle - Fired when expanded/collapsed state changes
21
+ *
22
+ * @cssprop [--uibit-text-clamp-font-size=inherit] - Font size
23
+ * @cssprop [--uibit-text-clamp-font-weight=inherit] - Font weight
24
+ * @cssprop [--uibit-text-clamp-color=inherit] - Text color
25
+ * @cssprop [--uibit-text-clamp-font-family=inherit] - Font family
26
+ * @cssprop [--uibit-text-clamp-line-height=1.5] - Line height (unitless recommended)
27
+ * @cssprop [--uibit-text-clamp-toggle-color=currentColor] - Toggle button color
28
+ * @cssprop [--uibit-text-clamp-toggle-font-weight=600] - Toggle button font weight
29
+ * @cssprop [--uibit-text-clamp-toggle-decoration=underline] - Toggle button text decoration
30
+ */
31
+ let TextClamp = class TextClamp extends UIBitElement {
32
+ constructor() {
33
+ super(...arguments);
34
+ /** Maximum number of lines to show when collapsed. */
35
+ this.lines = 3;
36
+ this._expanded = false;
37
+ this._overflows = false;
38
+ this._resize = new ResizeController(this, {
39
+ target: () => this._contentEl,
40
+ callback: () => this._measure(),
41
+ });
42
+ }
43
+ static { this.styles = styles; }
44
+ updated(changed) {
45
+ if (changed.has('lines'))
46
+ this._measure();
47
+ if (changed.has('_expanded'))
48
+ this._applyClamp();
49
+ }
50
+ _onSlotChange() {
51
+ this._measure();
52
+ }
53
+ _measure() {
54
+ const el = this._contentEl;
55
+ if (!el)
56
+ return;
57
+ // Unconstrain to get natural scroll height
58
+ el.style.removeProperty('-webkit-line-clamp');
59
+ el.style.removeProperty('display');
60
+ el.style.removeProperty('-webkit-box-orient');
61
+ el.style.removeProperty('overflow');
62
+ el.classList.remove('clamped');
63
+ const fullH = el.scrollHeight;
64
+ // Apply clamp to measure clamped height
65
+ el.classList.add('clamped');
66
+ el.style.webkitLineClamp = String(this.lines);
67
+ const clampedH = el.getBoundingClientRect().height;
68
+ const overflows = fullH > Math.round(clampedH) + 1;
69
+ this._overflows = overflows;
70
+ if (!overflows || this._expanded) {
71
+ el.classList.remove('clamped');
72
+ el.style.removeProperty('-webkit-line-clamp');
73
+ }
74
+ }
75
+ _applyClamp() {
76
+ const el = this._contentEl;
77
+ if (!el)
78
+ return;
79
+ if (this._expanded) {
80
+ el.classList.remove('clamped');
81
+ el.style.removeProperty('-webkit-line-clamp');
82
+ }
83
+ else if (this._overflows) {
84
+ el.classList.add('clamped');
85
+ el.style.webkitLineClamp = String(this.lines);
86
+ }
87
+ }
88
+ _toggle() {
89
+ this._expanded = !this._expanded;
90
+ this.dispatchCustomEvent('toggle', { expanded: this._expanded });
91
+ }
92
+ render() {
93
+ return html `
94
+ <div class="content" part="content">
95
+ <slot @slotchange=${this._onSlotChange}></slot>
96
+ </div>
97
+ ${this._overflows ? html `
98
+ <div class="toggle-container" part="toggle-container">
99
+ ${this._expanded ? html `
100
+ <slot name="less" @click=${this._toggle}>
101
+ <button
102
+ class="toggle"
103
+ part="toggle"
104
+ aria-expanded="true"
105
+ >Less</button>
106
+ </slot>
107
+ ` : html `
108
+ <slot name="more" @click=${this._toggle}>
109
+ <button
110
+ class="toggle"
111
+ part="toggle"
112
+ aria-expanded="false"
113
+ >… More</button>
114
+ </slot>
115
+ `}
116
+ </div>
117
+ ` : ''}
118
+ `;
119
+ }
120
+ };
121
+ __decorate([
122
+ property({ type: Number })
123
+ ], TextClamp.prototype, "lines", void 0);
124
+ __decorate([
125
+ state()
126
+ ], TextClamp.prototype, "_expanded", void 0);
127
+ __decorate([
128
+ state()
129
+ ], TextClamp.prototype, "_overflows", void 0);
130
+ __decorate([
131
+ query('.content')
132
+ ], TextClamp.prototype, "_contentEl", void 0);
133
+ TextClamp = __decorate([
134
+ customElement('uibit-text-clamp')
135
+ ], TextClamp);
136
+ export { TextClamp };
137
+ export default TextClamp;
138
+ //# sourceMappingURL=text-clamp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-clamp.js","sourceRoot":"","sources":["../src/text-clamp.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC;;;;;;;;;;;;;;;;;;;GAmBG;AAEI,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,YAAY;IAApC;;QAGL,sDAAsD;QAC1B,UAAK,GAAG,CAAC,CAAC;QAErB,cAAS,GAAG,KAAK,CAAC;QAClB,eAAU,GAAG,KAAK,CAAC;QAI1B,YAAO,GAAG,IAAI,gBAAgB,CAAC,IAAI,EAAE;YAC7C,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU;YAC7B,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE;SAChC,CAAC,CAAC;IAoFL,CAAC;aAjGQ,WAAM,GAAG,MAAM,AAAT,CAAU;IAevB,OAAO,CAAC,OAA6B;QACnC,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC1C,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,IAAI,CAAC,WAAW,EAAE,CAAC;IACnD,CAAC;IAEO,aAAa;QACnB,IAAI,CAAC,QAAQ,EAAE,CAAC;IAClB,CAAC;IAEO,QAAQ;QACd,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC;QAC3B,IAAI,CAAC,EAAE;YAAE,OAAO;QAEhB,2CAA2C;QAC3C,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,oBAAoB,CAAC,CAAC;QAC9C,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;QACnC,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,oBAAoB,CAAC,CAAC;QAC9C,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;QACpC,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAE/B,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC;QAE9B,wCAAwC;QACxC,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC5B,EAAE,CAAC,KAAK,CAAC,eAAe,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE9C,MAAM,QAAQ,GAAG,EAAE,CAAC,qBAAqB,EAAE,CAAC,MAAM,CAAC;QACnD,MAAM,SAAS,GAAG,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAEnD,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAE5B,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACjC,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC/B,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,oBAAoB,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IAEO,WAAW;QACjB,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC;QAC3B,IAAI,CAAC,EAAE;YAAE,OAAO;QAChB,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC/B,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,oBAAoB,CAAC,CAAC;QAChD,CAAC;aAAM,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YAC3B,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAC5B,EAAE,CAAC,KAAK,CAAC,eAAe,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IAEO,OAAO;QACb,IAAI,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC;QACjC,IAAI,CAAC,mBAAmB,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;IACnE,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAA;;4BAEa,IAAI,CAAC,aAAa;;QAEtC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAA;;YAElB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAA;uCACM,IAAI,CAAC,OAAO;;;;;;;WAOxC,CAAC,CAAC,CAAC,IAAI,CAAA;uCACqB,IAAI,CAAC,OAAO;;;;;;;WAOxC;;OAEJ,CAAC,CAAC,CAAC,EAAE;KACP,CAAC;IACJ,CAAC;CACF,CAAA;AA9F6B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;wCAAW;AAErB;IAAhB,KAAK,EAAE;4CAA2B;AAClB;IAAhB,KAAK,EAAE;6CAA4B;AAET;IAA1B,KAAK,CAAC,UAAU,CAAC;6CAAkC;AATzC,SAAS;IADrB,aAAa,CAAC,kBAAkB,CAAC;GACrB,SAAS,CAkGrB;;AAED,eAAe,SAAS,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type TextClamp from './text-clamp';
2
+ declare global {
3
+ interface HTMLElementTagNameMap {
4
+ 'uibit-text-clamp': TextClamp;
5
+ }
6
+ }
7
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,cAAc,CAAC;AAE1C,OAAO,CAAC,MAAM,CAAC;IACf,UAAU,qBAAqB;QAC3B,kBAAkB,EAAE,SAAS,CAAC;KACjC;CACA"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "@uibit/text-clamp",
3
+ "version": "0.1.0",
4
+ "description": "Clamps slotted text to an exact line count via a lines attribute. When content overflows, a More/Less toggle appears inline. A ResizeObserver recalculates overflow on every viewport resize so the clamp is always mathematically correct.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./custom-elements.json": "./custom-elements.json",
14
+ "./react": "./dist/frameworks/react/index.d.ts",
15
+ "./vue": "./dist/frameworks/vue/index.ts",
16
+ "./svelte": "./dist/frameworks/svelte/index.svelte",
17
+ "./angular": "./dist/frameworks/angular/index.ts",
18
+ "./solid": "./dist/frameworks/solid/index.d.ts",
19
+ "./astro": "./dist/frameworks/astro/index.astro",
20
+ "./qwik": "./dist/frameworks/qwik/index.tsx",
21
+ "./nuxt": "./dist/frameworks/nuxt/index.ts",
22
+ "./preact": "./dist/frameworks/preact/index.d.ts",
23
+ "./stencil": "./dist/frameworks/stencil/index.d.ts"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "package.json",
28
+ "README.md",
29
+ "LICENSE",
30
+ "custom-elements.json"
31
+ ],
32
+ "keywords": [
33
+ "clamp",
34
+ "read-more",
35
+ "truncate",
36
+ "text",
37
+ "web-component",
38
+ "lit"
39
+ ],
40
+ "author": "Jonathan Rawlings",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/Rawlings/uibit",
45
+ "directory": "packages/components/text-clamp"
46
+ },
47
+ "dependencies": {
48
+ "@uibit/core": "0.1.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^20.19.43",
52
+ "lit": "^3.3.3",
53
+ "typescript": "7.0.2",
54
+ "@uibit/codegen": "0.1.0"
55
+ },
56
+ "peerDependencies": {
57
+ "lit": "^3.0.0",
58
+ "react": ">=18",
59
+ "vue": ">=3",
60
+ "svelte": ">=4 || ^5",
61
+ "@angular/core": ">=14"
62
+ },
63
+ "customElements": "custom-elements.json",
64
+ "peerDependenciesMeta": {
65
+ "react": {
66
+ "optional": true
67
+ },
68
+ "vue": {
69
+ "optional": true
70
+ },
71
+ "svelte": {
72
+ "optional": true
73
+ },
74
+ "@angular/core": {
75
+ "optional": true
76
+ }
77
+ },
78
+ "uibit": {
79
+ "id": "text-clamp",
80
+ "title": "Text Clamp",
81
+ "category": "Text",
82
+ "tagName": "uibit-text-clamp"
83
+ },
84
+ "scripts": {
85
+ "build": "cem analyze --globs 'src/**/*.ts' --litelement && uibit-codegen --package . && tsc",
86
+ "dev": "concurrently \"cem analyze --globs 'src/**/*.ts' --litelement --watch\" \"tsc --watch\"",
87
+ "analyze": "cem analyze --globs 'src/**/*.ts' --litelement",
88
+ "typecheck": "tsc --noEmit"
89
+ }
90
+ }