@devalok/shilp-sutra 0.45.1 → 0.47.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/AGENTS.md +13 -1
- package/MIGRATION.md +846 -808
- package/dist/_chunks/chat.js +6 -6
- package/dist/_chunks/chat.js.map +1 -1
- package/dist/_chunks/document-preview.js +5 -5
- package/dist/_chunks/image-preview.js +7 -7
- package/dist/_chunks/success.js +16 -16
- package/dist/composed/activity-feed.js +19 -19
- package/dist/composed/deadline-indicator.d.ts.map +1 -1
- package/dist/composed/deadline-indicator.js +31 -31
- package/dist/composed/deadline-indicator.js.map +1 -1
- package/dist/composed/emoji-picker.js +15 -15
- package/dist/composed/file-preview.js +18 -18
- package/dist/composed/file-preview.js.map +1 -1
- package/dist/composed/filter-bar.js +1 -1
- package/dist/composed/filter-bar.js.map +1 -1
- package/dist/composed/multi-select-popover.js +1 -1
- package/dist/composed/multi-select-popover.js.map +1 -1
- package/dist/tokens/base.css +6 -0
- package/dist/tokens/index.css +1 -1
- package/dist/tokens/semantic.css +1 -1
- package/dist/tokens/typography.css +10 -0
- package/dist/tokens/utilities.css +6 -0
- package/dist/ui/avatar.d.ts.map +1 -1
- package/dist/ui/avatar.js +57 -56
- package/dist/ui/avatar.js.map +1 -1
- package/dist/ui/badge-indicator.js +1 -1
- package/dist/ui/badge-indicator.js.map +1 -1
- package/dist/ui/data-table-body.js +27 -27
- package/dist/ui/data-table-card.js +20 -20
- package/dist/ui/data-table-toolbar.d.ts.map +1 -1
- package/dist/ui/data-table-toolbar.js +40 -36
- package/dist/ui/data-table-toolbar.js.map +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +37 -36
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/radio.js +1 -1
- package/dist/ui/radio.js.map +1 -1
- package/dist/ui/sidebar.js +7 -7
- package/dist/ui/stat-card.d.ts.map +1 -1
- package/dist/ui/stat-card.js +48 -47
- package/dist/ui/stat-card.js.map +1 -1
- package/dist/ui/surface.d.ts +46 -0
- package/dist/ui/surface.d.ts.map +1 -0
- package/dist/ui/surface.js +51 -0
- package/dist/ui/surface.js.map +1 -0
- package/dist/ui/text.js +6 -6
- package/dist/ui/text.js.map +1 -1
- package/docs/components/ui/surface.md +55 -0
- package/docs/recipes/install-tanstack-start.md +91 -44
- package/fonts/Manrope-Variable.woff2 +0 -0
- package/llms.txt +2 -1
- package/make-kit/components/overview.md +7 -3
- package/make-kit/components/surface.md +60 -0
- package/mcp-manifest.json +135 -3
- package/mcp-manifest.schema.json +6 -0
- package/package.json +7 -2
- package/scripts/welcome.mjs +4 -2
- package/skill/SKILL.md +1 -1
- package/skill/references/components.md +2 -1
- package/skill/references/setup-tanstack-start.md +91 -44
|
@@ -6,13 +6,13 @@ import { SimpleTooltip as r } from "./simple-tooltip.js";
|
|
|
6
6
|
import * as i from "react";
|
|
7
7
|
import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
8
8
|
import { IconClock as s } from "@tabler/icons-react";
|
|
9
|
-
import { motion as c } from "framer-motion";
|
|
9
|
+
import { motion as c, useReducedMotion as l } from "framer-motion";
|
|
10
10
|
//#region src/composed/deadline-indicator.tsx
|
|
11
|
-
function
|
|
11
|
+
function u(e) {
|
|
12
12
|
let t = Math.abs(e), n = e < 0 ? "Overdue by " : "", r = e >= 0 ? " left" : "";
|
|
13
13
|
return t < 1 ? e < 0 ? "Overdue" : "Due now" : t < 60 ? `${n}${Math.round(t)}m${r}` : t < 1440 ? `${n}${Math.round(t / 60)}h${r}` : `${n}${Math.round(t / 1440)}d${r}`;
|
|
14
14
|
}
|
|
15
|
-
function
|
|
15
|
+
function d(e) {
|
|
16
16
|
return e.toLocaleDateString(void 0, {
|
|
17
17
|
month: "short",
|
|
18
18
|
day: "numeric",
|
|
@@ -20,20 +20,20 @@ function u(e) {
|
|
|
20
20
|
minute: "2-digit"
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
-
var
|
|
24
|
-
let [,
|
|
23
|
+
var f = i.forwardRef(({ deadline: f, warningThreshold: p = 1440, criticalThreshold: m = 240, format: h = "relative", showIcon: g = !1, refreshInterval: _ = 6e4, className: v, ...y }, b) => {
|
|
24
|
+
let [, x] = i.useReducer((e) => e + 1, 0);
|
|
25
25
|
i.useEffect(() => {
|
|
26
|
-
if (!
|
|
27
|
-
let e = setInterval(
|
|
26
|
+
if (!_) return;
|
|
27
|
+
let e = setInterval(x, _);
|
|
28
28
|
return () => clearInterval(e);
|
|
29
|
-
}, [
|
|
30
|
-
let
|
|
31
|
-
|
|
32
|
-
let
|
|
33
|
-
if (
|
|
29
|
+
}, [_]);
|
|
30
|
+
let S = i.useMemo(() => f instanceof Date ? f : new Date(f), [f]), C = (S.getTime() - Date.now()) / 6e4, w;
|
|
31
|
+
w = C <= 0 ? "text-error-11 font-semibold" : C <= m ? "text-error-11" : C <= p ? "text-warning-11" : "text-success-11";
|
|
32
|
+
let T = h === "relative" ? u(C) : d(S), E = C <= 0, D = C <= m && C > 0, O = l(), k = (E || D) && !O, A = h === "relative", j = S.toLocaleString();
|
|
33
|
+
if (k) {
|
|
34
34
|
let i = /* @__PURE__ */ o(c.span, {
|
|
35
|
-
ref:
|
|
36
|
-
className: t("inline-flex items-center gap-ds-01 font-sans text-ds-sm",
|
|
35
|
+
ref: b,
|
|
36
|
+
className: t("inline-flex items-center gap-ds-01 font-sans text-ds-sm", w, v),
|
|
37
37
|
animate: { opacity: [
|
|
38
38
|
1,
|
|
39
39
|
.7,
|
|
@@ -43,31 +43,31 @@ var d = i.forwardRef(({ deadline: d, warningThreshold: f = 1440, criticalThresho
|
|
|
43
43
|
duration: 2,
|
|
44
44
|
repeat: Infinity
|
|
45
45
|
},
|
|
46
|
-
...e(
|
|
47
|
-
children: [
|
|
46
|
+
...e(y),
|
|
47
|
+
children: [g && /* @__PURE__ */ a(n, {
|
|
48
48
|
icon: s,
|
|
49
49
|
size: "xs"
|
|
50
|
-
}),
|
|
50
|
+
}), T]
|
|
51
51
|
});
|
|
52
|
-
return
|
|
53
|
-
content:
|
|
52
|
+
return A ? /* @__PURE__ */ a(r, {
|
|
53
|
+
content: j,
|
|
54
54
|
children: i
|
|
55
55
|
}) : i;
|
|
56
56
|
}
|
|
57
|
-
let
|
|
58
|
-
ref:
|
|
59
|
-
className: t("inline-flex items-center gap-ds-01 font-sans text-ds-sm",
|
|
60
|
-
...
|
|
61
|
-
children: [
|
|
57
|
+
let M = /* @__PURE__ */ o("span", {
|
|
58
|
+
ref: b,
|
|
59
|
+
className: t("inline-flex items-center gap-ds-01 font-sans text-ds-sm", w, v),
|
|
60
|
+
...y,
|
|
61
|
+
children: [g && /* @__PURE__ */ a(n, {
|
|
62
62
|
icon: s,
|
|
63
63
|
size: "xs"
|
|
64
|
-
}),
|
|
64
|
+
}), T]
|
|
65
65
|
});
|
|
66
|
-
return
|
|
67
|
-
content:
|
|
68
|
-
children:
|
|
69
|
-
}) :
|
|
66
|
+
return A ? /* @__PURE__ */ a(r, {
|
|
67
|
+
content: j,
|
|
68
|
+
children: M
|
|
69
|
+
}) : M;
|
|
70
70
|
});
|
|
71
|
-
|
|
71
|
+
f.displayName = "DeadlineIndicator";
|
|
72
72
|
//#endregion
|
|
73
|
-
export {
|
|
73
|
+
export { f as DeadlineIndicator };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deadline-indicator.js","names":[],"sources":["../../src/composed/deadline-indicator.tsx"],"sourcesContent":["'use client'\n\nimport { IconClock } from '@tabler/icons-react'\nimport { motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { Icon } from '../ui/icon'\nimport { motionProps } from '../ui/lib/motion'\nimport { cn } from '../ui/lib/utils'\nimport { SimpleTooltip } from './simple-tooltip'\n\n// ============================================================\n// Types\n// ============================================================\n\nexport interface DeadlineIndicatorProps extends React.HTMLAttributes<HTMLSpanElement> {\n deadline: Date | string\n /** Minutes before deadline to show warning color @default 1440 (24h) */\n warningThreshold?: number\n /** Minutes before deadline to show critical color @default 240 (4h) */\n criticalThreshold?: number\n /** @default 'relative' */\n format?: 'relative' | 'absolute'\n /** Show clock icon prefix */\n showIcon?: boolean\n /** Auto-refresh interval in ms to keep relative time up to date @default 60000 */\n refreshInterval?: number\n}\n\n// ============================================================\n// Helpers\n// ============================================================\n\nfunction formatRelative(minutesRemaining: number): string {\n const abs = Math.abs(minutesRemaining)\n const prefix = minutesRemaining < 0 ? 'Overdue by ' : ''\n const suffix = minutesRemaining >= 0 ? ' left' : ''\n\n if (abs < 1) return minutesRemaining < 0 ? 'Overdue' : 'Due now'\n if (abs < 60) return `${prefix}${Math.round(abs)}m${suffix}`\n if (abs < 1440) return `${prefix}${Math.round(abs / 60)}h${suffix}`\n return `${prefix}${Math.round(abs / 1440)}d${suffix}`\n}\n\nfunction formatAbsolute(date: Date): string {\n return date.toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n })\n}\n\n// ============================================================\n// DeadlineIndicator\n// ============================================================\n\nconst DeadlineIndicator = React.forwardRef<HTMLSpanElement, DeadlineIndicatorProps>(({\n deadline,\n warningThreshold = 1440,\n criticalThreshold = 240,\n format = 'relative',\n showIcon = false,\n refreshInterval = 60000,\n className,\n ...props\n}, ref) => {\n const [, forceUpdate] = React.useReducer((x: number) => x + 1, 0)\n React.useEffect(() => {\n if (!refreshInterval) return\n const id = setInterval(forceUpdate, refreshInterval)\n return () => clearInterval(id)\n }, [refreshInterval])\n\n const deadlineDate = React.useMemo(\n () => (deadline instanceof Date ? deadline : new Date(deadline)),\n [deadline],\n )\n const minutesRemaining = (deadlineDate.getTime() - Date.now()) / 60000\n\n let colorClass: string\n if (minutesRemaining <= 0) {\n colorClass = 'text-error-11 font-semibold'\n } else if (minutesRemaining <= criticalThreshold) {\n colorClass = 'text-error-11'\n } else if (minutesRemaining <= warningThreshold) {\n colorClass = 'text-warning-11'\n } else {\n colorClass = 'text-success-11'\n }\n\n const text = format === 'relative'\n ? formatRelative(minutesRemaining)\n : formatAbsolute(deadlineDate)\n\n const isOverdue = minutesRemaining <= 0\n const isCritical = minutesRemaining <= criticalThreshold && minutesRemaining > 0\n const shouldPulse = isOverdue || isCritical\n\n const showTooltip = format === 'relative'\n const tooltipContent = deadlineDate.toLocaleString()\n\n if (shouldPulse) {\n const inner = (\n <motion.span\n ref={ref}\n className={cn('inline-flex items-center gap-ds-01 font-sans text-ds-sm', colorClass, className)}\n animate={{ opacity: [1, 0.7, 1] }}\n transition={{ duration: 2, repeat: Infinity }}\n {...motionProps(props)}\n >\n {showIcon && <Icon icon={IconClock} size=\"xs\" />}\n {text}\n </motion.span>\n )\n\n return showTooltip ? <SimpleTooltip content={tooltipContent}>{inner}</SimpleTooltip> : inner\n }\n\n const inner = (\n <span\n ref={ref}\n className={cn('inline-flex items-center gap-ds-01 font-sans text-ds-sm', colorClass, className)}\n {...props}\n >\n {showIcon && <Icon icon={IconClock} size=\"xs\" />}\n {text}\n </span>\n )\n\n return showTooltip ? <SimpleTooltip content={tooltipContent}>{inner}</SimpleTooltip> : inner\n})\nDeadlineIndicator.displayName = 'DeadlineIndicator'\n\nexport { DeadlineIndicator }\n"],"mappings":";;;;;;;;;;AAiCA,SAAS,EAAe,GAAkC;CACxD,IAAM,IAAM,KAAK,IAAI,EAAiB,EAChC,IAAS,IAAmB,IAAI,gBAAgB,IAChD,IAAS,KAAoB,IAAI,UAAU;AAKjD,QAHI,IAAM,IAAU,IAAmB,IAAI,YAAY,YACnD,IAAM,KAAW,GAAG,IAAS,KAAK,MAAM,EAAI,CAAC,GAAG,MAChD,IAAM,OAAa,GAAG,IAAS,KAAK,MAAM,IAAM,GAAG,CAAC,GAAG,MACpD,GAAG,IAAS,KAAK,MAAM,IAAM,KAAK,CAAC,GAAG;;AAG/C,SAAS,EAAe,GAAoB;AAC1C,QAAO,EAAK,mBAAmB,KAAA,GAAW;EACxC,OAAO;EACP,KAAK;EACL,MAAM;EACN,QAAQ;EACT,CAAC;;AAOJ,IAAM,IAAoB,EAAM,YAAqD,EACnF,aACA,sBAAmB,MACnB,uBAAoB,KACpB,YAAS,YACT,cAAW,IACX,qBAAkB,KAClB,cACA,GAAG,KACF,MAAQ;CACT,IAAM,GAAG,KAAe,EAAM,YAAY,MAAc,IAAI,GAAG,EAAE;AACjE,GAAM,gBAAgB;AACpB,MAAI,CAAC,EAAiB;EACtB,IAAM,IAAK,YAAY,GAAa,EAAgB;AACpD,eAAa,cAAc,EAAG;IAC7B,CAAC,EAAgB,CAAC;CAErB,IAAM,IAAe,EAAM,cAClB,aAAoB,OAAO,IAAW,IAAI,KAAK,EAAS,EAC/D,CAAC,EAAS,CACX,EACK,KAAoB,EAAa,SAAS,GAAG,KAAK,KAAK,IAAI,KAE7D;AACJ,CAOE,IAPE,KAAoB,IACT,gCACJ,KAAoB,IAChB,kBACJ,KAAoB,IAChB,oBAEA;CAGf,IAAM,IAAO,MAAW,aACpB,EAAe,EAAiB,GAChC,EAAe,EAAa,
|
|
1
|
+
{"version":3,"file":"deadline-indicator.js","names":[],"sources":["../../src/composed/deadline-indicator.tsx"],"sourcesContent":["'use client'\n\nimport { IconClock } from '@tabler/icons-react'\nimport { motion, useReducedMotion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { Icon } from '../ui/icon'\nimport { motionProps } from '../ui/lib/motion'\nimport { cn } from '../ui/lib/utils'\nimport { SimpleTooltip } from './simple-tooltip'\n\n// ============================================================\n// Types\n// ============================================================\n\nexport interface DeadlineIndicatorProps extends React.HTMLAttributes<HTMLSpanElement> {\n deadline: Date | string\n /** Minutes before deadline to show warning color @default 1440 (24h) */\n warningThreshold?: number\n /** Minutes before deadline to show critical color @default 240 (4h) */\n criticalThreshold?: number\n /** @default 'relative' */\n format?: 'relative' | 'absolute'\n /** Show clock icon prefix */\n showIcon?: boolean\n /** Auto-refresh interval in ms to keep relative time up to date @default 60000 */\n refreshInterval?: number\n}\n\n// ============================================================\n// Helpers\n// ============================================================\n\nfunction formatRelative(minutesRemaining: number): string {\n const abs = Math.abs(minutesRemaining)\n const prefix = minutesRemaining < 0 ? 'Overdue by ' : ''\n const suffix = minutesRemaining >= 0 ? ' left' : ''\n\n if (abs < 1) return minutesRemaining < 0 ? 'Overdue' : 'Due now'\n if (abs < 60) return `${prefix}${Math.round(abs)}m${suffix}`\n if (abs < 1440) return `${prefix}${Math.round(abs / 60)}h${suffix}`\n return `${prefix}${Math.round(abs / 1440)}d${suffix}`\n}\n\nfunction formatAbsolute(date: Date): string {\n return date.toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n })\n}\n\n// ============================================================\n// DeadlineIndicator\n// ============================================================\n\nconst DeadlineIndicator = React.forwardRef<HTMLSpanElement, DeadlineIndicatorProps>(({\n deadline,\n warningThreshold = 1440,\n criticalThreshold = 240,\n format = 'relative',\n showIcon = false,\n refreshInterval = 60000,\n className,\n ...props\n}, ref) => {\n const [, forceUpdate] = React.useReducer((x: number) => x + 1, 0)\n React.useEffect(() => {\n if (!refreshInterval) return\n const id = setInterval(forceUpdate, refreshInterval)\n return () => clearInterval(id)\n }, [refreshInterval])\n\n const deadlineDate = React.useMemo(\n () => (deadline instanceof Date ? deadline : new Date(deadline)),\n [deadline],\n )\n const minutesRemaining = (deadlineDate.getTime() - Date.now()) / 60000\n\n let colorClass: string\n if (minutesRemaining <= 0) {\n colorClass = 'text-error-11 font-semibold'\n } else if (minutesRemaining <= criticalThreshold) {\n colorClass = 'text-error-11'\n } else if (minutesRemaining <= warningThreshold) {\n colorClass = 'text-warning-11'\n } else {\n colorClass = 'text-success-11'\n }\n\n const text = format === 'relative'\n ? formatRelative(minutesRemaining)\n : formatAbsolute(deadlineDate)\n\n const isOverdue = minutesRemaining <= 0\n const isCritical = minutesRemaining <= criticalThreshold && minutesRemaining > 0\n // Non-transform opacity loop — MotionConfig won't stop it, so drop the pulse\n // under reduced motion. Colour still signals urgency.\n const prefersReduced = useReducedMotion()\n const shouldPulse = (isOverdue || isCritical) && !prefersReduced\n\n const showTooltip = format === 'relative'\n const tooltipContent = deadlineDate.toLocaleString()\n\n if (shouldPulse) {\n const inner = (\n <motion.span\n ref={ref}\n className={cn('inline-flex items-center gap-ds-01 font-sans text-ds-sm', colorClass, className)}\n animate={{ opacity: [1, 0.7, 1] }}\n transition={{ duration: 2, repeat: Infinity }}\n {...motionProps(props)}\n >\n {showIcon && <Icon icon={IconClock} size=\"xs\" />}\n {text}\n </motion.span>\n )\n\n return showTooltip ? <SimpleTooltip content={tooltipContent}>{inner}</SimpleTooltip> : inner\n }\n\n const inner = (\n <span\n ref={ref}\n className={cn('inline-flex items-center gap-ds-01 font-sans text-ds-sm', colorClass, className)}\n {...props}\n >\n {showIcon && <Icon icon={IconClock} size=\"xs\" />}\n {text}\n </span>\n )\n\n return showTooltip ? <SimpleTooltip content={tooltipContent}>{inner}</SimpleTooltip> : inner\n})\nDeadlineIndicator.displayName = 'DeadlineIndicator'\n\nexport { DeadlineIndicator }\n"],"mappings":";;;;;;;;;;AAiCA,SAAS,EAAe,GAAkC;CACxD,IAAM,IAAM,KAAK,IAAI,EAAiB,EAChC,IAAS,IAAmB,IAAI,gBAAgB,IAChD,IAAS,KAAoB,IAAI,UAAU;AAKjD,QAHI,IAAM,IAAU,IAAmB,IAAI,YAAY,YACnD,IAAM,KAAW,GAAG,IAAS,KAAK,MAAM,EAAI,CAAC,GAAG,MAChD,IAAM,OAAa,GAAG,IAAS,KAAK,MAAM,IAAM,GAAG,CAAC,GAAG,MACpD,GAAG,IAAS,KAAK,MAAM,IAAM,KAAK,CAAC,GAAG;;AAG/C,SAAS,EAAe,GAAoB;AAC1C,QAAO,EAAK,mBAAmB,KAAA,GAAW;EACxC,OAAO;EACP,KAAK;EACL,MAAM;EACN,QAAQ;EACT,CAAC;;AAOJ,IAAM,IAAoB,EAAM,YAAqD,EACnF,aACA,sBAAmB,MACnB,uBAAoB,KACpB,YAAS,YACT,cAAW,IACX,qBAAkB,KAClB,cACA,GAAG,KACF,MAAQ;CACT,IAAM,GAAG,KAAe,EAAM,YAAY,MAAc,IAAI,GAAG,EAAE;AACjE,GAAM,gBAAgB;AACpB,MAAI,CAAC,EAAiB;EACtB,IAAM,IAAK,YAAY,GAAa,EAAgB;AACpD,eAAa,cAAc,EAAG;IAC7B,CAAC,EAAgB,CAAC;CAErB,IAAM,IAAe,EAAM,cAClB,aAAoB,OAAO,IAAW,IAAI,KAAK,EAAS,EAC/D,CAAC,EAAS,CACX,EACK,KAAoB,EAAa,SAAS,GAAG,KAAK,KAAK,IAAI,KAE7D;AACJ,CAOE,IAPE,KAAoB,IACT,gCACJ,KAAoB,IAChB,kBACJ,KAAoB,IAChB,oBAEA;CAGf,IAAM,IAAO,MAAW,aACpB,EAAe,EAAiB,GAChC,EAAe,EAAa,EAE1B,IAAY,KAAoB,GAChC,IAAa,KAAoB,KAAqB,IAAmB,GAGzE,IAAiB,GAAkB,EACnC,KAAe,KAAa,MAAe,CAAC,GAE5C,IAAc,MAAW,YACzB,IAAiB,EAAa,gBAAgB;AAEpD,KAAI,GAAa;EACf,IAAM,IACJ,kBAAC,EAAO,MAAR;GACO;GACL,WAAW,EAAG,2DAA2D,GAAY,EAAU;GAC/F,SAAS,EAAE,SAAS;IAAC;IAAG;IAAK;IAAE,EAAE;GACjC,YAAY;IAAE,UAAU;IAAG,QAAQ;IAAU;GAC7C,GAAI,EAAY,EAAM;aALxB,CAOG,KAAY,kBAAC,GAAD;IAAM,MAAM;IAAW,MAAK;IAAO,CAAA,EAC/C,EACW;;AAGhB,SAAO,IAAc,kBAAC,GAAD;GAAe,SAAS;aAAiB;GAAsB,CAAA,GAAG;;CAGzF,IAAM,IACJ,kBAAC,QAAD;EACO;EACL,WAAW,EAAG,2DAA2D,GAAY,EAAU;EAC/F,GAAI;YAHN,CAKG,KAAY,kBAAC,GAAD;GAAM,MAAM;GAAW,MAAK;GAAO,CAAA,EAC/C,EACI;;AAGT,QAAO,IAAc,kBAAC,GAAD;EAAe,SAAS;YAAiB;EAAsB,CAAA,GAAG;EACvF;AACF,EAAkB,cAAc"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { springs as e } from "../ui/lib/motion.js";
|
|
3
3
|
import { cn as t } from "../ui/lib/utils.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { Skeleton as n } from "../ui/skeleton.js";
|
|
5
|
+
import { i as r, r as i, t as a } from "../_chunks/popover.js";
|
|
6
6
|
import * as o from "react";
|
|
7
7
|
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
8
8
|
import { AnimatePresence as l, motion as u } from "framer-motion";
|
|
@@ -17,11 +17,11 @@ var d = o.lazy(() => import("@emoji-mart/react").then((e) => ({ default: e.defau
|
|
|
17
17
|
function p(e) {
|
|
18
18
|
return e === "auto" ? typeof document > "u" ? "light" : document.documentElement.classList.contains("dark") ? "dark" : "light" : e;
|
|
19
19
|
}
|
|
20
|
-
function m({ onSelect:
|
|
20
|
+
function m({ onSelect: r, set: i = "native", theme: a = "auto", previewPosition: c = "none", skinTonePosition: m = "search", className: h }) {
|
|
21
21
|
let [g, _] = o.useState(!1), [v, y] = o.useState(null);
|
|
22
22
|
return o.useEffect(() => {
|
|
23
|
-
_(!0), (f[
|
|
24
|
-
}, [
|
|
23
|
+
_(!0), (f[i] ?? f.native)().then((e) => y(e.default));
|
|
24
|
+
}, [i]), /* @__PURE__ */ s(l, {
|
|
25
25
|
mode: "wait",
|
|
26
26
|
children: g && v ? /* @__PURE__ */ s(u.div, {
|
|
27
27
|
initial: {
|
|
@@ -37,13 +37,13 @@ function m({ onSelect: n, set: r = "native", theme: i = "auto", previewPosition:
|
|
|
37
37
|
children: /* @__PURE__ */ s(o.Suspense, {
|
|
38
38
|
fallback: /* @__PURE__ */ s("div", {
|
|
39
39
|
className: t("rounded-surface", h),
|
|
40
|
-
children: /* @__PURE__ */ s(
|
|
40
|
+
children: /* @__PURE__ */ s(n, { className: "h-[435px] w-[352px] rounded-surface" })
|
|
41
41
|
}),
|
|
42
42
|
children: /* @__PURE__ */ s(d, {
|
|
43
43
|
data: v,
|
|
44
|
-
set:
|
|
45
|
-
onEmojiSelect:
|
|
46
|
-
theme: p(
|
|
44
|
+
set: i,
|
|
45
|
+
onEmojiSelect: r,
|
|
46
|
+
theme: p(a),
|
|
47
47
|
previewPosition: c,
|
|
48
48
|
skinTonePosition: m
|
|
49
49
|
})
|
|
@@ -54,25 +54,25 @@ function m({ onSelect: n, set: r = "native", theme: i = "auto", previewPosition:
|
|
|
54
54
|
exit: { opacity: 0 },
|
|
55
55
|
transition: e.snappy,
|
|
56
56
|
className: t("rounded-surface", h),
|
|
57
|
-
children: /* @__PURE__ */ s(
|
|
57
|
+
children: /* @__PURE__ */ s(n, { className: "h-[435px] w-[352px] rounded-surface" })
|
|
58
58
|
}, "skeleton")
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
-
function h({ children: e, align: t = "start", onSelect:
|
|
61
|
+
function h({ children: e, align: t = "start", onSelect: n, set: l, theme: u, previewPosition: d, skinTonePosition: f, className: p }) {
|
|
62
62
|
let [h, g] = o.useState(!1);
|
|
63
|
-
return /* @__PURE__ */ c(
|
|
63
|
+
return /* @__PURE__ */ c(a, {
|
|
64
64
|
open: h,
|
|
65
65
|
onOpenChange: g,
|
|
66
|
-
children: [/* @__PURE__ */ s(
|
|
66
|
+
children: [/* @__PURE__ */ s(r, {
|
|
67
67
|
asChild: !0,
|
|
68
68
|
children: e
|
|
69
|
-
}), /* @__PURE__ */ s(
|
|
69
|
+
}), /* @__PURE__ */ s(i, {
|
|
70
70
|
align: t,
|
|
71
71
|
className: "w-auto border-none bg-transparent p-0 shadow-none",
|
|
72
72
|
sideOffset: 8,
|
|
73
73
|
children: /* @__PURE__ */ s(m, {
|
|
74
74
|
onSelect: (e) => {
|
|
75
|
-
|
|
75
|
+
n(e), g(!1);
|
|
76
76
|
},
|
|
77
77
|
set: l,
|
|
78
78
|
theme: u,
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { springs as e, tweens as t } from "../ui/lib/motion.js";
|
|
3
3
|
import { cn as n } from "../ui/lib/utils.js";
|
|
4
4
|
import { Icon as r } from "../ui/icon.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { Skeleton as i } from "../ui/skeleton.js";
|
|
6
|
+
import { n as a } from "../_chunks/badge-group.js";
|
|
7
|
+
import { Button as o } from "../ui/button.js";
|
|
8
8
|
import { TruncatedText as s } from "../ui/truncated-text.js";
|
|
9
9
|
import { a as c, n as l, o as u, t as d } from "../_chunks/shared.js";
|
|
10
10
|
import * as f from "react";
|
|
@@ -13,10 +13,10 @@ import { IconDownload as h, IconMaximize as g, IconPlayerPause as _, IconPlayerP
|
|
|
13
13
|
import { AnimatePresence as y, motion as b } from "framer-motion";
|
|
14
14
|
//#region src/composed/file-preview/audio-preview.tsx
|
|
15
15
|
function x({ url: e, fileName: t, onError: n }) {
|
|
16
|
-
let i = f.useRef(null),
|
|
16
|
+
let i = f.useRef(null), a = f.useRef(null), [h, g] = f.useState(!1), [y, b] = f.useState(0), [x, S] = f.useState(0), [C, w] = f.useState(0), [T, E] = f.useState(1), [D, O] = f.useState(!1), [k, A] = f.useState(!1);
|
|
17
17
|
f.useEffect(() => {
|
|
18
18
|
function e(e) {
|
|
19
|
-
let t =
|
|
19
|
+
let t = a.current;
|
|
20
20
|
if (!t || !t.contains(document.activeElement) && document.activeElement !== t) return;
|
|
21
21
|
let n = i.current;
|
|
22
22
|
if (n) switch (e.key) {
|
|
@@ -51,7 +51,7 @@ function x({ url: e, fileName: t, onError: n }) {
|
|
|
51
51
|
message: "Could not load audio",
|
|
52
52
|
url: e
|
|
53
53
|
}) : /* @__PURE__ */ m("div", {
|
|
54
|
-
ref:
|
|
54
|
+
ref: a,
|
|
55
55
|
className: "rounded-surface bg-surface-raised shadow-raised overflow-hidden",
|
|
56
56
|
tabIndex: -1,
|
|
57
57
|
children: [
|
|
@@ -81,7 +81,7 @@ function x({ url: e, fileName: t, onError: n }) {
|
|
|
81
81
|
/* @__PURE__ */ m("div", {
|
|
82
82
|
className: "flex items-center gap-ds-04 px-ds-05 py-ds-04",
|
|
83
83
|
children: [
|
|
84
|
-
/* @__PURE__ */ p(
|
|
84
|
+
/* @__PURE__ */ p(o, {
|
|
85
85
|
variant: "solid",
|
|
86
86
|
size: "icon-sm",
|
|
87
87
|
onClick: j,
|
|
@@ -130,27 +130,27 @@ function x({ url: e, fileName: t, onError: n }) {
|
|
|
130
130
|
//#endregion
|
|
131
131
|
//#region src/composed/file-preview/embed-preview.tsx
|
|
132
132
|
function S({ url: e, embedUrl: n, onError: r }) {
|
|
133
|
-
let [
|
|
133
|
+
let [a, o] = f.useState(!1), [s, c] = f.useState(!1);
|
|
134
134
|
return f.useEffect(() => {
|
|
135
135
|
let e = setTimeout(() => {
|
|
136
|
-
|
|
136
|
+
a || (c(!0), r?.("Embed timed out"));
|
|
137
137
|
}, 15e3);
|
|
138
138
|
return () => clearTimeout(e);
|
|
139
|
-
}, [
|
|
139
|
+
}, [a, r]), s ? /* @__PURE__ */ p(d, {
|
|
140
140
|
message: "Could not load embed",
|
|
141
141
|
url: e
|
|
142
142
|
}) : /* @__PURE__ */ m("div", {
|
|
143
143
|
className: "relative w-full rounded-control overflow-hidden",
|
|
144
144
|
style: { aspectRatio: "16 / 9" },
|
|
145
|
-
children: [!
|
|
145
|
+
children: [!a && /* @__PURE__ */ p(i, { className: "absolute inset-0" }), /* @__PURE__ */ p(b.iframe, {
|
|
146
146
|
src: n,
|
|
147
147
|
title: "Embedded content",
|
|
148
|
-
onLoad: () =>
|
|
148
|
+
onLoad: () => o(!0),
|
|
149
149
|
onError: () => {
|
|
150
150
|
c(!0), r?.("Embed failed to load");
|
|
151
151
|
},
|
|
152
152
|
initial: { opacity: 0 },
|
|
153
|
-
animate: { opacity: +!!
|
|
153
|
+
animate: { opacity: +!!a },
|
|
154
154
|
transition: t.fade,
|
|
155
155
|
className: "absolute inset-0 h-full w-full border-none",
|
|
156
156
|
allowFullScreen: !0
|
|
@@ -323,7 +323,7 @@ function w({ url: n, onError: i }) {
|
|
|
323
323
|
variant: "dark"
|
|
324
324
|
}),
|
|
325
325
|
/* @__PURE__ */ m("span", {
|
|
326
|
-
className: "text-
|
|
326
|
+
className: "text-ds-sm font-mono text-white/70 tabular-nums",
|
|
327
327
|
children: [
|
|
328
328
|
u(E),
|
|
329
329
|
" / ",
|
|
@@ -333,7 +333,7 @@ function w({ url: n, onError: i }) {
|
|
|
333
333
|
/* @__PURE__ */ p("div", { className: "flex-1" }),
|
|
334
334
|
/* @__PURE__ */ m("button", {
|
|
335
335
|
onClick: () => R(1),
|
|
336
|
-
className: "text-
|
|
336
|
+
className: "text-ds-sm font-mono text-white/70 hover:text-white px-1 rounded-control-inner hover:bg-white/10 transition-colors",
|
|
337
337
|
"aria-label": `Playback speed: ${P}x`,
|
|
338
338
|
children: [P, "x"]
|
|
339
339
|
}),
|
|
@@ -396,7 +396,7 @@ function O(e) {
|
|
|
396
396
|
return r ? `https://www.loom.com/embed/${r[1]}` : e;
|
|
397
397
|
}
|
|
398
398
|
function k({ url: e, type: t, mimeType: c, alt: l, initialPage: u = 1, fileName: d, fileSize: g, onError: _, className: v, ...y }) {
|
|
399
|
-
let b = t ?? D(e, c), C = /* @__PURE__ */ p(
|
|
399
|
+
let b = t ?? D(e, c), C = /* @__PURE__ */ p(i, { className: "h-64 w-full rounded-control" });
|
|
400
400
|
return /* @__PURE__ */ m("div", {
|
|
401
401
|
className: n("flex flex-col gap-ds-03", v),
|
|
402
402
|
...y,
|
|
@@ -407,7 +407,7 @@ function k({ url: e, type: t, mimeType: c, alt: l, initialPage: u = 1, fileName:
|
|
|
407
407
|
mode: "middle",
|
|
408
408
|
className: "min-w-0 text-ds-sm font-semibold text-surface-fg",
|
|
409
409
|
children: d
|
|
410
|
-
}), g && /* @__PURE__ */ p(
|
|
410
|
+
}), g && /* @__PURE__ */ p(a, {
|
|
411
411
|
variant: "subtle",
|
|
412
412
|
size: "xs",
|
|
413
413
|
children: g
|
|
@@ -445,7 +445,7 @@ function k({ url: e, type: t, mimeType: c, alt: l, initialPage: u = 1, fileName:
|
|
|
445
445
|
}),
|
|
446
446
|
b !== "audio" && /* @__PURE__ */ p("div", {
|
|
447
447
|
className: "flex justify-end",
|
|
448
|
-
children: /* @__PURE__ */ p(
|
|
448
|
+
children: /* @__PURE__ */ p(o, {
|
|
449
449
|
variant: "ghost",
|
|
450
450
|
size: "xs",
|
|
451
451
|
startIcon: /* @__PURE__ */ p(r, { icon: h }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-preview.js","names":[],"sources":["../../src/composed/file-preview/audio-preview.tsx","../../src/composed/file-preview/embed-preview.tsx","../../src/composed/file-preview/video-preview.tsx","../../src/composed/file-preview.tsx"],"sourcesContent":["'use client'\n\nimport { IconPlayerPause,IconPlayerPlay } from '@tabler/icons-react'\nimport * as React from 'react'\n\nimport { Button } from '../../ui/button'\nimport { Icon } from '../../ui/icon'\nimport { TruncatedText } from '../../ui/truncated-text'\nimport { ErrorFallback, formatTime,MediaSlider,VolumeControl } from './shared'\n\n// ============================================================\n// Audio Preview — Branded mini-player (Spotify/SoundCloud style)\n// ============================================================\n\nexport default function AudioPreview({ url, fileName, onError }: { url: string; fileName?: string; onError?: (msg: string) => void }) {\n const audioRef = React.useRef<HTMLAudioElement>(null)\n const containerRef = React.useRef<HTMLDivElement>(null)\n const [playing, setPlaying] = React.useState(false)\n const [progress, setProgress] = React.useState(0)\n const [duration, setDuration] = React.useState(0)\n const [currentTime, setCurrentTime] = React.useState(0)\n const [volume, setVolume] = React.useState(1)\n const [muted, setMuted] = React.useState(false)\n const [error, setError] = React.useState(false)\n\n // Keyboard shortcuts\n React.useEffect(() => {\n function handleKeyDown(e: KeyboardEvent) {\n const el = containerRef.current\n if (!el || !el.contains(document.activeElement) && document.activeElement !== el) return\n const a = audioRef.current\n if (!a) return\n\n switch (e.key) {\n case ' ':\n e.preventDefault()\n setPlaying(prev => {\n if (prev) a.pause(); else a.play()\n return !prev\n })\n break\n case 'ArrowRight':\n e.preventDefault()\n a.currentTime = Math.min(a.duration || 0, a.currentTime + 5)\n break\n case 'ArrowLeft':\n e.preventDefault()\n a.currentTime = Math.max(0, a.currentTime - 5)\n break\n case 'm':\n e.preventDefault()\n setMuted(prev => {\n a.muted = !prev\n return !prev\n })\n break\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n \n }, [])\n\n function togglePlay() {\n if (!audioRef.current) return\n if (playing) audioRef.current.pause()\n else audioRef.current.play()\n setPlaying(!playing)\n }\n\n function handleSeek(nextProgress: number) {\n if (!audioRef.current || !duration) return\n audioRef.current.currentTime = (nextProgress / 100) * duration\n }\n\n function toggleMute() {\n if (!audioRef.current) return\n const next = !muted\n setMuted(next)\n audioRef.current.muted = next\n }\n\n if (error) return <ErrorFallback message=\"Could not load audio\" url={url} />\n\n return (\n <div ref={containerRef} className=\"rounded-surface bg-surface-raised shadow-raised overflow-hidden\" tabIndex={-1}>\n {/* eslint-disable-next-line jsx-a11y/media-has-caption */}\n <audio\n ref={audioRef}\n src={url}\n onTimeUpdate={() => {\n if (!audioRef.current) return\n setCurrentTime(audioRef.current.currentTime)\n if (audioRef.current.duration) setProgress((audioRef.current.currentTime / audioRef.current.duration) * 100)\n }}\n onLoadedMetadata={() => setDuration(audioRef.current?.duration ?? 0)}\n onEnded={() => setPlaying(false)}\n onError={() => { setError(true); onError?.('Audio failed to load') }}\n />\n\n {/* Progress bar — full width at top (Spotify style) */}\n <div className=\"px-ds-03 pt-ds-02\">\n <MediaSlider\n value={progress}\n max={100}\n step={0.1}\n onValueChange={handleSeek}\n tone=\"light\"\n ariaLabel=\"Audio progress\"\n />\n </div>\n\n {/* Controls */}\n <div className=\"flex items-center gap-ds-04 px-ds-05 py-ds-04\">\n <Button\n variant=\"solid\"\n size=\"icon-sm\"\n onClick={togglePlay}\n aria-label={playing ? 'Pause' : 'Play'}\n title={playing ? 'Pause' : 'Play'}\n className=\"shrink-0\"\n >\n {playing ? (\n <Icon icon={IconPlayerPause} size=\"sm\" />\n ) : (\n <Icon icon={IconPlayerPlay} size=\"sm\" className=\"ml-0.5\" />\n )}\n </Button>\n\n {/* File name + time */}\n <div className=\"flex-1 min-w-0\">\n {fileName && (\n <TruncatedText as=\"p\" mode=\"middle\" className=\"text-ds-sm font-semibold text-surface-fg\">\n {fileName}\n </TruncatedText>\n )}\n <p className=\"text-ds-xs font-mono text-surface-fg-muted tabular-nums\">\n {formatTime(currentTime)} / {formatTime(duration)}\n </p>\n </div>\n\n {/* Volume */}\n <VolumeControl\n volume={volume}\n muted={muted}\n onVolumeChange={(v) => {\n setVolume(v)\n if (audioRef.current) audioRef.current.volume = v\n if (v > 0 && muted) {\n setMuted(false)\n if (audioRef.current) audioRef.current.muted = false\n }\n }}\n onMuteToggle={toggleMute}\n variant=\"light\"\n />\n </div>\n </div>\n )\n}\n","'use client'\n\nimport { motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { tweens } from '../../ui/lib/motion'\nimport { Skeleton } from '../../ui/skeleton'\nimport { ErrorFallback } from './shared'\n\n// ============================================================\n// Embed Preview — 16:9 with error timeout\n// ============================================================\n\nexport default function EmbedPreview({ url, embedUrl, onError }: { url: string; embedUrl: string; onError?: (msg: string) => void }) {\n const [loaded, setLoaded] = React.useState(false)\n const [error, setError] = React.useState(false)\n\n // Timeout — if iframe doesn't load in 15s, show error\n React.useEffect(() => {\n const timer = setTimeout(() => {\n if (!loaded) { setError(true); onError?.('Embed timed out') }\n }, 15000)\n return () => clearTimeout(timer)\n }, [loaded, onError])\n\n if (error) return <ErrorFallback message=\"Could not load embed\" url={url} />\n\n return (\n <div className=\"relative w-full rounded-control overflow-hidden\" style={{ aspectRatio: '16 / 9' }}>\n {!loaded && <Skeleton className=\"absolute inset-0\" />}\n <motion.iframe\n src={embedUrl}\n title=\"Embedded content\"\n onLoad={() => setLoaded(true)}\n onError={() => { setError(true); onError?.('Embed failed to load') }}\n initial={{ opacity: 0 }}\n animate={{ opacity: loaded ? 1 : 0 }}\n transition={tweens.fade}\n className=\"absolute inset-0 h-full w-full border-none\"\n allowFullScreen\n />\n </div>\n )\n}\n","'use client'\n\nimport {\n IconMaximize,\n IconPlayerPause,\n IconPlayerPlay,\n} from '@tabler/icons-react'\nimport { AnimatePresence,motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { Icon } from '../../ui/icon'\nimport { springs, tweens } from '../../ui/lib/motion'\nimport { ErrorFallback, formatTime,MediaSlider,VolumeControl } from './shared'\n\n// ============================================================\n// Video Preview — Custom player with DS styling\n// ============================================================\n\nconst PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2] as const\n\nexport default function VideoPreview({ url, onError }: { url: string; onError?: (msg: string) => void }) {\n const videoRef = React.useRef<HTMLVideoElement>(null)\n const containerRef = React.useRef<HTMLDivElement>(null)\n const [playing, setPlaying] = React.useState(false)\n const [progress, setProgress] = React.useState(0)\n const [duration, setDuration] = React.useState(0)\n const [currentTime, setCurrentTime] = React.useState(0)\n const [muted, setMuted] = React.useState(false)\n const [error, setError] = React.useState(false)\n const [showControls, setShowControls] = React.useState(true)\n const [playbackRate, setPlaybackRate] = React.useState(1)\n // Kept current every render so the mount-time keydown handler (empty deps)\n // reads the live rate instead of the stale initial 1x. See #91.\n const playbackRateRef = React.useRef(playbackRate)\n playbackRateRef.current = playbackRate\n const hideTimer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n\n React.useEffect(() => {\n return () => clearTimeout(hideTimer.current)\n }, [])\n\n // Keyboard shortcuts — YouTube style\n React.useEffect(() => {\n function handleKeyDown(e: KeyboardEvent) {\n const el = containerRef.current\n if (!el || !el.contains(document.activeElement) && document.activeElement !== el) return\n const v = videoRef.current\n if (!v) return\n\n switch (e.key) {\n case ' ':\n case 'k':\n e.preventDefault()\n setPlaying(prev => {\n if (prev) v.pause(); else v.play()\n return !prev\n })\n break\n case 'ArrowRight':\n e.preventDefault()\n v.currentTime = Math.min(v.duration, v.currentTime + 5)\n break\n case 'ArrowLeft':\n e.preventDefault()\n v.currentTime = Math.max(0, v.currentTime - 5)\n break\n case 'j':\n e.preventDefault()\n v.currentTime = Math.max(0, v.currentTime - 10)\n break\n case 'l':\n e.preventDefault()\n v.currentTime = Math.min(v.duration, v.currentTime + 10)\n break\n case 'm':\n e.preventDefault()\n setMuted(prev => {\n v.muted = !prev\n return !prev\n })\n break\n case 'f':\n e.preventDefault()\n v.requestFullscreen?.()\n break\n case '>':\n e.preventDefault()\n cyclePlaybackRate(1)\n break\n case '<':\n e.preventDefault()\n cyclePlaybackRate(-1)\n break\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [])\n\n function cyclePlaybackRate(direction: 1 | -1) {\n const idx = PLAYBACK_RATES.indexOf(playbackRateRef.current as typeof PLAYBACK_RATES[number])\n const nextIdx = Math.max(0, Math.min(PLAYBACK_RATES.length - 1, idx + direction))\n const next = PLAYBACK_RATES[nextIdx]\n setPlaybackRate(next)\n if (videoRef.current) videoRef.current.playbackRate = next\n }\n\n function togglePlay() {\n if (!videoRef.current) return\n if (playing) videoRef.current.pause()\n else videoRef.current.play()\n setPlaying(!playing)\n }\n\n function handleSeek(nextProgress: number) {\n if (!videoRef.current || !duration) return\n videoRef.current.currentTime = (nextProgress / 100) * duration\n }\n\n function handleMouseMove() {\n setShowControls(true)\n clearTimeout(hideTimer.current)\n if (playing) {\n hideTimer.current = setTimeout(() => setShowControls(false), 3000)\n }\n }\n\n if (error) return <ErrorFallback message=\"Could not load video\" url={url} />\n\n return (\n <div\n ref={containerRef}\n className=\"group relative rounded-control bg-black overflow-hidden\"\n onMouseMove={handleMouseMove}\n onMouseLeave={() => playing && setShowControls(false)}\n tabIndex={-1}\n >\n {/* eslint-disable-next-line jsx-a11y/media-has-caption */}\n <video\n ref={videoRef}\n src={url}\n className=\"max-h-[70vh] w-full\"\n onClick={togglePlay}\n muted={muted}\n onTimeUpdate={() => {\n if (!videoRef.current) return\n setCurrentTime(videoRef.current.currentTime)\n if (videoRef.current.duration) setProgress((videoRef.current.currentTime / videoRef.current.duration) * 100)\n }}\n onLoadedMetadata={() => setDuration(videoRef.current?.duration ?? 0)}\n onEnded={() => { setPlaying(false); setShowControls(true) }}\n onError={() => { setError(true); onError?.('Video failed to load') }}\n playsInline\n />\n\n {/* Play button overlay — shown when paused */}\n {!playing && (\n <motion.button\n initial={{ opacity: 0, scale: 0.8 }}\n animate={{ opacity: 1, scale: 1 }}\n transition={springs.snappy}\n onClick={togglePlay}\n className=\"absolute inset-0 flex items-center justify-center bg-black/30\"\n aria-label=\"Play video\"\n >\n <div className=\"flex h-14 w-14 items-center justify-center rounded-pill bg-white/90 shadow-floating\">\n <Icon icon={IconPlayerPlay} size=\"xl\" className=\"text-surface-fg ml-0.5\" />\n </div>\n </motion.button>\n )}\n\n {/* Bottom controls — auto-hide after 3s during playback */}\n <AnimatePresence>\n {showControls && duration > 0 && (\n <motion.div\n initial={{ opacity: 0, y: 10 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 10 }}\n transition={tweens.fade}\n className=\"absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/80 to-transparent px-ds-04 pb-ds-04 pt-ds-08\"\n >\n {/* Progress bar */}\n <MediaSlider\n className=\"mb-ds-03\"\n value={progress}\n max={100}\n step={0.1}\n onValueChange={handleSeek}\n tone=\"dark\"\n ariaLabel=\"Video progress\"\n />\n\n {/* Controls row */}\n <div className=\"flex items-center gap-ds-03\">\n <button onClick={togglePlay} className=\"text-white hover:text-white/80\" aria-label={playing ? 'Pause' : 'Play'} title={playing ? 'Pause' : 'Play'}>\n {playing ? <Icon icon={IconPlayerPause} size=\"lg\" /> : <Icon icon={IconPlayerPlay} size=\"lg\" />}\n </button>\n <VolumeControl\n volume={muted ? 0 : 1}\n muted={muted}\n onVolumeChange={(v) => {\n if (videoRef.current) videoRef.current.volume = v\n if (v > 0 && muted) { setMuted(false); if (videoRef.current) videoRef.current.muted = false }\n }}\n onMuteToggle={() => { setMuted(!muted); if (videoRef.current) videoRef.current.muted = !muted }}\n variant=\"dark\"\n />\n <span className=\"text-[11px] font-mono text-white/70 tabular-nums\">\n {formatTime(currentTime)} / {formatTime(duration)}\n </span>\n <div className=\"flex-1\" />\n <button\n onClick={() => cyclePlaybackRate(1)}\n className=\"text-[11px] font-mono text-white/70 hover:text-white px-1 rounded-control-inner hover:bg-white/10 transition-colors\"\n aria-label={`Playback speed: ${playbackRate}x`}\n >\n {playbackRate}x\n </button>\n <button\n onClick={() => videoRef.current?.requestFullscreen?.()}\n className=\"text-white hover:text-white/80\"\n aria-label=\"Fullscreen (F)\"\n title=\"Fullscreen\"\n >\n <Icon icon={IconMaximize} size=\"sm\" />\n </button>\n </div>\n </motion.div>\n )}\n </AnimatePresence>\n </div>\n )\n}\n","'use client'\n\nimport { IconDownload } from '@tabler/icons-react'\nimport * as React from 'react'\n\nimport { Badge } from '../ui/badge'\nimport { Button } from '../ui/button'\nimport { Icon } from '../ui/icon'\nimport { cn } from '../ui/lib/utils'\nimport { Skeleton } from '../ui/skeleton'\nimport { TruncatedText } from '../ui/truncated-text'\n\n// Sub-components: direct imports for non-peer-dep renderers,\n// React.lazy for renderers that pull in optional peer deps\n// (react-zoom-pan-pinch, react-pdf) to preserve code-splitting.\nconst LazyImagePreview = React.lazy(() => import('./file-preview/image-preview'))\nconst LazyDocumentPreview = React.lazy(() => import('./file-preview/document-preview'))\nimport AudioPreview from './file-preview/audio-preview'\nimport EmbedPreview from './file-preview/embed-preview'\nimport VideoPreview from './file-preview/video-preview'\n\n// ============================================================\n// Types\n// ============================================================\n\ntype FileType = 'image' | 'pdf' | 'video' | 'audio' | 'embed'\n\nexport interface FilePreviewProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onError'> {\n url: string\n /** Auto-detected from URL/mimeType if omitted */\n type?: FileType\n mimeType?: string\n alt?: string\n /** PDF starting page @default 1 */\n initialPage?: number\n /** File name for display */\n fileName?: string\n /** File size for display (e.g. '2.4 MB') */\n fileSize?: string\n /** Called on errors (broken URL, CORS, timeout) */\n onError?: (error: string) => void\n}\n\n// ============================================================\n// Type Detection\n// ============================================================\n\nfunction detectType(url: string, mimeType?: string): FileType {\n if (mimeType) {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType === 'application/pdf') return 'pdf'\n if (mimeType.startsWith('video/')) return 'video'\n if (mimeType.startsWith('audio/')) return 'audio'\n }\n\n const ext = url.split('?')[0].split('.').pop()?.toLowerCase()\n if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'avif', 'bmp'].includes(ext ?? '')) return 'image'\n if (ext === 'pdf') return 'pdf'\n if (['mp4', 'webm', 'ogg', 'mov'].includes(ext ?? '')) return 'video'\n if (['mp3', 'wav', 'ogg', 'aac', 'flac'].includes(ext ?? '')) return 'audio'\n\n if (url.includes('figma.com') || url.includes('loom.com') || url.includes('youtube.com') || url.includes('youtu.be') || url.includes('vimeo.com')) {\n return 'embed'\n }\n\n return 'image'\n}\n\nfunction getEmbedUrl(url: string): string {\n const ytMatch = url.match(/(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/)([^&?]+)/)\n if (ytMatch) return `https://www.youtube.com/embed/${ytMatch[1]}`\n\n const vimeoMatch = url.match(/vimeo\\.com\\/(\\d+)/)\n if (vimeoMatch) return `https://player.vimeo.com/video/${vimeoMatch[1]}`\n\n if (url.includes('figma.com')) return `https://www.figma.com/embed?embed_host=share&url=${encodeURIComponent(url)}`\n\n const loomMatch = url.match(/loom\\.com\\/share\\/([^?]+)/)\n if (loomMatch) return `https://www.loom.com/embed/${loomMatch[1]}`\n\n return url\n}\n\n// ============================================================\n// FilePreview — Main component (router)\n// ============================================================\n\nfunction FilePreview({\n url,\n type: typeProp,\n mimeType,\n alt,\n initialPage = 1,\n fileName,\n fileSize,\n onError,\n className,\n ...props\n}: FilePreviewProps) {\n const type = typeProp ?? detectType(url, mimeType)\n const fallback = <Skeleton className=\"h-64 w-full rounded-control\" />\n\n return (\n <div className={cn('flex flex-col gap-ds-03', className)} {...props}>\n {/* File info bar */}\n {(fileName || fileSize) && (\n <div className=\"flex items-center gap-ds-03\">\n {fileName && (\n <TruncatedText mode=\"middle\" className=\"min-w-0 text-ds-sm font-semibold text-surface-fg\">\n {fileName}\n </TruncatedText>\n )}\n {fileSize && <Badge variant=\"subtle\" size=\"xs\">{fileSize}</Badge>}\n </div>\n )}\n\n {/* Preview */}\n {type === 'image' && (\n <React.Suspense fallback={fallback}>\n <LazyImagePreview url={url} alt={alt} onError={onError} />\n </React.Suspense>\n )}\n {type === 'pdf' && (\n <React.Suspense fallback={fallback}>\n <LazyDocumentPreview url={url} initialPage={initialPage} onError={onError} />\n </React.Suspense>\n )}\n {type === 'video' && <VideoPreview url={url} onError={onError} />}\n {type === 'audio' && <AudioPreview url={url} fileName={fileName} onError={onError} />}\n {type === 'embed' && <EmbedPreview url={url} embedUrl={getEmbedUrl(url)} onError={onError} />}\n\n {/* Download — not shown for audio (it has its own layout) */}\n {type !== 'audio' && (\n <div className=\"flex justify-end\">\n <Button variant=\"ghost\" size=\"xs\" startIcon={<Icon icon={IconDownload} />} asChild>\n <a href={url} download target=\"_blank\" rel=\"noopener noreferrer\">Download</a>\n </Button>\n </div>\n )}\n </div>\n )\n}\n\nexport { FilePreview }\nexport type { FileType }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-preview.js","names":[],"sources":["../../src/composed/file-preview/audio-preview.tsx","../../src/composed/file-preview/embed-preview.tsx","../../src/composed/file-preview/video-preview.tsx","../../src/composed/file-preview.tsx"],"sourcesContent":["'use client'\n\nimport { IconPlayerPause,IconPlayerPlay } from '@tabler/icons-react'\nimport * as React from 'react'\n\nimport { Button } from '../../ui/button'\nimport { Icon } from '../../ui/icon'\nimport { TruncatedText } from '../../ui/truncated-text'\nimport { ErrorFallback, formatTime,MediaSlider,VolumeControl } from './shared'\n\n// ============================================================\n// Audio Preview — Branded mini-player (Spotify/SoundCloud style)\n// ============================================================\n\nexport default function AudioPreview({ url, fileName, onError }: { url: string; fileName?: string; onError?: (msg: string) => void }) {\n const audioRef = React.useRef<HTMLAudioElement>(null)\n const containerRef = React.useRef<HTMLDivElement>(null)\n const [playing, setPlaying] = React.useState(false)\n const [progress, setProgress] = React.useState(0)\n const [duration, setDuration] = React.useState(0)\n const [currentTime, setCurrentTime] = React.useState(0)\n const [volume, setVolume] = React.useState(1)\n const [muted, setMuted] = React.useState(false)\n const [error, setError] = React.useState(false)\n\n // Keyboard shortcuts\n React.useEffect(() => {\n function handleKeyDown(e: KeyboardEvent) {\n const el = containerRef.current\n if (!el || !el.contains(document.activeElement) && document.activeElement !== el) return\n const a = audioRef.current\n if (!a) return\n\n switch (e.key) {\n case ' ':\n e.preventDefault()\n setPlaying(prev => {\n if (prev) a.pause(); else a.play()\n return !prev\n })\n break\n case 'ArrowRight':\n e.preventDefault()\n a.currentTime = Math.min(a.duration || 0, a.currentTime + 5)\n break\n case 'ArrowLeft':\n e.preventDefault()\n a.currentTime = Math.max(0, a.currentTime - 5)\n break\n case 'm':\n e.preventDefault()\n setMuted(prev => {\n a.muted = !prev\n return !prev\n })\n break\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n \n }, [])\n\n function togglePlay() {\n if (!audioRef.current) return\n if (playing) audioRef.current.pause()\n else audioRef.current.play()\n setPlaying(!playing)\n }\n\n function handleSeek(nextProgress: number) {\n if (!audioRef.current || !duration) return\n audioRef.current.currentTime = (nextProgress / 100) * duration\n }\n\n function toggleMute() {\n if (!audioRef.current) return\n const next = !muted\n setMuted(next)\n audioRef.current.muted = next\n }\n\n if (error) return <ErrorFallback message=\"Could not load audio\" url={url} />\n\n return (\n <div ref={containerRef} className=\"rounded-surface bg-surface-raised shadow-raised overflow-hidden\" tabIndex={-1}>\n {/* eslint-disable-next-line jsx-a11y/media-has-caption */}\n <audio\n ref={audioRef}\n src={url}\n onTimeUpdate={() => {\n if (!audioRef.current) return\n setCurrentTime(audioRef.current.currentTime)\n if (audioRef.current.duration) setProgress((audioRef.current.currentTime / audioRef.current.duration) * 100)\n }}\n onLoadedMetadata={() => setDuration(audioRef.current?.duration ?? 0)}\n onEnded={() => setPlaying(false)}\n onError={() => { setError(true); onError?.('Audio failed to load') }}\n />\n\n {/* Progress bar — full width at top (Spotify style) */}\n <div className=\"px-ds-03 pt-ds-02\">\n <MediaSlider\n value={progress}\n max={100}\n step={0.1}\n onValueChange={handleSeek}\n tone=\"light\"\n ariaLabel=\"Audio progress\"\n />\n </div>\n\n {/* Controls */}\n <div className=\"flex items-center gap-ds-04 px-ds-05 py-ds-04\">\n <Button\n variant=\"solid\"\n size=\"icon-sm\"\n onClick={togglePlay}\n aria-label={playing ? 'Pause' : 'Play'}\n title={playing ? 'Pause' : 'Play'}\n className=\"shrink-0\"\n >\n {playing ? (\n <Icon icon={IconPlayerPause} size=\"sm\" />\n ) : (\n <Icon icon={IconPlayerPlay} size=\"sm\" className=\"ml-0.5\" />\n )}\n </Button>\n\n {/* File name + time */}\n <div className=\"flex-1 min-w-0\">\n {fileName && (\n <TruncatedText as=\"p\" mode=\"middle\" className=\"text-ds-sm font-semibold text-surface-fg\">\n {fileName}\n </TruncatedText>\n )}\n <p className=\"text-ds-xs font-mono text-surface-fg-muted tabular-nums\">\n {formatTime(currentTime)} / {formatTime(duration)}\n </p>\n </div>\n\n {/* Volume */}\n <VolumeControl\n volume={volume}\n muted={muted}\n onVolumeChange={(v) => {\n setVolume(v)\n if (audioRef.current) audioRef.current.volume = v\n if (v > 0 && muted) {\n setMuted(false)\n if (audioRef.current) audioRef.current.muted = false\n }\n }}\n onMuteToggle={toggleMute}\n variant=\"light\"\n />\n </div>\n </div>\n )\n}\n","'use client'\n\nimport { motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { tweens } from '../../ui/lib/motion'\nimport { Skeleton } from '../../ui/skeleton'\nimport { ErrorFallback } from './shared'\n\n// ============================================================\n// Embed Preview — 16:9 with error timeout\n// ============================================================\n\nexport default function EmbedPreview({ url, embedUrl, onError }: { url: string; embedUrl: string; onError?: (msg: string) => void }) {\n const [loaded, setLoaded] = React.useState(false)\n const [error, setError] = React.useState(false)\n\n // Timeout — if iframe doesn't load in 15s, show error\n React.useEffect(() => {\n const timer = setTimeout(() => {\n if (!loaded) { setError(true); onError?.('Embed timed out') }\n }, 15000)\n return () => clearTimeout(timer)\n }, [loaded, onError])\n\n if (error) return <ErrorFallback message=\"Could not load embed\" url={url} />\n\n return (\n <div className=\"relative w-full rounded-control overflow-hidden\" style={{ aspectRatio: '16 / 9' }}>\n {!loaded && <Skeleton className=\"absolute inset-0\" />}\n <motion.iframe\n src={embedUrl}\n title=\"Embedded content\"\n onLoad={() => setLoaded(true)}\n onError={() => { setError(true); onError?.('Embed failed to load') }}\n initial={{ opacity: 0 }}\n animate={{ opacity: loaded ? 1 : 0 }}\n transition={tweens.fade}\n className=\"absolute inset-0 h-full w-full border-none\"\n allowFullScreen\n />\n </div>\n )\n}\n","'use client'\n\nimport {\n IconMaximize,\n IconPlayerPause,\n IconPlayerPlay,\n} from '@tabler/icons-react'\nimport { AnimatePresence,motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { Icon } from '../../ui/icon'\nimport { springs, tweens } from '../../ui/lib/motion'\nimport { ErrorFallback, formatTime,MediaSlider,VolumeControl } from './shared'\n\n// ============================================================\n// Video Preview — Custom player with DS styling\n// ============================================================\n\nconst PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2] as const\n\nexport default function VideoPreview({ url, onError }: { url: string; onError?: (msg: string) => void }) {\n const videoRef = React.useRef<HTMLVideoElement>(null)\n const containerRef = React.useRef<HTMLDivElement>(null)\n const [playing, setPlaying] = React.useState(false)\n const [progress, setProgress] = React.useState(0)\n const [duration, setDuration] = React.useState(0)\n const [currentTime, setCurrentTime] = React.useState(0)\n const [muted, setMuted] = React.useState(false)\n const [error, setError] = React.useState(false)\n const [showControls, setShowControls] = React.useState(true)\n const [playbackRate, setPlaybackRate] = React.useState(1)\n // Kept current every render so the mount-time keydown handler (empty deps)\n // reads the live rate instead of the stale initial 1x. See #91.\n const playbackRateRef = React.useRef(playbackRate)\n playbackRateRef.current = playbackRate\n const hideTimer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n\n React.useEffect(() => {\n return () => clearTimeout(hideTimer.current)\n }, [])\n\n // Keyboard shortcuts — YouTube style\n React.useEffect(() => {\n function handleKeyDown(e: KeyboardEvent) {\n const el = containerRef.current\n if (!el || !el.contains(document.activeElement) && document.activeElement !== el) return\n const v = videoRef.current\n if (!v) return\n\n switch (e.key) {\n case ' ':\n case 'k':\n e.preventDefault()\n setPlaying(prev => {\n if (prev) v.pause(); else v.play()\n return !prev\n })\n break\n case 'ArrowRight':\n e.preventDefault()\n v.currentTime = Math.min(v.duration, v.currentTime + 5)\n break\n case 'ArrowLeft':\n e.preventDefault()\n v.currentTime = Math.max(0, v.currentTime - 5)\n break\n case 'j':\n e.preventDefault()\n v.currentTime = Math.max(0, v.currentTime - 10)\n break\n case 'l':\n e.preventDefault()\n v.currentTime = Math.min(v.duration, v.currentTime + 10)\n break\n case 'm':\n e.preventDefault()\n setMuted(prev => {\n v.muted = !prev\n return !prev\n })\n break\n case 'f':\n e.preventDefault()\n v.requestFullscreen?.()\n break\n case '>':\n e.preventDefault()\n cyclePlaybackRate(1)\n break\n case '<':\n e.preventDefault()\n cyclePlaybackRate(-1)\n break\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [])\n\n function cyclePlaybackRate(direction: 1 | -1) {\n const idx = PLAYBACK_RATES.indexOf(playbackRateRef.current as typeof PLAYBACK_RATES[number])\n const nextIdx = Math.max(0, Math.min(PLAYBACK_RATES.length - 1, idx + direction))\n const next = PLAYBACK_RATES[nextIdx]\n setPlaybackRate(next)\n if (videoRef.current) videoRef.current.playbackRate = next\n }\n\n function togglePlay() {\n if (!videoRef.current) return\n if (playing) videoRef.current.pause()\n else videoRef.current.play()\n setPlaying(!playing)\n }\n\n function handleSeek(nextProgress: number) {\n if (!videoRef.current || !duration) return\n videoRef.current.currentTime = (nextProgress / 100) * duration\n }\n\n function handleMouseMove() {\n setShowControls(true)\n clearTimeout(hideTimer.current)\n if (playing) {\n hideTimer.current = setTimeout(() => setShowControls(false), 3000)\n }\n }\n\n if (error) return <ErrorFallback message=\"Could not load video\" url={url} />\n\n return (\n <div\n ref={containerRef}\n className=\"group relative rounded-control bg-black overflow-hidden\"\n onMouseMove={handleMouseMove}\n onMouseLeave={() => playing && setShowControls(false)}\n tabIndex={-1}\n >\n {/* eslint-disable-next-line jsx-a11y/media-has-caption */}\n <video\n ref={videoRef}\n src={url}\n className=\"max-h-[70vh] w-full\"\n onClick={togglePlay}\n muted={muted}\n onTimeUpdate={() => {\n if (!videoRef.current) return\n setCurrentTime(videoRef.current.currentTime)\n if (videoRef.current.duration) setProgress((videoRef.current.currentTime / videoRef.current.duration) * 100)\n }}\n onLoadedMetadata={() => setDuration(videoRef.current?.duration ?? 0)}\n onEnded={() => { setPlaying(false); setShowControls(true) }}\n onError={() => { setError(true); onError?.('Video failed to load') }}\n playsInline\n />\n\n {/* Play button overlay — shown when paused */}\n {!playing && (\n <motion.button\n initial={{ opacity: 0, scale: 0.8 }}\n animate={{ opacity: 1, scale: 1 }}\n transition={springs.snappy}\n onClick={togglePlay}\n className=\"absolute inset-0 flex items-center justify-center bg-black/30\"\n aria-label=\"Play video\"\n >\n <div className=\"flex h-14 w-14 items-center justify-center rounded-pill bg-white/90 shadow-floating\">\n <Icon icon={IconPlayerPlay} size=\"xl\" className=\"text-surface-fg ml-0.5\" />\n </div>\n </motion.button>\n )}\n\n {/* Bottom controls — auto-hide after 3s during playback */}\n <AnimatePresence>\n {showControls && duration > 0 && (\n <motion.div\n initial={{ opacity: 0, y: 10 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 10 }}\n transition={tweens.fade}\n className=\"absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/80 to-transparent px-ds-04 pb-ds-04 pt-ds-08\"\n >\n {/* Progress bar */}\n <MediaSlider\n className=\"mb-ds-03\"\n value={progress}\n max={100}\n step={0.1}\n onValueChange={handleSeek}\n tone=\"dark\"\n ariaLabel=\"Video progress\"\n />\n\n {/* Controls row */}\n <div className=\"flex items-center gap-ds-03\">\n <button onClick={togglePlay} className=\"text-white hover:text-white/80\" aria-label={playing ? 'Pause' : 'Play'} title={playing ? 'Pause' : 'Play'}>\n {playing ? <Icon icon={IconPlayerPause} size=\"lg\" /> : <Icon icon={IconPlayerPlay} size=\"lg\" />}\n </button>\n <VolumeControl\n volume={muted ? 0 : 1}\n muted={muted}\n onVolumeChange={(v) => {\n if (videoRef.current) videoRef.current.volume = v\n if (v > 0 && muted) { setMuted(false); if (videoRef.current) videoRef.current.muted = false }\n }}\n onMuteToggle={() => { setMuted(!muted); if (videoRef.current) videoRef.current.muted = !muted }}\n variant=\"dark\"\n />\n <span className=\"text-ds-sm font-mono text-white/70 tabular-nums\">\n {formatTime(currentTime)} / {formatTime(duration)}\n </span>\n <div className=\"flex-1\" />\n <button\n onClick={() => cyclePlaybackRate(1)}\n className=\"text-ds-sm font-mono text-white/70 hover:text-white px-1 rounded-control-inner hover:bg-white/10 transition-colors\"\n aria-label={`Playback speed: ${playbackRate}x`}\n >\n {playbackRate}x\n </button>\n <button\n onClick={() => videoRef.current?.requestFullscreen?.()}\n className=\"text-white hover:text-white/80\"\n aria-label=\"Fullscreen (F)\"\n title=\"Fullscreen\"\n >\n <Icon icon={IconMaximize} size=\"sm\" />\n </button>\n </div>\n </motion.div>\n )}\n </AnimatePresence>\n </div>\n )\n}\n","'use client'\n\nimport { IconDownload } from '@tabler/icons-react'\nimport * as React from 'react'\n\nimport { Badge } from '../ui/badge'\nimport { Button } from '../ui/button'\nimport { Icon } from '../ui/icon'\nimport { cn } from '../ui/lib/utils'\nimport { Skeleton } from '../ui/skeleton'\nimport { TruncatedText } from '../ui/truncated-text'\n\n// Sub-components: direct imports for non-peer-dep renderers,\n// React.lazy for renderers that pull in optional peer deps\n// (react-zoom-pan-pinch, react-pdf) to preserve code-splitting.\nconst LazyImagePreview = React.lazy(() => import('./file-preview/image-preview'))\nconst LazyDocumentPreview = React.lazy(() => import('./file-preview/document-preview'))\nimport AudioPreview from './file-preview/audio-preview'\nimport EmbedPreview from './file-preview/embed-preview'\nimport VideoPreview from './file-preview/video-preview'\n\n// ============================================================\n// Types\n// ============================================================\n\ntype FileType = 'image' | 'pdf' | 'video' | 'audio' | 'embed'\n\nexport interface FilePreviewProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onError'> {\n url: string\n /** Auto-detected from URL/mimeType if omitted */\n type?: FileType\n mimeType?: string\n alt?: string\n /** PDF starting page @default 1 */\n initialPage?: number\n /** File name for display */\n fileName?: string\n /** File size for display (e.g. '2.4 MB') */\n fileSize?: string\n /** Called on errors (broken URL, CORS, timeout) */\n onError?: (error: string) => void\n}\n\n// ============================================================\n// Type Detection\n// ============================================================\n\nfunction detectType(url: string, mimeType?: string): FileType {\n if (mimeType) {\n if (mimeType.startsWith('image/')) return 'image'\n if (mimeType === 'application/pdf') return 'pdf'\n if (mimeType.startsWith('video/')) return 'video'\n if (mimeType.startsWith('audio/')) return 'audio'\n }\n\n const ext = url.split('?')[0].split('.').pop()?.toLowerCase()\n if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'avif', 'bmp'].includes(ext ?? '')) return 'image'\n if (ext === 'pdf') return 'pdf'\n if (['mp4', 'webm', 'ogg', 'mov'].includes(ext ?? '')) return 'video'\n if (['mp3', 'wav', 'ogg', 'aac', 'flac'].includes(ext ?? '')) return 'audio'\n\n if (url.includes('figma.com') || url.includes('loom.com') || url.includes('youtube.com') || url.includes('youtu.be') || url.includes('vimeo.com')) {\n return 'embed'\n }\n\n return 'image'\n}\n\nfunction getEmbedUrl(url: string): string {\n const ytMatch = url.match(/(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/)([^&?]+)/)\n if (ytMatch) return `https://www.youtube.com/embed/${ytMatch[1]}`\n\n const vimeoMatch = url.match(/vimeo\\.com\\/(\\d+)/)\n if (vimeoMatch) return `https://player.vimeo.com/video/${vimeoMatch[1]}`\n\n if (url.includes('figma.com')) return `https://www.figma.com/embed?embed_host=share&url=${encodeURIComponent(url)}`\n\n const loomMatch = url.match(/loom\\.com\\/share\\/([^?]+)/)\n if (loomMatch) return `https://www.loom.com/embed/${loomMatch[1]}`\n\n return url\n}\n\n// ============================================================\n// FilePreview — Main component (router)\n// ============================================================\n\nfunction FilePreview({\n url,\n type: typeProp,\n mimeType,\n alt,\n initialPage = 1,\n fileName,\n fileSize,\n onError,\n className,\n ...props\n}: FilePreviewProps) {\n const type = typeProp ?? detectType(url, mimeType)\n const fallback = <Skeleton className=\"h-64 w-full rounded-control\" />\n\n return (\n <div className={cn('flex flex-col gap-ds-03', className)} {...props}>\n {/* File info bar */}\n {(fileName || fileSize) && (\n <div className=\"flex items-center gap-ds-03\">\n {fileName && (\n <TruncatedText mode=\"middle\" className=\"min-w-0 text-ds-sm font-semibold text-surface-fg\">\n {fileName}\n </TruncatedText>\n )}\n {fileSize && <Badge variant=\"subtle\" size=\"xs\">{fileSize}</Badge>}\n </div>\n )}\n\n {/* Preview */}\n {type === 'image' && (\n <React.Suspense fallback={fallback}>\n <LazyImagePreview url={url} alt={alt} onError={onError} />\n </React.Suspense>\n )}\n {type === 'pdf' && (\n <React.Suspense fallback={fallback}>\n <LazyDocumentPreview url={url} initialPage={initialPage} onError={onError} />\n </React.Suspense>\n )}\n {type === 'video' && <VideoPreview url={url} onError={onError} />}\n {type === 'audio' && <AudioPreview url={url} fileName={fileName} onError={onError} />}\n {type === 'embed' && <EmbedPreview url={url} embedUrl={getEmbedUrl(url)} onError={onError} />}\n\n {/* Download — not shown for audio (it has its own layout) */}\n {type !== 'audio' && (\n <div className=\"flex justify-end\">\n <Button variant=\"ghost\" size=\"xs\" startIcon={<Icon icon={IconDownload} />} asChild>\n <a href={url} download target=\"_blank\" rel=\"noopener noreferrer\">Download</a>\n </Button>\n </div>\n )}\n </div>\n )\n}\n\nexport { FilePreview }\nexport type { FileType }\n"],"mappings":"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"}
|
|
@@ -83,7 +83,7 @@ function b({ label: i, value: a, onValueChange: o, options: s, className: c }) {
|
|
|
83
83
|
}), u > 0 && /* @__PURE__ */ p(g.span, {
|
|
84
84
|
initial: { scale: .8 },
|
|
85
85
|
animate: { scale: 1 },
|
|
86
|
-
transition: e.
|
|
86
|
+
transition: e.snappy,
|
|
87
87
|
className: "inline-flex",
|
|
88
88
|
children: /* @__PURE__ */ p(r, {
|
|
89
89
|
size: "xs",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filter-bar.js","names":[],"sources":["../../src/composed/filter-bar.tsx"],"sourcesContent":["'use client'\n\nimport { IconChevronDown } from '@tabler/icons-react'\nimport { motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { Badge } from '../ui/badge'\nimport { Button } from '../ui/button'\nimport { Icon } from '../ui/icon'\nimport { springs } from '../ui/lib/motion'\nimport { cn } from '../ui/lib/utils'\nimport { SearchInput } from '../ui/search-input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../ui/select'\nimport { type MultiSelectItem,MultiSelectPopover } from './multi-select-popover'\n\n// ============================================================\n// Context\n// ============================================================\n\ntype FilterBarSize = 'xs' | 'sm' | 'md'\n\nconst FilterBarContext = React.createContext<{ size: FilterBarSize }>({ size: 'sm' })\n\n// ============================================================\n// FilterBar\n// ============================================================\n\n/**\n * A toolbar for filtering lists/tables. Composes a search input, slot-based\n * filter controls (Select, MultiSelect), and an optional \"Clear all\" button.\n * Size is propagated to all child controls via context.\n */\nexport interface FilterBarProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Controlled search input value. Omit `onSearchChange` to hide the search input entirely. */\n searchValue?: string\n /** Called when the search input changes. If not provided, the search input is hidden. */\n onSearchChange?: (value: string) => void\n searchPlaceholder?: string\n /** When provided, a \"Clear all\" button appears that calls this handler. */\n onClearAll?: () => void\n /** Size propagated to all child controls. @default 'sm' */\n size?: FilterBarSize\n}\n\nconst FilterBar = React.forwardRef<HTMLDivElement, FilterBarProps>(({\n searchValue,\n onSearchChange,\n searchPlaceholder = 'Search...',\n onClearAll,\n size = 'sm',\n children,\n className,\n ...props\n}, ref) => {\n return (\n <FilterBarContext.Provider value={{ size }}>\n <div\n ref={ref}\n className={cn('flex flex-wrap items-center gap-ds-03', className)}\n role=\"toolbar\"\n aria-label=\"Filters\"\n {...props}\n >\n {onSearchChange && (\n <SearchInput\n size={size}\n value={searchValue}\n onChange={(e) => onSearchChange(e.target.value)}\n onClear={searchValue ? () => onSearchChange('') : undefined}\n placeholder={searchPlaceholder}\n className=\"w-48\"\n />\n )}\n {children}\n {onClearAll && (\n <Button variant=\"ghost\" size={size} onClick={onClearAll}>\n Clear all\n </Button>\n )}\n </div>\n </FilterBarContext.Provider>\n )\n})\nFilterBar.displayName = 'FilterBar'\n\n// ============================================================\n// FilterSelect\n// ============================================================\n\nexport interface FilterSelectProps {\n label: string\n value: string\n onValueChange: (value: string) => void\n options: { value: string; label: string }[]\n /** Label for the \"all\" option @default 'All' */\n allLabel?: string\n className?: string\n}\n\nfunction FilterSelect({\n label,\n value,\n onValueChange,\n options,\n allLabel = 'All',\n className,\n}: FilterSelectProps) {\n const { size } = React.useContext(FilterBarContext)\n const isFiltered = value !== '' && value !== 'all'\n\n return (\n <Select value={value} onValueChange={onValueChange}>\n <SelectTrigger\n size={size}\n className={cn('w-40', isFiltered && 'border-accent-7', className)}\n >\n <SelectValue placeholder={label} />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">{allLabel}</SelectItem>\n {options.map((o) => (\n <SelectItem key={o.value} value={o.value}>\n {o.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\n// ============================================================\n// FilterMultiSelect\n// ============================================================\n\nexport interface FilterMultiSelectProps {\n label: string\n value: string[]\n onValueChange: (values: string[]) => void\n options: { value: string; label: string }[]\n className?: string\n}\n\nfunction FilterMultiSelect({\n label,\n value,\n onValueChange,\n options,\n className,\n}: FilterMultiSelectProps) {\n const { size } = React.useContext(FilterBarContext)\n const count = value.length\n\n const items: MultiSelectItem[] = React.useMemo(\n () => options.map((o) => ({ id: o.value, label: o.label })),\n [options],\n )\n\n // Trigger button sized by FilterBar context\n const triggerSizeClasses: Record<string, string> = {\n xs: 'h-ds-xs-plus text-ds-sm px-ds-02',\n sm: 'h-ds-sm text-ds-sm px-ds-03',\n md: 'h-ds-md text-ds-md px-ds-04',\n }\n\n return (\n <MultiSelectPopover\n items={items}\n value={value}\n onValueChange={onValueChange}\n searchPlaceholder={`Search ${label.toLowerCase()}...`}\n >\n <button\n type=\"button\"\n className={cn(\n 'flex items-center justify-between gap-ds-02 whitespace-nowrap rounded-control border bg-surface-raised-hover text-surface-fg',\n 'hover:bg-surface-raised-active transition-colors duration-fast-01 ease-productive-standard',\n triggerSizeClasses[size],\n count > 0 ? 'border-accent-7' : 'border-surface-border-strong',\n 'w-40',\n className,\n )}\n >\n <span className=\"flex min-w-0 items-center gap-ds-02\">\n <span className=\"truncate\">{label}</span>\n {count > 0 && (\n <motion.span\n key={count}\n initial={{ scale: 0.8 }}\n animate={{ scale: 1 }}\n transition={springs.
|
|
1
|
+
{"version":3,"file":"filter-bar.js","names":[],"sources":["../../src/composed/filter-bar.tsx"],"sourcesContent":["'use client'\n\nimport { IconChevronDown } from '@tabler/icons-react'\nimport { motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { Badge } from '../ui/badge'\nimport { Button } from '../ui/button'\nimport { Icon } from '../ui/icon'\nimport { springs } from '../ui/lib/motion'\nimport { cn } from '../ui/lib/utils'\nimport { SearchInput } from '../ui/search-input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../ui/select'\nimport { type MultiSelectItem,MultiSelectPopover } from './multi-select-popover'\n\n// ============================================================\n// Context\n// ============================================================\n\ntype FilterBarSize = 'xs' | 'sm' | 'md'\n\nconst FilterBarContext = React.createContext<{ size: FilterBarSize }>({ size: 'sm' })\n\n// ============================================================\n// FilterBar\n// ============================================================\n\n/**\n * A toolbar for filtering lists/tables. Composes a search input, slot-based\n * filter controls (Select, MultiSelect), and an optional \"Clear all\" button.\n * Size is propagated to all child controls via context.\n */\nexport interface FilterBarProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Controlled search input value. Omit `onSearchChange` to hide the search input entirely. */\n searchValue?: string\n /** Called when the search input changes. If not provided, the search input is hidden. */\n onSearchChange?: (value: string) => void\n searchPlaceholder?: string\n /** When provided, a \"Clear all\" button appears that calls this handler. */\n onClearAll?: () => void\n /** Size propagated to all child controls. @default 'sm' */\n size?: FilterBarSize\n}\n\nconst FilterBar = React.forwardRef<HTMLDivElement, FilterBarProps>(({\n searchValue,\n onSearchChange,\n searchPlaceholder = 'Search...',\n onClearAll,\n size = 'sm',\n children,\n className,\n ...props\n}, ref) => {\n return (\n <FilterBarContext.Provider value={{ size }}>\n <div\n ref={ref}\n className={cn('flex flex-wrap items-center gap-ds-03', className)}\n role=\"toolbar\"\n aria-label=\"Filters\"\n {...props}\n >\n {onSearchChange && (\n <SearchInput\n size={size}\n value={searchValue}\n onChange={(e) => onSearchChange(e.target.value)}\n onClear={searchValue ? () => onSearchChange('') : undefined}\n placeholder={searchPlaceholder}\n className=\"w-48\"\n />\n )}\n {children}\n {onClearAll && (\n <Button variant=\"ghost\" size={size} onClick={onClearAll}>\n Clear all\n </Button>\n )}\n </div>\n </FilterBarContext.Provider>\n )\n})\nFilterBar.displayName = 'FilterBar'\n\n// ============================================================\n// FilterSelect\n// ============================================================\n\nexport interface FilterSelectProps {\n label: string\n value: string\n onValueChange: (value: string) => void\n options: { value: string; label: string }[]\n /** Label for the \"all\" option @default 'All' */\n allLabel?: string\n className?: string\n}\n\nfunction FilterSelect({\n label,\n value,\n onValueChange,\n options,\n allLabel = 'All',\n className,\n}: FilterSelectProps) {\n const { size } = React.useContext(FilterBarContext)\n const isFiltered = value !== '' && value !== 'all'\n\n return (\n <Select value={value} onValueChange={onValueChange}>\n <SelectTrigger\n size={size}\n className={cn('w-40', isFiltered && 'border-accent-7', className)}\n >\n <SelectValue placeholder={label} />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">{allLabel}</SelectItem>\n {options.map((o) => (\n <SelectItem key={o.value} value={o.value}>\n {o.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\n// ============================================================\n// FilterMultiSelect\n// ============================================================\n\nexport interface FilterMultiSelectProps {\n label: string\n value: string[]\n onValueChange: (values: string[]) => void\n options: { value: string; label: string }[]\n className?: string\n}\n\nfunction FilterMultiSelect({\n label,\n value,\n onValueChange,\n options,\n className,\n}: FilterMultiSelectProps) {\n const { size } = React.useContext(FilterBarContext)\n const count = value.length\n\n const items: MultiSelectItem[] = React.useMemo(\n () => options.map((o) => ({ id: o.value, label: o.label })),\n [options],\n )\n\n // Trigger button sized by FilterBar context\n const triggerSizeClasses: Record<string, string> = {\n xs: 'h-ds-xs-plus text-ds-sm px-ds-02',\n sm: 'h-ds-sm text-ds-sm px-ds-03',\n md: 'h-ds-md text-ds-md px-ds-04',\n }\n\n return (\n <MultiSelectPopover\n items={items}\n value={value}\n onValueChange={onValueChange}\n searchPlaceholder={`Search ${label.toLowerCase()}...`}\n >\n <button\n type=\"button\"\n className={cn(\n 'flex items-center justify-between gap-ds-02 whitespace-nowrap rounded-control border bg-surface-raised-hover text-surface-fg',\n 'hover:bg-surface-raised-active transition-colors duration-fast-01 ease-productive-standard',\n triggerSizeClasses[size],\n count > 0 ? 'border-accent-7' : 'border-surface-border-strong',\n 'w-40',\n className,\n )}\n >\n <span className=\"flex min-w-0 items-center gap-ds-02\">\n <span className=\"truncate\">{label}</span>\n {count > 0 && (\n <motion.span\n key={count}\n initial={{ scale: 0.8 }}\n animate={{ scale: 1 }}\n transition={springs.snappy}\n className=\"inline-flex\"\n >\n <Badge size=\"xs\" variant=\"solid\" className=\"ml-ds-01\">\n {count}\n </Badge>\n </motion.span>\n )}\n </span>\n <Icon icon={IconChevronDown} size=\"xs\" className=\"opacity-50 shrink-0\" />\n </button>\n </MultiSelectPopover>\n )\n}\n\nexport { FilterBar, FilterMultiSelect,FilterSelect }\n"],"mappings":";;;;;;;;;;;;;;AA2BA,IAAM,IAAmB,EAAM,cAAuC,EAAE,MAAM,MAAM,CAAC,EAuB/E,IAAY,EAAM,YAA4C,EAClE,gBACA,mBACA,uBAAoB,aACpB,eACA,UAAO,MACP,aACA,cACA,GAAG,KACF,MAEC,kBAAC,EAAiB,UAAlB;CAA2B,OAAO,EAAE,SAAM;WACxC,kBAAC,OAAD;EACO;EACL,WAAW,EAAG,yCAAyC,EAAU;EACjE,MAAK;EACL,cAAW;EACX,GAAI;YALN;GAOG,KACC,kBAAC,GAAD;IACQ;IACN,OAAO;IACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;IAC/C,SAAS,UAAoB,EAAe,GAAG,GAAG,KAAA;IAClD,aAAa;IACb,WAAU;IACV,CAAA;GAEH;GACA,KACC,kBAAC,GAAD;IAAQ,SAAQ;IAAc;IAAM,SAAS;cAAY;IAEhD,CAAA;GAEP;;CACoB,CAAA,CAE9B;AACF,EAAU,cAAc;AAgBxB,SAAS,EAAa,EACpB,UACA,UACA,kBACA,YACA,cAAW,OACX,gBACoB;CACpB,IAAM,EAAE,YAAS,EAAM,WAAW,EAAiB;AAGnD,QACE,kBAAC,GAAD;EAAe;EAAsB;YAArC,CACE,kBAAC,GAAD;GACQ;GACN,WAAW,EAAG,QAND,MAAU,MAAM,MAAU,SAMH,mBAAmB,EAAU;aAEjE,kBAAC,GAAD,EAAa,aAAa,GAAS,CAAA;GACrB,CAAA,EAChB,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;GAAY,OAAM;aAAO;GAAsB,CAAA,EAC9C,EAAQ,KAAK,MACZ,kBAAC,GAAD;GAA0B,OAAO,EAAE;aAChC,EAAE;GACQ,EAFI,EAAE,MAEN,CACb,CACY,EAAA,CAAA,CACT;;;AAgBb,SAAS,EAAkB,EACzB,UACA,UACA,kBACA,YACA,gBACyB;CACzB,IAAM,EAAE,YAAS,EAAM,WAAW,EAAiB,EAC7C,IAAQ,EAAM;AAcpB,QACE,kBAAC,GAAD;EACS,OAdsB,EAAM,cAC/B,EAAQ,KAAK,OAAO;GAAE,IAAI,EAAE;GAAO,OAAO,EAAE;GAAO,EAAE,EAC3D,CAAC,EAAQ,CACV;EAYU;EACQ;EACf,mBAAmB,UAAU,EAAM,aAAa,CAAC;YAEjD,kBAAC,UAAD;GACE,MAAK;GACL,WAAW,EACT,gIACA,8FAjB2C;IACjD,IAAI;IACJ,IAAI;IACJ,IAAI;IACL,CAc0B,IACnB,IAAQ,IAAI,oBAAoB,gCAChC,QACA,EACD;aATH,CAWE,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACE,kBAAC,QAAD;KAAM,WAAU;eAAY;KAAa,CAAA,EACxC,IAAQ,KACP,kBAAC,EAAO,MAAR;KAEE,SAAS,EAAE,OAAO,IAAK;KACvB,SAAS,EAAE,OAAO,GAAG;KACrB,YAAY,EAAQ;KACpB,WAAU;eAEV,kBAAC,GAAD;MAAO,MAAK;MAAK,SAAQ;MAAQ,WAAU;gBACxC;MACK,CAAA;KACI,EATP,EASO,CAEX;OACP,kBAAC,GAAD;IAAM,MAAM;IAAiB,MAAK;IAAK,WAAU;IAAwB,CAAA,CAClE;;EACU,CAAA"}
|
|
@@ -120,7 +120,7 @@ var m = s.forwardRef(({ items: m, groups: h, value: g, onValueChange: _, searchP
|
|
|
120
120
|
})] }), i && /* @__PURE__ */ l(p.span, {
|
|
121
121
|
initial: { scale: 0 },
|
|
122
122
|
animate: { scale: 1 },
|
|
123
|
-
transition: e.
|
|
123
|
+
transition: e.snappy,
|
|
124
124
|
className: "inline-flex shrink-0",
|
|
125
125
|
children: /* @__PURE__ */ l(r, {
|
|
126
126
|
icon: d,
|