@uibit/read-time 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) 2025 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,22 @@
1
+ # Text Read Timer
2
+
3
+ [Interactive Demonstration](https://rawlings.github.io/uibit/text-read-timer)
4
+
5
+ Text Read Timer automatically analyzes document length and displays a reading time estimate to users. By calculating read times directly on the client, it helps readers set expectations before committing to long-form articles, improving retention rates and page loyalty.
6
+
7
+ ## Value Delivery
8
+
9
+ - **Improved Engagement** – Transparent reading estimates give readers context, reducing bounce rates on detailed pages.
10
+ - **Client-Side Calculation** – Counts words and estimates reading durations in real-time on the client side, eliminating backend plugins or metadata fields.
11
+ - **Flexible Integration** – Renders dynamically as a badge, with support for custom slotting so that the timer can be positioned anywhere within article bylines or headers.
12
+ - **Adjustable Parameters** – Supports configurable words-per-minute calibrations to accommodate different audiences and languages.
13
+
14
+ ## Ideal Applications
15
+
16
+ - **Editorial Platforms & Blogs** – Displaying read times in article summaries, headers, and indexes.
17
+ - **Documentation Sites** – Providing guides and tutorials with clear completion timeframes.
18
+ - **Knowledge Bases** – Helping users locate quick-read answers or detailed guides.
19
+
20
+ ## Further Information
21
+
22
+ Detailed design guidelines, customizable attributes, and integration examples are available on our documentation site.
@@ -0,0 +1,244 @@
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": "./read-time"
16
+ }
17
+ },
18
+ {
19
+ "kind": "js",
20
+ "name": "ReadTime",
21
+ "declaration": {
22
+ "name": "ReadTime",
23
+ "module": "./read-time"
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/read-time.ts",
39
+ "declarations": [
40
+ {
41
+ "kind": "class",
42
+ "description": "Analyzes the word count of its slotted content and renders a reading-time\nbadge. The calculation happens inside the shadow DOM via a slot\nchange observer — no backend metadata required.\n\nPlace your HTML content in the default slot. The component counts only text\nnodes, ignoring markup and hidden elements.",
43
+ "name": "ReadTime",
44
+ "cssProperties": [
45
+ {
46
+ "description": "Badge font size",
47
+ "name": "--uibit-read-time-font-size",
48
+ "default": "0.8125rem"
49
+ },
50
+ {
51
+ "description": "Badge font weight",
52
+ "name": "--uibit-read-time-font-weight",
53
+ "default": "500"
54
+ },
55
+ {
56
+ "description": "Badge text color",
57
+ "name": "--uibit-read-time-color",
58
+ "default": "#6b7280"
59
+ },
60
+ {
61
+ "description": "Badge font family",
62
+ "name": "--uibit-read-time-font-family",
63
+ "default": "inherit"
64
+ },
65
+ {
66
+ "description": "Gap between icon and label",
67
+ "name": "--uibit-read-time-gap",
68
+ "default": "0.375rem"
69
+ },
70
+ {
71
+ "description": "Size of the clock icon",
72
+ "name": "--uibit-read-time-icon-size",
73
+ "default": "0.875rem"
74
+ },
75
+ {
76
+ "description": "Color of the clock icon",
77
+ "name": "--uibit-read-time-icon-color",
78
+ "default": "currentColor"
79
+ }
80
+ ],
81
+ "slots": [
82
+ {
83
+ "description": "The content whose text will be measured",
84
+ "name": ""
85
+ },
86
+ {
87
+ "description": "Optional named slot to position or override the read-time label/badge",
88
+ "name": "timer"
89
+ }
90
+ ],
91
+ "members": [
92
+ {
93
+ "kind": "field",
94
+ "name": "wpm",
95
+ "type": {
96
+ "text": "number"
97
+ },
98
+ "default": "238",
99
+ "description": "Average reading speed in words per minute.",
100
+ "attribute": "wpm"
101
+ },
102
+ {
103
+ "kind": "field",
104
+ "name": "showIcon",
105
+ "type": {
106
+ "text": "boolean"
107
+ },
108
+ "default": "true",
109
+ "description": "Show the clock icon before the label.",
110
+ "attribute": "show-icon"
111
+ },
112
+ {
113
+ "kind": "field",
114
+ "name": "_label",
115
+ "type": {
116
+ "text": "string"
117
+ },
118
+ "privacy": "private",
119
+ "default": "''"
120
+ },
121
+ {
122
+ "kind": "field",
123
+ "name": "_slot",
124
+ "type": {
125
+ "text": "HTMLSlotElement | undefined"
126
+ },
127
+ "privacy": "private"
128
+ },
129
+ {
130
+ "kind": "field",
131
+ "name": "_timerSlot",
132
+ "type": {
133
+ "text": "HTMLSlotElement | undefined"
134
+ },
135
+ "privacy": "private"
136
+ },
137
+ {
138
+ "kind": "method",
139
+ "name": "_calculate",
140
+ "privacy": "private"
141
+ },
142
+ {
143
+ "kind": "method",
144
+ "name": "_extractText",
145
+ "privacy": "private",
146
+ "return": {
147
+ "type": {
148
+ "text": "string"
149
+ }
150
+ },
151
+ "parameters": [
152
+ {
153
+ "name": "node",
154
+ "type": {
155
+ "text": "Node"
156
+ }
157
+ }
158
+ ]
159
+ }
160
+ ],
161
+ "attributes": [
162
+ {
163
+ "name": "wpm",
164
+ "type": {
165
+ "text": "number"
166
+ },
167
+ "default": "238",
168
+ "description": "Average reading speed in words per minute.",
169
+ "fieldName": "wpm"
170
+ },
171
+ {
172
+ "name": "show-icon",
173
+ "type": {
174
+ "text": "boolean"
175
+ },
176
+ "default": "true",
177
+ "description": "Show the clock icon before the label.",
178
+ "fieldName": "showIcon"
179
+ }
180
+ ],
181
+ "superclass": {
182
+ "name": "UIBitElement",
183
+ "package": "@uibit/core"
184
+ },
185
+ "tagName": "uibit-read-time",
186
+ "customElement": true
187
+ }
188
+ ],
189
+ "exports": [
190
+ {
191
+ "kind": "js",
192
+ "name": "ReadTime",
193
+ "declaration": {
194
+ "name": "ReadTime",
195
+ "module": "src/read-time.ts"
196
+ }
197
+ },
198
+ {
199
+ "kind": "custom-element-definition",
200
+ "name": "uibit-read-time",
201
+ "declaration": {
202
+ "name": "ReadTime",
203
+ "module": "src/read-time.ts"
204
+ }
205
+ },
206
+ {
207
+ "kind": "js",
208
+ "name": "default",
209
+ "declaration": {
210
+ "name": "ReadTime",
211
+ "module": "src/read-time.ts"
212
+ }
213
+ }
214
+ ]
215
+ },
216
+ {
217
+ "kind": "javascript-module",
218
+ "path": "src/styles.ts",
219
+ "declarations": [
220
+ {
221
+ "kind": "variable",
222
+ "name": "styles",
223
+ "default": "css` :host { display: block; font-family: var(--uibit-read-time-font-family, inherit); } .timer { display: inline-flex; align-items: center; gap: var(--uibit-read-time-gap, 0.375rem); font-size: var(--uibit-read-time-font-size, var(--uibit-font-size-sm, 0.8125rem)); font-weight: var(--uibit-read-time-font-weight, var(--uibit-font-weight-medium, 500)); color: var(--uibit-read-time-color, #6b7280); line-height: var(--uibit-line-height-normal, 1.5); } .icon { display: inline-flex; align-items: center; flex-shrink: 0; color: var(--uibit-read-time-icon-color, currentColor); width: var(--uibit-read-time-icon-size, 0.875rem); height: var(--uibit-read-time-icon-size, 0.875rem); } .icon svg { width: 100%; height: 100%; } .label { white-space: nowrap; } `"
224
+ }
225
+ ],
226
+ "exports": [
227
+ {
228
+ "kind": "js",
229
+ "name": "styles",
230
+ "declaration": {
231
+ "name": "styles",
232
+ "module": "src/styles.ts"
233
+ }
234
+ }
235
+ ]
236
+ },
237
+ {
238
+ "kind": "javascript-module",
239
+ "path": "src/types.ts",
240
+ "declarations": [],
241
+ "exports": []
242
+ }
243
+ ]
244
+ }
@@ -0,0 +1,27 @@
1
+ import { Component, ChangeDetectionStrategy, ElementRef, input, effect, booleanAttribute, numberAttribute } from '@angular/core';
2
+ import '@uibit/read-time';
3
+ import type { ReadTime as HTMLElementClass } from '@uibit/read-time';
4
+
5
+ @Component({
6
+ selector: 'uibit-read-time',
7
+ template: '<ng-content></ng-content>',
8
+ changeDetection: ChangeDetectionStrategy.OnPush,
9
+ standalone: true
10
+ })
11
+ export class NgxReadTime {
12
+ constructor(private el: ElementRef<HTMLElementClass>) {
13
+ effect(() => {
14
+ if (this.el.nativeElement) {
15
+ this.el.nativeElement.wpm = this.wpm();
16
+ }
17
+ });
18
+ effect(() => {
19
+ if (this.el.nativeElement) {
20
+ this.el.nativeElement.showIcon = this.showIcon();
21
+ }
22
+ });
23
+ }
24
+
25
+ readonly wpm = input<number, any>(238, { transform: numberAttribute });
26
+ readonly showIcon = input<boolean, any>(true, { transform: booleanAttribute });
27
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReadTime as HTMLElementClass } from '@uibit/read-time';
2
+ import '@uibit/read-time';
3
+
4
+ declare global {
5
+ namespace astroHTML.JSX {
6
+ interface IntrinsicElements {
7
+ 'uibit-read-time': 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/read-time');
6
+ }
7
+ });
@@ -0,0 +1,14 @@
1
+ import type { JSX } from 'preact';
2
+ import type { ReadTime as HTMLElementClass } from '@uibit/read-time';
3
+ import '@uibit/read-time';
4
+
5
+ declare module 'preact' {
6
+ namespace JSX {
7
+ interface IntrinsicElements {
8
+ 'uibit-read-time': JSX.HTMLAttributes<HTMLElementClass> & {
9
+ wpm?: number;
10
+ showIcon?: boolean;
11
+ };
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,10 @@
1
+ import { component$, Slot } from '@builder.io/qwik';
2
+ import '@uibit/read-time';
3
+
4
+ export const ReadTime = component$<any>((props) => {
5
+ return (
6
+ <uibit-read-time {...props}>
7
+ <Slot />
8
+ </uibit-read-time>
9
+ );
10
+ });
@@ -0,0 +1,18 @@
1
+ import type { HTMLAttributes, ClassAttributes } from 'react';
2
+ import type { ReadTime as HTMLElementClass } from '@uibit/read-time';
3
+ import '@uibit/read-time';
4
+
5
+ declare global {
6
+ namespace React {
7
+ namespace JSX {
8
+ interface IntrinsicElements {
9
+ 'uibit-read-time': ClassAttributes<HTMLElementClass> & HTMLAttributes<HTMLElementClass> & {
10
+ children?: React.ReactNode;
11
+ class?: string;
12
+ wpm?: number;
13
+ showIcon?: boolean;
14
+ };
15
+ }
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,15 @@
1
+ import type { JSX } from 'solid-js';
2
+ import type { ReadTime as HTMLElementClass } from '@uibit/read-time';
3
+ import '@uibit/read-time';
4
+
5
+ declare module 'solid-js' {
6
+ namespace JSX {
7
+ interface IntrinsicElements {
8
+ 'uibit-read-time': Partial<HTMLElementClass> & JSX.HTMLAttributes<HTMLElementClass> & {
9
+ wpm?: number;
10
+ showIcon?: boolean;
11
+
12
+ };
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,13 @@
1
+ import type { ReadTime as HTMLElementClass } from '@uibit/read-time';
2
+ import '@uibit/read-time';
3
+
4
+ declare module '@stencil/core' {
5
+ export namespace JSX {
6
+ interface IntrinsicElements {
7
+ 'uibit-read-time': HTMLElementClass & {
8
+ wpm?: number;
9
+ showIcon?: boolean;
10
+ };
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import '@uibit/read-time';
3
+ import type { ReadTime as HTMLElementClass } from '@uibit/read-time';
4
+
5
+ let {
6
+ wpm = undefined,
7
+ showIcon = undefined,
8
+ timer = undefined,
9
+ children
10
+ } = $props<{
11
+ children?: any;
12
+ wpm?: number;
13
+ showIcon?: boolean;
14
+ timer?: import('svelte').Snippet;
15
+ }>();
16
+
17
+ let elementRef: HTMLElementClass | null = $state(null);
18
+
19
+ $effect(() => {
20
+ if (elementRef && wpm !== undefined) {
21
+ elementRef.wpm = wpm;
22
+ }
23
+ if (elementRef && showIcon !== undefined) {
24
+ elementRef.showIcon = showIcon;
25
+ }
26
+ });
27
+
28
+ </script>
29
+
30
+ <uibit-read-time bind:this={elementRef} {...$$restProps}>
31
+ {#if timer}
32
+ <div slot="timer">
33
+ {@render timer()}
34
+ </div>
35
+ {/if}
36
+ {#if children}
37
+ {@render children()}
38
+ {:else}
39
+ <slot />
40
+ {/if}
41
+ </uibit-read-time>
@@ -0,0 +1,23 @@
1
+ import { defineComponent, h } from 'vue';
2
+ import type { ReadTime as HTMLElementClass } from '@uibit/read-time';
3
+ import '@uibit/read-time';
4
+
5
+ export const ReadTime = defineComponent({
6
+ name: 'ReadTime',
7
+ props: {
8
+ wpm: { type: [String, Number, Boolean, Array, Object] as any },
9
+ showIcon: { type: [String, Number, Boolean, Array, Object] as any }
10
+ },
11
+ emits: [],
12
+ setup(props, { slots, emit }) {
13
+ return () => {
14
+ const eventListeners: Record<string, any> = {};
15
+
16
+
17
+ return h('uibit-read-time', {
18
+ ...props,
19
+ ...eventListeners
20
+ }, slots.default?.());
21
+ };
22
+ }
23
+ });
@@ -0,0 +1,3 @@
1
+ export { default, ReadTime } from './read-time';
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,QAAQ,EAAE,MAAM,aAAa,CAAC;AAChD,cAAc,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { default, ReadTime } from './read-time';
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,QAAQ,EAAE,MAAM,aAAa,CAAC;AAChD,cAAc,SAAS,CAAC"}
@@ -0,0 +1,36 @@
1
+ import { UIBitElement } from '@uibit/core';
2
+ /**
3
+ * Analyzes the word count of its slotted content and renders a reading-time
4
+ * badge. The calculation happens inside the shadow DOM via a slot
5
+ * change observer — no backend metadata required.
6
+ *
7
+ * Place your HTML content in the default slot. The component counts only text
8
+ * nodes, ignoring markup and hidden elements.
9
+ *
10
+ * @slot - The content whose text will be measured
11
+ * @slot timer - Optional named slot to position or override the read-time label/badge
12
+ *
13
+ * @cssprop [--uibit-read-time-font-size=0.8125rem] - Badge font size
14
+ * @cssprop [--uibit-read-time-font-weight=500] - Badge font weight
15
+ * @cssprop [--uibit-read-time-color=#6b7280] - Badge text color
16
+ * @cssprop [--uibit-read-time-font-family=inherit] - Badge font family
17
+ * @cssprop [--uibit-read-time-gap=0.375rem] - Gap between icon and label
18
+ * @cssprop [--uibit-read-time-icon-size=0.875rem] - Size of the clock icon
19
+ * @cssprop [--uibit-read-time-icon-color=currentColor] - Color of the clock icon
20
+ */
21
+ export declare class ReadTime extends UIBitElement {
22
+ static styles: import("lit").CSSResult;
23
+ /** Average reading speed in words per minute. */
24
+ wpm: number;
25
+ /** Show the clock icon before the label. */
26
+ showIcon: boolean;
27
+ private _label;
28
+ private _slot?;
29
+ private _timerSlot?;
30
+ firstUpdated(): void;
31
+ private _calculate;
32
+ private _extractText;
33
+ render(): import("lit").TemplateResult<1>;
34
+ }
35
+ export default ReadTime;
36
+ //# sourceMappingURL=read-time.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-time.d.ts","sourceRoot":"","sources":["../src/read-time.ts"],"names":[],"mappings":"AACA,OAAO,EAAsC,YAAY,EAAY,MAAM,aAAa,CAAC;AAKzF;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,QAAS,SAAQ,YAAY;IACxC,MAAM,CAAC,MAAM,0BAAU;IAEvB,iDAAiD;IACH,GAAG,SAAO;IACxD,4CAA4C;IACS,QAAQ,UAAQ;IAE5D,OAAO,CAAC,MAAM,CAAM;IAE7B,OAAO,CAAC,KAAK,CAAC,CAAkB;IAChC,OAAO,CAAC,UAAU,CAAC,CAAkB;IAErC,YAAY,SAQX;IAED,OAAO,CAAC,UAAU;IA0ClB,OAAO,CAAC,YAAY;IAkBpB,MAAM,oCAUL;CACF;eAEc,QAAQ"}
@@ -0,0 +1,133 @@
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, fromLucide, getIcon, UIBitElement, msg, str } from '@uibit/core';
9
+ import { Clock } from 'lucide';
10
+ import { property, state } from 'lit/decorators.js';
11
+ import { styles } from './styles';
12
+ /**
13
+ * Analyzes the word count of its slotted content and renders a reading-time
14
+ * badge. The calculation happens inside the shadow DOM via a slot
15
+ * change observer — no backend metadata required.
16
+ *
17
+ * Place your HTML content in the default slot. The component counts only text
18
+ * nodes, ignoring markup and hidden elements.
19
+ *
20
+ * @slot - The content whose text will be measured
21
+ * @slot timer - Optional named slot to position or override the read-time label/badge
22
+ *
23
+ * @cssprop [--uibit-read-time-font-size=0.8125rem] - Badge font size
24
+ * @cssprop [--uibit-read-time-font-weight=500] - Badge font weight
25
+ * @cssprop [--uibit-read-time-color=#6b7280] - Badge text color
26
+ * @cssprop [--uibit-read-time-font-family=inherit] - Badge font family
27
+ * @cssprop [--uibit-read-time-gap=0.375rem] - Gap between icon and label
28
+ * @cssprop [--uibit-read-time-icon-size=0.875rem] - Size of the clock icon
29
+ * @cssprop [--uibit-read-time-icon-color=currentColor] - Color of the clock icon
30
+ */
31
+ let ReadTime = class ReadTime extends UIBitElement {
32
+ constructor() {
33
+ super(...arguments);
34
+ /** Average reading speed in words per minute. */
35
+ this.wpm = 238;
36
+ /** Show the clock icon before the label. */
37
+ this.showIcon = true;
38
+ this._label = '';
39
+ }
40
+ static { this.styles = styles; }
41
+ firstUpdated() {
42
+ this._slot = this.shadowRoot?.querySelector('slot:not([name])');
43
+ this._slot?.addEventListener('slotchange', () => this._calculate());
44
+ this._timerSlot = this.shadowRoot?.querySelector('slot[name="timer"]');
45
+ this._timerSlot?.addEventListener('slotchange', () => this._calculate());
46
+ this._calculate();
47
+ }
48
+ _calculate() {
49
+ if (!this._slot)
50
+ return;
51
+ const assigned = this._slot.assignedNodes({ flatten: true });
52
+ let text = '';
53
+ for (const node of assigned) {
54
+ text += this._extractText(node);
55
+ }
56
+ const words = text.trim().split(/\s+/).filter(Boolean).length;
57
+ const minutes = Math.ceil(words / this.wpm);
58
+ const timeStr = minutes < 1 ? msg('less than 1 min') : msg(str `${minutes} min`);
59
+ const defaultLabel = minutes < 1 ? msg('Less than 1 min read') : msg(str `${minutes} min read`);
60
+ this._label = defaultLabel;
61
+ // Update any slotted elements inside slot="timer"
62
+ if (this._timerSlot) {
63
+ const assignedTimer = this._timerSlot.assignedElements({ flatten: true });
64
+ for (const el of assignedTimer) {
65
+ let template = el.getAttribute('data-template');
66
+ if (template === null) {
67
+ const rawText = el.textContent || '';
68
+ if (rawText.includes('{time}')) {
69
+ template = rawText;
70
+ el.setAttribute('data-template', template);
71
+ }
72
+ else {
73
+ template = '';
74
+ el.setAttribute('data-template', '');
75
+ }
76
+ }
77
+ if (template) {
78
+ el.textContent = template.replace('{time}', timeStr);
79
+ }
80
+ else if (!el.textContent || el.textContent.trim() === '' || el.hasAttribute('data-auto-update')) {
81
+ el.textContent = this._label;
82
+ el.setAttribute('data-auto-update', '');
83
+ }
84
+ }
85
+ }
86
+ this.dispatchCustomEvent('read-time-change', { words, minutes });
87
+ }
88
+ _extractText(node) {
89
+ if (node.nodeType === Node.TEXT_NODE)
90
+ return node.textContent || '';
91
+ if (node.nodeType === Node.ELEMENT_NODE) {
92
+ const el = node;
93
+ // Skip the timer slot itself if it happens to be nested or queried
94
+ if (el.getAttribute('slot') === 'timer')
95
+ return '';
96
+ const style = window.getComputedStyle(el);
97
+ if (style.display === 'none' || style.visibility === 'hidden')
98
+ return '';
99
+ let t = '';
100
+ for (const child of Array.from(el.childNodes)) {
101
+ t += this._extractText(child);
102
+ }
103
+ return t;
104
+ }
105
+ return '';
106
+ }
107
+ render() {
108
+ return html `
109
+ <slot name="timer">
110
+ <div class="timer" part="timer">
111
+ ${this.showIcon ? html `<span class="icon" part="icon">${getIcon('clock', 14, fromLucide(Clock))}</span>` : ''}
112
+ <span class="label" part="label">${this._label}</span>
113
+ </div>
114
+ </slot>
115
+ <slot></slot>
116
+ `;
117
+ }
118
+ };
119
+ __decorate([
120
+ property({ type: Number, attribute: 'wpm' })
121
+ ], ReadTime.prototype, "wpm", void 0);
122
+ __decorate([
123
+ property({ type: Boolean, attribute: 'show-icon' })
124
+ ], ReadTime.prototype, "showIcon", void 0);
125
+ __decorate([
126
+ state()
127
+ ], ReadTime.prototype, "_label", void 0);
128
+ ReadTime = __decorate([
129
+ customElement('uibit-read-time')
130
+ ], ReadTime);
131
+ export { ReadTime };
132
+ export default ReadTime;
133
+ //# sourceMappingURL=read-time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-time.js","sourceRoot":"","sources":["../src/read-time.ts"],"names":[],"mappings":"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"}
@@ -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,yBAiClB,CAAC"}
package/dist/styles.js ADDED
@@ -0,0 +1,36 @@
1
+ import { css } from 'lit';
2
+ export const styles = css `
3
+ :host {
4
+ display: block;
5
+ font-family: var(--uibit-read-time-font-family, inherit);
6
+ }
7
+
8
+ .timer {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ gap: var(--uibit-read-time-gap, 0.375rem);
12
+ font-size: var(--uibit-read-time-font-size, var(--uibit-font-size-sm, 0.8125rem));
13
+ font-weight: var(--uibit-read-time-font-weight, var(--uibit-font-weight-medium, 500));
14
+ color: var(--uibit-read-time-color, #6b7280);
15
+ line-height: var(--uibit-line-height-normal, 1.5);
16
+ }
17
+
18
+ .icon {
19
+ display: inline-flex;
20
+ align-items: center;
21
+ flex-shrink: 0;
22
+ color: var(--uibit-read-time-icon-color, currentColor);
23
+ width: var(--uibit-read-time-icon-size, 0.875rem);
24
+ height: var(--uibit-read-time-icon-size, 0.875rem);
25
+ }
26
+
27
+ .icon svg {
28
+ width: 100%;
29
+ height: 100%;
30
+ }
31
+
32
+ .label {
33
+ white-space: nowrap;
34
+ }
35
+ `;
36
+ //# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCxB,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type ReadTime from './read-time';
2
+ declare global {
3
+ interface HTMLElementTagNameMap {
4
+ 'uibit-read-time': ReadTime;
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,QAAQ,MAAM,aAAa,CAAC;AAExC,OAAO,CAAC,MAAM,CAAC;IACf,UAAU,qBAAqB;QAC3B,iBAAiB,EAAE,QAAQ,CAAC;KAC/B;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,91 @@
1
+ {
2
+ "name": "@uibit/read-time",
3
+ "version": "0.1.0",
4
+ "description": "Text-analyzer badge that scans the word count of its slotted content and outputs an estimated reading time string. No backend metadata required — works directly in any CMS template.",
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
+ "reading-time",
34
+ "text-analysis",
35
+ "badge",
36
+ "cms",
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/read-time"
46
+ },
47
+ "dependencies": {
48
+ "lucide": "^1.24.0",
49
+ "@uibit/core": "0.1.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^20.19.43",
53
+ "lit": "^3.3.3",
54
+ "typescript": "7.0.2",
55
+ "@uibit/codegen": "0.1.0"
56
+ },
57
+ "peerDependencies": {
58
+ "lit": "^3.0.0",
59
+ "react": ">=18",
60
+ "vue": ">=3",
61
+ "svelte": ">=4 || ^5",
62
+ "@angular/core": ">=14"
63
+ },
64
+ "customElements": "custom-elements.json",
65
+ "peerDependenciesMeta": {
66
+ "react": {
67
+ "optional": true
68
+ },
69
+ "vue": {
70
+ "optional": true
71
+ },
72
+ "svelte": {
73
+ "optional": true
74
+ },
75
+ "@angular/core": {
76
+ "optional": true
77
+ }
78
+ },
79
+ "uibit": {
80
+ "id": "read-time",
81
+ "title": "Read Time",
82
+ "category": "Text",
83
+ "tagName": "uibit-read-time"
84
+ },
85
+ "scripts": {
86
+ "build": "cem analyze --globs 'src/**/*.ts' --litelement && uibit-codegen --package . && tsc",
87
+ "dev": "concurrently \"cem analyze --globs 'src/**/*.ts' --litelement --watch\" \"tsc --watch\"",
88
+ "analyze": "cem analyze --globs 'src/**/*.ts' --litelement",
89
+ "typecheck": "tsc --noEmit"
90
+ }
91
+ }