@caratlogic/design-system 0.1.9 → 0.1.11
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/components/atoms/ToggleSwitch/ToggleSwitch.d.ts.map +1 -1
- package/dist/components/atoms/ToggleSwitch/ToggleSwitch.utils.d.ts +9 -0
- package/dist/components/atoms/ToggleSwitch/ToggleSwitch.utils.d.ts.map +1 -1
- package/dist/components/layouts/CreateFormPageShell/CreateFormPageShell.d.ts.map +1 -1
- package/dist/components/organisms/MediaGallery/MediaGallery.d.ts +1 -1
- package/dist/components/organisms/MediaGallery/MediaGallery.d.ts.map +1 -1
- package/dist/components/organisms/MediaGallery/MediaGallery.utils.d.ts +6 -0
- package/dist/components/organisms/MediaGallery/MediaGallery.utils.d.ts.map +1 -1
- package/dist/components/ui/switch.d.ts.map +1 -1
- package/dist/constants/filePatterns.d.ts +3 -0
- package/dist/constants/filePatterns.d.ts.map +1 -1
- package/dist/index.cjs.js +78 -32
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +78 -32
- package/dist/index.esm.js.map +1 -1
- package/dist/ui.cjs.js +26 -20
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.esm.js +26 -20
- package/dist/ui.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -5742,21 +5742,33 @@ const TextHyperlink = ({ to, href, children, showIcon = true, className, 'aria-l
|
|
|
5742
5742
|
return (jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className: cn(LINK_CLASS, className), children: content }));
|
|
5743
5743
|
};
|
|
5744
5744
|
|
|
5745
|
+
/**
|
|
5746
|
+
* Track, thumb and travel, sized so the thumb keeps an even 2px inset on all
|
|
5747
|
+
* four sides. Travel is stated in px rather than `calc(100% - Npx)`: that form
|
|
5748
|
+
* resolves against the thumb's own width, so the gap silently changed whenever a
|
|
5749
|
+
* thumb size did — `md` ended up with a 0.2px vertical inset and no gap at
|
|
5750
|
+
* either end, which read as a thumb bulging out of its track.
|
|
5751
|
+
*
|
|
5752
|
+
* inner = track - 2px border | thumb = innerHeight - 4 | on = innerWidth - thumb - 2
|
|
5753
|
+
*/
|
|
5745
5754
|
const switchSizes = {
|
|
5755
|
+
// track 28x16 → inner 26x14, thumb 10, on 14
|
|
5746
5756
|
sm: {
|
|
5747
5757
|
switch: 'h-4 w-7',
|
|
5748
|
-
thumb: 'size-
|
|
5749
|
-
translate: 'data-[state=checked]:translate-x-[
|
|
5758
|
+
thumb: 'size-2.5',
|
|
5759
|
+
translate: 'data-[state=unchecked]:translate-x-[2px] data-[state=checked]:translate-x-[14px]',
|
|
5750
5760
|
},
|
|
5761
|
+
// track 32x20 → inner 30x18, thumb 14, on 14
|
|
5751
5762
|
md: {
|
|
5752
|
-
switch: 'h-
|
|
5753
|
-
thumb: 'size-
|
|
5754
|
-
translate: 'data-[state=
|
|
5763
|
+
switch: 'h-5 w-8',
|
|
5764
|
+
thumb: 'size-3.5',
|
|
5765
|
+
translate: 'data-[state=unchecked]:translate-x-[2px] data-[state=checked]:translate-x-[14px]',
|
|
5755
5766
|
},
|
|
5767
|
+
// track 44x24 → inner 42x22, thumb 18, on 22
|
|
5756
5768
|
lg: {
|
|
5757
5769
|
switch: 'h-6 w-11',
|
|
5758
|
-
thumb: 'size-
|
|
5759
|
-
translate: 'data-[state=checked]:translate-x-[
|
|
5770
|
+
thumb: 'size-[1.125rem]',
|
|
5771
|
+
translate: 'data-[state=unchecked]:translate-x-[2px] data-[state=checked]:translate-x-[22px]',
|
|
5760
5772
|
},
|
|
5761
5773
|
};
|
|
5762
5774
|
|
|
@@ -5767,7 +5779,7 @@ function ToggleSwitch({ checked = false, onCheckedChange, disabled = false, id,
|
|
|
5767
5779
|
}
|
|
5768
5780
|
};
|
|
5769
5781
|
const currentSize = switchSizes[size];
|
|
5770
|
-
const switchButton = (jsx("button", { type: "button", role: "switch", "aria-checked": checked, "data-state": checked ? 'checked' : 'unchecked', value: checked ? 'on' : 'off', "data-slot": "switch", disabled: disabled, onClick: handleClick, id: id, name: name, className: cn('peer inline-flex shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-all outline-none', 'focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:ring-[3px]', 'disabled:cursor-not-allowed disabled:opacity-50', 'data-[state=unchecked]:bg-muted', 'data-[state=checked]:bg-primary', currentSize.switch, className), ...props, children: jsx("span", { "data-state": checked ? 'checked' : 'unchecked', "data-slot": "switch-thumb", className: cn('pointer-events-none block rounded-full ring-0 transition-transform', 'bg-card data-[state=checked]:bg-primary-foreground',
|
|
5782
|
+
const switchButton = (jsx("button", { type: "button", role: "switch", "aria-checked": checked, "data-state": checked ? 'checked' : 'unchecked', value: checked ? 'on' : 'off', "data-slot": "switch", disabled: disabled, onClick: handleClick, id: id, name: name, className: cn('peer inline-flex shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-all outline-none', 'focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:ring-[3px]', 'disabled:cursor-not-allowed disabled:opacity-50', 'data-[state=unchecked]:bg-muted', 'data-[state=checked]:bg-primary', currentSize.switch, className), ...props, children: jsx("span", { "data-state": checked ? 'checked' : 'unchecked', "data-slot": "switch-thumb", className: cn('pointer-events-none block rounded-full ring-0 transition-transform', 'bg-card data-[state=checked]:bg-primary-foreground', currentSize.thumb, currentSize.translate) }) }));
|
|
5771
5783
|
if (!label) {
|
|
5772
5784
|
return switchButton;
|
|
5773
5785
|
}
|
|
@@ -58436,33 +58448,39 @@ function ZoomSplitViewer({ src, alt = 'Preview', zoom = 2, lensSize = 160, paneO
|
|
|
58436
58448
|
}) })), isTouchDevice && (jsx("p", { className: `text-xs text-muted-foreground text-center mt-2 ${isMobile ? 'text-2xs' : ''}`, children: isMobile ? 'Touch & hold to zoom' : 'Touch and hold to zoom' }))] }));
|
|
58437
58449
|
}
|
|
58438
58450
|
|
|
58439
|
-
|
|
58440
|
-
|
|
58441
|
-
|
|
58442
|
-
|
|
58443
|
-
|
|
58444
|
-
|
|
58445
|
-
},
|
|
58446
|
-
md: {
|
|
58447
|
-
track: { width: 44, height: 24 },
|
|
58448
|
-
thumb: 20,
|
|
58449
|
-
on: 22,
|
|
58450
|
-
off: 2,
|
|
58451
|
-
},
|
|
58452
|
-
lg: {
|
|
58453
|
-
track: { width: 52, height: 28 },
|
|
58454
|
-
thumb: 24,
|
|
58455
|
-
on: 26,
|
|
58456
|
-
off: 2,
|
|
58457
|
-
},
|
|
58451
|
+
/** 1px border on the track, and the gap between thumb and track. */
|
|
58452
|
+
const TRACK_BORDER = 1;
|
|
58453
|
+
const THUMB_INSET = 2;
|
|
58454
|
+
const TRACK_SIZE = {
|
|
58455
|
+
sm: { width: 28, height: 16 },
|
|
58456
|
+
md: { width: 44, height: 24 },
|
|
58457
|
+
lg: { width: 52, height: 28 },
|
|
58458
58458
|
};
|
|
58459
|
+
/**
|
|
58460
|
+
* Thumb size and travel are derived from the track rather than listed per size:
|
|
58461
|
+
* hardcoding them left the thumb 1px from the top and bottom but flush against
|
|
58462
|
+
* the inner right edge when on, so it bulged past the track's rounded end.
|
|
58463
|
+
* Deriving keeps the inset equal on all four sides at every size.
|
|
58464
|
+
*/
|
|
58465
|
+
function switchGeometry(size) {
|
|
58466
|
+
const track = TRACK_SIZE[size];
|
|
58467
|
+
const innerWidth = track.width - TRACK_BORDER * 2;
|
|
58468
|
+
const innerHeight = track.height - TRACK_BORDER * 2;
|
|
58469
|
+
const thumb = innerHeight - THUMB_INSET * 2;
|
|
58470
|
+
return {
|
|
58471
|
+
track,
|
|
58472
|
+
thumb,
|
|
58473
|
+
off: THUMB_INSET,
|
|
58474
|
+
on: innerWidth - thumb - THUMB_INSET,
|
|
58475
|
+
};
|
|
58476
|
+
}
|
|
58459
58477
|
const Switch = React.forwardRef(({ checked = false, onCheckedChange, disabled = false, id, name, className, size = 'md', 'aria-label': ariaLabel, }, ref) => {
|
|
58460
58478
|
const handleClick = () => {
|
|
58461
58479
|
if (!disabled && onCheckedChange) {
|
|
58462
58480
|
onCheckedChange(!checked);
|
|
58463
58481
|
}
|
|
58464
58482
|
};
|
|
58465
|
-
const dims =
|
|
58483
|
+
const dims = switchGeometry(size);
|
|
58466
58484
|
return (jsx("button", { type: "button", role: "switch", "aria-checked": checked, "aria-label": ariaLabel, "data-state": checked ? 'checked' : 'unchecked', disabled: disabled, onClick: handleClick, ref: ref, id: id, name: name, style: {
|
|
58467
58485
|
width: dims.track.width,
|
|
58468
58486
|
height: dims.track.height,
|
|
@@ -64623,6 +64641,8 @@ const FILE_TYPE_PATTERNS = {
|
|
|
64623
64641
|
DOCUMENT: /\.(doc|docx|xls|xlsx|ppt|pptx)($|\?)/i,
|
|
64624
64642
|
VIDEO: /\.(mp4|m4v|avi|mov|wmv|flv|webm|mkv|ogv|mpe?g|3gp)($|\?)/i,
|
|
64625
64643
|
AUDIO: /\.(mp3|wav|flac|aac|ogg)($|\?)/i,
|
|
64644
|
+
/** Interactive viewer pages (supplier 360° spinners), not media files. */
|
|
64645
|
+
EMBED: /\.html?($|\?|#)/i,
|
|
64626
64646
|
};
|
|
64627
64647
|
const isFileType = {
|
|
64628
64648
|
pdf: (url) => FILE_TYPE_PATTERNS.PDF.test(url),
|
|
@@ -64630,6 +64650,7 @@ const isFileType = {
|
|
|
64630
64650
|
document: (url) => FILE_TYPE_PATTERNS.DOCUMENT.test(url),
|
|
64631
64651
|
video: (url) => FILE_TYPE_PATTERNS.VIDEO.test(url),
|
|
64632
64652
|
audio: (url) => FILE_TYPE_PATTERNS.AUDIO.test(url),
|
|
64653
|
+
embed: (url) => FILE_TYPE_PATTERNS.EMBED.test(url),
|
|
64633
64654
|
};
|
|
64634
64655
|
|
|
64635
64656
|
/** Seek to ~1st frame for static video thumbnails (HTML5 media fragment). */
|
|
@@ -64645,6 +64666,12 @@ const createVideoMediaItem = (src, index, idPrefix = 'vid') => ({
|
|
|
64645
64666
|
src,
|
|
64646
64667
|
thumbSrc: videoThumbnailSrc(src),
|
|
64647
64668
|
});
|
|
64669
|
+
/**
|
|
64670
|
+
* True when a "video" is really an interactive viewer page. Supplier 360°
|
|
64671
|
+
* spinners (Vision360, Varni) serve `text/html`, so `<video>` renders nothing
|
|
64672
|
+
* and there is no frame to seek for a thumbnail — they need an iframe.
|
|
64673
|
+
*/
|
|
64674
|
+
const isEmbedMediaItem = (item) => item.kind === 'video' && isFileType.embed(item.src);
|
|
64648
64675
|
const isPdfMediaItem = (item) => item.isPdf === true ||
|
|
64649
64676
|
isFileType.pdf(item.src) ||
|
|
64650
64677
|
(item.kind === 'certificate' && !isFileType.image(item.src));
|
|
@@ -65752,9 +65779,22 @@ const PdfPreview = ({ item }) => {
|
|
|
65752
65779
|
const docsViewer = `https://docs.google.com/gview?embedded=1&url=${encodeURIComponent(item.src)}`;
|
|
65753
65780
|
return (jsx("object", { data: item.src, type: "application/pdf", className: "h-full w-full bg-card", "aria-label": item.name ?? 'Certificate PDF', children: jsx("iframe", { title: item.name ?? 'Certificate PDF', className: "h-full w-full border-0 bg-card", src: docsViewer }) }));
|
|
65754
65781
|
};
|
|
65782
|
+
/**
|
|
65783
|
+
* Videos arrive as either a real file or a supplier's 360° viewer page. The URL
|
|
65784
|
+
* settles most cases, but not every viewer ends in `.html` (v360, loupe360),
|
|
65785
|
+
* so a `<video>` that fails to load falls back to the iframe as well. Remounted
|
|
65786
|
+
* per selection via `key`, which resets that fallback.
|
|
65787
|
+
*/
|
|
65788
|
+
const VideoOrEmbed = ({ item }) => {
|
|
65789
|
+
const [useEmbed, setUseEmbed] = useState(() => isEmbedMediaItem(item));
|
|
65790
|
+
if (useEmbed) {
|
|
65791
|
+
return (jsx("iframe", { src: item.src, title: item.name ?? '360\u00B0 viewer', className: "h-full w-full border-0 bg-card", allowFullScreen: true }));
|
|
65792
|
+
}
|
|
65793
|
+
return (jsx("video", { src: item.src, controls: true, onError: () => setUseEmbed(true), className: "h-full w-full object-cover", "aria-label": item.name ?? 'Video' }));
|
|
65794
|
+
};
|
|
65755
65795
|
const MainMedia = ({ item }) => {
|
|
65756
65796
|
if (item.kind === 'video') {
|
|
65757
|
-
return
|
|
65797
|
+
return jsx(VideoOrEmbed, { item: item });
|
|
65758
65798
|
}
|
|
65759
65799
|
if (isPdfMediaItem(item)) {
|
|
65760
65800
|
return jsx(PdfPreview, { item: item });
|
|
@@ -65764,7 +65804,7 @@ const MainMedia = ({ item }) => {
|
|
|
65764
65804
|
const Thumbnail = ({ item, isActive, onSelect }) => {
|
|
65765
65805
|
const Icon = KIND_ICON[item.kind];
|
|
65766
65806
|
const isPdf = isPdfMediaItem(item);
|
|
65767
|
-
const isVideoWithoutThumb = item.kind === 'video' && !item.thumbSrc;
|
|
65807
|
+
const isVideoWithoutThumb = item.kind === 'video' && (!item.thumbSrc || isEmbedMediaItem(item));
|
|
65768
65808
|
return (jsxs("button", { type: "button", onClick: onSelect, "aria-label": `Select ${item.name ?? item.kind}`, "aria-pressed": isActive, className: cn('group relative flex aspect-square w-full cursor-pointer overflow-hidden rounded-lg border transition-colors', isActive
|
|
65769
65809
|
? 'border-primary ring-2 ring-primary/30'
|
|
65770
65810
|
: 'border-border hover:border-primary/40'), children: [isPdf ? (jsxs("span", { className: "flex h-full w-full flex-col items-center justify-center gap-1 bg-muted p-2 text-muted-foreground", children: [jsx(Icon, { size: 20 }), jsx("span", { className: "text-[10px] font-medium uppercase", children: "PDF" })] })) : isVideoWithoutThumb ? (jsx("div", { className: "h-full w-full flex items-center justify-center bg-muted", children: jsx(Icon, { size: 24, className: "text-muted-foreground" }) })) : item.kind === 'video' ? (jsx("video", { src: item.thumbSrc ?? item.src, muted: true, playsInline: true, preload: "metadata", className: "h-full w-full object-cover", "aria-hidden": "true" })) : (jsx("img", { src: item.thumbSrc ?? item.src, alt: "", className: "h-full w-full object-cover" })), !isPdf && item.kind !== 'image' && !isVideoWithoutThumb && (jsx("span", { className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/30 text-white", children: jsx(Icon, { size: 20 }) })), isVideoWithoutThumb && (jsx("span", { className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/30" }))] }));
|
|
@@ -65785,7 +65825,7 @@ const MediaGallery = ({ items, thumbsVisible = 4, hideTabs = false, mainAspectCl
|
|
|
65785
65825
|
(kinds.length > 0 ? (jsx(Tabs, { value: activeKind ?? undefined, onValueChange: (v) => setActiveKind(v), tabStyle: "boxed", size: "sm", children: jsx(TabsList, { children: kinds.map((kind) => {
|
|
65786
65826
|
const Icon = KIND_ICON[kind];
|
|
65787
65827
|
return (jsx(TabsTrigger, { value: kind, icon: jsx(Icon, { size: 14 }), children: MEDIA_KIND_LABEL[kind] }, kind));
|
|
65788
|
-
}) }) })) : (jsx("div", { className: "h-9", "aria-hidden": "true" }))), selected && isZoomableImage ? (jsxs("div", { className: "relative", children: [jsx(ZoomSplitViewer, { src: selected.src, alt: selected.name ?? 'Media' }), !hideDownload && (jsx("div", { className: "absolute right-3 top-3 z-20", children: jsx("button", { type: "button", onClick: handleDownload, "aria-label": "Download media", className: "inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-md border border-border bg-card text-foreground transition-all hover:border-primary hover:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:border-primary focus-visible:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:outline-none", children: jsx(Download, { className: "h-4 w-4" }) }) }))] })) : selected ? (jsxs("div", { className: cn('relative overflow-hidden rounded-xl border border-border bg-muted', mainAspectClassName), children: [jsx(MainMedia, { item: selected }), !hideDownload && (jsx("div", { className: "absolute right-3 top-3", children: jsx("button", { type: "button", onClick: handleDownload, "aria-label": "Download media", className: "inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-md border border-border bg-card text-foreground transition-all hover:border-primary hover:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:border-primary focus-visible:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:outline-none", children: jsx(Download, { className: "h-4 w-4" }) }) }))] })) : (jsx("div", { className: cn('flex items-center justify-center rounded-xl border border-dashed border-border bg-muted/40 text-sm text-muted-foreground', mainAspectClassName), children: emptyText })), visibleItems.length > 1 && (jsx("div", { className: "grid gap-3", style: {
|
|
65828
|
+
}) }) })) : (jsx("div", { className: "h-9", "aria-hidden": "true" }))), selected && isZoomableImage ? (jsxs("div", { className: "relative", children: [jsx(ZoomSplitViewer, { src: selected.src, alt: selected.name ?? 'Media' }), !hideDownload && (jsx("div", { className: "absolute right-3 top-3 z-20", children: jsx("button", { type: "button", onClick: handleDownload, "aria-label": "Download media", className: "inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-md border border-border bg-card text-foreground transition-all hover:border-primary hover:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:border-primary focus-visible:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:outline-none", children: jsx(Download, { className: "h-4 w-4" }) }) }))] })) : selected ? (jsxs("div", { className: cn('relative overflow-hidden rounded-xl border border-border bg-muted', mainAspectClassName), children: [jsx(MainMedia, { item: selected }, selected.id), !hideDownload && !isEmbedMediaItem(selected) && (jsx("div", { className: "absolute right-3 top-3", children: jsx("button", { type: "button", onClick: handleDownload, "aria-label": "Download media", className: "inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-md border border-border bg-card text-foreground transition-all hover:border-primary hover:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:border-primary focus-visible:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:outline-none", children: jsx(Download, { className: "h-4 w-4" }) }) }))] })) : (jsx("div", { className: cn('flex items-center justify-center rounded-xl border border-dashed border-border bg-muted/40 text-sm text-muted-foreground', mainAspectClassName), children: emptyText })), visibleItems.length > 1 && (jsx("div", { className: "grid gap-3", style: {
|
|
65789
65829
|
gridTemplateColumns: `repeat(${Math.min(visibleItems.length, thumbsVisible)}, minmax(0, 1fr))`,
|
|
65790
65830
|
}, children: visibleItems.slice(0, thumbsVisible).map((item) => (jsx(Thumbnail, { item: item, isActive: selected?.id === item.id, onSelect: () => setSelected(item) }, item.id))) }))] }));
|
|
65791
65831
|
};
|
|
@@ -66086,7 +66126,13 @@ function CreateFormPageHeader({ title, onBack, backLabel = 'Go back', }) {
|
|
|
66086
66126
|
function CreateFormPageShell({ title, onBack, children, backLabel, scrollable = false, containerClassName, }) {
|
|
66087
66127
|
const content = (jsxs("div", { className: cn('mx-auto w-full max-w-4xl px-4 py-4', containerClassName), children: [jsx(CreateFormPageHeader, { title: title, onBack: onBack, backLabel: backLabel }), children] }));
|
|
66088
66128
|
if (scrollable) {
|
|
66089
|
-
|
|
66129
|
+
// `shrink-0` is the load-bearing part. As a flex item in the shell's
|
|
66130
|
+
// `flex-col overflow-y-auto` parent this div was shrunk to the parent's
|
|
66131
|
+
// height while its content overflowed, so the background stopped one
|
|
66132
|
+
// screenful down and taller forms showed the page background below it.
|
|
66133
|
+
// `min-h-full` alone cannot fix that — it resolves to the same shrunken
|
|
66134
|
+
// height. The parent already scrolls, so no inner scroller is needed.
|
|
66135
|
+
return (jsx("div", { className: "min-h-full shrink-0 bg-card", children: content }));
|
|
66090
66136
|
}
|
|
66091
66137
|
return content;
|
|
66092
66138
|
}
|