@openrails/contentkit-ui 0.86.0 → 0.87.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -3
- package/dist/client/content/types.d.ts +4 -2
- package/dist/client/content/types.js.map +1 -1
- package/dist/client/discovery.js +3 -0
- package/dist/client/discovery.js.map +1 -1
- package/dist/client/errors.d.ts +6 -0
- package/dist/client/errors.js +6 -2
- package/dist/client/errors.js.map +1 -1
- package/dist/client/gallery.d.ts +3 -5
- package/dist/client/gallery.js +16 -20
- package/dist/client/gallery.js.map +1 -1
- package/dist/client/generated/error-codes.d.ts +1 -1
- package/dist/client/generated/error-codes.js +8 -0
- package/dist/client/generated/error-codes.js.map +1 -1
- package/dist/client/generated/routes.d.ts +15 -2
- package/dist/client/generated/routes.js +27 -2
- package/dist/client/generated/routes.js.map +1 -1
- package/dist/client/generated/wire.d.ts +32 -11
- package/dist/client/generated/wire.js.map +1 -1
- package/dist/client/media/windows.js +1 -1
- package/dist/client/media/windows.js.map +1 -1
- package/dist/client/versions.d.ts +6 -4
- package/dist/client/versions.js +24 -26
- package/dist/client/versions.js.map +1 -1
- package/dist/components/content/card.d.ts +5 -3
- package/dist/components/content/card.js +131 -109
- package/dist/components/content/card.js.map +1 -1
- package/dist/components/content/download-button.d.ts +26 -0
- package/dist/components/content/download-button.js +51 -0
- package/dist/components/content/download-button.js.map +1 -0
- package/dist/components/content/grid.d.ts +8 -1
- package/dist/components/content/grid.js +125 -76
- package/dist/components/content/grid.js.map +1 -1
- package/dist/components/content/index.d.ts +2 -1
- package/dist/components/content/listing.d.ts +3 -1
- package/dist/components/content/listing.js.map +1 -1
- package/dist/components/media-gallery.d.ts +1 -4
- package/dist/components/media-gallery.js +19 -34
- package/dist/components/media-gallery.js.map +1 -1
- package/dist/components/social/comments.js +1 -1
- package/dist/components/social/comments.js.map +1 -1
- package/dist/components/social/engagement.d.ts +2 -1
- package/dist/components/social/engagement.js +5 -3
- package/dist/components/social/engagement.js.map +1 -1
- package/dist/index.js +10 -9
- package/dist/locales/de.js +13 -2
- package/dist/locales/de.js.map +1 -1
- package/dist/locales/discovery/de.js +3 -0
- package/dist/locales/discovery/de.js.map +1 -1
- package/dist/locales/discovery/en.d.ts +3 -0
- package/dist/locales/discovery/en.js +3 -0
- package/dist/locales/discovery/en.js.map +1 -1
- package/dist/locales/discovery/es.js +3 -0
- package/dist/locales/discovery/es.js.map +1 -1
- package/dist/locales/discovery/ja.js +3 -0
- package/dist/locales/discovery/ja.js.map +1 -1
- package/dist/locales/discovery/ko.js +3 -0
- package/dist/locales/discovery/ko.js.map +1 -1
- package/dist/locales/discovery/zh.js +3 -0
- package/dist/locales/discovery/zh.js.map +1 -1
- package/dist/locales/en.d.ts +16 -2
- package/dist/locales/en.js +13 -2
- package/dist/locales/en.js.map +1 -1
- package/dist/locales/es.js +13 -2
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/ja.js +13 -2
- package/dist/locales/ja.js.map +1 -1
- package/dist/locales/ko.js +13 -2
- package/dist/locales/ko.js.map +1 -1
- package/dist/locales/zh.js +13 -2
- package/dist/locales/zh.js.map +1 -1
- package/dist/react/comments.d.ts +1 -1
- package/dist/react/comments.js +3 -2
- package/dist/react/comments.js.map +1 -1
- package/dist/react/context.d.ts +1 -1
- package/dist/react/context.js.map +1 -1
- package/dist/react/discovery.d.ts +11 -0
- package/dist/react/discovery.js +49 -30
- package/dist/react/discovery.js.map +1 -1
- package/dist/react/engagement.d.ts +6 -2
- package/dist/react/engagement.js +2 -6
- package/dist/react/engagement.js.map +1 -1
- package/dist/react/index.d.ts +2 -2
- package/dist/react/versions.d.ts +25 -5
- package/dist/react/versions.js +58 -37
- package/dist/react/versions.js.map +1 -1
- package/dist/react.js +9 -9
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -11,11 +11,19 @@ import { cn as h } from "cn";
|
|
|
11
11
|
import { FavouriteIcon as g, LockIcon as _, ThumbsUpIcon as v } from "@hugeicons/core-free-icons";
|
|
12
12
|
import { HugeiconsIcon as y } from "@hugeicons/react";
|
|
13
13
|
//#region src/components/content/card.tsx
|
|
14
|
-
|
|
14
|
+
function b({ card: e, decorate: t }) {
|
|
15
|
+
let n = t?.(e);
|
|
16
|
+
return n == null || n === !1 ? null : /* @__PURE__ */ p("span", {
|
|
17
|
+
className: "pointer-events-none absolute inset-0",
|
|
18
|
+
"data-ckui": "card-decorations",
|
|
19
|
+
children: n
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
var x = (e, t) => new Intl.NumberFormat(t, {
|
|
15
23
|
notation: "compact",
|
|
16
24
|
maximumFractionDigits: 1
|
|
17
25
|
}).format(e);
|
|
18
|
-
function
|
|
26
|
+
function S(e, t, n = [], r) {
|
|
19
27
|
if (t.allowed) return null;
|
|
20
28
|
if (t.unknown) return e.t("contents.unknownAccess");
|
|
21
29
|
let i = t.offers?.[0];
|
|
@@ -23,7 +31,7 @@ function x(e, t, n = [], r) {
|
|
|
23
31
|
let a = t.sell?.find((e) => n.includes(e));
|
|
24
32
|
return a ? l(r, a) : t.sell?.some((e) => e.includes(":group:")) ? e.t("contents.members") : t.sell?.length ? e.t("contents.forSale") : e.t("contents.locked");
|
|
25
33
|
}
|
|
26
|
-
function
|
|
34
|
+
function C(e, t) {
|
|
27
35
|
let n = f(r)?.navigate;
|
|
28
36
|
return d((r) => {
|
|
29
37
|
t?.(), !(!n || !e || r.defaultPrevented || r.button !== 0 || r.metaKey || r.ctrlKey || r.shiftKey || r.altKey) && (r.preventDefault(), n(e));
|
|
@@ -33,110 +41,116 @@ function S(e, t) {
|
|
|
33
41
|
t
|
|
34
42
|
]);
|
|
35
43
|
}
|
|
36
|
-
function
|
|
37
|
-
let
|
|
38
|
-
...
|
|
39
|
-
...
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
].filter(Boolean),
|
|
44
|
+
function w({ card: r, href: a, onOpen: s, shape: c = "portrait", groupKinds: u, chips: f = 3, tiers: w, labels: D, owner: O = !0, ownerHref: k, decorate: A, children: j, className: M }) {
|
|
45
|
+
let N = n(), { t: P } = N, F = t(), I = a ? a(r) : r.path, L = C(I, d(() => s?.(r), [s, r])), R = S(N, r.access, w, D), z = r.version, B = r.viewer && r.viewer.progress_max > 0 ? Math.min(1, r.viewer.progress / r.viewer.progress_max) : 0, V = u ?? Object.keys(r.groups), H = z.duration_ms ? Math.round(z.duration_ms / 6e4) : 0, U = [
|
|
46
|
+
...z.languages.map(F),
|
|
47
|
+
...z.traits.map((e) => l(D, e)),
|
|
48
|
+
z.pages ? N.plural("contents.pages", z.pages) : null,
|
|
49
|
+
H ? N.plural("contents.minutes", H) : null
|
|
50
|
+
].filter(Boolean), W = r.title || r.code;
|
|
43
51
|
return /* @__PURE__ */ m("article", {
|
|
44
|
-
className: h("group/card grid content-start gap-2",
|
|
52
|
+
className: h("group/card grid content-start gap-2", M),
|
|
45
53
|
"data-ckui": "content-card",
|
|
46
54
|
"data-id": r.id,
|
|
47
|
-
"data-locked":
|
|
55
|
+
"data-locked": R ? "" : void 0,
|
|
48
56
|
lang: r.language || void 0,
|
|
49
|
-
children: [/* @__PURE__ */ m("
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
className: "
|
|
66
|
-
|
|
57
|
+
children: [/* @__PURE__ */ m("div", {
|
|
58
|
+
className: "relative overflow-hidden rounded-lg",
|
|
59
|
+
children: [/* @__PURE__ */ m("a", {
|
|
60
|
+
href: I,
|
|
61
|
+
onClick: L,
|
|
62
|
+
className: "relative block overflow-hidden rounded-lg bg-muted",
|
|
63
|
+
tabIndex: -1,
|
|
64
|
+
"aria-hidden": !0,
|
|
65
|
+
children: [
|
|
66
|
+
/* @__PURE__ */ p("div", {
|
|
67
|
+
className: c === "landscape" ? "aspect-video" : "aspect-[3/4]",
|
|
68
|
+
children: /* @__PURE__ */ p(e, {
|
|
69
|
+
outputs: r.cover,
|
|
70
|
+
alt: "",
|
|
71
|
+
fit: "cover",
|
|
72
|
+
skeleton: !0,
|
|
73
|
+
className: "size-full transition-transform group-hover/card:scale-[1.02]",
|
|
74
|
+
fallback: /* @__PURE__ */ p("span", {
|
|
75
|
+
className: "grid size-full place-items-center text-2xl font-semibold text-muted-foreground",
|
|
76
|
+
children: W.charAt(0).toUpperCase()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
}),
|
|
80
|
+
R && /* @__PURE__ */ m("span", {
|
|
81
|
+
className: "absolute top-2 left-2 inline-flex items-center gap-1 rounded-md bg-background/85 px-1.5 py-0.5 text-xs font-medium text-foreground shadow-sm",
|
|
82
|
+
"data-ckui": "card-lock",
|
|
83
|
+
children: [/* @__PURE__ */ p(y, {
|
|
84
|
+
icon: _,
|
|
85
|
+
strokeWidth: 2,
|
|
86
|
+
className: "size-3.5",
|
|
87
|
+
"aria-hidden": !0
|
|
88
|
+
}), R]
|
|
89
|
+
}),
|
|
90
|
+
B > 0 && /* @__PURE__ */ p("span", {
|
|
91
|
+
className: "absolute inset-x-0 bottom-0 h-1 bg-background/60",
|
|
92
|
+
"data-ckui": "card-progress",
|
|
93
|
+
children: /* @__PURE__ */ p("span", {
|
|
94
|
+
className: "block h-full bg-primary",
|
|
95
|
+
style: { width: `${Math.round(B * 100)}%` }
|
|
67
96
|
})
|
|
68
97
|
})
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
icon: _,
|
|
75
|
-
strokeWidth: 2,
|
|
76
|
-
className: "size-3.5",
|
|
77
|
-
"aria-hidden": !0
|
|
78
|
-
}), I]
|
|
79
|
-
}),
|
|
80
|
-
R > 0 && /* @__PURE__ */ p("span", {
|
|
81
|
-
className: "absolute inset-x-0 bottom-0 h-1 bg-background/60",
|
|
82
|
-
"data-ckui": "card-progress",
|
|
83
|
-
children: /* @__PURE__ */ p("span", {
|
|
84
|
-
className: "block h-full bg-primary",
|
|
85
|
-
style: { width: `${Math.round(R * 100)}%` }
|
|
86
|
-
})
|
|
87
|
-
})
|
|
88
|
-
]
|
|
98
|
+
]
|
|
99
|
+
}), /* @__PURE__ */ p(b, {
|
|
100
|
+
card: r,
|
|
101
|
+
decorate: A
|
|
102
|
+
})]
|
|
89
103
|
}), /* @__PURE__ */ m("div", {
|
|
90
104
|
className: "grid gap-1",
|
|
91
105
|
children: [
|
|
92
106
|
/* @__PURE__ */ m("a", {
|
|
93
|
-
href:
|
|
94
|
-
onClick:
|
|
107
|
+
href: I,
|
|
108
|
+
onClick: L,
|
|
95
109
|
className: "line-clamp-2 font-medium break-words hover:underline",
|
|
96
110
|
"data-ckui": "card-title",
|
|
97
|
-
children: [
|
|
111
|
+
children: [W, R && /* @__PURE__ */ m("span", {
|
|
98
112
|
className: "sr-only",
|
|
99
113
|
children: [
|
|
100
114
|
" (",
|
|
101
|
-
|
|
115
|
+
R,
|
|
102
116
|
")"
|
|
103
117
|
]
|
|
104
118
|
})]
|
|
105
119
|
}),
|
|
106
|
-
|
|
120
|
+
O && r.owner && /* @__PURE__ */ p(T, {
|
|
107
121
|
group: r.owner,
|
|
108
|
-
href:
|
|
122
|
+
href: k
|
|
109
123
|
}),
|
|
110
|
-
|
|
124
|
+
U.length > 0 && /* @__PURE__ */ p("p", {
|
|
111
125
|
className: "text-xs text-muted-foreground",
|
|
112
|
-
children:
|
|
126
|
+
children: U.join(" · ")
|
|
113
127
|
}),
|
|
114
128
|
r.viewer?.completed && /* @__PURE__ */ p(i, {
|
|
115
129
|
variant: "secondary",
|
|
116
130
|
className: "justify-self-start",
|
|
117
|
-
children:
|
|
131
|
+
children: P("contents.completed")
|
|
118
132
|
}),
|
|
119
133
|
!r.access.allowed && r.access.free_after && /* @__PURE__ */ p("p", {
|
|
120
134
|
className: "text-xs text-muted-foreground",
|
|
121
|
-
children:
|
|
135
|
+
children: P("contents.freeFrom", { date: o(N, r.access.free_after, !1) })
|
|
122
136
|
}),
|
|
123
|
-
|
|
137
|
+
V.some((e) => r.groups[e]?.length) && /* @__PURE__ */ p("ul", {
|
|
124
138
|
className: "flex flex-wrap gap-1",
|
|
125
139
|
"data-ckui": "card-groups",
|
|
126
|
-
children:
|
|
140
|
+
children: V.flatMap((e) => (r.groups[e] ?? []).slice(0, f).map((t) => /* @__PURE__ */ p("li", {
|
|
127
141
|
"data-kind": e,
|
|
128
|
-
children: /* @__PURE__ */ p(
|
|
142
|
+
children: /* @__PURE__ */ p(E, {
|
|
129
143
|
url: t.path,
|
|
130
144
|
name: t.name || t.slug
|
|
131
145
|
})
|
|
132
146
|
}, `${e}:${t.id}`)))
|
|
133
147
|
}),
|
|
134
|
-
/* @__PURE__ */ m("p", {
|
|
148
|
+
(r.counts.favorites !== void 0 || r.counts.likes !== void 0) && /* @__PURE__ */ m("p", {
|
|
135
149
|
className: "flex items-center gap-3 text-xs text-muted-foreground tabular-nums",
|
|
136
150
|
"data-ckui": "card-counts",
|
|
137
|
-
children: [/* @__PURE__ */ m("span", {
|
|
151
|
+
children: [r.counts.favorites !== void 0 && /* @__PURE__ */ m("span", {
|
|
138
152
|
className: "inline-flex items-center gap-1",
|
|
139
|
-
title:
|
|
153
|
+
title: P("contents.favorites"),
|
|
140
154
|
children: [
|
|
141
155
|
/* @__PURE__ */ p(y, {
|
|
142
156
|
icon: g,
|
|
@@ -146,13 +160,13 @@ function C({ card: r, href: a, onOpen: s, shape: c = "portrait", groupKinds: u,
|
|
|
146
160
|
}),
|
|
147
161
|
/* @__PURE__ */ p("span", {
|
|
148
162
|
className: "sr-only",
|
|
149
|
-
children:
|
|
163
|
+
children: P("contents.favorites")
|
|
150
164
|
}),
|
|
151
|
-
|
|
165
|
+
x(r.counts.favorites, N.language)
|
|
152
166
|
]
|
|
153
|
-
}), /* @__PURE__ */ m("span", {
|
|
167
|
+
}), r.counts.likes !== void 0 && /* @__PURE__ */ m("span", {
|
|
154
168
|
className: "inline-flex items-center gap-1",
|
|
155
|
-
title:
|
|
169
|
+
title: P("contents.likes"),
|
|
156
170
|
children: [
|
|
157
171
|
/* @__PURE__ */ p(y, {
|
|
158
172
|
icon: v,
|
|
@@ -162,18 +176,18 @@ function C({ card: r, href: a, onOpen: s, shape: c = "portrait", groupKinds: u,
|
|
|
162
176
|
}),
|
|
163
177
|
/* @__PURE__ */ p("span", {
|
|
164
178
|
className: "sr-only",
|
|
165
|
-
children:
|
|
179
|
+
children: P("contents.likes")
|
|
166
180
|
}),
|
|
167
|
-
|
|
181
|
+
x(r.counts.likes, N.language)
|
|
168
182
|
]
|
|
169
183
|
})]
|
|
170
184
|
}),
|
|
171
|
-
|
|
185
|
+
j
|
|
172
186
|
]
|
|
173
187
|
})]
|
|
174
188
|
});
|
|
175
189
|
}
|
|
176
|
-
function
|
|
190
|
+
function T({ group: e, href: t, className: n }) {
|
|
177
191
|
return /* @__PURE__ */ p("span", {
|
|
178
192
|
className: h("flex min-w-0 text-xs text-muted-foreground", n),
|
|
179
193
|
"data-ckui": "card-owner",
|
|
@@ -192,8 +206,8 @@ function w({ group: e, href: t, className: n }) {
|
|
|
192
206
|
})
|
|
193
207
|
});
|
|
194
208
|
}
|
|
195
|
-
function
|
|
196
|
-
let n =
|
|
209
|
+
function E({ url: e, name: t }) {
|
|
210
|
+
let n = C(e), r = "inline-block rounded-md border border-border px-1.5 py-0.5 text-xs text-muted-foreground";
|
|
197
211
|
return e ? /* @__PURE__ */ p("a", {
|
|
198
212
|
href: e,
|
|
199
213
|
onClick: n,
|
|
@@ -204,61 +218,69 @@ function T({ url: e, name: t }) {
|
|
|
204
218
|
children: t
|
|
205
219
|
});
|
|
206
220
|
}
|
|
207
|
-
function
|
|
208
|
-
let
|
|
221
|
+
function D({ card: t, href: r, onOpen: i, tiers: o, labels: s, byline: c, owner: l = !0, ownerHref: u, decorate: f, children: v, className: w }) {
|
|
222
|
+
let E = n(), D = r ? r(t) : t.path, O = C(D, d(() => i?.(t), [i, t])), k = S(E, t.access, o, s), A = t.title || t.code;
|
|
209
223
|
return /* @__PURE__ */ m("article", {
|
|
210
|
-
className: h("grid gap-3 rounded-lg border border-border p-3",
|
|
224
|
+
className: h("grid gap-3 rounded-lg border border-border p-3", w),
|
|
211
225
|
"data-ckui": "post-card",
|
|
212
226
|
"data-id": t.id,
|
|
213
|
-
"data-locked":
|
|
227
|
+
"data-locked": k ? "" : void 0,
|
|
214
228
|
lang: t.language || void 0,
|
|
215
229
|
children: [
|
|
216
|
-
c ?? (l && t.owner && /* @__PURE__ */ p(
|
|
230
|
+
c ?? (l && t.owner && /* @__PURE__ */ p(T, {
|
|
217
231
|
group: t.owner,
|
|
218
232
|
href: u,
|
|
219
233
|
className: "text-sm"
|
|
220
234
|
})),
|
|
221
235
|
/* @__PURE__ */ m("a", {
|
|
222
|
-
href:
|
|
223
|
-
onClick:
|
|
236
|
+
href: D,
|
|
237
|
+
onClick: O,
|
|
224
238
|
className: "grid gap-2",
|
|
225
239
|
children: [/* @__PURE__ */ p("span", {
|
|
226
240
|
className: "font-medium break-words hover:underline",
|
|
227
241
|
"data-ckui": "card-title",
|
|
228
|
-
children:
|
|
242
|
+
children: A
|
|
229
243
|
}), t.cover.length > 0 && /* @__PURE__ */ m("span", {
|
|
230
244
|
className: "relative block aspect-video overflow-hidden rounded-md bg-muted",
|
|
231
|
-
children: [
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"
|
|
247
|
-
|
|
245
|
+
children: [
|
|
246
|
+
/* @__PURE__ */ p(e, {
|
|
247
|
+
outputs: t.cover,
|
|
248
|
+
alt: "",
|
|
249
|
+
fit: "cover",
|
|
250
|
+
skeleton: !0,
|
|
251
|
+
className: h("size-full", k && "blur-md")
|
|
252
|
+
}),
|
|
253
|
+
/* @__PURE__ */ p(b, {
|
|
254
|
+
card: t,
|
|
255
|
+
decorate: f
|
|
256
|
+
}),
|
|
257
|
+
k && /* @__PURE__ */ p("span", {
|
|
258
|
+
className: "absolute inset-0 grid place-items-center",
|
|
259
|
+
children: /* @__PURE__ */ m("span", {
|
|
260
|
+
className: "inline-flex items-center gap-1 rounded-md bg-background/85 px-2 py-1 text-xs font-medium shadow-sm",
|
|
261
|
+
"data-ckui": "card-lock",
|
|
262
|
+
children: [/* @__PURE__ */ p(y, {
|
|
263
|
+
icon: _,
|
|
264
|
+
strokeWidth: 2,
|
|
265
|
+
className: "size-3.5",
|
|
266
|
+
"aria-hidden": !0
|
|
267
|
+
}), k]
|
|
268
|
+
})
|
|
248
269
|
})
|
|
249
|
-
|
|
270
|
+
]
|
|
250
271
|
})]
|
|
251
272
|
}),
|
|
252
273
|
/* @__PURE__ */ m("p", {
|
|
253
274
|
className: "flex flex-wrap items-center gap-3 text-xs text-muted-foreground",
|
|
254
275
|
children: [
|
|
255
276
|
t.published_at && /* @__PURE__ */ p(a, { at: t.published_at }),
|
|
256
|
-
!t.cover.length &&
|
|
277
|
+
!t.cover.length && k && /* @__PURE__ */ p("span", {
|
|
257
278
|
"data-ckui": "card-lock",
|
|
258
|
-
children:
|
|
279
|
+
children: k
|
|
259
280
|
}),
|
|
260
|
-
/* @__PURE__ */ m("span", {
|
|
281
|
+
t.counts.favorites !== void 0 && /* @__PURE__ */ m("span", {
|
|
261
282
|
className: "inline-flex items-center gap-1 tabular-nums",
|
|
283
|
+
"data-ckui": "card-counts",
|
|
262
284
|
children: [
|
|
263
285
|
/* @__PURE__ */ p(y, {
|
|
264
286
|
icon: g,
|
|
@@ -268,18 +290,18 @@ function E({ card: t, href: r, onOpen: i, tiers: o, labels: s, byline: c, owner:
|
|
|
268
290
|
}),
|
|
269
291
|
/* @__PURE__ */ p("span", {
|
|
270
292
|
className: "sr-only",
|
|
271
|
-
children:
|
|
293
|
+
children: E.t("contents.favorites")
|
|
272
294
|
}),
|
|
273
|
-
|
|
295
|
+
x(t.counts.favorites, E.language)
|
|
274
296
|
]
|
|
275
297
|
})
|
|
276
298
|
]
|
|
277
299
|
}),
|
|
278
|
-
|
|
300
|
+
v
|
|
279
301
|
]
|
|
280
302
|
});
|
|
281
303
|
}
|
|
282
304
|
//#endregion
|
|
283
|
-
export {
|
|
305
|
+
export { w as ContentCard, T as OwnerChip, D as PostCard, S as lockLabel };
|
|
284
306
|
|
|
285
307
|
//# sourceMappingURL=card.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card.js","names":[],"sources":["../../../src/components/content/card.tsx"],"sourcesContent":["import { FavouriteIcon, LockIcon, ThumbsUpIcon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport { useCallback, useContext, type MouseEvent, type ReactNode } from \"react\";\nimport type { Card, CardAccess } from \"../../client/generated/wire.js\";\nimport { useLanguageName, useMessages } from \"../../i18n/context.js\";\nimport type { Translator } from \"../../i18n/messages.js\";\nimport { ContentKitContext } from \"../../react/context.js\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { RenditionImg } from \"../rendition-img.js\";\nimport { formatDate, RelativeTime } from \"../social/parts.js\";\nimport { GroupAvatar, GroupLink, labelOf, type GroupLike, type Labels } from \"../groups/parts.js\";\nimport { priceLabel } from \"./paywall.js\";\n\nexport interface ContentCardProps {\n card: Card;\n /** The card's link; default its canonical path. */\n href?: (card: Card) => string | undefined;\n /** Before the card's link is followed: a click signal. */\n onOpen?: (card: Card) => void;\n /** Cover shape: \"portrait\" (3:4, readers) or \"landscape\" (16:9, players). Default portrait. */\n shape?: \"portrait\" | \"landscape\";\n /** The group kinds whose chips show, in order; default every kind the card names. */\n groupKinds?: readonly string[];\n /** Chips per group kind. Default 3. */\n chips?: number;\n /** Tiers the site sells, so a lock names them; others show as \"for sale\". */\n tiers?: readonly string[];\n /** Display names of kinds, traits and tiers by identifier. */\n labels?: Labels;\n /** Show the owner group (a channel) when the card has one; default true, false on that group's own feed. */\n owner?: boolean;\n /** The owner group's link; default its canonical path. */\n ownerHref?: (g: GroupLike) => string | undefined;\n /** Below the meta line: the host's own. */\n children?: ReactNode;\n className?: string;\n}\n\nconst compact = (n: number, language?: string) => new Intl.NumberFormat(language, { notation: \"compact\", maximumFractionDigits: 1 }).format(n);\n\n/** Why a card is locked, in words (its price when it is on sale); null when it opens. */\nexport function lockLabel(m: Translator, a: CardAccess, tiers: readonly string[] = [], labels?: Labels): string | null {\n if (a.allowed) return null;\n if (a.unknown) return m.t(\"contents.unknownAccess\");\n const offer = a.offers?.[0];\n if (offer) return offer.target.kind === \"group\" && Number(offer.terms.amount) === 0 ? m.t(\"contents.joinFree\") : priceLabel(m, offer.terms);\n const tier = a.sell?.find((k) => tiers.includes(k));\n if (tier) return labelOf(labels, tier);\n if (a.sell?.some((k) => k.includes(\":group:\"))) return m.t(\"contents.members\");\n if (a.sell?.length) return m.t(\"contents.forSale\");\n return m.t(\"contents.locked\");\n}\n\n/** A link followed in-app through the provider's navigate. */\nfunction useLink(url: string | undefined, before?: () => void) {\n const navigate = useContext(ContentKitContext)?.navigate;\n return useCallback(\n (e: MouseEvent<HTMLAnchorElement>) => {\n before?.();\n if (!navigate || !url || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n navigate(url);\n },\n [navigate, url, before],\n );\n}\n\n/** One content in a listing: cover, title, version, groups, counts, the reader's progress and why it is locked. */\nexport function ContentCard({ card, href, onOpen, shape = \"portrait\", groupKinds, chips = 3, tiers, labels, owner = true, ownerHref, children, className }: ContentCardProps) {\n const m = useMessages();\n const { t } = m;\n const languageName = useLanguageName();\n const url = href ? href(card) : card.path;\n const open = useCallback(() => onOpen?.(card), [onOpen, card]);\n const onClick = useLink(url, open);\n const lock = lockLabel(m, card.access, tiers, labels);\n const v = card.version;\n const progress = card.viewer && card.viewer.progress_max > 0 ? Math.min(1, card.viewer.progress / card.viewer.progress_max) : 0;\n const kinds = groupKinds ?? Object.keys(card.groups);\n const minutes = v.duration_ms ? Math.round(v.duration_ms / 60_000) : 0;\n const meta = [\n ...v.languages.map(languageName),\n ...v.traits.map((x) => labelOf(labels, x)),\n v.pages ? m.plural(\"contents.pages\", v.pages) : null,\n minutes ? m.plural(\"contents.minutes\", minutes) : null,\n ].filter(Boolean);\n const title = card.title || card.code;\n return (\n <article className={cn(\"group/card grid content-start gap-2\", className)} data-ckui=\"content-card\" data-id={card.id} data-locked={lock ? \"\" : undefined} lang={card.language || undefined}>\n <a href={url} onClick={onClick} className=\"relative block overflow-hidden rounded-lg bg-muted\" tabIndex={-1} aria-hidden>\n <div className={shape === \"landscape\" ? \"aspect-video\" : \"aspect-[3/4]\"}>\n <RenditionImg\n outputs={card.cover}\n alt=\"\"\n fit=\"cover\"\n skeleton\n className=\"size-full transition-transform group-hover/card:scale-[1.02]\"\n fallback={<span className=\"grid size-full place-items-center text-2xl font-semibold text-muted-foreground\">{title.charAt(0).toUpperCase()}</span>}\n />\n </div>\n {lock && (\n <span className=\"absolute top-2 left-2 inline-flex items-center gap-1 rounded-md bg-background/85 px-1.5 py-0.5 text-xs font-medium text-foreground shadow-sm\" data-ckui=\"card-lock\">\n <HugeiconsIcon icon={LockIcon} strokeWidth={2} className=\"size-3.5\" aria-hidden />\n {lock}\n </span>\n )}\n {progress > 0 && (\n <span className=\"absolute inset-x-0 bottom-0 h-1 bg-background/60\" data-ckui=\"card-progress\">\n <span className=\"block h-full bg-primary\" style={{ width: `${Math.round(progress * 100)}%` }} />\n </span>\n )}\n </a>\n <div className=\"grid gap-1\">\n <a href={url} onClick={onClick} className=\"line-clamp-2 font-medium break-words hover:underline\" data-ckui=\"card-title\">\n {title}\n {lock && <span className=\"sr-only\"> ({lock})</span>}\n </a>\n {owner && card.owner && <OwnerChip group={card.owner} href={ownerHref} />}\n {meta.length > 0 && <p className=\"text-xs text-muted-foreground\">{meta.join(\" · \")}</p>}\n {card.viewer?.completed && (\n <Badge variant=\"secondary\" className=\"justify-self-start\">\n {t(\"contents.completed\")}\n </Badge>\n )}\n {!card.access.allowed && card.access.free_after && <p className=\"text-xs text-muted-foreground\">{t(\"contents.freeFrom\", { date: formatDate(m, card.access.free_after, false) })}</p>}\n {kinds.some((k) => card.groups[k]?.length) && (\n <ul className=\"flex flex-wrap gap-1\" data-ckui=\"card-groups\">\n {kinds.flatMap((k) =>\n (card.groups[k] ?? []).slice(0, chips).map((g) => (\n <li key={`${k}:${g.id}`} data-kind={k}>\n <Chip url={g.path} name={g.name || g.slug} />\n </li>\n )),\n )}\n </ul>\n )}\n <p className=\"flex items-center gap-3 text-xs text-muted-foreground tabular-nums\" data-ckui=\"card-counts\">\n <span className=\"inline-flex items-center gap-1\" title={t(\"contents.favorites\")}>\n <HugeiconsIcon icon={FavouriteIcon} strokeWidth={2} className={cn(\"size-3.5\", card.viewer?.favorited && \"fill-current text-primary\")} aria-hidden />\n <span className=\"sr-only\">{t(\"contents.favorites\")}</span>\n {compact(card.counts.favorites, m.language)}\n </span>\n <span className=\"inline-flex items-center gap-1\" title={t(\"contents.likes\")}>\n <HugeiconsIcon icon={ThumbsUpIcon} strokeWidth={2} className=\"size-3.5\" aria-hidden />\n <span className=\"sr-only\">{t(\"contents.likes\")}</span>\n {compact(card.counts.likes, m.language)}\n </span>\n </p>\n {children}\n </div>\n </article>\n );\n}\n\n/** The group that owns a content (a channel): its avatar and name, linking to its page. */\nexport function OwnerChip({ group, href, className }: { group: GroupLike; href?: (g: GroupLike) => string | undefined; className?: string }) {\n return (\n <span className={cn(\"flex min-w-0 text-xs text-muted-foreground\", className)} data-ckui=\"card-owner\" data-id={group.id}>\n <GroupLink group={group} href={href} className=\"inline-flex min-w-0 items-center gap-1.5 hover:text-foreground\">\n <GroupAvatar group={group} className=\"size-5 shrink-0 text-[0.625rem]\" />\n <span className=\"truncate\">{group.name || group.slug}</span>\n </GroupLink>\n </span>\n );\n}\n\nfunction Chip({ url, name }: { url?: string; name: string }) {\n const onClick = useLink(url);\n const cls = \"inline-block rounded-md border border-border px-1.5 py-0.5 text-xs text-muted-foreground\";\n return url ? (\n <a href={url} onClick={onClick} className={cn(cls, \"hover:bg-muted hover:text-foreground\")}>\n {name}\n </a>\n ) : (\n <span className={cls}>{name}</span>\n );\n}\n\nexport interface PostCardProps extends Omit<ContentCardProps, \"shape\" | \"chips\" | \"groupKinds\"> {\n /** Above the title: the post's channel or author; default its owner group's chip. */\n byline?: ReactNode;\n}\n\n/** A post in a feed: a wide cover, its title, when it was published and what unlocking it costs. */\nexport function PostCard({ card, href, onOpen, tiers, labels, byline, owner = true, ownerHref, children, className }: PostCardProps) {\n const m = useMessages();\n const url = href ? href(card) : card.path;\n const open = useCallback(() => onOpen?.(card), [onOpen, card]);\n const onClick = useLink(url, open);\n const lock = lockLabel(m, card.access, tiers, labels);\n const title = card.title || card.code;\n return (\n <article className={cn(\"grid gap-3 rounded-lg border border-border p-3\", className)} data-ckui=\"post-card\" data-id={card.id} data-locked={lock ? \"\" : undefined} lang={card.language || undefined}>\n {byline ?? (owner && card.owner && <OwnerChip group={card.owner} href={ownerHref} className=\"text-sm\" />)}\n <a href={url} onClick={onClick} className=\"grid gap-2\">\n <span className=\"font-medium break-words hover:underline\" data-ckui=\"card-title\">\n {title}\n </span>\n {card.cover.length > 0 && (\n <span className=\"relative block aspect-video overflow-hidden rounded-md bg-muted\">\n <RenditionImg outputs={card.cover} alt=\"\" fit=\"cover\" skeleton className={cn(\"size-full\", lock && \"blur-md\")} />\n {lock && (\n <span className=\"absolute inset-0 grid place-items-center\">\n <span className=\"inline-flex items-center gap-1 rounded-md bg-background/85 px-2 py-1 text-xs font-medium shadow-sm\" data-ckui=\"card-lock\">\n <HugeiconsIcon icon={LockIcon} strokeWidth={2} className=\"size-3.5\" aria-hidden />\n {lock}\n </span>\n </span>\n )}\n </span>\n )}\n </a>\n <p className=\"flex flex-wrap items-center gap-3 text-xs text-muted-foreground\">\n {card.published_at && <RelativeTime at={card.published_at} />}\n {!card.cover.length && lock && <span data-ckui=\"card-lock\">{lock}</span>}\n <span className=\"inline-flex items-center gap-1 tabular-nums\">\n <HugeiconsIcon icon={FavouriteIcon} strokeWidth={2} className={cn(\"size-3.5\", card.viewer?.favorited && \"fill-current text-primary\")} aria-hidden />\n <span className=\"sr-only\">{m.t(\"contents.favorites\")}</span>\n {compact(card.counts.favorites, m.language)}\n </span>\n </p>\n {children}\n </article>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"card.js","names":[],"sources":["../../../src/components/content/card.tsx"],"sourcesContent":["import { FavouriteIcon, LockIcon, ThumbsUpIcon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport { useCallback, useContext, type MouseEvent, type ReactNode } from \"react\";\nimport type { Card, CardAccess } from \"../../client/generated/wire.js\";\nimport { useLanguageName, useMessages } from \"../../i18n/context.js\";\nimport type { Translator } from \"../../i18n/messages.js\";\nimport { ContentKitContext } from \"../../react/context.js\";\nimport { Badge } from \"#ckui/ui/badge\";\nimport { RenditionImg } from \"../rendition-img.js\";\nimport { formatDate, RelativeTime } from \"../social/parts.js\";\nimport { GroupAvatar, GroupLink, labelOf, type GroupLike, type Labels } from \"../groups/parts.js\";\nimport { priceLabel } from \"./paywall.js\";\n\nexport interface ContentCardProps {\n card: Card;\n /** The card's link; default its canonical path. */\n href?: (card: Card) => string | undefined;\n /** Before the card's link is followed: a click signal. */\n onOpen?: (card: Card) => void;\n /** Cover shape: \"portrait\" (3:4, readers) or \"landscape\" (16:9, players). Default portrait. */\n shape?: \"portrait\" | \"landscape\";\n /** The group kinds whose chips show, in order; default every kind the card names. */\n groupKinds?: readonly string[];\n /** Chips per group kind. Default 3. */\n chips?: number;\n /** Tiers the site sells, so a lock names them; others show as \"for sale\". */\n tiers?: readonly string[];\n /** Display names of kinds, traits and tiers by identifier. */\n labels?: Labels;\n /** Show the owner group (a channel) when the card has one; default true, false on that group's own feed. */\n owner?: boolean;\n /** The owner group's link; default its canonical path. */\n ownerHref?: (g: GroupLike) => string | undefined;\n /** Over the cover: the host's badges for the card, placed in the cover's box; clicks reach the card. */\n decorate?: (card: Card) => ReactNode;\n /** Below the meta line: the host's own. */\n children?: ReactNode;\n className?: string;\n}\n\n/** The host's badges over a cover. */\nfunction Decorations({ card, decorate }: { card: Card; decorate?: (card: Card) => ReactNode }) {\n const badges = decorate?.(card);\n if (badges == null || badges === false) return null;\n return (\n <span className=\"pointer-events-none absolute inset-0\" data-ckui=\"card-decorations\">\n {badges}\n </span>\n );\n}\n\nconst compact = (n: number, language?: string) => new Intl.NumberFormat(language, { notation: \"compact\", maximumFractionDigits: 1 }).format(n);\n\n/** Why a card is locked, in words (its price when it is on sale); null when it opens. */\nexport function lockLabel(m: Translator, a: CardAccess, tiers: readonly string[] = [], labels?: Labels): string | null {\n if (a.allowed) return null;\n if (a.unknown) return m.t(\"contents.unknownAccess\");\n const offer = a.offers?.[0];\n if (offer) return offer.target.kind === \"group\" && Number(offer.terms.amount) === 0 ? m.t(\"contents.joinFree\") : priceLabel(m, offer.terms);\n const tier = a.sell?.find((k) => tiers.includes(k));\n if (tier) return labelOf(labels, tier);\n if (a.sell?.some((k) => k.includes(\":group:\"))) return m.t(\"contents.members\");\n if (a.sell?.length) return m.t(\"contents.forSale\");\n return m.t(\"contents.locked\");\n}\n\n/** A link followed in-app through the provider's navigate. */\nfunction useLink(url: string | undefined, before?: () => void) {\n const navigate = useContext(ContentKitContext)?.navigate;\n return useCallback(\n (e: MouseEvent<HTMLAnchorElement>) => {\n before?.();\n if (!navigate || !url || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n navigate(url);\n },\n [navigate, url, before],\n );\n}\n\n/** One content in a listing: cover, title, version, groups, counts, the reader's progress and why it is locked; what its kind's locked view hides is absent. */\nexport function ContentCard({ card, href, onOpen, shape = \"portrait\", groupKinds, chips = 3, tiers, labels, owner = true, ownerHref, decorate, children, className }: ContentCardProps) {\n const m = useMessages();\n const { t } = m;\n const languageName = useLanguageName();\n const url = href ? href(card) : card.path;\n const open = useCallback(() => onOpen?.(card), [onOpen, card]);\n const onClick = useLink(url, open);\n const lock = lockLabel(m, card.access, tiers, labels);\n const v = card.version;\n const progress = card.viewer && card.viewer.progress_max > 0 ? Math.min(1, card.viewer.progress / card.viewer.progress_max) : 0;\n const kinds = groupKinds ?? Object.keys(card.groups);\n const minutes = v.duration_ms ? Math.round(v.duration_ms / 60_000) : 0;\n const meta = [\n ...v.languages.map(languageName),\n ...v.traits.map((x) => labelOf(labels, x)),\n v.pages ? m.plural(\"contents.pages\", v.pages) : null,\n minutes ? m.plural(\"contents.minutes\", minutes) : null,\n ].filter(Boolean);\n const title = card.title || card.code;\n return (\n <article className={cn(\"group/card grid content-start gap-2\", className)} data-ckui=\"content-card\" data-id={card.id} data-locked={lock ? \"\" : undefined} lang={card.language || undefined}>\n <div className=\"relative overflow-hidden rounded-lg\">\n <a href={url} onClick={onClick} className=\"relative block overflow-hidden rounded-lg bg-muted\" tabIndex={-1} aria-hidden>\n <div className={shape === \"landscape\" ? \"aspect-video\" : \"aspect-[3/4]\"}>\n <RenditionImg\n outputs={card.cover}\n alt=\"\"\n fit=\"cover\"\n skeleton\n className=\"size-full transition-transform group-hover/card:scale-[1.02]\"\n fallback={<span className=\"grid size-full place-items-center text-2xl font-semibold text-muted-foreground\">{title.charAt(0).toUpperCase()}</span>}\n />\n </div>\n {lock && (\n <span className=\"absolute top-2 left-2 inline-flex items-center gap-1 rounded-md bg-background/85 px-1.5 py-0.5 text-xs font-medium text-foreground shadow-sm\" data-ckui=\"card-lock\">\n <HugeiconsIcon icon={LockIcon} strokeWidth={2} className=\"size-3.5\" aria-hidden />\n {lock}\n </span>\n )}\n {progress > 0 && (\n <span className=\"absolute inset-x-0 bottom-0 h-1 bg-background/60\" data-ckui=\"card-progress\">\n <span className=\"block h-full bg-primary\" style={{ width: `${Math.round(progress * 100)}%` }} />\n </span>\n )}\n </a>\n <Decorations card={card} decorate={decorate} />\n </div>\n <div className=\"grid gap-1\">\n <a href={url} onClick={onClick} className=\"line-clamp-2 font-medium break-words hover:underline\" data-ckui=\"card-title\">\n {title}\n {lock && <span className=\"sr-only\"> ({lock})</span>}\n </a>\n {owner && card.owner && <OwnerChip group={card.owner} href={ownerHref} />}\n {meta.length > 0 && <p className=\"text-xs text-muted-foreground\">{meta.join(\" · \")}</p>}\n {card.viewer?.completed && (\n <Badge variant=\"secondary\" className=\"justify-self-start\">\n {t(\"contents.completed\")}\n </Badge>\n )}\n {!card.access.allowed && card.access.free_after && <p className=\"text-xs text-muted-foreground\">{t(\"contents.freeFrom\", { date: formatDate(m, card.access.free_after, false) })}</p>}\n {kinds.some((k) => card.groups[k]?.length) && (\n <ul className=\"flex flex-wrap gap-1\" data-ckui=\"card-groups\">\n {kinds.flatMap((k) =>\n (card.groups[k] ?? []).slice(0, chips).map((g) => (\n <li key={`${k}:${g.id}`} data-kind={k}>\n <Chip url={g.path} name={g.name || g.slug} />\n </li>\n )),\n )}\n </ul>\n )}\n {(card.counts.favorites !== undefined || card.counts.likes !== undefined) && (\n <p className=\"flex items-center gap-3 text-xs text-muted-foreground tabular-nums\" data-ckui=\"card-counts\">\n {card.counts.favorites !== undefined && (\n <span className=\"inline-flex items-center gap-1\" title={t(\"contents.favorites\")}>\n <HugeiconsIcon icon={FavouriteIcon} strokeWidth={2} className={cn(\"size-3.5\", card.viewer?.favorited && \"fill-current text-primary\")} aria-hidden />\n <span className=\"sr-only\">{t(\"contents.favorites\")}</span>\n {compact(card.counts.favorites, m.language)}\n </span>\n )}\n {card.counts.likes !== undefined && (\n <span className=\"inline-flex items-center gap-1\" title={t(\"contents.likes\")}>\n <HugeiconsIcon icon={ThumbsUpIcon} strokeWidth={2} className=\"size-3.5\" aria-hidden />\n <span className=\"sr-only\">{t(\"contents.likes\")}</span>\n {compact(card.counts.likes, m.language)}\n </span>\n )}\n </p>\n )}\n {children}\n </div>\n </article>\n );\n}\n\n/** The group that owns a content (a channel): its avatar and name, linking to its page. */\nexport function OwnerChip({ group, href, className }: { group: GroupLike; href?: (g: GroupLike) => string | undefined; className?: string }) {\n return (\n <span className={cn(\"flex min-w-0 text-xs text-muted-foreground\", className)} data-ckui=\"card-owner\" data-id={group.id}>\n <GroupLink group={group} href={href} className=\"inline-flex min-w-0 items-center gap-1.5 hover:text-foreground\">\n <GroupAvatar group={group} className=\"size-5 shrink-0 text-[0.625rem]\" />\n <span className=\"truncate\">{group.name || group.slug}</span>\n </GroupLink>\n </span>\n );\n}\n\nfunction Chip({ url, name }: { url?: string; name: string }) {\n const onClick = useLink(url);\n const cls = \"inline-block rounded-md border border-border px-1.5 py-0.5 text-xs text-muted-foreground\";\n return url ? (\n <a href={url} onClick={onClick} className={cn(cls, \"hover:bg-muted hover:text-foreground\")}>\n {name}\n </a>\n ) : (\n <span className={cls}>{name}</span>\n );\n}\n\nexport interface PostCardProps extends Omit<ContentCardProps, \"shape\" | \"chips\" | \"groupKinds\"> {\n /** Above the title: the post's channel or author; default its owner group's chip. */\n byline?: ReactNode;\n}\n\n/** A post in a feed: a wide cover, its title, when it was published and what unlocking it costs. */\nexport function PostCard({ card, href, onOpen, tiers, labels, byline, owner = true, ownerHref, decorate, children, className }: PostCardProps) {\n const m = useMessages();\n const url = href ? href(card) : card.path;\n const open = useCallback(() => onOpen?.(card), [onOpen, card]);\n const onClick = useLink(url, open);\n const lock = lockLabel(m, card.access, tiers, labels);\n const title = card.title || card.code;\n return (\n <article className={cn(\"grid gap-3 rounded-lg border border-border p-3\", className)} data-ckui=\"post-card\" data-id={card.id} data-locked={lock ? \"\" : undefined} lang={card.language || undefined}>\n {byline ?? (owner && card.owner && <OwnerChip group={card.owner} href={ownerHref} className=\"text-sm\" />)}\n <a href={url} onClick={onClick} className=\"grid gap-2\">\n <span className=\"font-medium break-words hover:underline\" data-ckui=\"card-title\">\n {title}\n </span>\n {card.cover.length > 0 && (\n <span className=\"relative block aspect-video overflow-hidden rounded-md bg-muted\">\n <RenditionImg outputs={card.cover} alt=\"\" fit=\"cover\" skeleton className={cn(\"size-full\", lock && \"blur-md\")} />\n <Decorations card={card} decorate={decorate} />\n {lock && (\n <span className=\"absolute inset-0 grid place-items-center\">\n <span className=\"inline-flex items-center gap-1 rounded-md bg-background/85 px-2 py-1 text-xs font-medium shadow-sm\" data-ckui=\"card-lock\">\n <HugeiconsIcon icon={LockIcon} strokeWidth={2} className=\"size-3.5\" aria-hidden />\n {lock}\n </span>\n </span>\n )}\n </span>\n )}\n </a>\n <p className=\"flex flex-wrap items-center gap-3 text-xs text-muted-foreground\">\n {card.published_at && <RelativeTime at={card.published_at} />}\n {!card.cover.length && lock && <span data-ckui=\"card-lock\">{lock}</span>}\n {card.counts.favorites !== undefined && (\n <span className=\"inline-flex items-center gap-1 tabular-nums\" data-ckui=\"card-counts\">\n <HugeiconsIcon icon={FavouriteIcon} strokeWidth={2} className={cn(\"size-3.5\", card.viewer?.favorited && \"fill-current text-primary\")} aria-hidden />\n <span className=\"sr-only\">{m.t(\"contents.favorites\")}</span>\n {compact(card.counts.favorites, m.language)}\n </span>\n )}\n </p>\n {children}\n </article>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AA0CA,SAAS,EAAY,EAAE,SAAM,eAAkE;CAC7F,IAAM,IAAS,IAAW,CAAI;CAE9B,OADI,KAAU,QAAQ,MAAW,KAAc,OAE7C,kBAAC,QAAD;EAAM,WAAU;EAAuC,aAAU;EAC9D,UAAA;CACG,CAAA;AAEV;AAEA,IAAM,KAAW,GAAW,MAAsB,IAAI,KAAK,aAAa,GAAU;CAAE,UAAU;CAAW,uBAAuB;AAAE,CAAC,CAAC,CAAC,OAAO,CAAC;AAG7I,SAAgB,EAAU,GAAe,GAAe,IAA2B,CAAC,GAAG,GAAgC;CACrH,IAAI,EAAE,SAAS,OAAO;CACtB,IAAI,EAAE,SAAS,OAAO,EAAE,EAAE,wBAAwB;CAClD,IAAM,IAAQ,EAAE,SAAS;CACzB,IAAI,GAAO,OAAO,EAAM,OAAO,SAAS,WAAW,OAAO,EAAM,MAAM,MAAM,MAAM,IAAI,EAAE,EAAE,mBAAmB,IAAI,EAAW,GAAG,EAAM,KAAK;CAC1I,IAAM,IAAO,EAAE,MAAM,MAAM,MAAM,EAAM,SAAS,CAAC,CAAC;CAIlD,OAHI,IAAa,EAAQ,GAAQ,CAAI,IACjC,EAAE,MAAM,MAAM,MAAM,EAAE,SAAS,SAAS,CAAC,IAAU,EAAE,EAAE,kBAAkB,IACzE,EAAE,MAAM,SAAe,EAAE,EAAE,kBAAkB,IAC1C,EAAE,EAAE,iBAAiB;AAC9B;AAGA,SAAS,EAAQ,GAAyB,GAAqB;CAC7D,IAAM,IAAW,EAAW,CAAiB,CAAC,EAAE;CAChD,OAAO,GACJ,MAAqC;EACpC,IAAS,GACL,GAAC,KAAY,CAAC,KAAO,EAAE,oBAAoB,EAAE,WAAW,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,YAC3G,EAAE,eAAe,GACjB,EAAS,CAAG;CACd,GACA;EAAC;EAAU;EAAK;CAAM,CACxB;AACF;AAGA,SAAgB,EAAY,EAAE,SAAM,SAAM,WAAQ,WAAQ,YAAY,eAAY,WAAQ,GAAG,UAAO,WAAQ,WAAQ,IAAM,cAAW,aAAU,aAAU,gBAA+B;CACtL,IAAM,IAAI,EAAY,GAChB,EAAE,SAAM,GACR,IAAe,EAAgB,GAC/B,IAAM,IAAO,EAAK,CAAI,IAAI,EAAK,MAE/B,IAAU,EAAQ,GADX,QAAkB,IAAS,CAAI,GAAG,CAAC,GAAQ,CAAI,CAC/B,CAAI,GAC3B,IAAO,EAAU,GAAG,EAAK,QAAQ,GAAO,CAAM,GAC9C,IAAI,EAAK,SACT,IAAW,EAAK,UAAU,EAAK,OAAO,eAAe,IAAI,KAAK,IAAI,GAAG,EAAK,OAAO,WAAW,EAAK,OAAO,YAAY,IAAI,GACxH,IAAQ,KAAc,OAAO,KAAK,EAAK,MAAM,GAC7C,IAAU,EAAE,cAAc,KAAK,MAAM,EAAE,cAAc,GAAM,IAAI,GAC/D,IAAO;EACX,GAAG,EAAE,UAAU,IAAI,CAAY;EAC/B,GAAG,EAAE,OAAO,KAAK,MAAM,EAAQ,GAAQ,CAAC,CAAC;EACzC,EAAE,QAAQ,EAAE,OAAO,kBAAkB,EAAE,KAAK,IAAI;EAChD,IAAU,EAAE,OAAO,oBAAoB,CAAO,IAAI;CACpD,CAAC,CAAC,OAAO,OAAO,GACV,IAAQ,EAAK,SAAS,EAAK;CACjC,OACE,kBAAC,WAAD;EAAS,WAAW,EAAG,uCAAuC,CAAS;EAAG,aAAU;EAAe,WAAS,EAAK;EAAI,eAAa,IAAO,KAAK,KAAA;EAAW,MAAM,EAAK,YAAY,KAAA;EAAhL,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,kBAAC,KAAD;IAAG,MAAM;IAAc;IAAS,WAAU;IAAqD,UAAU;IAAI,eAAA;IAA7G,UAAA;KACE,kBAAC,OAAD;MAAK,WAAW,MAAU,cAAc,iBAAiB;MACvD,UAAA,kBAAC,GAAD;OACE,SAAS,EAAK;OACd,KAAI;OACJ,KAAI;OACJ,UAAA;OACA,WAAU;OACV,UAAU,kBAAC,QAAD;QAAM,WAAU;QAAkF,UAAA,EAAM,OAAO,CAAC,CAAC,CAAC,YAAY;OAAQ,CAAA;MACjJ,CAAA;KACE,CAAA;KACJ,KACC,kBAAC,QAAD;MAAM,WAAU;MAA+I,aAAU;MAAzK,UAAA,CACE,kBAAC,GAAD;OAAe,MAAM;OAAU,aAAa;OAAG,WAAU;OAAW,eAAA;MAAa,CAAA,GAChF,CACG;;KAEP,IAAW,KACV,kBAAC,QAAD;MAAM,WAAU;MAAmD,aAAU;MAC3E,UAAA,kBAAC,QAAD;OAAM,WAAU;OAA0B,OAAO,EAAE,OAAO,GAAG,KAAK,MAAM,IAAW,GAAG,EAAE,GAAG;MAAI,CAAA;KAC3F,CAAA;IAEP;GACH,CAAA,GAAA,kBAAC,GAAD;IAAmB;IAAgB;GAAW,CAAA,CAC3C;EACL,CAAA,GAAA,kBAAC,OAAD;GAAK,WAAU;GAAf,UAAA;IACE,kBAAC,KAAD;KAAG,MAAM;KAAc;KAAS,WAAU;KAAuD,aAAU;KAA3G,UAAA,CACG,GACA,KAAQ,kBAAC,QAAD;MAAM,WAAU;MAAhB,UAAA;OAA0B;OAAG;OAAK;MAAO;KACjD,CAAA,CAAA;;IACF,KAAS,EAAK,SAAS,kBAAC,GAAD;KAAW,OAAO,EAAK;KAAO,MAAM;IAAY,CAAA;IACvE,EAAK,SAAS,KAAK,kBAAC,KAAD;KAAG,WAAU;KAAiC,UAAA,EAAK,KAAK,KAAK;IAAK,CAAA;IACrF,EAAK,QAAQ,aACZ,kBAAC,GAAD;KAAO,SAAQ;KAAY,WAAU;KAClC,UAAA,EAAE,oBAAoB;IAClB,CAAA;IAER,CAAC,EAAK,OAAO,WAAW,EAAK,OAAO,cAAc,kBAAC,KAAD;KAAG,WAAU;KAAiC,UAAA,EAAE,qBAAqB,EAAE,MAAM,EAAW,GAAG,EAAK,OAAO,YAAY,EAAK,EAAE,CAAC;IAAK,CAAA;IAClL,EAAM,MAAM,MAAM,EAAK,OAAO,EAAE,EAAE,MAAM,KACvC,kBAAC,MAAD;KAAI,WAAU;KAAuB,aAAU;KAC5C,UAAA,EAAM,SAAS,OACb,EAAK,OAAO,MAAM,CAAC,EAAA,CAAG,MAAM,GAAG,CAAK,CAAC,CAAC,KAAK,MAC1C,kBAAC,MAAD;MAAyB,aAAW;MAClC,UAAA,kBAAC,GAAD;OAAM,KAAK,EAAE;OAAM,MAAM,EAAE,QAAQ,EAAE;MAAO,CAAA;KAC1C,GAFK,GAAG,EAAE,GAAG,EAAE,IAEf,CACL,CACH;IACE,CAAA;KAEJ,EAAK,OAAO,cAAc,KAAA,KAAa,EAAK,OAAO,UAAU,KAAA,MAC7D,kBAAC,KAAD;KAAG,WAAU;KAAqE,aAAU;KAA5F,UAAA,CACG,EAAK,OAAO,cAAc,KAAA,KACzB,kBAAC,QAAD;MAAM,WAAU;MAAiC,OAAO,EAAE,oBAAoB;MAA9E,UAAA;OACE,kBAAC,GAAD;QAAe,MAAM;QAAe,aAAa;QAAG,WAAW,EAAG,YAAY,EAAK,QAAQ,aAAa,2BAA2B;QAAG,eAAA;OAAa,CAAA;OACnJ,kBAAC,QAAD;QAAM,WAAU;QAAW,UAAA,EAAE,oBAAoB;OAAQ,CAAA;OACxD,EAAQ,EAAK,OAAO,WAAW,EAAE,QAAQ;MACtC;KAEP,CAAA,GAAA,EAAK,OAAO,UAAU,KAAA,KACrB,kBAAC,QAAD;MAAM,WAAU;MAAiC,OAAO,EAAE,gBAAgB;MAA1E,UAAA;OACE,kBAAC,GAAD;QAAe,MAAM;QAAc,aAAa;QAAG,WAAU;QAAW,eAAA;OAAa,CAAA;OACrF,kBAAC,QAAD;QAAM,WAAU;QAAW,UAAA,EAAE,gBAAgB;OAAQ,CAAA;OACpD,EAAQ,EAAK,OAAO,OAAO,EAAE,QAAQ;MAClC;KAEP,CAAA,CAAA;;IAEJ;GACE;EACE,CAAA,CAAA;;AAEb;AAGA,SAAgB,EAAU,EAAE,UAAO,SAAM,gBAAoG;CAC3I,OACE,kBAAC,QAAD;EAAM,WAAW,EAAG,8CAA8C,CAAS;EAAG,aAAU;EAAa,WAAS,EAAM;EAClH,UAAA,kBAAC,GAAD;GAAkB;GAAa;GAAM,WAAU;GAA/C,UAAA,CACE,kBAAC,GAAD;IAAoB;IAAO,WAAU;GAAmC,CAAA,GACxE,kBAAC,QAAD;IAAM,WAAU;IAAY,UAAA,EAAM,QAAQ,EAAM;GAAW,CAAA,CAClD;;CACP,CAAA;AAEV;AAEA,SAAS,EAAK,EAAE,QAAK,WAAwC;CAC3D,IAAM,IAAU,EAAQ,CAAG,GACrB,IAAM;CACZ,OAAO,IACL,kBAAC,KAAD;EAAG,MAAM;EAAc;EAAS,WAAW,EAAG,GAAK,sCAAsC;EACtF,UAAA;CACA,CAAA,IAEH,kBAAC,QAAD;EAAM,WAAW;EAAM,UAAA;CAAW,CAAA;AAEtC;AAQA,SAAgB,EAAS,EAAE,SAAM,SAAM,WAAQ,UAAO,WAAQ,WAAQ,WAAQ,IAAM,cAAW,aAAU,aAAU,gBAA4B;CAC7I,IAAM,IAAI,EAAY,GAChB,IAAM,IAAO,EAAK,CAAI,IAAI,EAAK,MAE/B,IAAU,EAAQ,GADX,QAAkB,IAAS,CAAI,GAAG,CAAC,GAAQ,CAAI,CAC/B,CAAI,GAC3B,IAAO,EAAU,GAAG,EAAK,QAAQ,GAAO,CAAM,GAC9C,IAAQ,EAAK,SAAS,EAAK;CACjC,OACE,kBAAC,WAAD;EAAS,WAAW,EAAG,kDAAkD,CAAS;EAAG,aAAU;EAAY,WAAS,EAAK;EAAI,eAAa,IAAO,KAAK,KAAA;EAAW,MAAM,EAAK,YAAY,KAAA;EAAxL,UAAA;GACG,MAAW,KAAS,EAAK,SAAS,kBAAC,GAAD;IAAW,OAAO,EAAK;IAAO,MAAM;IAAW,WAAU;GAAW,CAAA;GACvG,kBAAC,KAAD;IAAG,MAAM;IAAc;IAAS,WAAU;IAA1C,UAAA,CACE,kBAAC,QAAD;KAAM,WAAU;KAA0C,aAAU;KACjE,UAAA;IACG,CAAA,GACL,EAAK,MAAM,SAAS,KACnB,kBAAC,QAAD;KAAM,WAAU;KAAhB,UAAA;MACE,kBAAC,GAAD;OAAc,SAAS,EAAK;OAAO,KAAI;OAAG,KAAI;OAAQ,UAAA;OAAS,WAAW,EAAG,aAAa,KAAQ,SAAS;MAAI,CAAA;MAC/G,kBAAC,GAAD;OAAmB;OAAgB;MAAW,CAAA;MAC7C,KACC,kBAAC,QAAD;OAAM,WAAU;OACd,UAAA,kBAAC,QAAD;QAAM,WAAU;QAAqG,aAAU;QAA/H,UAAA,CACE,kBAAC,GAAD;SAAe,MAAM;SAAU,aAAa;SAAG,WAAU;SAAW,eAAA;QAAa,CAAA,GAChF,CACG;;MACF,CAAA;KAEJ;IAEP,CAAA,CAAA;;GACH,kBAAC,KAAD;IAAG,WAAU;IAAb,UAAA;KACG,EAAK,gBAAgB,kBAAC,GAAD,EAAc,IAAI,EAAK,aAAe,CAAA;KAC3D,CAAC,EAAK,MAAM,UAAU,KAAQ,kBAAC,QAAD;MAAM,aAAU;MAAa,UAAA;KAAW,CAAA;KACtE,EAAK,OAAO,cAAc,KAAA,KACzB,kBAAC,QAAD;MAAM,WAAU;MAA8C,aAAU;MAAxE,UAAA;OACE,kBAAC,GAAD;QAAe,MAAM;QAAe,aAAa;QAAG,WAAW,EAAG,YAAY,EAAK,QAAQ,aAAa,2BAA2B;QAAG,eAAA;OAAa,CAAA;OACnJ,kBAAC,QAAD;QAAM,WAAU;QAAW,UAAA,EAAE,EAAE,oBAAoB;OAAQ,CAAA;OAC1D,EAAQ,EAAK,OAAO,WAAW,EAAE,QAAQ;MACtC;;IAEP;;GACF;EACM;;AAEb"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ContentKitClient } from '../../client/client.js';
|
|
2
|
+
import { FileDownload } from '../../client/generated/wire.js';
|
|
3
|
+
import { ContentKitUiAppearance } from '../../appearance.js';
|
|
4
|
+
import { ContentKitErrorHandler } from '../../react/context.js';
|
|
5
|
+
export interface VersionDownloadButtonProps {
|
|
6
|
+
content: string;
|
|
7
|
+
version: string;
|
|
8
|
+
/** Hears each granted download. */
|
|
9
|
+
onDownload?: (d: FileDownload) => void;
|
|
10
|
+
/** Called instead of showing the locked notice (a paywall, sign in). */
|
|
11
|
+
onLocked?: () => void;
|
|
12
|
+
size?: "sm" | "default";
|
|
13
|
+
variant?: "default" | "secondary" | "outline" | "ghost";
|
|
14
|
+
client?: ContentKitClient;
|
|
15
|
+
onError?: ContentKitErrorHandler;
|
|
16
|
+
className?: string;
|
|
17
|
+
appearance?: ContentKitUiAppearance;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Downloads a version, as its kind serves it (the original video, a
|
|
21
|
+
* rendition, its pages as a zip): the browser saves the file, charged to the
|
|
22
|
+
* viewer's download quota. It waits while the file is prepared, says when a
|
|
23
|
+
* quota refused and for how long, when the version is locked, and when the
|
|
24
|
+
* site's download policy holds it back ("available to download in N days").
|
|
25
|
+
*/
|
|
26
|
+
export declare function VersionDownloadButton({ content, version, onDownload, onLocked, size, variant, client, onError, className, appearance }: VersionDownloadButtonProps): import('react').JSX.Element;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useMessages as e } from "../../i18n/context.js";
|
|
2
|
+
import { ContentKitUiRoot as t } from "../../scope.js";
|
|
3
|
+
import { useErrorReporter as n } from "../../react/context.js";
|
|
4
|
+
import { Button as r } from "../../ui/button.js";
|
|
5
|
+
import { useVersionDownload as i } from "../../react/versions.js";
|
|
6
|
+
import { useEffect as a, useState as o } from "react";
|
|
7
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
8
|
+
import { cn as l } from "cn";
|
|
9
|
+
import { Download01Icon as u } from "@hugeicons/core-free-icons";
|
|
10
|
+
import { HugeiconsIcon as d } from "@hugeicons/react";
|
|
11
|
+
//#region src/components/content/download-button.tsx
|
|
12
|
+
function f({ content: f, version: p, onDownload: m, onLocked: h, size: g = "default", variant: _ = "secondary", client: v, onError: y, className: b, appearance: x }) {
|
|
13
|
+
let { t: S } = e(), C = i(f, p, { client: v }), w = n(y), [T, E] = o(() => Date.now());
|
|
14
|
+
a(() => {
|
|
15
|
+
if (C.retryAt === void 0) return;
|
|
16
|
+
let e = () => E(Date.now()), t = setTimeout(e, 0), n = setInterval(e, 15e3);
|
|
17
|
+
return () => {
|
|
18
|
+
clearTimeout(t), clearInterval(n);
|
|
19
|
+
};
|
|
20
|
+
}, [C.retryAt]);
|
|
21
|
+
let D = C.retryAt === void 0 ? 0 : C.retryAt - T, O = D > 0, k = () => {
|
|
22
|
+
C.download().then(m, (e) => {
|
|
23
|
+
if (e.code === "forbidden" && h) return h();
|
|
24
|
+
w(e, "version.download");
|
|
25
|
+
});
|
|
26
|
+
}, A = O ? S("versionDownload.limited", { minutes: Math.max(1, Math.ceil(D / 6e4)) }) : C.error?.code === "rate_limited" || C.error?.code === "forbidden" && h ? void 0 : C.message;
|
|
27
|
+
return /* @__PURE__ */ c(t, {
|
|
28
|
+
appearance: x,
|
|
29
|
+
className: l("inline-flex flex-col items-start gap-1", b),
|
|
30
|
+
"data-ckui": "version-download",
|
|
31
|
+
children: [/* @__PURE__ */ c(r, {
|
|
32
|
+
variant: _,
|
|
33
|
+
size: g,
|
|
34
|
+
disabled: C.preparing || O,
|
|
35
|
+
"aria-busy": C.preparing,
|
|
36
|
+
onClick: k,
|
|
37
|
+
children: [/* @__PURE__ */ s(d, {
|
|
38
|
+
icon: u,
|
|
39
|
+
strokeWidth: 2
|
|
40
|
+
}), /* @__PURE__ */ s("span", { children: C.preparing ? S("versionDownload.preparing") : S("versionDownload.label") })]
|
|
41
|
+
}), A && /* @__PURE__ */ s("p", {
|
|
42
|
+
role: "status",
|
|
43
|
+
className: "text-xs text-muted-foreground",
|
|
44
|
+
children: A
|
|
45
|
+
})]
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { f as VersionDownloadButton };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=download-button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download-button.js","names":[],"sources":["../../../src/components/content/download-button.tsx"],"sourcesContent":["import { Download01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport { useEffect, useState } from \"react\";\nimport type { ContentKitClient } from \"../../client/client.js\";\nimport type { FileDownload } from \"../../client/generated/wire.js\";\nimport type { ContentKitUiAppearance } from \"../../appearance.js\";\nimport { useMessages } from \"../../i18n/context.js\";\nimport { useErrorReporter, type ContentKitErrorHandler } from \"../../react/context.js\";\nimport { useVersionDownload } from \"../../react/versions.js\";\nimport { ContentKitUiRoot } from \"../../scope.js\";\nimport { Button } from \"#ckui/ui/button\";\n\nexport interface VersionDownloadButtonProps {\n content: string;\n version: string;\n /** Hears each granted download. */\n onDownload?: (d: FileDownload) => void;\n /** Called instead of showing the locked notice (a paywall, sign in). */\n onLocked?: () => void;\n size?: \"sm\" | \"default\";\n variant?: \"default\" | \"secondary\" | \"outline\" | \"ghost\";\n client?: ContentKitClient;\n onError?: ContentKitErrorHandler;\n className?: string;\n appearance?: ContentKitUiAppearance;\n}\n\n/**\n * Downloads a version, as its kind serves it (the original video, a\n * rendition, its pages as a zip): the browser saves the file, charged to the\n * viewer's download quota. It waits while the file is prepared, says when a\n * quota refused and for how long, when the version is locked, and when the\n * site's download policy holds it back (\"available to download in N days\").\n */\nexport function VersionDownloadButton({ content, version, onDownload, onLocked, size = \"default\", variant = \"secondary\", client, onError, className, appearance }: VersionDownloadButtonProps) {\n const { t } = useMessages();\n const d = useVersionDownload(content, version, { client });\n const report = useErrorReporter(onError);\n // The clock the quota's countdown reads, kept while one holds.\n const [now, setNow] = useState(() => Date.now());\n useEffect(() => {\n if (d.retryAt === undefined) return;\n const tick = () => setNow(Date.now());\n const first = setTimeout(tick, 0);\n const every = setInterval(tick, 15_000);\n return () => {\n clearTimeout(first);\n clearInterval(every);\n };\n }, [d.retryAt]);\n const left = d.retryAt === undefined ? 0 : d.retryAt - now;\n const limited = left > 0;\n const run = () => {\n d.download().then(onDownload, (err) => {\n if (err.code === \"forbidden\" && onLocked) return onLocked();\n report(err, \"version.download\");\n });\n };\n // A quota's notice counts down; a locked version goes to the host's onLocked.\n const notice = limited\n ? t(\"versionDownload.limited\", { minutes: Math.max(1, Math.ceil(left / 60_000)) })\n : d.error?.code === \"rate_limited\" || (d.error?.code === \"forbidden\" && onLocked)\n ? undefined\n : d.message;\n return (\n <ContentKitUiRoot appearance={appearance} className={cn(\"inline-flex flex-col items-start gap-1\", className)} data-ckui=\"version-download\">\n <Button variant={variant} size={size} disabled={d.preparing || limited} aria-busy={d.preparing} onClick={run}>\n <HugeiconsIcon icon={Download01Icon} strokeWidth={2} />\n <span>{d.preparing ? t(\"versionDownload.preparing\") : t(\"versionDownload.label\")}</span>\n </Button>\n {notice && (\n <p role=\"status\" className=\"text-xs text-muted-foreground\">\n {notice}\n </p>\n )}\n </ContentKitUiRoot>\n );\n}\n"],"mappings":";;;;;;;;;;;AAmCA,SAAgB,EAAsB,EAAE,YAAS,YAAS,eAAY,aAAU,UAAO,WAAW,aAAU,aAAa,WAAQ,YAAS,cAAW,iBAA0C;CAC7L,IAAM,EAAE,SAAM,EAAY,GACpB,IAAI,EAAmB,GAAS,GAAS,EAAE,UAAO,CAAC,GACnD,IAAS,EAAiB,CAAO,GAEjC,CAAC,GAAK,KAAU,QAAe,KAAK,IAAI,CAAC;CAC/C,QAAgB;EACd,IAAI,EAAE,YAAY,KAAA,GAAW;EAC7B,IAAM,UAAa,EAAO,KAAK,IAAI,CAAC,GAC9B,IAAQ,WAAW,GAAM,CAAC,GAC1B,IAAQ,YAAY,GAAM,IAAM;EACtC,aAAa;GAEX,AADA,aAAa,CAAK,GAClB,cAAc,CAAK;EACrB;CACF,GAAG,CAAC,EAAE,OAAO,CAAC;CACd,IAAM,IAAO,EAAE,YAAY,KAAA,IAAY,IAAI,EAAE,UAAU,GACjD,IAAU,IAAO,GACjB,UAAY;EAChB,EAAE,SAAS,CAAC,CAAC,KAAK,IAAa,MAAQ;GACrC,IAAI,EAAI,SAAS,eAAe,GAAU,OAAO,EAAS;GAC1D,EAAO,GAAK,kBAAkB;EAChC,CAAC;CACH,GAEM,IAAS,IACX,EAAE,2BAA2B,EAAE,SAAS,KAAK,IAAI,GAAG,KAAK,KAAK,IAAO,GAAM,CAAC,EAAE,CAAC,IAC/E,EAAE,OAAO,SAAS,kBAAmB,EAAE,OAAO,SAAS,eAAe,IACpE,KAAA,IACA,EAAE;CACR,OACE,kBAAC,GAAD;EAA8B;EAAY,WAAW,EAAG,0CAA0C,CAAS;EAAG,aAAU;EAAxH,UAAA,CACE,kBAAC,GAAD;GAAiB;GAAe;GAAM,UAAU,EAAE,aAAa;GAAS,aAAW,EAAE;GAAW,SAAS;GAAzG,UAAA,CACE,kBAAC,GAAD;IAAe,MAAM;IAAgB,aAAa;GAAI,CAAA,GACtD,kBAAC,QAAD,EAAA,UAAO,EAAE,YAAY,EAAE,2BAA2B,IAAI,EAAE,uBAAuB,EAAQ,CAAA,CACjF;EACP,CAAA,GAAA,KACC,kBAAC,KAAD;GAAG,MAAK;GAAS,WAAU;GACxB,UAAA;EACA,CAAA,CAEW;;AAEtB"}
|
|
@@ -24,9 +24,16 @@ export declare function CardGrid({ list, render, ...p }: CardListProps & {
|
|
|
24
24
|
list: UseList<Card>;
|
|
25
25
|
render: string;
|
|
26
26
|
}): import('react').JSX.Element;
|
|
27
|
+
/**
|
|
28
|
+
* What a list does with cards the reader doesn't open: shows them marked
|
|
29
|
+
* (show), leaves them out (hide), or lists them apart, counted, behind a
|
|
30
|
+
* toggle that loads them when opened (collapse).
|
|
31
|
+
*/
|
|
32
|
+
export type LockedCards = "show" | "hide" | "collapse";
|
|
27
33
|
/** Contents as cards (GET /v1/contents): any sort and filter, paged. */
|
|
28
|
-
export declare function ContentGrid({ query, ...p }: CardListProps & {
|
|
34
|
+
export declare function ContentGrid({ query, locked, ...p }: CardListProps & {
|
|
29
35
|
query: ContentQuery;
|
|
36
|
+
locked?: LockedCards;
|
|
30
37
|
}): import('react').JSX.Element;
|
|
31
38
|
/** Search results as cards; nothing until the query has text. */
|
|
32
39
|
export declare function SearchResults({ query, ...p }: CardListProps & {
|