@thl-ui/core 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -2
  3. package/dist/config.d.ts +132 -0
  4. package/dist/config.js +159 -0
  5. package/dist/contracts.d.ts +151 -0
  6. package/dist/contracts.js +1 -0
  7. package/dist/headless/calendar.d.ts +21 -0
  8. package/dist/headless/calendar.js +56 -0
  9. package/dist/headless/carousel.d.ts +17 -0
  10. package/dist/headless/carousel.js +39 -0
  11. package/dist/headless/disclosure.d.ts +22 -0
  12. package/dist/headless/disclosure.js +25 -0
  13. package/dist/headless/idle.d.ts +20 -0
  14. package/dist/headless/idle.js +57 -0
  15. package/dist/headless/number.d.ts +15 -0
  16. package/dist/headless/number.js +16 -0
  17. package/dist/headless/pagination.d.ts +23 -0
  18. package/dist/headless/pagination.js +32 -0
  19. package/dist/headless/select.d.ts +47 -0
  20. package/dist/headless/select.js +141 -0
  21. package/dist/headless/sidebar.d.ts +50 -0
  22. package/dist/headless/sidebar.js +120 -0
  23. package/dist/headless/steps.d.ts +16 -0
  24. package/dist/headless/steps.js +18 -0
  25. package/dist/headless/table.d.ts +80 -0
  26. package/dist/headless/table.js +133 -0
  27. package/dist/headless/tabs.d.ts +23 -0
  28. package/dist/headless/tabs.js +43 -0
  29. package/dist/headless/toast.d.ts +36 -0
  30. package/dist/headless/toast.js +59 -0
  31. package/dist/headless/upload.d.ts +52 -0
  32. package/dist/headless/upload.js +120 -0
  33. package/dist/headless/validation.d.ts +23 -0
  34. package/dist/headless/validation.js +31 -0
  35. package/dist/i18n.d.ts +35 -0
  36. package/dist/i18n.js +106 -0
  37. package/dist/index.d.ts +25 -0
  38. package/dist/index.js +23 -0
  39. package/dist/store.d.ts +8 -0
  40. package/dist/store.js +18 -0
  41. package/dist/utils/bem.d.ts +14 -0
  42. package/dist/utils/bem.js +37 -0
  43. package/dist/utils/cx.d.ts +3 -0
  44. package/dist/utils/cx.js +18 -0
  45. package/dist/utils/dom.d.ts +29 -0
  46. package/dist/utils/dom.js +146 -0
  47. package/package.json +44 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 thl-ui 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 CHANGED
@@ -1,3 +1,42 @@
1
- # Temporary Holding Version
1
+ # @thl-ui/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ thl-ui headless core: global config, i18n and framework-free state machines for UI components.
4
+
5
+ Part of [thl-ui](https://thl.tandd.dev): one UI kit for React, Vue and Angular with a headless TypeScript core, global config, 12 skins, dark mode and i18n (en, ja, vi, zh).
6
+
7
+ Framework-free TypeScript used by `@thl-ui/react`, `@thl-ui/vue` and `@thl-ui/angular`. You normally get it through one of those; install it directly to share one config between frameworks or to build your own adapter.
8
+
9
+ ```bash
10
+ npm i @thl-ui/core
11
+ ```
12
+
13
+ ```ts
14
+ import { createThl, createI18n } from '@thl-ui/core';
15
+
16
+ const thl = createThl({ locale: 'ja', theme: 'purple-dark', layout: 'horizontal', persist: true });
17
+ thl.t('common.save'); // 保存
18
+ thl.set({ colorMode: 'dark' });
19
+ ```
20
+
21
+ - **Config:** theme (12 skins), color mode, layout, size, radius, z-index and per-component defaults, optionally saved to `localStorage`.
22
+ - **i18n:** plural rules, fallback locale, lazy-loaded locales, `Intl` date and number formatting.
23
+ - **Headless state machines:** table, select, tabs, toast, upload, calendar, validation, pagination, carousel, sidebar.
24
+
25
+ ## Live demos
26
+
27
+ | Framework | Package | Live demo |
28
+ |---|---|---|
29
+ | React ≥ 18 | [`@thl-ui/react`](https://www.npmjs.com/package/@thl-ui/react) | [thl-react.tandd.dev](https://thl-react.tandd.dev) |
30
+ | Vue ≥ 3.5 | [`@thl-ui/vue`](https://www.npmjs.com/package/@thl-ui/vue) | [thl-vue.tandd.dev](https://thl-vue.tandd.dev) |
31
+ | Angular ≥ 20 | [`@thl-ui/angular`](https://www.npmjs.com/package/@thl-ui/angular) | [thl-angular.tandd.dev](https://thl-angular.tandd.dev) |
32
+
33
+ Each demo is the full 74-page admin template, built with the packages installed from npm. Open the ⚙ panel to switch skin, layout, dark mode and language.
34
+
35
+ ## Links
36
+
37
+ - Docs: [thl.tandd.dev/guide/config](https://thl.tandd.dev/guide/config) (English, 日本語, Tiếng Việt, 简体中文)
38
+ - Source and issues: [GitLab](https://gitlab.com/tandd.packages/thl-ui)
39
+
40
+ ## License
41
+
42
+ MIT
@@ -0,0 +1,132 @@
1
+ import { type Store } from './store.js';
2
+ import { type I18n } from './i18n.js';
3
+ import { type Messages } from '@thl-ui/locales';
4
+ import type { ThlSkin } from '@thl-ui/tokens';
5
+ export type ColorMode = 'light' | 'dark' | 'auto';
6
+ export type LayoutMode = 'vertical' | 'mini' | 'horizontal';
7
+ export type Size = 'sm' | 'md' | 'lg';
8
+ /** Per-component default props. Extend via declaration merging: `declare module '@thl-ui/core' { interface ThlComponentDefaults { MyComp: {...} } }` */
9
+ export interface ThlComponentDefaults {
10
+ Button: {
11
+ variant?: string;
12
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
13
+ rounded?: boolean;
14
+ outline?: boolean;
15
+ };
16
+ Card: {
17
+ bordered?: boolean;
18
+ shadow?: boolean;
19
+ };
20
+ Modal: {
21
+ size?: 'sm' | 'md' | 'lg';
22
+ closeOnBackdrop?: boolean;
23
+ closeOnEsc?: boolean;
24
+ centered?: boolean;
25
+ };
26
+ Toast: {
27
+ position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center';
28
+ duration?: number;
29
+ max?: number;
30
+ };
31
+ Table: {
32
+ striped?: boolean;
33
+ hover?: boolean;
34
+ bordered?: boolean;
35
+ pageSize?: number;
36
+ pageSizes?: number[];
37
+ };
38
+ Pagination: {
39
+ maxPages?: number;
40
+ showTotal?: boolean;
41
+ };
42
+ Input: {
43
+ size?: Size;
44
+ variant?: 'default' | 'material' | 'line';
45
+ };
46
+ Select: {
47
+ searchable?: boolean;
48
+ clearable?: boolean;
49
+ };
50
+ DatePicker: {
51
+ format?: Intl.DateTimeFormatOptions;
52
+ weekStartsOn?: 0 | 1;
53
+ };
54
+ Upload: {
55
+ maxSize?: number;
56
+ accept?: string;
57
+ multiple?: boolean;
58
+ };
59
+ Tooltip: {
60
+ placement?: string;
61
+ delay?: number;
62
+ };
63
+ Progress: {
64
+ size?: 'sm' | 'md' | 'lg';
65
+ striped?: boolean;
66
+ animated?: boolean;
67
+ };
68
+ Tabs: {
69
+ variant?: 'default' | 'underline' | 'filled' | 'pills' | 'vertical';
70
+ };
71
+ Sidebar: {
72
+ breakpoint?: number;
73
+ };
74
+ [component: string]: Record<string, unknown> | undefined;
75
+ }
76
+ export interface ThlConfig {
77
+ /** CSS class & custom property prefix. */
78
+ prefix: string;
79
+ theme: ThlSkin;
80
+ colorMode: ColorMode;
81
+ layout: LayoutMode;
82
+ fixedHeader: boolean;
83
+ fixedSidebar: boolean;
84
+ size: Size;
85
+ radius: 'sharp' | 'rounded';
86
+ /** Base z-index for overlays (dropdown = base, modal = base + 50, toast = base + 80). */
87
+ zIndex: number;
88
+ locale: string;
89
+ fallbackLocale: string;
90
+ /** Extra / override messages, keyed by locale. Merged deeply with built-ins. */
91
+ messages: Record<string, Messages>;
92
+ /** Locales offered in the language switcher. */
93
+ locales: string[];
94
+ components: Partial<ThlComponentDefaults>;
95
+ /** Persist theme/colorMode/locale/layout/fixed* to localStorage. */
96
+ persist: boolean | {
97
+ key: string;
98
+ };
99
+ /** Element receiving data-* attributes. Defaults to <html>. */
100
+ root?: HTMLElement | null;
101
+ }
102
+ export type ThlUserConfig = Partial<Omit<ThlConfig, 'components' | 'messages'>> & {
103
+ components?: Partial<ThlComponentDefaults>;
104
+ messages?: Record<string, Messages>;
105
+ loadLocale?: (locale: string) => Promise<Messages | {
106
+ default: Messages;
107
+ }>;
108
+ };
109
+ export declare const defaultConfig: ThlConfig;
110
+ /** Keys persisted between sessions when `persist` is on. */
111
+ export declare const PERSIST_KEYS: readonly ["theme", "colorMode", "locale", "layout", "fixedHeader", "fixedSidebar", "size", "radius"];
112
+ export interface ThlInstance {
113
+ config: Store<ThlConfig>;
114
+ i18n: I18n;
115
+ get(): ThlConfig;
116
+ set(patch: Partial<ThlConfig> | ((c: ThlConfig) => Partial<ThlConfig>)): void;
117
+ subscribe(fn: (c: ThlConfig, prev: ThlConfig) => void): () => void;
118
+ /** Merged defaults for a component: built-in < config.components[name] < props (undefined props ignored). */
119
+ defaults<K extends keyof ThlComponentDefaults>(name: K, props?: Partial<NonNullable<ThlComponentDefaults[K]>>): NonNullable<ThlComponentDefaults[K]>;
120
+ t: I18n['t'];
121
+ setLocale(locale: string): Promise<void>;
122
+ /** Resolved color mode ('auto' resolved against prefers-color-scheme). */
123
+ resolvedColorMode(): 'light' | 'dark';
124
+ /** Apply data attributes / lang / dir to the root element. Called automatically in browsers. */
125
+ applyToDocument(): void;
126
+ /** Re-attach listeners after destroy() (idempotent). */
127
+ attach(): void;
128
+ /** Stop listening (persistence, document sync, media queries). Reversible via attach(). */
129
+ destroy(): void;
130
+ }
131
+ /** Create the global thl-ui instance (config + i18n). One per app; adapters provide it via context/DI. */
132
+ export declare function createThl(user?: ThlUserConfig): ThlInstance;
package/dist/config.js ADDED
@@ -0,0 +1,159 @@
1
+ import { createStore } from './store.js';
2
+ import { createI18n, deepMerge } from './i18n.js';
3
+ import { en, ja, vi, zh, localeInfo } from '@thl-ui/locales';
4
+ import { isBrowser } from './utils/dom.js';
5
+ export const defaultConfig = {
6
+ prefix: 'thl',
7
+ theme: 'default',
8
+ colorMode: 'light',
9
+ layout: 'vertical',
10
+ fixedHeader: true,
11
+ fixedSidebar: true,
12
+ size: 'md',
13
+ radius: 'rounded',
14
+ zIndex: 1000,
15
+ locale: 'en',
16
+ fallbackLocale: 'en',
17
+ messages: {},
18
+ locales: ['en', 'ja', 'vi', 'zh'],
19
+ components: {
20
+ Toast: { position: 'top-right', duration: 3500, max: 5 },
21
+ Table: { pageSize: 10, pageSizes: [10, 25, 50, 100], hover: true },
22
+ Modal: { closeOnBackdrop: true, closeOnEsc: true },
23
+ Pagination: { maxPages: 7 },
24
+ Sidebar: { breakpoint: 1170 },
25
+ },
26
+ persist: false,
27
+ };
28
+ /** Keys persisted between sessions when `persist` is on. */
29
+ export const PERSIST_KEYS = ['theme', 'colorMode', 'locale', 'layout', 'fixedHeader', 'fixedSidebar', 'size', 'radius'];
30
+ function storageKey(c) {
31
+ return typeof c.persist === 'object' ? c.persist.key : `${c.prefix}-ui-config`;
32
+ }
33
+ function readPersisted(key) {
34
+ if (!isBrowser)
35
+ return {};
36
+ try {
37
+ const raw = window.localStorage.getItem(key);
38
+ return raw ? JSON.parse(raw) : {};
39
+ }
40
+ catch {
41
+ return {};
42
+ }
43
+ }
44
+ function writePersisted(c) {
45
+ if (!isBrowser || !c.persist)
46
+ return;
47
+ try {
48
+ const data = {};
49
+ for (const k of PERSIST_KEYS)
50
+ data[k] = c[k];
51
+ window.localStorage.setItem(storageKey(c), JSON.stringify(data));
52
+ }
53
+ catch {
54
+ /* storage unavailable (private mode, quota) */
55
+ }
56
+ }
57
+ const builtinMessages = { en, ja, vi, zh };
58
+ /** Create the global thl-ui instance (config + i18n). One per app; adapters provide it via context/DI. */
59
+ export function createThl(user = {}) {
60
+ const { loadLocale, ...rest } = user;
61
+ let initial = deepMerge(defaultConfig, rest);
62
+ if (initial.persist)
63
+ initial = { ...initial, ...pick(readPersisted(storageKey(initial))) };
64
+ const config = createStore(initial);
65
+ const messages = { ...builtinMessages };
66
+ for (const [l, m] of Object.entries(initial.messages))
67
+ messages[l] = deepMerge(messages[l] ?? {}, m);
68
+ const i18n = createI18n({
69
+ locale: initial.locale,
70
+ fallbackLocale: initial.fallbackLocale,
71
+ messages,
72
+ loadLocale,
73
+ intlLocale: Object.fromEntries(Object.entries(localeInfo).map(([k, v]) => [k, v.intl])),
74
+ });
75
+ const mql = isBrowser && window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
76
+ const resolvedColorMode = () => {
77
+ const m = config.get().colorMode;
78
+ return m === 'auto' ? (mql?.matches ? 'dark' : 'light') : m;
79
+ };
80
+ function applyToDocument() {
81
+ if (!isBrowser)
82
+ return;
83
+ const c = config.get();
84
+ const root = c.root ?? document.documentElement;
85
+ const p = c.prefix;
86
+ root.setAttribute(`data-${p}-theme`, c.theme);
87
+ root.setAttribute(`data-${p}-color-mode`, resolvedColorMode());
88
+ root.setAttribute(`data-${p}-layout`, c.layout);
89
+ root.setAttribute(`data-${p}-size`, c.size);
90
+ root.setAttribute(`data-${p}-radius`, c.radius);
91
+ root.toggleAttribute(`data-${p}-fixed-header`, c.fixedHeader);
92
+ root.toggleAttribute(`data-${p}-fixed-sidebar`, c.fixedSidebar);
93
+ root.style.setProperty(`--${p}-z-base`, String(c.zIndex));
94
+ if (root === document.documentElement) {
95
+ root.lang = i18n.getLocale();
96
+ root.dir = i18n.dir();
97
+ }
98
+ }
99
+ // Listeners are (re)attachable so frameworks that mount → unmount → remount (React StrictMode, HMR)
100
+ // can call destroy() then attach() again without losing persistence / document sync.
101
+ let unsubs = [];
102
+ function attach() {
103
+ if (unsubs.length)
104
+ return;
105
+ unsubs.push(config.subscribe((c, prev) => {
106
+ if (c.locale !== prev.locale && c.locale !== i18n.getLocale())
107
+ void i18n.setLocale(c.locale);
108
+ writePersisted(c);
109
+ applyToDocument();
110
+ }));
111
+ unsubs.push(i18n.locale.subscribe((l) => {
112
+ if (config.get().locale !== l)
113
+ config.set((c) => ({ ...c, locale: l }));
114
+ applyToDocument();
115
+ }));
116
+ if (mql) {
117
+ const onChange = () => config.get().colorMode === 'auto' && applyToDocument();
118
+ mql.addEventListener?.('change', onChange);
119
+ unsubs.push(() => mql.removeEventListener?.('change', onChange));
120
+ }
121
+ applyToDocument();
122
+ }
123
+ attach();
124
+ const instance = {
125
+ config,
126
+ i18n,
127
+ get: () => config.get(),
128
+ set(patch) {
129
+ config.set((c) => ({ ...c, ...(typeof patch === 'function' ? patch(c) : patch) }));
130
+ },
131
+ subscribe: (fn) => config.subscribe(fn),
132
+ defaults(name, props) {
133
+ const base = (config.get().components[name] ?? {});
134
+ const out = { ...base };
135
+ if (props)
136
+ for (const k in props)
137
+ if (props[k] !== undefined)
138
+ out[k] = props[k];
139
+ return out;
140
+ },
141
+ t: (k, p) => i18n.t(k, p),
142
+ setLocale: (l) => i18n.setLocale(l),
143
+ resolvedColorMode,
144
+ applyToDocument,
145
+ attach,
146
+ destroy() {
147
+ unsubs.forEach((u) => u());
148
+ unsubs = [];
149
+ },
150
+ };
151
+ return instance;
152
+ }
153
+ function pick(o) {
154
+ const out = {};
155
+ for (const k of PERSIST_KEYS)
156
+ if (o[k] !== undefined)
157
+ out[k] = o[k];
158
+ return out;
159
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Shared prop contracts — every adapter (React/Vue/Angular) implements these names
3
+ * so the API is identical across frameworks.
4
+ */
5
+ import type { ThlSemanticColor } from '@thl-ui/tokens';
6
+ export type Variant = ThlSemanticColor | 'link';
7
+ export type ControlSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
8
+ export type SocialBrand = 'facebook' | 'twitter' | 'googleplus' | 'linkedin' | 'dribbble' | 'instagram' | 'pinterest' | 'youtube' | 'github' | 'skype' | 'dropbox' | 'tumblr';
9
+ export interface ButtonProps {
10
+ variant?: Variant | SocialBrand;
11
+ size?: ControlSize;
12
+ outline?: boolean;
13
+ rounded?: boolean;
14
+ circle?: boolean;
15
+ block?: boolean;
16
+ loading?: boolean;
17
+ disabled?: boolean;
18
+ icon?: string;
19
+ iconRight?: string;
20
+ type?: 'button' | 'submit' | 'reset';
21
+ href?: string;
22
+ }
23
+ export interface BadgeProps {
24
+ color?: ThlSemanticColor;
25
+ size?: 'xs' | 'sm' | 'md';
26
+ dot?: boolean;
27
+ pill?: boolean;
28
+ }
29
+ export interface LabelProps {
30
+ color?: ThlSemanticColor | 'white';
31
+ rounded?: boolean;
32
+ outline?: boolean;
33
+ }
34
+ export interface AlertProps {
35
+ type?: ThlSemanticColor;
36
+ title?: string;
37
+ icon?: string;
38
+ dismissible?: boolean;
39
+ }
40
+ export interface CardProps {
41
+ title?: string;
42
+ subtitle?: string;
43
+ bordered?: boolean;
44
+ shadow?: boolean;
45
+ padding?: boolean;
46
+ }
47
+ export interface PanelProps {
48
+ title?: string;
49
+ color?: ThlSemanticColor;
50
+ collapsible?: boolean;
51
+ collapsed?: boolean;
52
+ dismissible?: boolean;
53
+ loading?: boolean;
54
+ fullscreen?: boolean;
55
+ }
56
+ export interface ProgressProps {
57
+ value: number;
58
+ max?: number;
59
+ color?: ThlSemanticColor;
60
+ size?: 'xs' | 'sm' | 'md' | 'lg';
61
+ striped?: boolean;
62
+ animated?: boolean;
63
+ vertical?: boolean;
64
+ label?: boolean | string;
65
+ }
66
+ export interface RibbonProps {
67
+ text: string;
68
+ color?: ThlSemanticColor;
69
+ variant?: 'default' | 'bookmark' | 'corner' | 'vertical-left' | 'vertical-right' | 'badge';
70
+ position?: 'left' | 'right';
71
+ placement?: 'top' | 'bottom';
72
+ }
73
+ export interface ModalProps {
74
+ open: boolean;
75
+ title?: string;
76
+ size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
77
+ closeOnBackdrop?: boolean;
78
+ closeOnEsc?: boolean;
79
+ centered?: boolean;
80
+ scrollable?: boolean;
81
+ hideClose?: boolean;
82
+ }
83
+ export interface TabsItem {
84
+ key: string;
85
+ label: string;
86
+ icon?: string;
87
+ disabled?: boolean;
88
+ badge?: string | number;
89
+ }
90
+ export interface TabsProps {
91
+ items: TabsItem[];
92
+ active?: string;
93
+ variant?: 'default' | 'underline' | 'filled' | 'pills' | 'pills-rounded' | 'vertical' | 'line';
94
+ justified?: boolean;
95
+ }
96
+ export interface StepItem {
97
+ title: string;
98
+ description?: string;
99
+ icon?: string;
100
+ }
101
+ export interface StepsProps {
102
+ items: StepItem[];
103
+ current?: number;
104
+ variant?: 'default' | 'no-bg' | 'thin' | 'line' | 'numbered';
105
+ clickable?: boolean;
106
+ }
107
+ export interface PaginationProps {
108
+ page: number;
109
+ total: number;
110
+ pageSize?: number;
111
+ maxPages?: number;
112
+ size?: 'sm' | 'md' | 'lg';
113
+ showTotal?: boolean;
114
+ showFirstLast?: boolean;
115
+ split?: boolean;
116
+ /** accessible name of the <nav> (needed when a page has several paginations) */ ariaLabel?: string;
117
+ }
118
+ export interface BreadcrumbItem {
119
+ label: string;
120
+ href?: string;
121
+ icon?: string;
122
+ }
123
+ export interface TimelineItem {
124
+ title: string;
125
+ content?: string;
126
+ time?: string;
127
+ color?: ThlSemanticColor;
128
+ icon?: string;
129
+ image?: string;
130
+ }
131
+ export interface TimelineProps {
132
+ items: TimelineItem[];
133
+ variant?: 'default' | 'alternate' | 'left' | 'activity-feed' | 'horizontal';
134
+ }
135
+ export interface StatBoxProps {
136
+ title: string;
137
+ value: string | number;
138
+ icon?: string;
139
+ color?: ThlSemanticColor;
140
+ trend?: 'up' | 'down';
141
+ delta?: string;
142
+ progress?: number;
143
+ variant?: 'default' | 'filled' | 'icon-left' | 'soft';
144
+ }
145
+ export interface AvatarProps {
146
+ src?: string;
147
+ name?: string;
148
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number;
149
+ status?: 'online' | 'offline' | 'away' | 'busy';
150
+ shape?: 'circle' | 'rounded' | 'square';
151
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ /** Date helpers for DatePicker / DateRangePicker / Calendar. */
2
+ export declare const startOfDay: (d: Date) => Date;
3
+ export declare const addDays: (d: Date, n: number) => Date;
4
+ export declare const addMonths: (d: Date, n: number) => Date;
5
+ export declare const isSameDay: (a?: Date | null, b?: Date | null) => boolean;
6
+ export declare const isBetween: (d: Date, a?: Date | null, b?: Date | null) => boolean;
7
+ export interface DayCell {
8
+ date: Date;
9
+ inMonth: boolean;
10
+ today: boolean;
11
+ key: string;
12
+ }
13
+ /** 6x7 grid for a month view. */
14
+ export declare function monthGrid(year: number, month: number, weekStartsOn?: 0 | 1): DayCell[][];
15
+ /** Localised weekday names using Intl. */
16
+ export declare function weekdayNames(locale: string, weekStartsOn?: 0 | 1, style?: 'narrow' | 'short' | 'long'): string[];
17
+ export declare function monthNames(locale: string, style?: 'short' | 'long'): string[];
18
+ export declare const toISODate: (d: Date) => string;
19
+ export declare function parseISODate(s?: string | null): Date | null;
20
+ /** Preset ranges for DateRangePicker (labels resolved via i18n `datepicker.ranges.*`). */
21
+ export declare function presetRanges(now?: Date): Record<string, [Date, Date]>;
@@ -0,0 +1,56 @@
1
+ /** Date helpers for DatePicker / DateRangePicker / Calendar. */
2
+ export const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
3
+ export const addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
4
+ export const addMonths = (d, n) => {
5
+ const r = new Date(d.getFullYear(), d.getMonth() + n, 1);
6
+ const last = new Date(r.getFullYear(), r.getMonth() + 1, 0).getDate();
7
+ r.setDate(Math.min(d.getDate(), last));
8
+ return r;
9
+ };
10
+ export const isSameDay = (a, b) => !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
11
+ export const isBetween = (d, a, b) => !!a && !!b && startOfDay(d) >= startOfDay(a < b ? a : b) && startOfDay(d) <= startOfDay(a < b ? b : a);
12
+ /** 6x7 grid for a month view. */
13
+ export function monthGrid(year, month, weekStartsOn = 1) {
14
+ const first = new Date(year, month, 1);
15
+ const offset = (first.getDay() - weekStartsOn + 7) % 7;
16
+ const start = addDays(first, -offset);
17
+ const today = startOfDay(new Date());
18
+ const weeks = [];
19
+ for (let w = 0; w < 6; w++) {
20
+ const row = [];
21
+ for (let i = 0; i < 7; i++) {
22
+ const date = addDays(start, w * 7 + i);
23
+ row.push({ date, inMonth: date.getMonth() === month, today: isSameDay(date, today), key: `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}` });
24
+ }
25
+ weeks.push(row);
26
+ }
27
+ return weeks;
28
+ }
29
+ /** Localised weekday names using Intl. */
30
+ export function weekdayNames(locale, weekStartsOn = 1, style = 'short') {
31
+ const fmt = new Intl.DateTimeFormat(locale, { weekday: style });
32
+ return Array.from({ length: 7 }, (_, i) => fmt.format(new Date(2024, 0, 7 + ((i + weekStartsOn) % 7)))); // 2024-01-07 is Sunday
33
+ }
34
+ export function monthNames(locale, style = 'long') {
35
+ const fmt = new Intl.DateTimeFormat(locale, { month: style });
36
+ return Array.from({ length: 12 }, (_, i) => fmt.format(new Date(2024, i, 1)));
37
+ }
38
+ export const toISODate = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
39
+ export function parseISODate(s) {
40
+ if (!s)
41
+ return null;
42
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s);
43
+ return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null;
44
+ }
45
+ /** Preset ranges for DateRangePicker (labels resolved via i18n `datepicker.ranges.*`). */
46
+ export function presetRanges(now = new Date()) {
47
+ const t = startOfDay(now);
48
+ return {
49
+ today: [t, t],
50
+ yesterday: [addDays(t, -1), addDays(t, -1)],
51
+ last7: [addDays(t, -6), t],
52
+ last30: [addDays(t, -29), t],
53
+ thisMonth: [new Date(t.getFullYear(), t.getMonth(), 1), new Date(t.getFullYear(), t.getMonth() + 1, 0)],
54
+ lastMonth: [new Date(t.getFullYear(), t.getMonth() - 1, 1), new Date(t.getFullYear(), t.getMonth(), 0)],
55
+ };
56
+ }
@@ -0,0 +1,17 @@
1
+ export declare function createCarousel(opts: {
2
+ count: number;
3
+ index?: number;
4
+ loop?: boolean;
5
+ interval?: number;
6
+ onChange?: (i: number) => void;
7
+ }): {
8
+ state: import("../store.js").Store<number>;
9
+ index: () => number;
10
+ next: () => void;
11
+ prev: () => void;
12
+ goTo: (i: number) => void;
13
+ setCount: (n: number) => void;
14
+ play(): void;
15
+ pause(): void;
16
+ destroy(): void;
17
+ };
@@ -0,0 +1,39 @@
1
+ import { createStore } from '../store.js';
2
+ export function createCarousel(opts) {
3
+ let count = opts.count;
4
+ const state = createStore(opts.index ?? 0);
5
+ state.subscribe((v) => opts.onChange?.(v));
6
+ let timer;
7
+ const go = (i) => {
8
+ if (count <= 0)
9
+ return;
10
+ const loop = opts.loop ?? true;
11
+ state.set(loop ? (i + count) % count : Math.max(0, Math.min(count - 1, i)));
12
+ };
13
+ const api = {
14
+ state,
15
+ index: () => state.get(),
16
+ next: () => go(state.get() + 1),
17
+ prev: () => go(state.get() - 1),
18
+ goTo: go,
19
+ setCount: (n) => {
20
+ count = n;
21
+ if (state.get() >= n)
22
+ state.set(Math.max(0, n - 1));
23
+ },
24
+ play() {
25
+ api.pause();
26
+ if (opts.interval)
27
+ timer = setInterval(api.next, opts.interval);
28
+ },
29
+ pause() {
30
+ if (timer)
31
+ clearInterval(timer);
32
+ timer = undefined;
33
+ },
34
+ destroy() {
35
+ api.pause();
36
+ },
37
+ };
38
+ return api;
39
+ }
@@ -0,0 +1,22 @@
1
+ export interface DisclosureOptions {
2
+ open?: boolean;
3
+ onChange?: (open: boolean) => void;
4
+ }
5
+ /** Open/close state — collapse, panel, accordion item, sidebar submenu, dropdown, modal. */
6
+ export declare function createDisclosure(opts?: DisclosureOptions): {
7
+ state: import("../store.js").Store<boolean>;
8
+ isOpen: () => boolean;
9
+ open: () => void;
10
+ close: () => void;
11
+ toggle: () => void;
12
+ set: (v: boolean) => void;
13
+ };
14
+ /** Accordion: a group of disclosures, optionally only one open at a time. */
15
+ export declare function createAccordion(opts?: {
16
+ multiple?: boolean;
17
+ open?: string[];
18
+ }): {
19
+ state: import("../store.js").Store<string[]>;
20
+ isOpen: (id: string) => boolean;
21
+ toggle(id: string): void;
22
+ };