@shlinkio/shlink-frontend-kit 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,15 @@
1
1
  # Shlink frontend kit
2
2
 
3
3
  React components and utilities for Shlink frontend projects
4
+
5
+ ## Tailwind alternatives
6
+
7
+ This library provides some tailwindcss-based components. To use them make sure to import components from `@shlinkio/shlink-frontend-kit/tailwind` and you add the following instructions to your tailwind stylesheet:
8
+
9
+ ```css
10
+ @import 'tailwindcss' prefix(tw);
11
+
12
+ /* Add these two lines */
13
+ @source '../node_modules/@shlinkio/shlink-frontend-kit';
14
+ @import '@shlinkio/shlink-frontend-kit/tailwind.preset.css';
15
+ ```
package/dist/index.d.ts CHANGED
@@ -193,7 +193,7 @@ declare type ToggleResult = [boolean, () => void, () => void, () => void];
193
193
 
194
194
  export declare const ToggleSwitch: FC<BooleanControlProps>;
195
195
 
196
- export declare const useElementRef: <T>() => RefObject<T | null>;
196
+ export declare const useElementRef: <T extends HTMLElement>() => RefObject<T | null>;
197
197
 
198
198
  export declare const useOrder: <T>(initialOrder: Order<T>) => [Order<T>, (orderField?: T, orderDir?: OrderDir) => void];
199
199
 
@@ -0,0 +1,224 @@
1
+ import { FC } from 'react';
2
+ import { HTMLProps } from 'react';
3
+ import { InputHTMLAttributes } from 'react';
4
+ import { LinkProps } from 'react-router';
5
+ import { PropsWithChildren } from 'react';
6
+ import { ReactNode } from 'react';
7
+
8
+ export declare type BaseInputProps = {
9
+ size?: Size;
10
+ feedback?: 'error';
11
+ };
12
+
13
+ export declare const Button: FC<ButtonProps>;
14
+
15
+ export declare type ButtonProps = PropsWithChildren<{
16
+ disabled?: boolean;
17
+ className?: string;
18
+ variant?: 'primary' | 'secondary' | 'danger';
19
+ size?: Size;
20
+ inline?: boolean;
21
+ solid?: boolean;
22
+ } & (RegularButtonProps | LinkButtonProps_2)>;
23
+
24
+ export declare type Callback = () => unknown;
25
+
26
+ export declare const Card: FC<CardProps> & {
27
+ Body: FC<CardProps>;
28
+ Header: FC<CardProps>;
29
+ Footer: FC<CardProps>;
30
+ };
31
+
32
+ export declare type CardProps = HTMLProps<HTMLDivElement>;
33
+
34
+ export declare type CellProps = HTMLProps<HTMLTableCellElement> & {
35
+ /**
36
+ * The name of the column to be displayed in small resolutions when the table is responsive, where the cells collapse.
37
+ * It is ignored for non-responsive tables.
38
+ */
39
+ columnName?: string;
40
+ /**
41
+ * Whether to use a th or td tag. If not provided, it is inferred based on the section, using td when inside tbody,
42
+ * and th when inside thead or tfoot
43
+ */
44
+ type?: 'td' | 'th';
45
+ };
46
+
47
+ export declare const CloseButton: FC<CloseButtonProps>;
48
+
49
+ export declare type CloseButtonProps = {
50
+ label?: string;
51
+ onClick?: HTMLProps<HTMLButtonElement>['onClick'];
52
+ };
53
+
54
+ declare type CommonModalDialogProps = {
55
+ open: boolean;
56
+ size?: Size | 'xl' | 'full';
57
+ /** Modal header title */
58
+ title: string;
59
+ /** Invoked when the modal is closed for any reason */
60
+ onClose?: () => void;
61
+ };
62
+
63
+ declare type CoverModalDialogProps = CommonModalDialogProps & {
64
+ /**
65
+ * Cover dialogs have a body that span the whole dialog, and no buttons.
66
+ * The header overlaps the body with semi-transparent background.
67
+ */
68
+ variant: 'cover';
69
+ };
70
+
71
+ export declare const ELLIPSIS = "...";
72
+
73
+ declare type Ellipsis = typeof ELLIPSIS;
74
+
75
+ export declare const formatNumber: (number: number | string) => string;
76
+
77
+ export declare const Input: FC<InputProps>;
78
+
79
+ export declare type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> & BaseInputProps & {
80
+ borderless?: boolean;
81
+ };
82
+
83
+ export declare const keyForPage: (pageNumber: NumberOrEllipsis, index: number) => string;
84
+
85
+ export declare const Label: FC<LabelProps>;
86
+
87
+ export declare const LabelledInput: FC<LabelledInputProps>;
88
+
89
+ export declare type LabelledInputProps = Omit<InputProps, 'className' | 'id' | 'feedback'> & {
90
+ label: string;
91
+ inputClassName?: string;
92
+ error?: string;
93
+ /** Alternative to `required`. Causes the input to be required, without displaying an asterisk */
94
+ hiddenRequired?: boolean;
95
+ };
96
+
97
+ export declare const LabelledSelect: FC<LabelledSelectProps>;
98
+
99
+ export declare type LabelledSelectProps = Omit<SelectProps, 'className' | 'id'> & {
100
+ label: string;
101
+ selectClassName?: string;
102
+ /** Alternative to `required`. Causes the input to be required, without displaying an asterisk */
103
+ hiddenRequired?: boolean;
104
+ };
105
+
106
+ export declare type LabelProps = HTMLProps<HTMLLabelElement> & {
107
+ required?: boolean;
108
+ };
109
+
110
+ export declare const LinkButton: FC<LinkButtonProps>;
111
+
112
+ export declare type LinkButtonProps = Omit<HTMLProps<HTMLButtonElement>, 'size' | 'type'> & {
113
+ size?: Size;
114
+ type?: HTMLButtonElement['type'];
115
+ };
116
+
117
+ declare type LinkButtonProps_2 = LinkProps;
118
+
119
+ export declare const ModalDialog: FC<ModalDialogProps>;
120
+
121
+ export declare type ModalDialogProps = Omit<HTMLProps<HTMLDialogElement>, 'title' | 'size'> & (CoverModalDialogProps | RegularModalDialogProps);
122
+
123
+ declare type NoTitleProps = {
124
+ title?: never;
125
+ titleSize?: never;
126
+ };
127
+
128
+ export declare type NumberOrEllipsis = number | Ellipsis;
129
+
130
+ export declare const pageIsEllipsis: (pageNumber: NumberOrEllipsis) => pageNumber is Ellipsis;
131
+
132
+ export declare const Paginator: FC<PaginatorProps>;
133
+
134
+ export declare type PaginatorProps = {
135
+ pagesCount: number;
136
+ currentPage: number;
137
+ } & ({
138
+ onPageChange: (currentPage: number) => void;
139
+ } | {
140
+ urlForPage: (pageNumber: number) => string;
141
+ });
142
+
143
+ export declare const prettifyPageNumber: (pageNumber: NumberOrEllipsis) => string;
144
+
145
+ export declare const progressivePagination: (currentPage: number, pageCount: number) => NumberOrEllipsis[];
146
+
147
+ declare type RegularButtonProps = Omit<HTMLProps<HTMLButtonElement>, 'size'>;
148
+
149
+ declare type RegularModalDialogProps = CommonModalDialogProps & {
150
+ /** Danger dialogs use danger variants in title and confirm button */
151
+ variant?: 'default' | 'danger';
152
+ /** Value to display in confirm button. Defaults to 'Confirm' */
153
+ confirmText?: string;
154
+ /** Invoked when the modal is closed via confirm button */
155
+ onConfirm?: () => void;
156
+ };
157
+
158
+ export declare const roundTen: (number: number) => number;
159
+
160
+ export declare const SearchInput: FC<SearchInputProps>;
161
+
162
+ export declare type SearchInputProps = Omit<InputProps, 'className' | 'onChange' | 'value'> & {
163
+ onChange: (searchTerm: string) => void;
164
+ containerClassName?: string;
165
+ inputClassName?: string;
166
+ };
167
+
168
+ export declare type SectionType = 'head' | 'body' | 'footer';
169
+
170
+ export declare const Select: FC<SelectProps>;
171
+
172
+ declare type SelectElementProps = Omit<HTMLProps<HTMLSelectElement>, 'size'>;
173
+
174
+ export declare type SelectProps = PropsWithChildren<SelectElementProps & BaseInputProps>;
175
+
176
+ export declare const SimpleCard: FC<SimpleCardProps>;
177
+
178
+ export declare type SimpleCardProps = Omit<CardProps, 'title' | 'size'> & {
179
+ bodyClassName?: string;
180
+ } & (TitleProps | NoTitleProps);
181
+
182
+ declare type Size = 'sm' | 'md' | 'lg';
183
+
184
+ export declare const Table: FC<TableProps> & {
185
+ Row: FC<HTMLProps<HTMLTableRowElement>>;
186
+ Cell: FC<CellProps>;
187
+ };
188
+
189
+ export declare type TableElementProps = PropsWithChildren & {
190
+ className?: string;
191
+ };
192
+
193
+ export declare type TableProps = HTMLProps<HTMLTableElement> & {
194
+ header: ReactNode;
195
+ footer?: ReactNode;
196
+ /**
197
+ * By default, the table rows will collapse under large resolutions, and the headers will be hidden.
198
+ * Set `responsive={false}` to avoid this behavior.
199
+ */
200
+ responsive?: boolean;
201
+ };
202
+
203
+ declare type TitleProps = {
204
+ title: ReactNode;
205
+ titleSize?: Size;
206
+ };
207
+
208
+ export declare function useTimeout(defaultDelay: number,
209
+ /** Test seam. Defaults to global setTimeout */
210
+ setTimeout_?: typeof globalThis.setTimeout,
211
+ /** Test seam. Defaults to global clearTimeout */
212
+ clearTimeout_?: typeof clearTimeout): UseTimeoutResult;
213
+
214
+ export declare type UseTimeoutResult = {
215
+ /**
216
+ * Clears any in-progress timeout, and schedules a new callback.
217
+ * It optionally allows a delay to be provided, or falls back to the default delay otherwise.
218
+ */
219
+ setTimeout: (callback: Callback, delay?: number) => void;
220
+ /** Clears any in-progress timeout */
221
+ clearCurrentTimeout: () => void;
222
+ };
223
+
224
+ export { }
@@ -0,0 +1,534 @@
1
+ import { jsx as n, jsxs as c } from "react/jsx-runtime";
2
+ import w from "clsx";
3
+ import { createContext as I, useContext as b, useId as B, useRef as S, useCallback as p, useEffect as F, useMemo as T } from "react";
4
+ import { Link as L } from "react-router";
5
+ import { faClose as $, faSearch as O, faChevronLeft as q, faChevronRight as _ } from "@fortawesome/free-solid-svg-icons";
6
+ import { FontAwesomeIcon as k } from "@fortawesome/react-fontawesome";
7
+ const y = I(void 0), f = I({ responsive: !0 }), G = ({ children: t, className: r }) => {
8
+ const { responsive: e } = b(f);
9
+ return /* @__PURE__ */ n(y.Provider, { value: { section: "head" }, children: /* @__PURE__ */ n(
10
+ "thead",
11
+ {
12
+ className: w(
13
+ { "tw:hidden tw:lg:table-header-group": e },
14
+ r
15
+ ),
16
+ children: t
17
+ }
18
+ ) });
19
+ }, J = ({ children: t, className: r }) => {
20
+ const { responsive: e } = b(f);
21
+ return /* @__PURE__ */ n(y.Provider, { value: { section: "body" }, children: /* @__PURE__ */ n(
22
+ "tbody",
23
+ {
24
+ className: w(
25
+ { "tw:lg:table-row-group tw:flex tw:flex-col tw:gap-y-3": e },
26
+ r
27
+ ),
28
+ children: t
29
+ }
30
+ ) });
31
+ }, K = ({ children: t, className: r }) => {
32
+ const { responsive: e } = b(f);
33
+ return /* @__PURE__ */ n(y.Provider, { value: { section: "footer" }, children: /* @__PURE__ */ n(
34
+ "tfoot",
35
+ {
36
+ className: w(
37
+ { "tw:lg:table-row-group tw:flex tw:flex-col tw:gap-y-3 tw:mt-4": e },
38
+ r
39
+ ),
40
+ children: t
41
+ }
42
+ ) });
43
+ }, Q = ({ children: t, className: r, ...e }) => {
44
+ const o = b(y), d = (o == null ? void 0 : o.section) === "body", { responsive: l } = b(f);
45
+ return /* @__PURE__ */ n(
46
+ "tr",
47
+ {
48
+ className: w(
49
+ "tw:group",
50
+ {
51
+ "tw:lg:table-row tw:flex tw:flex-col": l,
52
+ "tw:lg:border-0 tw:border-y-2 tw:border-lm-border tw:dark:border-dm-border": l,
53
+ "tw:hover:bg-lm-primary tw:dark:hover:bg-dm-primary": d,
54
+ // Use a different hover bg color depending on the table being inside a card or not
55
+ "tw:group-[&]/card:hover:bg-lm-secondary tw:dark:group-[&]/card:hover:bg-dm-secondary": d
56
+ },
57
+ r
58
+ ),
59
+ ...e,
60
+ children: t
61
+ }
62
+ );
63
+ }, V = ({ children: t, className: r, columnName: e, type: o, ...d }) => {
64
+ const l = b(y), a = o ?? ((l == null ? void 0 : l.section) !== "body" ? "th" : "td"), { responsive: s } = b(f);
65
+ return /* @__PURE__ */ n(
66
+ a,
67
+ {
68
+ "data-column": s ? e : void 0,
69
+ className: w(
70
+ "tw:p-2 tw:border-lm-border tw:dark:border-dm-border",
71
+ {
72
+ "tw:border-b-1": !s,
73
+ "tw:block tw:lg:table-cell tw:not-last:border-b-1 tw:lg:border-b-1": s,
74
+ // For md and lower, display the content in data-column attribute as before
75
+ "tw:before:lg:hidden tw:before:content-[attr(data-column)] tw:before:font-bold tw:before:mr-1": s && a === "td"
76
+ },
77
+ r
78
+ ),
79
+ ...d,
80
+ children: t
81
+ }
82
+ );
83
+ }, W = ({ header: t, footer: r, children: e, responsive: o = !0, ...d }) => /* @__PURE__ */ n(f.Provider, { value: { responsive: o }, children: /* @__PURE__ */ c("table", { className: "tw:w-full", ...d, children: [
84
+ /* @__PURE__ */ n(G, { children: t }),
85
+ /* @__PURE__ */ n(J, { children: e }),
86
+ r && /* @__PURE__ */ n(K, { children: r })
87
+ ] }) }), yt = Object.assign(W, { Row: Q, Cell: V }), X = ({
88
+ children: t,
89
+ className: r,
90
+ disabled: e,
91
+ variant: o = "primary",
92
+ size: d = "md",
93
+ inline: l = !1,
94
+ solid: a = !1,
95
+ ...s
96
+ }) => {
97
+ const m = "to" in s ? L : "button";
98
+ return (
99
+ // @ts-expect-error We are explicitly checking for the `to` prop before using Link
100
+ /* @__PURE__ */ n(
101
+ m,
102
+ {
103
+ className: w(
104
+ {
105
+ "tw:inline-flex": l,
106
+ "tw:flex": !l
107
+ },
108
+ "tw:gap-2 tw:items-center tw:justify-center",
109
+ "tw:border tw:rounded-md tw:no-underline",
110
+ "tw:transition-colors",
111
+ {
112
+ "tw:focus-ring": o !== "danger",
113
+ "tw:focus-ring-danger": o === "danger"
114
+ },
115
+ {
116
+ "tw:px-1.5 tw:py-1 tw:text-sm": d === "sm",
117
+ "tw:px-3 tw:py-1.5": d === "md",
118
+ "tw:px-4 tw:py-2 tw:text-lg": d === "lg"
119
+ },
120
+ {
121
+ "tw:border-brand tw:text-brand": o === "primary",
122
+ "tw:border-zinc-500": o === "secondary",
123
+ "tw:text-zinc-500": o === "secondary" && !a,
124
+ "tw:border-danger": o === "danger",
125
+ "tw:text-danger": o === "danger" && !a
126
+ },
127
+ a && {
128
+ "tw:text-white": !0,
129
+ "tw:bg-brand": o === "primary",
130
+ "tw:highlight:bg-brand-dark tw:highlight:border-brand-dark": o === "primary",
131
+ "tw:bg-zinc-500": o === "secondary",
132
+ "tw:highlight:bg-zinc-600 tw:highlight:border-zinc-600": o === "secondary",
133
+ "tw:bg-danger": o === "danger",
134
+ "tw:highlight:bg-danger-dark tw:highlight:border-danger-dark": o === "danger"
135
+ },
136
+ !e && {
137
+ "tw:highlight:text-white": !a,
138
+ "tw:highlight:bg-brand": o === "primary",
139
+ "tw:highlight:bg-zinc-500": o === "secondary",
140
+ "tw:highlight:bg-danger": o === "danger"
141
+ },
142
+ {
143
+ "tw:pointer-events-none tw:opacity-65": e
144
+ },
145
+ r
146
+ ),
147
+ disabled: e,
148
+ ...s,
149
+ children: t
150
+ }
151
+ )
152
+ );
153
+ }, Y = ({ onClick: t, label: r = "Close" }) => /* @__PURE__ */ n(
154
+ "button",
155
+ {
156
+ onClick: t,
157
+ className: w(
158
+ "tw:opacity-50 tw:highlight:opacity-80 tw:transition-opacity",
159
+ "tw:rounded-md tw:focus-ring"
160
+ ),
161
+ "aria-label": r,
162
+ children: /* @__PURE__ */ n(k, { icon: $, size: "xl" })
163
+ }
164
+ ), R = ({
165
+ borderless: t = !1,
166
+ size: r = "md",
167
+ feedback: e,
168
+ className: o,
169
+ disabled: d,
170
+ readOnly: l,
171
+ ...a
172
+ }) => {
173
+ const s = !d && !l;
174
+ return /* @__PURE__ */ n(
175
+ "input",
176
+ {
177
+ className: w(
178
+ "tw:w-full",
179
+ {
180
+ "tw:focus-ring": !e,
181
+ "tw:focus-ring-danger": e === "error"
182
+ },
183
+ {
184
+ "tw:px-2 tw:py-1 tw:text-sm": r === "sm",
185
+ "tw:px-3 tw:py-1.5": r === "md",
186
+ "tw:px-4 tw:py-2 tw:text-xl": r === "lg"
187
+ },
188
+ {
189
+ "tw:rounded-md tw:border": !t,
190
+ "tw:border-lm-input-border tw:dark:border-dm-input-border": !t && !e,
191
+ "tw:border-danger": !t && e === "error",
192
+ "tw:bg-lm-disabled-input tw:dark:bg-dm-disabled-input": !s,
193
+ "tw:bg-lm-primary tw:dark:bg-dm-primary": s,
194
+ // Use different background color when rendered inside a card
195
+ "tw:group-[&]/card:bg-lm-input tw:group-[&]/card:dark:bg-dm-input": s
196
+ },
197
+ o
198
+ ),
199
+ disabled: d,
200
+ readOnly: l,
201
+ ...a
202
+ }
203
+ );
204
+ }, E = ({ required: t, children: r, ...e }) => /* @__PURE__ */ c("label", { ...e, children: [
205
+ r,
206
+ t && /* @__PURE__ */ n("span", { className: "tw:text-danger tw:ml-1", "data-testid": "required-indicator", children: "*" })
207
+ ] }), kt = ({ label: t, inputClassName: r, required: e, hiddenRequired: o, error: d, ...l }) => {
208
+ const a = B();
209
+ return /* @__PURE__ */ c("div", { className: "tw:flex tw:flex-col tw:gap-1", children: [
210
+ /* @__PURE__ */ n(E, { htmlFor: a, required: e, children: t }),
211
+ /* @__PURE__ */ n(
212
+ R,
213
+ {
214
+ id: a,
215
+ className: r,
216
+ required: e || o,
217
+ feedback: d ? "error" : void 0,
218
+ ...l
219
+ }
220
+ ),
221
+ d && /* @__PURE__ */ n("span", { className: "tw:text-danger", children: d })
222
+ ] });
223
+ }, Z = String.raw`data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/></svg>`, z = ({
224
+ className: t,
225
+ size: r = "md",
226
+ feedback: e,
227
+ style: o = {},
228
+ disabled: d,
229
+ ...l
230
+ }) => /* @__PURE__ */ n(
231
+ "select",
232
+ {
233
+ className: w(
234
+ "tw:w-full tw:appearance-none tw:pr-9 tw:bg-no-repeat",
235
+ {
236
+ "tw:focus-ring": !e,
237
+ "tw:focus-ring-danger": e === "error"
238
+ },
239
+ "tw:rounded-md tw:border",
240
+ {
241
+ "tw:border-lm-input-border tw:dark:border-dm-input-border": !e,
242
+ "tw:border-danger": e === "error"
243
+ },
244
+ {
245
+ "tw:pl-2 tw:py-1 tw:text-sm": r === "sm",
246
+ "tw:pl-3 tw:py-1.5": r === "md",
247
+ "tw:pl-4 tw:py-2 tw:text-xl": r === "lg",
248
+ "tw:bg-lm-disabled-input tw:dark:bg-dm-disabled-input": d,
249
+ // Apply different background color when rendered inside a card
250
+ "tw:bg-lm-primary tw:dark:bg-dm-primary tw:group-[&]/card:bg-lm-input tw:group-[&]/card:dark:bg-dm-input": !d
251
+ },
252
+ t
253
+ ),
254
+ style: {
255
+ ...o,
256
+ backgroundImage: `url("${Z}")`,
257
+ backgroundSize: "16px 12px",
258
+ backgroundPosition: "right 0.75rem center"
259
+ },
260
+ disabled: d,
261
+ ...l
262
+ }
263
+ ), vt = ({ selectClassName: t, label: r, required: e, hiddenRequired: o, ...d }) => {
264
+ const l = B();
265
+ return /* @__PURE__ */ c("div", { className: "tw:flex tw:flex-col tw:gap-1", children: [
266
+ /* @__PURE__ */ n(E, { htmlFor: l, required: e, children: r }),
267
+ /* @__PURE__ */ n(z, { id: l, className: t, required: e || o, ...d })
268
+ ] });
269
+ };
270
+ function tt(t, r = globalThis.setTimeout.bind(globalThis), e = globalThis.clearTimeout.bind(globalThis)) {
271
+ const o = S(null), d = p(() => {
272
+ o.current && e(o.current);
273
+ }, [e]), l = p((a, s) => {
274
+ d(), o.current = r(() => {
275
+ a(), o.current = null;
276
+ }, s ?? t);
277
+ }, [d, t, r]);
278
+ return F(() => d, [d]), T(
279
+ () => ({ setTimeout: l, clearCurrentTimeout: d }),
280
+ [d, l]
281
+ );
282
+ }
283
+ const Nt = ({
284
+ onChange: t,
285
+ containerClassName: r,
286
+ inputClassName: e,
287
+ // Inputs have a default 'md' size. Search inputs are usually 'lg' as they are rendered at the top of sections
288
+ size: o = "lg",
289
+ ...d
290
+ }) => {
291
+ const { setTimeout: l, clearCurrentTimeout: a } = tt(500), s = p((m) => {
292
+ m ? l(() => t(m)) : (a(), t(m));
293
+ }, [a, t, l]);
294
+ return /* @__PURE__ */ c("div", { className: w("tw:group tw:relative tw:focus-within:z-10", r), children: [
295
+ /* @__PURE__ */ n(
296
+ k,
297
+ {
298
+ icon: O,
299
+ className: w(
300
+ "tw:absolute tw:top-[50%] tw:translate-y-[-50%] tw:transition-colors",
301
+ "tw:text-placeholder tw:group-focus-within:text-lm-text tw:dark:group-focus-within:text-dm-text",
302
+ {
303
+ "tw:left-3": o !== "sm",
304
+ "tw:scale-85 tw:left-2": o === "sm"
305
+ }
306
+ )
307
+ }
308
+ ),
309
+ /* @__PURE__ */ n(
310
+ R,
311
+ {
312
+ type: "search",
313
+ className: w(
314
+ {
315
+ "tw:pl-9": o !== "sm",
316
+ "tw:pl-7": o === "sm"
317
+ },
318
+ e
319
+ ),
320
+ placeholder: "Search...",
321
+ onChange: (m) => s(m.target.value),
322
+ size: o,
323
+ ...d
324
+ }
325
+ )
326
+ ] });
327
+ }, rt = ({ className: t, disabled: r, size: e = "md", ...o }) => /* @__PURE__ */ n(
328
+ "button",
329
+ {
330
+ className: w(
331
+ "tw:inline-flex tw:rounded-md tw:focus-ring",
332
+ "tw:text-brand tw:highlight:text-brand-dark tw:highlight:underline",
333
+ {
334
+ "tw:px-1.5 tw:py-1 tw:text-sm": e === "sm",
335
+ "tw:px-3 tw:py-1.5": e === "md",
336
+ "tw:px-4 tw:py-2 tw:text-lg": e === "lg",
337
+ "tw:pointer-events-none tw:opacity-65": r
338
+ },
339
+ t
340
+ ),
341
+ disabled: r,
342
+ ...o
343
+ }
344
+ ), et = new Intl.NumberFormat("en-US"), ot = (t) => et.format(Number(t)), P = 10, Tt = (t) => Math.ceil(t / P) * P, h = 2, v = "...", nt = (t, r) => Array.from({ length: r - t }, (e, o) => t + o), dt = (t, r) => {
345
+ const e = nt(
346
+ Math.max(h, t - h),
347
+ Math.min(r - 1, t + h) + 1
348
+ );
349
+ return t - h > h && e.unshift(v), t + h < r - 1 && e.push(v), e.unshift(1), e.push(r), e;
350
+ }, x = (t) => t === v, lt = (t) => x(t) ? t : ot(t), at = (t, r) => x(t) ? `${t}_${r}` : `${t}`, M = (t = !1) => w(
351
+ "tw:border tw:border-r-0 tw:last:border-r tw:border-lm-border tw:dark:border-dm-border",
352
+ "tw:first:rounded-l tw:last:rounded-r",
353
+ "tw:px-3 py-2 tw:cursor-pointer tw:no-underline",
354
+ "tw:outline-none tw:focus-visible:ring-3 tw:focus-visible:ring-brand/75 tw:focus-visible:z-1",
355
+ {
356
+ "tw:hover:bg-lm-secondary tw:dark:hover:bg-dm-secondary tw:text-brand tw:border-r-lm-border tw:dark:border-r-dm-border": !t,
357
+ "tw:bg-brand tw:text-white tw:border-r-brand": t
358
+ }
359
+ );
360
+ function j() {
361
+ return /* @__PURE__ */ n(
362
+ "span",
363
+ {
364
+ "aria-hidden": !0,
365
+ className: "tw:border-r tw:last:border-none tw:px-3 py-2 tw:text-gray-400 tw:border-r-(--border-color)",
366
+ children: v
367
+ }
368
+ );
369
+ }
370
+ function st({ children: t, active: r, isEllipsis: e, href: o, ...d }) {
371
+ const l = T(() => M(r), [r]);
372
+ return e ? /* @__PURE__ */ n(j, {}) : /* @__PURE__ */ n(L, { className: l, to: o, ...d, children: t });
373
+ }
374
+ function wt({ children: t, active: r, isEllipsis: e, ...o }) {
375
+ const d = T(() => M(r), [r]);
376
+ return e ? /* @__PURE__ */ n(j, {}) : /* @__PURE__ */ n("button", { type: "button", className: d, ...o, children: t });
377
+ }
378
+ const Ct = ({ currentPage: t, pagesCount: r, ...e }) => {
379
+ const o = "urlForPage" in e, d = o ? st : wt, l = p(
380
+ (a) => o ? { href: x(a) ? void 0 : e.urlForPage(a) } : { onClick: () => !x(a) && e.onPageChange(a) },
381
+ [o, e]
382
+ );
383
+ return r < 2 ? null : /* @__PURE__ */ c("div", { className: "tw:select-none tw:flex", "data-testid": "paginator", children: [
384
+ /* @__PURE__ */ n(d, { ...l(Math.max(1, t - 1)), "aria-label": "Previous", children: /* @__PURE__ */ n(k, { size: "xs", icon: q }) }),
385
+ dt(t, r).map((a, s) => /* @__PURE__ */ n(
386
+ d,
387
+ {
388
+ active: a === t,
389
+ isEllipsis: x(a),
390
+ ...l(a),
391
+ children: lt(a)
392
+ },
393
+ at(a, s)
394
+ )),
395
+ /* @__PURE__ */ n(d, { ...l(Math.min(r, t + 1)), "aria-label": "Next", children: /* @__PURE__ */ n(k, { size: "xs", icon: _ }) })
396
+ ] });
397
+ }, it = ({ className: t, ...r }) => /* @__PURE__ */ n(
398
+ "div",
399
+ {
400
+ className: w(
401
+ "tw:px-4 tw:py-3 tw:rounded-t-md",
402
+ "tw:bg-lm-primary tw:dark:bg-dm-primary tw:border-b tw:border-lm-border tw:dark:border-dm-border",
403
+ t
404
+ ),
405
+ ...r
406
+ }
407
+ ), ct = ({ className: t, ...r }) => /* @__PURE__ */ n(
408
+ "div",
409
+ {
410
+ className: w(
411
+ "tw:p-4 tw:bg-lm-primary tw:dark:bg-dm-primary tw:first:rounded-t-md",
412
+ "tw:first:rounded-t-md tw:last:rounded-b-md",
413
+ t
414
+ ),
415
+ ...r
416
+ }
417
+ ), mt = ({ className: t, ...r }) => /* @__PURE__ */ n(
418
+ "div",
419
+ {
420
+ className: w(
421
+ "tw:px-4 tw:py-3 tw:rounded-b-md",
422
+ "tw:bg-lm-primary tw:dark:bg-dm-primary tw:border-t tw:border-lm-border tw:dark:border-dm-border",
423
+ t
424
+ ),
425
+ ...r
426
+ }
427
+ ), gt = ({ className: t, ...r }) => /* @__PURE__ */ n(
428
+ "div",
429
+ {
430
+ className: w(
431
+ "tw:group/card tw:rounded-md tw:shadow-md",
432
+ "tw:border tw:border-lm-border tw:dark:border-dm-border tw:bg-lm-primary tw:dark:bg-dm-primary",
433
+ t
434
+ ),
435
+ ...r
436
+ }
437
+ ), g = Object.assign(gt, { Body: ct, Header: it, Footer: mt }), Pt = ({ bodyClassName: t, children: r, ...e }) => {
438
+ const { title: o, titleSize: d = "md", ...l } = "title" in e ? e : {
439
+ ...e,
440
+ title: void 0,
441
+ titleSize: void 0
442
+ };
443
+ return /* @__PURE__ */ c(g, { ...l, children: [
444
+ o && /* @__PURE__ */ c(g.Header, { children: [
445
+ d === "lg" && /* @__PURE__ */ n("h4", { children: o }),
446
+ d === "md" && /* @__PURE__ */ n("h5", { children: o }),
447
+ d === "sm" && /* @__PURE__ */ n("h6", { children: o })
448
+ ] }),
449
+ /* @__PURE__ */ n(g.Body, { className: t, children: r })
450
+ ] });
451
+ }, It = ({
452
+ open: t,
453
+ variant: r = "default",
454
+ size: e = "md",
455
+ title: o,
456
+ children: d,
457
+ className: l,
458
+ ...a
459
+ }) => {
460
+ const { onConfirm: s, confirmText: m = "Confirm", ...H } = "onConfirm" in a ? a : {
461
+ ...a,
462
+ onConfirm: void 0,
463
+ confirmText: void 0
464
+ }, N = S(null), u = p(() => {
465
+ var i;
466
+ return (i = N.current) == null ? void 0 : i.close();
467
+ }, []), A = p(() => {
468
+ s == null || s(), u();
469
+ }, [u, s]);
470
+ return F(() => {
471
+ var C;
472
+ const i = document.querySelector("body"), D = i.style.overflow, U = i.style.paddingRight;
473
+ return t ? (i.style.overflow = "hidden", i.scrollHeight > i.clientHeight && (i.style.paddingRight = "15px"), (C = N.current) == null || C.showModal()) : u(), () => {
474
+ i.style.overflow = D, i.style.paddingRight = U;
475
+ };
476
+ }, [u, t]), /* @__PURE__ */ n(
477
+ "dialog",
478
+ {
479
+ ref: N,
480
+ className: w(
481
+ "tw:bg-transparent tw:backdrop:bg-black/50",
482
+ {
483
+ "tw:flex tw:w-screen tw:h-screen tw:max-w-screen tw:max-h-screen tw:px-4": t
484
+ },
485
+ l
486
+ ),
487
+ ...H,
488
+ children: t && /* @__PURE__ */ c(g, { className: w(
489
+ "tw:m-auto tw:w-full",
490
+ {
491
+ "tw:md:w-sm": e === "sm",
492
+ "tw:md:w-lg": e === "md",
493
+ "tw:md:w-4xl": e === "lg",
494
+ "tw:md:w-6xl": e === "xl",
495
+ "tw:w-full": e === "full"
496
+ }
497
+ ), children: [
498
+ /* @__PURE__ */ c(g.Header, { className: "tw:flex tw:items-center tw:justify-between tw:sticky tw:top-0", children: [
499
+ /* @__PURE__ */ n("h5", { className: w({ "tw:text-danger": r === "danger" }), children: o }),
500
+ /* @__PURE__ */ n(Y, { onClick: u, label: "Close dialog" })
501
+ ] }),
502
+ /* @__PURE__ */ n(g.Body, { children: d }),
503
+ /* @__PURE__ */ c(g.Footer, { className: "tw:flex tw:flex-row-reverse tw:gap-x-2 tw:items-center tw:py-4 tw:sticky tw:bottom-0", children: [
504
+ s && /* @__PURE__ */ n(X, { variant: r === "danger" ? "danger" : "primary", onClick: A, children: m }),
505
+ /* @__PURE__ */ n(rt, { onClick: u, children: "Cancel" })
506
+ ] })
507
+ ] })
508
+ }
509
+ );
510
+ };
511
+ export {
512
+ X as Button,
513
+ g as Card,
514
+ Y as CloseButton,
515
+ v as ELLIPSIS,
516
+ R as Input,
517
+ E as Label,
518
+ kt as LabelledInput,
519
+ vt as LabelledSelect,
520
+ rt as LinkButton,
521
+ It as ModalDialog,
522
+ Ct as Paginator,
523
+ Nt as SearchInput,
524
+ z as Select,
525
+ Pt as SimpleCard,
526
+ yt as Table,
527
+ ot as formatNumber,
528
+ at as keyForPage,
529
+ x as pageIsEllipsis,
530
+ lt as prettifyPageNumber,
531
+ dt as progressivePagination,
532
+ Tt as roundTen,
533
+ tt as useTimeout
534
+ };
@@ -0,0 +1,153 @@
1
+ /*
2
+ * Override `dark:` variant, which applies based on `prefers-color-scheme` by default, and make it apply based on the
3
+ * presence of `data-theme="dark"` attribute instead.
4
+ */
5
+ @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
6
+
7
+ @theme static inline {
8
+ /* Light mode */
9
+ /*--color-lm-main: #2078CF;*/ /*Properly accessible with white background*/
10
+ --color-lm-main: #4696e5; /* TODO Rename to "brand" */
11
+ --color-lm-main-dark: #1f69c0;
12
+ --color-lm-primary: #ffffff;
13
+ --color-lm-primary-alfa: rgba(var(--tw-color-lm-primary), .5);
14
+ --color-lm-secondary: #f5f6fe;
15
+ --color-lm-text: #232323;
16
+ --color-lm-border: rgb(0 0 0 / .125);
17
+ --color-lm-table-border: #dee2e6;
18
+ --color-lm-active: #eeeeee;
19
+ --color-lm-brand: var(--tw-color-lm-main); /* TODO Rename to "main" */
20
+ --color-lm-input: var(--tw-color-lm-primary);
21
+ --color-lm-disabled-input: var(--tw-color-lm-secondary);
22
+ --color-lm-input-text: #495057;
23
+ --color-lm-input-border: rgb(0 0 0 / .19);
24
+ --color-lm-table-highlight: rgb(0 0 0 / .075);
25
+
26
+ /* Dark mode */
27
+ --color-dm-main: #4696e5; /* TODO Rename to "brand" */
28
+ --color-dm-main-dark: #1f69c0;
29
+ --color-dm-primary: #161b22;
30
+ --color-dm-primary-alfa: rgba(var(--tw-color-dm-primary), .8);
31
+ --color-dm-secondary: #0f131a;
32
+ --color-dm-text: rgb(201 209 217);
33
+ --color-dm-border: rgb(255 255 255 / .15);
34
+ --color-dm-table-border: #393d43;
35
+ --color-dm-active: var(--tw-color-dm-secondary);
36
+ --color-dm-brand: #0b2d4e; /* TODO Rename to "main" */
37
+ --color-dm-input: rgb(17.9928571429 22.0821428571 27.8071428571);
38
+ --color-dm-disabled-input: rgb(26.0071428571 31.9178571429 40.1928571429);
39
+ --color-dm-input-text: var(--tw-color-dm-text);
40
+ --color-dm-input-border: var(--tw-color-dm-border);
41
+ --color-dm-table-highlight: var(--tw-color-dm-border);
42
+
43
+ /* TODO Remove these two colors */
44
+ --color-brand: #4696e5;
45
+ --color-brand-dark: #1f69c0;
46
+
47
+ /* General color palette */
48
+ --color-danger: #dc3545;
49
+ --color-danger-dark: #bb2d3b;
50
+ --color-warning: #ffc107;
51
+ --color-warning-dark: #ffca2c;
52
+ --color-placeholder: #6c757d;
53
+ }
54
+
55
+ @layer base {
56
+ html:not([data-theme='dark']) {
57
+ --primary-color: var(--tw-color-lm-primary);
58
+ --primary-color-alfa: var(--tw-color-lm-primary-alfa);
59
+ --secondary-color: var(--tw-color-lm-secondary);
60
+ --text-color: var(--tw-color-lm-text);
61
+ --border-color: var(--tw-color-lm-border);
62
+ --active-color: var(--tw-color-lm-active);
63
+ --brand-color: var(--tw-color-lm-brand);
64
+ --input-color: var(--tw-color-lm-input);
65
+ --input-disabled-color: var(--tw-color-lm-disabled-input);
66
+ --input-text-color: var(--tw-color-lm-input-text);
67
+ --input-border-color: var(--tw-color-lm-input-border);
68
+ --table-border-color: var(--tw-color-lm-table-border);
69
+ --table-highlight-color: var(--tw-color-lm-table-highlight);
70
+ }
71
+
72
+ html[data-theme='dark'] {
73
+ --primary-color: var(--tw-color-dm-primary);
74
+ --primary-color-alfa: var(--tw-color-dm-primary-alfa);
75
+ --secondary-color: var(--tw-color-dm-secondary);
76
+ --text-color: var(--tw-color-dm-text);
77
+ --border-color: var(--tw-color-dm-border);
78
+ --active-color: var(--tw-color-dm-active);
79
+ --brand-color: var(--tw-color-dm-brand);
80
+ --input-color: var(--tw-color-dm-input);
81
+ --input-disabled-color: var(--tw-color-dm-disabled-input);
82
+ --input-text-color: var(--tw-color-dm-input-text);
83
+ --input-border-color: var(--tw-color-dm-input-border);
84
+ --table-border-color: var(--tw-color-dm-table-border);
85
+ --table-highlight-color: var(--tw-color-dm-table-highlight);
86
+ }
87
+ }
88
+
89
+ @layer base {
90
+ :root {
91
+ --header-height: 56px;
92
+ @apply tw:scheme-normal tw:dark:scheme-dark tw:scroll-auto;
93
+ }
94
+
95
+ html, body {
96
+ @apply tw:h-full tw:bg-lm-secondary tw:dark:bg-dm-secondary tw:text-lm-text tw:dark:text-dm-text;
97
+ }
98
+
99
+ a {
100
+ /* Set these styles as plain CSS instead of @apply to avoid higher specificity */
101
+ /* TODO Use `main` color, not `brand` color */
102
+ color: var(--tw-color-brand);
103
+ border-radius: var(--tw-radius-xs);
104
+
105
+ @apply tw:focus-visible:outline-3 tw:focus-visible:outline-brand/50 tw:focus-visible:outline-offset-3 tw:focus-visible:z-1;
106
+ }
107
+
108
+ h1 {
109
+ @apply tw:text-[2.5rem]/[1.2] tw:m-0 tw:font-medium;
110
+ }
111
+ h2 {
112
+ @apply tw:text-[2rem]/[1.2] tw:m-0 tw:font-medium;
113
+ }
114
+ h3 {
115
+ @apply tw:text-[1.75rem]/[1.2] tw:m-0 tw:font-medium;
116
+ }
117
+ h4 {
118
+ @apply tw:text-2xl/[1.2] tw:m-0 tw:font-medium;
119
+ }
120
+ h5 {
121
+ @apply tw:text-xl/[1.2] tw:m-0 tw:font-medium;
122
+ }
123
+ h6 {
124
+ @apply tw:text-base/[1.2] tw:m-0 tw:font-medium;
125
+ }
126
+
127
+ hr {
128
+ @apply tw:my-3
129
+ }
130
+
131
+ p {
132
+ @apply tw:m-0;
133
+ }
134
+ }
135
+
136
+ @utility focus-ring-base {
137
+ @apply tw:outline-none tw:focus-visible:ring-3 tw:focus-visible:z-1 tw:transition-[box-shadow];
138
+ }
139
+
140
+ @utility focus-ring {
141
+ @apply tw:focus-ring-base tw:focus-visible:ring-brand/50;
142
+ }
143
+
144
+ @utility focus-ring-danger {
145
+ @apply tw:focus-ring-base tw:focus-visible:ring-danger/50;
146
+ }
147
+
148
+ @custom-variant highlight {
149
+ &:hover,
150
+ &:focus {
151
+ @slot;
152
+ }
153
+ }
package/package.json CHANGED
@@ -7,19 +7,30 @@
7
7
  "type": "module",
8
8
  "main": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ },
15
+ "./tailwind": {
16
+ "import": "./dist/tailwind.js",
17
+ "types": "./dist/tailwind.d.ts"
18
+ },
19
+ "./tailwind.preset.css": {
20
+ "style": "./dist/tailwind.preset.css",
21
+ "default": "./dist/tailwind.preset.css"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
10
25
  "files": [
11
26
  "dist"
12
27
  ],
13
28
  "scripts": {
14
- "build": "vite build && cp ./src/base.scss ./dist/base.scss",
29
+ "build": "vite build && cp ./src/base.scss ./dist/base.scss && cp ./src/tailwind/tailwind.preset.css ./dist/tailwind.preset.css",
15
30
  "test": "vitest run",
16
31
  "test:ci": "npm run test -- --coverage",
17
- "lint": "npm run lint:css && npm run lint:js",
18
- "lint:css": "stylelint src/*.scss src/**/*.scss dev/*.scss dev/**/*.scss",
19
- "lint:js": "eslint dev src test",
20
- "lint:fix": "npm run lint:css:fix && npm run lint:js:fix",
21
- "lint:css:fix": "npm run lint:css -- --fix",
22
- "lint:js:fix": "npm run lint:js -- --fix",
32
+ "lint": "eslint dev src test",
33
+ "lint:fix": "npm run lint:js -- --fix",
23
34
  "types": "tsc",
24
35
  "dev": "vite serve --host=0.0.0.0 --port 3001"
25
36
  },
@@ -33,37 +44,42 @@
33
44
  "react": "^18.3 || ^19.0",
34
45
  "react-dom": "^18.3 || ^19.0",
35
46
  "react-router": "^7.0.2",
36
- "reactstrap": "^9.2.0"
47
+ "reactstrap": "^9.2.0",
48
+ "tailwindcss": "^4.0.1"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "tailwindcss": {
52
+ "optional": true
53
+ }
37
54
  },
38
55
  "devDependencies": {
39
56
  "@shlinkio/eslint-config-js-coding-standard": "~3.4.0",
40
- "@shlinkio/stylelint-config-css-coding-standard": "~1.1.1",
41
- "@stylistic/eslint-plugin": "^4.1.0",
57
+ "@stylistic/eslint-plugin": "^4.2.0",
58
+ "@tailwindcss/vite": "^4.0.16",
42
59
  "@testing-library/jest-dom": "^6.6.3",
43
60
  "@testing-library/react": "^16.2.0",
44
61
  "@testing-library/user-event": "^14.6.1",
45
62
  "@total-typescript/shoehorn": "^0.1.2",
46
- "@types/react": "^19.0.10",
63
+ "@types/react": "^19.0.12",
47
64
  "@types/react-dom": "^19.0.4",
48
65
  "@vitejs/plugin-react": "^4.3.4",
49
- "@vitest/coverage-v8": "^3.0.7",
50
- "axe-core": "^4.10.2",
66
+ "@vitest/coverage-v8": "^3.0.9",
67
+ "axe-core": "^4.10.3",
51
68
  "bootstrap": "5.2.3",
52
- "eslint": "^9.21.0",
69
+ "eslint": "^9.23.0",
53
70
  "eslint-plugin-jsx-a11y": "^6.10.2",
54
71
  "eslint-plugin-react": "^7.37.4",
55
- "eslint-plugin-react-compiler": "^19.0.0-beta-40c6c23-20250301",
72
+ "eslint-plugin-react-compiler": "^19.0.0-beta-714736e-20250131",
56
73
  "eslint-plugin-react-hooks": "^5.2.0",
57
74
  "eslint-plugin-simple-import-sort": "^12.1.1",
58
75
  "history": "^5.3.0",
59
76
  "jsdom": "^26.0.0",
60
77
  "resize-observer-polyfill": "^1.5.1",
61
- "sass": "^1.85.1",
62
- "stylelint": "^15.11.0",
63
- "typescript": "^5.7.3",
64
- "typescript-eslint": "^8.25.0",
65
- "vite": "^6.2.0",
66
- "vite-plugin-dts": "^4.5.1",
78
+ "sass": "^1.86.0",
79
+ "typescript": "^5.8.2",
80
+ "typescript-eslint": "^8.27.0",
81
+ "vite": "^6.2.2",
82
+ "vite-plugin-dts": "^4.5.3",
67
83
  "vitest": "^3.0.2"
68
84
  },
69
85
  "browserslist": [
@@ -72,5 +88,5 @@
72
88
  "not ie <= 11",
73
89
  "not op_mini all"
74
90
  ],
75
- "version": "0.7.3"
91
+ "version": "0.8.0"
76
92
  }