@dbcdk/react-components 0.0.109 → 0.0.110

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/dist/client.cjs CHANGED
@@ -84,6 +84,8 @@ var Grid = require('./components/grid/Grid');
84
84
  var Alert = require('./components/alert/Alert');
85
85
  var InlineStatus = require('./components/inline-status/InlineStatus');
86
86
  var MediaCard = require('./components/media-card/MediaCard');
87
+ var Lightbox = require('./components/lightbox/Lightbox');
88
+ var FileUpload = require('./components/forms/file-upload/FileUpload');
87
89
 
88
90
 
89
91
 
@@ -585,3 +587,15 @@ Object.keys(MediaCard).forEach(function (k) {
585
587
  get: function () { return MediaCard[k]; }
586
588
  });
587
589
  });
590
+ Object.keys(Lightbox).forEach(function (k) {
591
+ if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
592
+ enumerable: true,
593
+ get: function () { return Lightbox[k]; }
594
+ });
595
+ });
596
+ Object.keys(FileUpload).forEach(function (k) {
597
+ if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
598
+ enumerable: true,
599
+ get: function () { return FileUpload[k]; }
600
+ });
601
+ });
package/dist/client.d.ts CHANGED
@@ -81,3 +81,5 @@ export * from './components/grid/Grid';
81
81
  export * from './components/alert/Alert';
82
82
  export * from './components/inline-status/InlineStatus';
83
83
  export * from './components/media-card/MediaCard';
84
+ export * from './components/lightbox/Lightbox';
85
+ export * from './components/forms/file-upload/FileUpload';
package/dist/client.js CHANGED
@@ -82,3 +82,5 @@ export * from './components/grid/Grid';
82
82
  export * from './components/alert/Alert';
83
83
  export * from './components/inline-status/InlineStatus';
84
84
  export * from './components/media-card/MediaCard';
85
+ export * from './components/lightbox/Lightbox';
86
+ export * from './components/forms/file-upload/FileUpload';
@@ -0,0 +1,117 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var lucideReact = require('lucide-react');
6
+ var react = require('react');
7
+ var styles = require('./FileUpload.module.css');
8
+ var InputContainer = require('../input-container/InputContainer');
9
+
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ var styles__default = /*#__PURE__*/_interopDefault(styles);
13
+
14
+ function FileUpload({
15
+ label,
16
+ labelAction,
17
+ error,
18
+ helpText,
19
+ helpTextAddition,
20
+ orientation = "vertical",
21
+ labelWidth,
22
+ fullWidth = false,
23
+ required,
24
+ modified,
25
+ onChange,
26
+ children,
27
+ hint,
28
+ height,
29
+ minWidth,
30
+ width,
31
+ maxWidth,
32
+ id,
33
+ disabled,
34
+ className,
35
+ ...inputProps
36
+ }) {
37
+ const reactId = react.useId();
38
+ const inputId = id != null ? id : `file-upload-${reactId}`;
39
+ const inputRef = react.useRef(null);
40
+ const [dragging, setDragging] = react.useState(false);
41
+ function handleDragOver(e) {
42
+ e.preventDefault();
43
+ if (!disabled) setDragging(true);
44
+ }
45
+ function handleDragLeave(e) {
46
+ if (!e.currentTarget.contains(e.relatedTarget)) setDragging(false);
47
+ }
48
+ function handleDrop(e) {
49
+ e.preventDefault();
50
+ setDragging(false);
51
+ if (disabled) return;
52
+ if (inputRef.current) {
53
+ const dt = e.dataTransfer;
54
+ const files = dt.files;
55
+ onChange == null ? void 0 : onChange(files.length > 0 ? files : null);
56
+ }
57
+ }
58
+ return /* @__PURE__ */ jsxRuntime.jsx(
59
+ InputContainer.InputContainer,
60
+ {
61
+ label,
62
+ labelAction,
63
+ htmlFor: inputId,
64
+ error,
65
+ helpText,
66
+ helpTextAddition,
67
+ orientation,
68
+ labelWidth,
69
+ fullWidth,
70
+ required,
71
+ modified,
72
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
73
+ "label",
74
+ {
75
+ htmlFor: inputId,
76
+ className: [
77
+ styles__default.default.dropzone,
78
+ dragging ? styles__default.default.dragging : "",
79
+ disabled ? styles__default.default.disabled : "",
80
+ error ? styles__default.default.hasError : "",
81
+ fullWidth ? styles__default.default.fullWidth : "",
82
+ className != null ? className : ""
83
+ ].filter(Boolean).join(" "),
84
+ style: {
85
+ ...height != null ? { height } : void 0,
86
+ ...minWidth != null ? { minWidth } : void 0,
87
+ ...width != null ? { width } : void 0,
88
+ ...maxWidth != null ? { maxWidth } : void 0
89
+ },
90
+ onDragOver: handleDragOver,
91
+ onDragLeave: handleDragLeave,
92
+ onDrop: handleDrop,
93
+ children: [
94
+ /* @__PURE__ */ jsxRuntime.jsx(
95
+ "input",
96
+ {
97
+ ...inputProps,
98
+ ref: inputRef,
99
+ id: inputId,
100
+ type: "file",
101
+ disabled,
102
+ className: styles__default.default.input,
103
+ onChange: (e) => onChange == null ? void 0 : onChange(e.target.files)
104
+ }
105
+ ),
106
+ children != null ? children : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
107
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.UploadCloud, { className: styles__default.default.icon, "aria-hidden": true }),
108
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.hint, children: hint != null ? hint : "Tr\xE6k filer hertil eller klik for at v\xE6lge" })
109
+ ] })
110
+ ]
111
+ }
112
+ )
113
+ }
114
+ );
115
+ }
116
+
117
+ exports.FileUpload = FileUpload;
@@ -0,0 +1,15 @@
1
+ import type { InputHTMLAttributes, ReactNode } from 'react';
2
+ import type { InputContainerProps } from '../input-container/InputContainer';
3
+ export interface FileUploadProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'width' | 'onChange'>, Omit<InputContainerProps, 'children' | 'htmlFor' | 'size'> {
4
+ onChange?: (files: FileList | null) => void;
5
+ /** Custom content to replace the default icon + text hint */
6
+ children?: ReactNode;
7
+ /** Short hint shown below the icon when no custom children */
8
+ hint?: string;
9
+ fullWidth?: boolean;
10
+ height?: number | string;
11
+ minWidth?: string | number;
12
+ width?: string | number;
13
+ maxWidth?: string | number;
14
+ }
15
+ export declare function FileUpload({ label, labelAction, error, helpText, helpTextAddition, orientation, labelWidth, fullWidth, required, modified, onChange, children, hint, height, minWidth, width, maxWidth, id, disabled, className, ...inputProps }: FileUploadProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,111 @@
1
+ 'use client';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { UploadCloud } from 'lucide-react';
4
+ import { useId, useRef, useState } from 'react';
5
+ import styles from './FileUpload.module.css';
6
+ import { InputContainer } from '../input-container/InputContainer';
7
+
8
+ function FileUpload({
9
+ label,
10
+ labelAction,
11
+ error,
12
+ helpText,
13
+ helpTextAddition,
14
+ orientation = "vertical",
15
+ labelWidth,
16
+ fullWidth = false,
17
+ required,
18
+ modified,
19
+ onChange,
20
+ children,
21
+ hint,
22
+ height,
23
+ minWidth,
24
+ width,
25
+ maxWidth,
26
+ id,
27
+ disabled,
28
+ className,
29
+ ...inputProps
30
+ }) {
31
+ const reactId = useId();
32
+ const inputId = id != null ? id : `file-upload-${reactId}`;
33
+ const inputRef = useRef(null);
34
+ const [dragging, setDragging] = useState(false);
35
+ function handleDragOver(e) {
36
+ e.preventDefault();
37
+ if (!disabled) setDragging(true);
38
+ }
39
+ function handleDragLeave(e) {
40
+ if (!e.currentTarget.contains(e.relatedTarget)) setDragging(false);
41
+ }
42
+ function handleDrop(e) {
43
+ e.preventDefault();
44
+ setDragging(false);
45
+ if (disabled) return;
46
+ if (inputRef.current) {
47
+ const dt = e.dataTransfer;
48
+ const files = dt.files;
49
+ onChange == null ? void 0 : onChange(files.length > 0 ? files : null);
50
+ }
51
+ }
52
+ return /* @__PURE__ */ jsx(
53
+ InputContainer,
54
+ {
55
+ label,
56
+ labelAction,
57
+ htmlFor: inputId,
58
+ error,
59
+ helpText,
60
+ helpTextAddition,
61
+ orientation,
62
+ labelWidth,
63
+ fullWidth,
64
+ required,
65
+ modified,
66
+ children: /* @__PURE__ */ jsxs(
67
+ "label",
68
+ {
69
+ htmlFor: inputId,
70
+ className: [
71
+ styles.dropzone,
72
+ dragging ? styles.dragging : "",
73
+ disabled ? styles.disabled : "",
74
+ error ? styles.hasError : "",
75
+ fullWidth ? styles.fullWidth : "",
76
+ className != null ? className : ""
77
+ ].filter(Boolean).join(" "),
78
+ style: {
79
+ ...height != null ? { height } : void 0,
80
+ ...minWidth != null ? { minWidth } : void 0,
81
+ ...width != null ? { width } : void 0,
82
+ ...maxWidth != null ? { maxWidth } : void 0
83
+ },
84
+ onDragOver: handleDragOver,
85
+ onDragLeave: handleDragLeave,
86
+ onDrop: handleDrop,
87
+ children: [
88
+ /* @__PURE__ */ jsx(
89
+ "input",
90
+ {
91
+ ...inputProps,
92
+ ref: inputRef,
93
+ id: inputId,
94
+ type: "file",
95
+ disabled,
96
+ className: styles.input,
97
+ onChange: (e) => onChange == null ? void 0 : onChange(e.target.files)
98
+ }
99
+ ),
100
+ children != null ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
101
+ /* @__PURE__ */ jsx(UploadCloud, { className: styles.icon, "aria-hidden": true }),
102
+ /* @__PURE__ */ jsx("span", { className: styles.hint, children: hint != null ? hint : "Tr\xE6k filer hertil eller klik for at v\xE6lge" })
103
+ ] })
104
+ ]
105
+ }
106
+ )
107
+ }
108
+ );
109
+ }
110
+
111
+ export { FileUpload };
@@ -0,0 +1,75 @@
1
+ .dropzone {
2
+ display: inline-flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ justify-content: center;
6
+ gap: var(--spacing-xs);
7
+ padding: var(--spacing-xl) var(--spacing-lg);
8
+ background: var(--color-bg-surface);
9
+ border: var(--border-width-thin) dashed var(--color-border-default);
10
+ border-radius: var(--border-radius-default);
11
+ cursor: pointer;
12
+ color: var(--color-fg-muted);
13
+ font-family: var(--font-family);
14
+ font-size: var(--font-size-sm);
15
+ text-align: center;
16
+ box-sizing: border-box;
17
+ transition:
18
+ background-color var(--transition-fast) var(--ease-standard),
19
+ border-color var(--transition-fast) var(--ease-standard),
20
+ color var(--transition-fast) var(--ease-standard);
21
+ }
22
+
23
+ .fullWidth {
24
+ width: 100%;
25
+ }
26
+
27
+ .dropzone:hover:not(.disabled) {
28
+ background-color: var(--color-bg-selected);
29
+ border-color: var(--color-border-selected);
30
+ color: var(--color-fg-default);
31
+ }
32
+
33
+ .dragging {
34
+ background-color: var(--color-bg-selected);
35
+ border-color: var(--color-border-selected);
36
+ color: var(--color-fg-default);
37
+ box-shadow: inset 0 0 0 1px var(--color-border-selected);
38
+ }
39
+
40
+ .hasError {
41
+ border-color: var(--color-status-error);
42
+ }
43
+
44
+ .hasError:hover:not(.disabled) {
45
+ border-color: var(--color-status-error);
46
+ background-color: var(--color-status-error-bg);
47
+ }
48
+
49
+ .disabled {
50
+ background-color: var(--color-disabled-bg);
51
+ border-color: var(--color-disabled-border);
52
+ color: var(--color-disabled-fg);
53
+ cursor: not-allowed;
54
+ }
55
+
56
+ .input {
57
+ position: absolute;
58
+ width: 1px;
59
+ height: 1px;
60
+ opacity: 0;
61
+ overflow: hidden;
62
+ clip: rect(0 0 0 0);
63
+ white-space: nowrap;
64
+ }
65
+
66
+ .icon {
67
+ width: var(--icon-size-lg);
68
+ height: var(--icon-size-lg);
69
+ stroke-width: 1.5;
70
+ flex-shrink: 0;
71
+ }
72
+
73
+ .hint {
74
+ line-height: var(--line-height-normal);
75
+ }
@@ -0,0 +1,219 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var lucideReact = require('lucide-react');
6
+ var react = require('react');
7
+ var client = require('../../client');
8
+ var styles = require('./Lightbox.module.css');
9
+ var Button = require('../button/Button');
10
+
11
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
+
13
+ var styles__default = /*#__PURE__*/_interopDefault(styles);
14
+
15
+ function extFromMime(mime) {
16
+ var _a;
17
+ const map = {
18
+ "image/jpeg": "jpg",
19
+ "image/png": "png",
20
+ "image/webp": "webp",
21
+ "image/gif": "gif"
22
+ };
23
+ return (_a = mime && map[mime]) != null ? _a : "jpg";
24
+ }
25
+ function Lightbox({
26
+ items,
27
+ title,
28
+ initialIndex = 0,
29
+ thumbnailHeight = 300,
30
+ showDownload = true,
31
+ getDownloadFilename,
32
+ buttonAddition,
33
+ trigger
34
+ }) {
35
+ var _a;
36
+ const dialogRef = react.useRef(null);
37
+ const [open, setOpen] = react.useState(false);
38
+ const [index, setIndex] = react.useState(initialIndex);
39
+ const [downloading, setDownloading] = react.useState(false);
40
+ const current = items[index];
41
+ const multiple = items.length > 1;
42
+ react.useEffect(() => {
43
+ setIndex((i) => Math.min(Math.max(i, 0), Math.max(items.length - 1, 0)));
44
+ }, [items.length]);
45
+ const openAt = react.useCallback((i = 0) => {
46
+ setIndex(i);
47
+ setOpen(true);
48
+ }, []);
49
+ const close = react.useCallback(() => {
50
+ var _a2;
51
+ setDownloading(false);
52
+ (_a2 = dialogRef.current) == null ? void 0 : _a2.close();
53
+ setOpen(false);
54
+ }, []);
55
+ const prev = react.useCallback(() => {
56
+ setIndex((i) => (i - 1 + items.length) % items.length);
57
+ }, [items.length]);
58
+ const next = react.useCallback(() => {
59
+ setIndex((i) => (i + 1) % items.length);
60
+ }, [items.length]);
61
+ react.useEffect(() => {
62
+ const dialog = dialogRef.current;
63
+ if (!dialog) return;
64
+ if (open && !dialog.open) dialog.showModal();
65
+ else if (!open && dialog.open) dialog.close();
66
+ }, [open]);
67
+ react.useEffect(() => {
68
+ const dialog = dialogRef.current;
69
+ if (!dialog) return;
70
+ const onCancel = (e) => {
71
+ e.preventDefault();
72
+ close();
73
+ };
74
+ const onClose = () => {
75
+ setOpen(false);
76
+ setDownloading(false);
77
+ };
78
+ dialog.addEventListener("cancel", onCancel);
79
+ dialog.addEventListener("close", onClose);
80
+ return () => {
81
+ dialog.removeEventListener("cancel", onCancel);
82
+ dialog.removeEventListener("close", onClose);
83
+ };
84
+ }, [close]);
85
+ react.useEffect(() => {
86
+ if (!open || !multiple) return;
87
+ const onKeyDown = (e) => {
88
+ if (e.key === "ArrowLeft") prev();
89
+ if (e.key === "ArrowRight") next();
90
+ };
91
+ window.addEventListener("keydown", onKeyDown);
92
+ return () => window.removeEventListener("keydown", onKeyDown);
93
+ }, [open, multiple, prev, next]);
94
+ const download = react.useCallback(async () => {
95
+ var _a2;
96
+ if (!(current == null ? void 0 : current.downloadSrc)) return;
97
+ setDownloading(true);
98
+ try {
99
+ const res = await fetch(current.downloadSrc, { cache: "no-store" });
100
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
101
+ const blob = await res.blob();
102
+ const objectUrl = URL.createObjectURL(blob);
103
+ const ext = extFromMime((_a2 = current.mimeType) != null ? _a2 : blob.type);
104
+ const filename = getDownloadFilename ? getDownloadFilename(current, index) : `image-${index + 1}.${ext}`;
105
+ const a = document.createElement("a");
106
+ a.href = objectUrl;
107
+ a.download = filename;
108
+ document.body.appendChild(a);
109
+ a.click();
110
+ a.remove();
111
+ URL.revokeObjectURL(objectUrl);
112
+ } catch (err) {
113
+ console.warn("[Lightbox] download failed", err);
114
+ } finally {
115
+ setDownloading(false);
116
+ }
117
+ }, [current, index, getDownloadFilename]);
118
+ if (!items.length) return null;
119
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
120
+ trigger ? trigger(openAt) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.thumbnails, children: items.map((item, i) => {
121
+ var _a2;
122
+ return /* @__PURE__ */ jsxRuntime.jsxs(
123
+ "button",
124
+ {
125
+ type: "button",
126
+ className: styles__default.default.thumbnailBtn,
127
+ onClick: () => openAt(i),
128
+ "aria-label": (_a2 = item.title) != null ? _a2 : `Billede ${i + 1}`,
129
+ children: [
130
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.thumbnailImg, style: { height: thumbnailHeight }, children: item.thumbnail }),
131
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.thumbnailMeta, children: /* @__PURE__ */ jsxRuntime.jsx(
132
+ client.MetaBar,
133
+ {
134
+ items: [
135
+ { label: "H\xF8jde", value: item.height != null ? `${item.height}px` : "-" },
136
+ { label: "Bredde", value: item.width != null ? `${item.width}px` : "-" }
137
+ ]
138
+ }
139
+ ) })
140
+ ]
141
+ },
142
+ i
143
+ );
144
+ }) }),
145
+ /* @__PURE__ */ jsxRuntime.jsx(
146
+ "dialog",
147
+ {
148
+ ref: dialogRef,
149
+ "aria-label": title != null ? title : "Lightbox",
150
+ tabIndex: -1,
151
+ className: styles__default.default.dialog,
152
+ onClick: (e) => {
153
+ if (e.target === e.currentTarget) close();
154
+ },
155
+ children: current && /* @__PURE__ */ jsxRuntime.jsxs(
156
+ "div",
157
+ {
158
+ className: styles__default.default.panel,
159
+ "data-surface": "inverse",
160
+ onClick: (e) => {
161
+ if (e.target === e.currentTarget) close();
162
+ },
163
+ children: [
164
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.toolbar, children: [
165
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.toolbarInfo, children: [
166
+ title && /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.titleMain, children: title }),
167
+ current.title && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles__default.default.titleItem, children: [
168
+ title ? "\xB7 " : "",
169
+ current.title
170
+ ] }),
171
+ multiple && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles__default.default.counter, children: [
172
+ title || current.title ? "\xB7 " : "",
173
+ index + 1,
174
+ "/",
175
+ items.length
176
+ ] })
177
+ ] }),
178
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.toolbarActions, children: [
179
+ buttonAddition && /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.buttonAdditionSlot, children: buttonAddition }),
180
+ showDownload && current.downloadSrc && /* @__PURE__ */ jsxRuntime.jsx(
181
+ Button.Button,
182
+ {
183
+ variant: "outlined",
184
+ size: "sm",
185
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 16 }),
186
+ onClick: download,
187
+ loading: downloading,
188
+ "aria-label": "Download",
189
+ children: "Download"
190
+ }
191
+ ),
192
+ /* @__PURE__ */ jsxRuntime.jsx(
193
+ Button.Button,
194
+ {
195
+ variant: "outlined",
196
+ size: "sm",
197
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16 }),
198
+ onClick: close,
199
+ "aria-label": "Luk",
200
+ children: "Luk"
201
+ }
202
+ )
203
+ ] })
204
+ ] }),
205
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.imageArea, children: (_a = current.preview) != null ? _a : current.thumbnail }),
206
+ multiple && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.nav, children: [
207
+ /* @__PURE__ */ jsxRuntime.jsx(Button.Button, { variant: "outlined", size: "sm", onClick: prev, "aria-label": "Forrige billede", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeft, { size: 20 }) }),
208
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.navHint, children: "\u2190 \u2192 \xB7 Esc" }),
209
+ /* @__PURE__ */ jsxRuntime.jsx(Button.Button, { variant: "outlined", size: "sm", onClick: next, "aria-label": "N\xE6ste billede", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { size: 20 }) })
210
+ ] })
211
+ ]
212
+ }
213
+ )
214
+ }
215
+ )
216
+ ] });
217
+ }
218
+
219
+ exports.Lightbox = Lightbox;
@@ -0,0 +1,21 @@
1
+ import type { ReactElement, ReactNode } from 'react';
2
+ export type LightboxItem = {
3
+ thumbnail: ReactNode;
4
+ preview?: ReactNode;
5
+ title?: string;
6
+ downloadSrc?: string;
7
+ mimeType?: string;
8
+ width?: number;
9
+ height?: number;
10
+ };
11
+ export interface LightboxProps {
12
+ items: LightboxItem[];
13
+ title?: string;
14
+ initialIndex?: number;
15
+ thumbnailHeight?: number;
16
+ showDownload?: boolean;
17
+ getDownloadFilename?: (item: LightboxItem, index: number) => string;
18
+ buttonAddition?: ReactNode;
19
+ trigger?: (open: (index?: number) => void) => ReactNode;
20
+ }
21
+ export declare function Lightbox({ items, title, initialIndex, thumbnailHeight, showDownload, getDownloadFilename, buttonAddition, trigger, }: LightboxProps): ReactElement | null;
@@ -0,0 +1,213 @@
1
+ 'use client';
2
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
3
+ import { Download, X, ChevronLeft, ChevronRight } from 'lucide-react';
4
+ import { useRef, useState, useEffect, useCallback } from 'react';
5
+ import { MetaBar } from '../../client';
6
+ import styles from './Lightbox.module.css';
7
+ import { Button } from '../button/Button';
8
+
9
+ function extFromMime(mime) {
10
+ var _a;
11
+ const map = {
12
+ "image/jpeg": "jpg",
13
+ "image/png": "png",
14
+ "image/webp": "webp",
15
+ "image/gif": "gif"
16
+ };
17
+ return (_a = mime && map[mime]) != null ? _a : "jpg";
18
+ }
19
+ function Lightbox({
20
+ items,
21
+ title,
22
+ initialIndex = 0,
23
+ thumbnailHeight = 300,
24
+ showDownload = true,
25
+ getDownloadFilename,
26
+ buttonAddition,
27
+ trigger
28
+ }) {
29
+ var _a;
30
+ const dialogRef = useRef(null);
31
+ const [open, setOpen] = useState(false);
32
+ const [index, setIndex] = useState(initialIndex);
33
+ const [downloading, setDownloading] = useState(false);
34
+ const current = items[index];
35
+ const multiple = items.length > 1;
36
+ useEffect(() => {
37
+ setIndex((i) => Math.min(Math.max(i, 0), Math.max(items.length - 1, 0)));
38
+ }, [items.length]);
39
+ const openAt = useCallback((i = 0) => {
40
+ setIndex(i);
41
+ setOpen(true);
42
+ }, []);
43
+ const close = useCallback(() => {
44
+ var _a2;
45
+ setDownloading(false);
46
+ (_a2 = dialogRef.current) == null ? void 0 : _a2.close();
47
+ setOpen(false);
48
+ }, []);
49
+ const prev = useCallback(() => {
50
+ setIndex((i) => (i - 1 + items.length) % items.length);
51
+ }, [items.length]);
52
+ const next = useCallback(() => {
53
+ setIndex((i) => (i + 1) % items.length);
54
+ }, [items.length]);
55
+ useEffect(() => {
56
+ const dialog = dialogRef.current;
57
+ if (!dialog) return;
58
+ if (open && !dialog.open) dialog.showModal();
59
+ else if (!open && dialog.open) dialog.close();
60
+ }, [open]);
61
+ useEffect(() => {
62
+ const dialog = dialogRef.current;
63
+ if (!dialog) return;
64
+ const onCancel = (e) => {
65
+ e.preventDefault();
66
+ close();
67
+ };
68
+ const onClose = () => {
69
+ setOpen(false);
70
+ setDownloading(false);
71
+ };
72
+ dialog.addEventListener("cancel", onCancel);
73
+ dialog.addEventListener("close", onClose);
74
+ return () => {
75
+ dialog.removeEventListener("cancel", onCancel);
76
+ dialog.removeEventListener("close", onClose);
77
+ };
78
+ }, [close]);
79
+ useEffect(() => {
80
+ if (!open || !multiple) return;
81
+ const onKeyDown = (e) => {
82
+ if (e.key === "ArrowLeft") prev();
83
+ if (e.key === "ArrowRight") next();
84
+ };
85
+ window.addEventListener("keydown", onKeyDown);
86
+ return () => window.removeEventListener("keydown", onKeyDown);
87
+ }, [open, multiple, prev, next]);
88
+ const download = useCallback(async () => {
89
+ var _a2;
90
+ if (!(current == null ? void 0 : current.downloadSrc)) return;
91
+ setDownloading(true);
92
+ try {
93
+ const res = await fetch(current.downloadSrc, { cache: "no-store" });
94
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
95
+ const blob = await res.blob();
96
+ const objectUrl = URL.createObjectURL(blob);
97
+ const ext = extFromMime((_a2 = current.mimeType) != null ? _a2 : blob.type);
98
+ const filename = getDownloadFilename ? getDownloadFilename(current, index) : `image-${index + 1}.${ext}`;
99
+ const a = document.createElement("a");
100
+ a.href = objectUrl;
101
+ a.download = filename;
102
+ document.body.appendChild(a);
103
+ a.click();
104
+ a.remove();
105
+ URL.revokeObjectURL(objectUrl);
106
+ } catch (err) {
107
+ console.warn("[Lightbox] download failed", err);
108
+ } finally {
109
+ setDownloading(false);
110
+ }
111
+ }, [current, index, getDownloadFilename]);
112
+ if (!items.length) return null;
113
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
114
+ trigger ? trigger(openAt) : /* @__PURE__ */ jsx("div", { className: styles.thumbnails, children: items.map((item, i) => {
115
+ var _a2;
116
+ return /* @__PURE__ */ jsxs(
117
+ "button",
118
+ {
119
+ type: "button",
120
+ className: styles.thumbnailBtn,
121
+ onClick: () => openAt(i),
122
+ "aria-label": (_a2 = item.title) != null ? _a2 : `Billede ${i + 1}`,
123
+ children: [
124
+ /* @__PURE__ */ jsx("div", { className: styles.thumbnailImg, style: { height: thumbnailHeight }, children: item.thumbnail }),
125
+ /* @__PURE__ */ jsx("div", { className: styles.thumbnailMeta, children: /* @__PURE__ */ jsx(
126
+ MetaBar,
127
+ {
128
+ items: [
129
+ { label: "H\xF8jde", value: item.height != null ? `${item.height}px` : "-" },
130
+ { label: "Bredde", value: item.width != null ? `${item.width}px` : "-" }
131
+ ]
132
+ }
133
+ ) })
134
+ ]
135
+ },
136
+ i
137
+ );
138
+ }) }),
139
+ /* @__PURE__ */ jsx(
140
+ "dialog",
141
+ {
142
+ ref: dialogRef,
143
+ "aria-label": title != null ? title : "Lightbox",
144
+ tabIndex: -1,
145
+ className: styles.dialog,
146
+ onClick: (e) => {
147
+ if (e.target === e.currentTarget) close();
148
+ },
149
+ children: current && /* @__PURE__ */ jsxs(
150
+ "div",
151
+ {
152
+ className: styles.panel,
153
+ "data-surface": "inverse",
154
+ onClick: (e) => {
155
+ if (e.target === e.currentTarget) close();
156
+ },
157
+ children: [
158
+ /* @__PURE__ */ jsxs("div", { className: styles.toolbar, children: [
159
+ /* @__PURE__ */ jsxs("div", { className: styles.toolbarInfo, children: [
160
+ title && /* @__PURE__ */ jsx("span", { className: styles.titleMain, children: title }),
161
+ current.title && /* @__PURE__ */ jsxs("span", { className: styles.titleItem, children: [
162
+ title ? "\xB7 " : "",
163
+ current.title
164
+ ] }),
165
+ multiple && /* @__PURE__ */ jsxs("span", { className: styles.counter, children: [
166
+ title || current.title ? "\xB7 " : "",
167
+ index + 1,
168
+ "/",
169
+ items.length
170
+ ] })
171
+ ] }),
172
+ /* @__PURE__ */ jsxs("div", { className: styles.toolbarActions, children: [
173
+ buttonAddition && /* @__PURE__ */ jsx("div", { className: styles.buttonAdditionSlot, children: buttonAddition }),
174
+ showDownload && current.downloadSrc && /* @__PURE__ */ jsx(
175
+ Button,
176
+ {
177
+ variant: "outlined",
178
+ size: "sm",
179
+ icon: /* @__PURE__ */ jsx(Download, { size: 16 }),
180
+ onClick: download,
181
+ loading: downloading,
182
+ "aria-label": "Download",
183
+ children: "Download"
184
+ }
185
+ ),
186
+ /* @__PURE__ */ jsx(
187
+ Button,
188
+ {
189
+ variant: "outlined",
190
+ size: "sm",
191
+ icon: /* @__PURE__ */ jsx(X, { size: 16 }),
192
+ onClick: close,
193
+ "aria-label": "Luk",
194
+ children: "Luk"
195
+ }
196
+ )
197
+ ] })
198
+ ] }),
199
+ /* @__PURE__ */ jsx("div", { className: styles.imageArea, children: (_a = current.preview) != null ? _a : current.thumbnail }),
200
+ multiple && /* @__PURE__ */ jsxs("div", { className: styles.nav, children: [
201
+ /* @__PURE__ */ jsx(Button, { variant: "outlined", size: "sm", onClick: prev, "aria-label": "Forrige billede", children: /* @__PURE__ */ jsx(ChevronLeft, { size: 20 }) }),
202
+ /* @__PURE__ */ jsx("span", { className: styles.navHint, children: "\u2190 \u2192 \xB7 Esc" }),
203
+ /* @__PURE__ */ jsx(Button, { variant: "outlined", size: "sm", onClick: next, "aria-label": "N\xE6ste billede", children: /* @__PURE__ */ jsx(ChevronRight, { size: 20 }) })
204
+ ] })
205
+ ]
206
+ }
207
+ )
208
+ }
209
+ )
210
+ ] });
211
+ }
212
+
213
+ export { Lightbox };
@@ -0,0 +1,161 @@
1
+ .thumbnails {
2
+ display: flex;
3
+ flex-wrap: wrap;
4
+ align-items: flex-end;
5
+ gap: var(--spacing-md);
6
+ }
7
+
8
+ .thumbnailBtn {
9
+ background: none;
10
+ border: none;
11
+ padding: 0;
12
+ cursor: pointer;
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: var(--spacing-xs);
16
+ flex: none;
17
+ text-align: left;
18
+ }
19
+
20
+ .thumbnailImg {
21
+ overflow: hidden;
22
+ border-radius: var(--border-radius-default);
23
+ border: 1px solid var(--color-border-default);
24
+ box-shadow: var(--shadow-sm);
25
+ transition:
26
+ transform var(--transition-fast) var(--ease-standard),
27
+ box-shadow var(--transition-fast) var(--ease-standard);
28
+ }
29
+
30
+ .thumbnailBtn:hover .thumbnailImg {
31
+ transform: scale(1.02);
32
+ }
33
+
34
+ .thumbnailImg > img {
35
+ height: 100%;
36
+ width: auto;
37
+ display: block;
38
+ }
39
+
40
+ .thumbnailMeta {
41
+ display: flex;
42
+ gap: var(--spacing-2xs);
43
+ }
44
+
45
+ .dialog {
46
+ margin: 0;
47
+ padding: 0;
48
+ width: 100%;
49
+ max-width: none;
50
+ height: 100%;
51
+ max-height: none;
52
+ background: transparent;
53
+ border: none;
54
+ outline: none;
55
+ }
56
+
57
+ .dialog::backdrop {
58
+ background: color-mix(in srgb, black 85%, transparent);
59
+ }
60
+
61
+ .panel {
62
+ display: flex;
63
+ flex-direction: column;
64
+ align-items: center;
65
+ justify-content: center;
66
+ height: 100%;
67
+ width: 100%;
68
+ padding: var(--spacing-sm);
69
+ box-sizing: border-box;
70
+ gap: var(--spacing-sm);
71
+ }
72
+
73
+ /* ── Toolbar ────────────────────────────────────────────────── */
74
+
75
+ .toolbar {
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: space-between;
79
+ gap: var(--spacing-md);
80
+ width: 100%;
81
+ max-width: min(96vw, 1200px);
82
+ flex-shrink: 0;
83
+ }
84
+
85
+ .toolbarInfo {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: var(--spacing-xs);
89
+ color: var(--color-fg-default);
90
+ font-size: var(--font-size-sm);
91
+ font-family: var(--font-family);
92
+ min-width: 0;
93
+ overflow: hidden;
94
+ }
95
+
96
+ .titleMain {
97
+ font-weight: var(--font-weight-medium);
98
+ white-space: nowrap;
99
+ overflow: hidden;
100
+ text-overflow: ellipsis;
101
+ }
102
+
103
+ .titleItem {
104
+ color: var(--color-fg-muted);
105
+ white-space: nowrap;
106
+ overflow: hidden;
107
+ text-overflow: ellipsis;
108
+ }
109
+
110
+ .counter {
111
+ color: var(--color-fg-subtle);
112
+ white-space: nowrap;
113
+ flex-shrink: 0;
114
+ }
115
+
116
+ .toolbarActions {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: var(--spacing-xs);
120
+ flex-shrink: 0;
121
+ }
122
+
123
+ .buttonAdditionSlot {
124
+ margin-inline-end: var(--spacing-sm);
125
+ }
126
+
127
+ .imageArea {
128
+ width: 100%;
129
+ max-width: min(96vw, 1200px);
130
+ background: black;
131
+ border-radius: var(--border-radius-default);
132
+ overflow: hidden;
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ position: relative;
137
+ flex: 1 1 0;
138
+ min-height: 0;
139
+ }
140
+
141
+ .imageArea > img {
142
+ max-height: 100%;
143
+ max-width: 100%;
144
+ object-fit: contain;
145
+ display: block;
146
+ }
147
+
148
+ .nav {
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: space-between;
152
+ width: 100%;
153
+ max-width: min(96vw, 1200px);
154
+ flex-shrink: 0;
155
+ }
156
+
157
+ .navHint {
158
+ color: var(--color-fg-subtle);
159
+ font-size: var(--font-size-xs);
160
+ font-family: var(--font-family);
161
+ }
package/dist/index.css CHANGED
@@ -22,6 +22,7 @@
22
22
  @import './components/filtering/chip-multi-toggle/ChipMultiToggle.module.css';
23
23
  @import './components/forms/checkbox-group/CheckboxGroup.module.css';
24
24
  @import './components/forms/checkbox/Checkbox.module.css';
25
+ @import './components/forms/file-upload/FileUpload.module.css';
25
26
  @import './components/forms/form-select/FormSelect.module.css';
26
27
  @import './components/forms/input-container/InputContainer.module.css';
27
28
  @import './components/forms/input/Input.module.css';
@@ -35,6 +36,7 @@
35
36
  @import './components/icon/Icon.module.css';
36
37
  @import './components/inline-status/InlineStatus.module.css';
37
38
  @import './components/json-viewer/JsonViewer.module.css';
39
+ @import './components/lightbox/Lightbox.module.css';
38
40
  @import './components/media-card/MediaCard.module.css';
39
41
  @import './components/menu/Menu.module.css';
40
42
  @import './components/meta-bar/MetaBar.module.css';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.109",
3
+ "version": "0.0.110",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",