@roy-ui/ui 0.0.10 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Card-6L4M4GFX.css +270 -0
- package/dist/ImageCarousel-4L4MNUP2.css +148 -0
- package/dist/chunk-B7QN2JTN.js +307 -0
- package/dist/chunk-B7QN2JTN.js.map +1 -0
- package/dist/components/card/index.d.ts +92 -0
- package/dist/components/card/index.js +5 -0
- package/dist/components/card/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -2
- package/package.json +5 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
.royui-card {
|
|
2
|
+
/* Surfaces and ink — every one is a variable. The default is a premium
|
|
3
|
+
off-white (not pure white), which reads warmer and reads on a white page.
|
|
4
|
+
Opt into dark with theme="dark" (.royui-card--dark) or theme="auto". */
|
|
5
|
+
--royui-card-bg: #fafaf8;
|
|
6
|
+
--royui-card-fg: #1a1a1c;
|
|
7
|
+
--royui-card-muted: rgba(0, 0, 0, 0.52);
|
|
8
|
+
--royui-card-faint: rgba(0, 0, 0, 0.42);
|
|
9
|
+
--royui-card-line: rgba(0, 0, 0, 0.08);
|
|
10
|
+
--royui-card-radius: 24px;
|
|
11
|
+
/* A thin frame around the photo. Inner radius = radius − pad keeps the
|
|
12
|
+
corners concentric (24 − 8 = 16, the gallery's own radius). */
|
|
13
|
+
--royui-card-pad: 8px;
|
|
14
|
+
--royui-card-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
15
|
+
/* The price reads as premium when it's large and light, not bold. */
|
|
16
|
+
--royui-card-price-weight: 500;
|
|
17
|
+
/* Selection stays legible on the surface — a soft tint, not the browser blue. */
|
|
18
|
+
--royui-card-selection-bg: rgba(20, 20, 22, 0.12);
|
|
19
|
+
--royui-card-selection-fg: #1a1a1c;
|
|
20
|
+
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
width: 100%;
|
|
25
|
+
max-width: 360px;
|
|
26
|
+
/* The card is its own query container, so type scales to the card's width
|
|
27
|
+
(cqi units below) rather than the viewport — it fits in a sidebar, a
|
|
28
|
+
grid cell, or full-bleed without a media query. */
|
|
29
|
+
container-type: inline-size;
|
|
30
|
+
padding: var(--royui-card-pad);
|
|
31
|
+
background: var(--royui-card-bg);
|
|
32
|
+
color: var(--royui-card-fg);
|
|
33
|
+
border-radius: var(--royui-card-radius);
|
|
34
|
+
font-family: inherit;
|
|
35
|
+
/* A ring, a contact shadow, and a soft cast — the card rests, it doesn't
|
|
36
|
+
float. The ring is firm enough to hold the edge on a white background. */
|
|
37
|
+
box-shadow:
|
|
38
|
+
0 0 0 1px rgba(0, 0, 0, 0.07),
|
|
39
|
+
0 1px 2px rgba(0, 0, 0, 0.04),
|
|
40
|
+
0 14px 34px -16px rgba(0, 0, 0, 0.22);
|
|
41
|
+
transition:
|
|
42
|
+
transform 300ms var(--royui-card-ease),
|
|
43
|
+
box-shadow 300ms var(--royui-card-ease);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.royui-card--interactive:hover {
|
|
47
|
+
transform: translateY(-2px);
|
|
48
|
+
box-shadow:
|
|
49
|
+
0 0 0 1px rgba(0, 0, 0, 0.05),
|
|
50
|
+
0 2px 4px rgba(0, 0, 0, 0.05),
|
|
51
|
+
0 22px 48px -18px rgba(0, 0, 0, 0.28);
|
|
52
|
+
}
|
|
53
|
+
/* The photo eases in a hair as the whole card lifts — barely there. */
|
|
54
|
+
.royui-card--interactive:hover .royui-carousel__img {
|
|
55
|
+
transform: scale(1.045);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Force a legible selection — the browser default can wash text out on the
|
|
59
|
+
off-white surface. */
|
|
60
|
+
.royui-card ::selection {
|
|
61
|
+
background: var(--royui-card-selection-bg);
|
|
62
|
+
color: var(--royui-card-selection-fg);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ── Badge ───────────────────────────────────────────────────────────── */
|
|
66
|
+
.royui-card__badge {
|
|
67
|
+
position: absolute;
|
|
68
|
+
top: 11px;
|
|
69
|
+
left: 11px;
|
|
70
|
+
display: inline-flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 5px;
|
|
73
|
+
padding: 4px 9px 4px 7px;
|
|
74
|
+
background: rgba(255, 255, 255, 0.9);
|
|
75
|
+
-webkit-backdrop-filter: blur(10px) saturate(1.2);
|
|
76
|
+
backdrop-filter: blur(10px) saturate(1.2);
|
|
77
|
+
border-radius: 99px;
|
|
78
|
+
color: #1a1a1c;
|
|
79
|
+
font-size: 12px;
|
|
80
|
+
font-size: clamp(11px, 3.7cqi, 12px);
|
|
81
|
+
font-weight: 400;
|
|
82
|
+
letter-spacing: 0;
|
|
83
|
+
line-height: 1;
|
|
84
|
+
box-shadow:
|
|
85
|
+
0 0 0 0.5px rgba(0, 0, 0, 0.04),
|
|
86
|
+
0 1px 2px rgba(0, 0, 0, 0.1),
|
|
87
|
+
0 3px 10px -3px rgba(0, 0, 0, 0.22);
|
|
88
|
+
}
|
|
89
|
+
.royui-card__badge-icon {
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
color: #f5b400;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ── Body ────────────────────────────────────────────────────────────── */
|
|
95
|
+
/* Horizontal padding insets the text a touch past the photo edge; the photo
|
|
96
|
+
and button sit at the card padding so they share one clean vertical line. */
|
|
97
|
+
.royui-card__body {
|
|
98
|
+
padding: 14px 6px 2px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.royui-card__price-row {
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: baseline;
|
|
104
|
+
flex-wrap: wrap;
|
|
105
|
+
gap: 4px 8px;
|
|
106
|
+
min-width: 0;
|
|
107
|
+
}
|
|
108
|
+
.royui-card__price {
|
|
109
|
+
font-size: 23px;
|
|
110
|
+
font-size: clamp(18px, 7.2cqi, 23px);
|
|
111
|
+
font-weight: var(--royui-card-price-weight);
|
|
112
|
+
letter-spacing: -0.03em;
|
|
113
|
+
font-variant-numeric: tabular-nums;
|
|
114
|
+
}
|
|
115
|
+
.royui-card__price-label {
|
|
116
|
+
font-size: 14px;
|
|
117
|
+
font-size: clamp(12px, 4.4cqi, 14px);
|
|
118
|
+
font-weight: 450;
|
|
119
|
+
color: var(--royui-card-muted);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.royui-card__subtitle {
|
|
123
|
+
margin: 6px 0 0;
|
|
124
|
+
font-size: 14px;
|
|
125
|
+
font-size: clamp(12.5px, 4.4cqi, 14px);
|
|
126
|
+
line-height: 1.4;
|
|
127
|
+
color: var(--royui-card-muted);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.royui-card__divider {
|
|
131
|
+
height: 1px;
|
|
132
|
+
margin: 14px 0;
|
|
133
|
+
background: var(--royui-card-line);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Stats stay on one row and sit flush to both content edges, so the left and
|
|
137
|
+
right margins from the card border match. The hairline divider rides the
|
|
138
|
+
middle. */
|
|
139
|
+
.royui-card__stats {
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-wrap: nowrap;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: space-between;
|
|
144
|
+
gap: 10px;
|
|
145
|
+
gap: clamp(8px, 3.2cqi, 10px);
|
|
146
|
+
font-size: 14px;
|
|
147
|
+
font-size: clamp(12px, 4.4cqi, 14px);
|
|
148
|
+
color: var(--royui-card-fg);
|
|
149
|
+
}
|
|
150
|
+
.royui-card__stat {
|
|
151
|
+
display: inline-flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
gap: 6px;
|
|
154
|
+
flex: none;
|
|
155
|
+
white-space: nowrap;
|
|
156
|
+
}
|
|
157
|
+
.royui-card__stat-sep {
|
|
158
|
+
flex: none;
|
|
159
|
+
width: 1px;
|
|
160
|
+
height: 15px;
|
|
161
|
+
background: var(--royui-card-line);
|
|
162
|
+
}
|
|
163
|
+
.royui-card__stat-icon {
|
|
164
|
+
display: inline-flex;
|
|
165
|
+
flex: none;
|
|
166
|
+
color: var(--royui-card-faint);
|
|
167
|
+
}
|
|
168
|
+
/* The figure carries weight; the descriptor is dimmed, like the reference. */
|
|
169
|
+
.royui-card__stat-value {
|
|
170
|
+
color: var(--royui-card-fg);
|
|
171
|
+
}
|
|
172
|
+
.royui-card__stat-label {
|
|
173
|
+
color: var(--royui-card-muted);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.royui-card__footer {
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
justify-content: space-between;
|
|
180
|
+
flex-wrap: wrap;
|
|
181
|
+
gap: 6px 12px;
|
|
182
|
+
margin-top: 14px;
|
|
183
|
+
font-size: 14px;
|
|
184
|
+
font-size: clamp(12px, 4.4cqi, 14px);
|
|
185
|
+
}
|
|
186
|
+
.royui-card__author {
|
|
187
|
+
color: var(--royui-card-muted);
|
|
188
|
+
min-width: 0;
|
|
189
|
+
}
|
|
190
|
+
.royui-card__author-link {
|
|
191
|
+
color: var(--royui-card-fg);
|
|
192
|
+
font-weight: 550;
|
|
193
|
+
text-decoration: underline;
|
|
194
|
+
text-underline-offset: 2px;
|
|
195
|
+
text-decoration-thickness: 1px;
|
|
196
|
+
text-decoration-color: var(--royui-card-line);
|
|
197
|
+
transition: text-decoration-color 180ms var(--royui-card-ease);
|
|
198
|
+
}
|
|
199
|
+
a.royui-card__author-link:hover {
|
|
200
|
+
text-decoration-color: currentColor;
|
|
201
|
+
}
|
|
202
|
+
.royui-card__meta {
|
|
203
|
+
color: var(--royui-card-faint);
|
|
204
|
+
white-space: nowrap;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.royui-card__action {
|
|
208
|
+
margin-top: 16px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* ── Dark theme ──────────────────────────────────────────────────────────
|
|
212
|
+
Opt in with theme="dark" (.royui-card--dark). theme="auto" (.royui-card--auto)
|
|
213
|
+
applies the same tokens only when the OS asks for dark — so the default light
|
|
214
|
+
card is never silently swapped out from under a dark page. */
|
|
215
|
+
.royui-card--dark {
|
|
216
|
+
--royui-card-bg: #161617;
|
|
217
|
+
--royui-card-fg: #f5f5f7;
|
|
218
|
+
--royui-card-muted: rgba(255, 255, 255, 0.55);
|
|
219
|
+
--royui-card-faint: rgba(255, 255, 255, 0.4);
|
|
220
|
+
--royui-card-line: rgba(255, 255, 255, 0.1);
|
|
221
|
+
--royui-card-selection-bg: rgba(245, 245, 247, 0.2);
|
|
222
|
+
--royui-card-selection-fg: #f5f5f7;
|
|
223
|
+
box-shadow:
|
|
224
|
+
0 0 0 1px rgba(255, 255, 255, 0.1),
|
|
225
|
+
0 14px 34px -16px rgba(0, 0, 0, 0.6);
|
|
226
|
+
}
|
|
227
|
+
.royui-card--dark.royui-card--interactive:hover {
|
|
228
|
+
box-shadow:
|
|
229
|
+
0 0 0 1px rgba(255, 255, 255, 0.12),
|
|
230
|
+
0 22px 48px -18px rgba(0, 0, 0, 0.7);
|
|
231
|
+
}
|
|
232
|
+
.royui-card--dark .royui-card__badge {
|
|
233
|
+
background: rgba(28, 28, 30, 0.82);
|
|
234
|
+
color: #f5f5f7;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@media (prefers-color-scheme: dark) {
|
|
238
|
+
.royui-card--auto {
|
|
239
|
+
--royui-card-bg: #161617;
|
|
240
|
+
--royui-card-fg: #f5f5f7;
|
|
241
|
+
--royui-card-muted: rgba(255, 255, 255, 0.55);
|
|
242
|
+
--royui-card-faint: rgba(255, 255, 255, 0.4);
|
|
243
|
+
--royui-card-line: rgba(255, 255, 255, 0.1);
|
|
244
|
+
--royui-card-selection-bg: rgba(245, 245, 247, 0.2);
|
|
245
|
+
--royui-card-selection-fg: #f5f5f7;
|
|
246
|
+
box-shadow:
|
|
247
|
+
0 0 0 1px rgba(255, 255, 255, 0.1),
|
|
248
|
+
0 14px 34px -16px rgba(0, 0, 0, 0.6);
|
|
249
|
+
}
|
|
250
|
+
.royui-card--auto.royui-card--interactive:hover {
|
|
251
|
+
box-shadow:
|
|
252
|
+
0 0 0 1px rgba(255, 255, 255, 0.12),
|
|
253
|
+
0 22px 48px -18px rgba(0, 0, 0, 0.7);
|
|
254
|
+
}
|
|
255
|
+
.royui-card--auto .royui-card__badge {
|
|
256
|
+
background: rgba(28, 28, 30, 0.82);
|
|
257
|
+
color: #f5f5f7;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@media (prefers-reduced-motion: reduce) {
|
|
262
|
+
.royui-card,
|
|
263
|
+
.royui-card--interactive:hover {
|
|
264
|
+
transition: none;
|
|
265
|
+
transform: none;
|
|
266
|
+
}
|
|
267
|
+
.royui-card--interactive:hover .royui-carousel__img {
|
|
268
|
+
transform: none;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
.royui-carousel {
|
|
2
|
+
/* Tunables — radius and ratio are the ones you'll reach for most. */
|
|
3
|
+
--royui-carousel-ratio: 4 / 3;
|
|
4
|
+
--royui-carousel-radius: 16px;
|
|
5
|
+
--royui-carousel-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
6
|
+
--royui-carousel-dot: rgba(255, 255, 255, 0.55);
|
|
7
|
+
--royui-carousel-dot-active: #ffffff;
|
|
8
|
+
|
|
9
|
+
position: relative;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
border-radius: var(--royui-carousel-radius);
|
|
12
|
+
aspect-ratio: var(--royui-carousel-ratio);
|
|
13
|
+
background: rgba(0, 0, 0, 0.06);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.royui-carousel__viewport {
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
/* Let the browser own vertical scroll; we only claim the horizontal axis. */
|
|
20
|
+
touch-action: pan-y;
|
|
21
|
+
cursor: grab;
|
|
22
|
+
-webkit-tap-highlight-color: transparent;
|
|
23
|
+
}
|
|
24
|
+
.royui-carousel__viewport:active {
|
|
25
|
+
cursor: grabbing;
|
|
26
|
+
}
|
|
27
|
+
.royui-carousel__viewport:focus-visible {
|
|
28
|
+
outline: 2px solid rgba(255, 255, 255, 0.9);
|
|
29
|
+
outline-offset: -2px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.royui-carousel__track {
|
|
33
|
+
display: flex;
|
|
34
|
+
width: 100%;
|
|
35
|
+
height: 100%;
|
|
36
|
+
/* The signature slide — long, eased, with a touch of overshoot at the tail. */
|
|
37
|
+
transition: transform 520ms var(--royui-carousel-ease);
|
|
38
|
+
will-change: transform;
|
|
39
|
+
}
|
|
40
|
+
/* While the finger is down the track tracks the pointer 1:1, no easing. */
|
|
41
|
+
.royui-carousel__track--dragging {
|
|
42
|
+
transition: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.royui-carousel__slide {
|
|
46
|
+
position: relative;
|
|
47
|
+
flex: 0 0 100%;
|
|
48
|
+
width: 100%;
|
|
49
|
+
height: 100%;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.royui-carousel__img {
|
|
54
|
+
display: block;
|
|
55
|
+
width: 100%;
|
|
56
|
+
height: 100%;
|
|
57
|
+
object-fit: cover;
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
user-select: none;
|
|
60
|
+
-webkit-user-drag: none;
|
|
61
|
+
/* Hover-zoom is driven by the card; the transition lives here. */
|
|
62
|
+
transition: transform 700ms var(--royui-carousel-ease);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* A faint floor of shade so white dots stay legible over any photo. */
|
|
66
|
+
.royui-carousel__scrim {
|
|
67
|
+
position: absolute;
|
|
68
|
+
inset: auto 0 0 0;
|
|
69
|
+
height: 38%;
|
|
70
|
+
background: linear-gradient(to top, rgba(0, 0, 0, 0.3), transparent);
|
|
71
|
+
opacity: 0;
|
|
72
|
+
transition: opacity 320ms var(--royui-carousel-ease);
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
}
|
|
75
|
+
.royui-carousel--has-dots .royui-carousel__scrim {
|
|
76
|
+
opacity: 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.royui-carousel__overlay {
|
|
80
|
+
position: absolute;
|
|
81
|
+
inset: 0;
|
|
82
|
+
z-index: 3;
|
|
83
|
+
pointer-events: none;
|
|
84
|
+
}
|
|
85
|
+
/* Re-enable interaction for whatever the overlay actually contains. */
|
|
86
|
+
.royui-carousel__overlay > * {
|
|
87
|
+
pointer-events: auto;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.royui-carousel__dots {
|
|
91
|
+
position: absolute;
|
|
92
|
+
inset: auto 0 12px 0;
|
|
93
|
+
z-index: 4;
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
justify-content: center;
|
|
97
|
+
gap: 6px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.royui-carousel__dot {
|
|
101
|
+
width: 6px;
|
|
102
|
+
height: 6px;
|
|
103
|
+
padding: 0;
|
|
104
|
+
border: none;
|
|
105
|
+
border-radius: 99px;
|
|
106
|
+
background: var(--royui-carousel-dot);
|
|
107
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
-webkit-tap-highlight-color: transparent;
|
|
110
|
+
/* The active dot doesn't pop — it stretches into a pill. */
|
|
111
|
+
transition:
|
|
112
|
+
width 380ms var(--royui-carousel-ease),
|
|
113
|
+
background 380ms var(--royui-carousel-ease),
|
|
114
|
+
opacity 380ms var(--royui-carousel-ease);
|
|
115
|
+
}
|
|
116
|
+
.royui-carousel__dot:hover {
|
|
117
|
+
background: rgba(255, 255, 255, 0.85);
|
|
118
|
+
}
|
|
119
|
+
.royui-carousel__dot--active {
|
|
120
|
+
width: 20px;
|
|
121
|
+
background: var(--royui-carousel-dot-active);
|
|
122
|
+
}
|
|
123
|
+
.royui-carousel__dot:focus-visible {
|
|
124
|
+
outline: 2px solid #fff;
|
|
125
|
+
outline-offset: 2px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Screen-reader-only live region. */
|
|
129
|
+
.royui-carousel__status {
|
|
130
|
+
position: absolute;
|
|
131
|
+
width: 1px;
|
|
132
|
+
height: 1px;
|
|
133
|
+
margin: -1px;
|
|
134
|
+
padding: 0;
|
|
135
|
+
overflow: hidden;
|
|
136
|
+
clip: rect(0 0 0 0);
|
|
137
|
+
white-space: nowrap;
|
|
138
|
+
border: 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@media (prefers-reduced-motion: reduce) {
|
|
142
|
+
.royui-carousel__track,
|
|
143
|
+
.royui-carousel__img,
|
|
144
|
+
.royui-carousel__dot,
|
|
145
|
+
.royui-carousel__scrim {
|
|
146
|
+
transition: none;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Button } from './chunk-4SGMAZBG.js';
|
|
3
|
+
import { forwardRef, useId, useState, useCallback, useRef, useEffect, Fragment as Fragment$1 } from 'react';
|
|
4
|
+
import './ImageCarousel-4L4MNUP2.css';
|
|
5
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
import './Card-6L4M4GFX.css';
|
|
7
|
+
|
|
8
|
+
var COMMIT_THRESHOLD = 0.18;
|
|
9
|
+
var RUBBER_BAND = 0.35;
|
|
10
|
+
var ImageCarousel = forwardRef(
|
|
11
|
+
({
|
|
12
|
+
images,
|
|
13
|
+
index,
|
|
14
|
+
defaultIndex = 0,
|
|
15
|
+
onIndexChange,
|
|
16
|
+
showDots = true,
|
|
17
|
+
draggable = true,
|
|
18
|
+
ratio = "4 / 3",
|
|
19
|
+
overlay,
|
|
20
|
+
autoplay = false,
|
|
21
|
+
autoplayInterval = 2500,
|
|
22
|
+
pauseOnHover = true,
|
|
23
|
+
className = "",
|
|
24
|
+
style,
|
|
25
|
+
...rest
|
|
26
|
+
}, ref) => {
|
|
27
|
+
const count = images.length;
|
|
28
|
+
const multiple = count > 1;
|
|
29
|
+
const labelId = useId();
|
|
30
|
+
const [internal, setInternal] = useState(
|
|
31
|
+
() => Math.max(0, Math.min(count - 1, defaultIndex))
|
|
32
|
+
);
|
|
33
|
+
const active = index ?? internal;
|
|
34
|
+
const commit = useCallback(
|
|
35
|
+
(next) => {
|
|
36
|
+
const clamped = Math.max(0, Math.min(count - 1, next));
|
|
37
|
+
if (clamped === active) return;
|
|
38
|
+
if (index === void 0) setInternal(clamped);
|
|
39
|
+
onIndexChange?.(clamped);
|
|
40
|
+
},
|
|
41
|
+
[active, count, index, onIndexChange]
|
|
42
|
+
);
|
|
43
|
+
const viewportRef = useRef(null);
|
|
44
|
+
const startX = useRef(0);
|
|
45
|
+
const widthRef = useRef(0);
|
|
46
|
+
const [drag, setDrag] = useState(0);
|
|
47
|
+
const [dragging, setDragging] = useState(false);
|
|
48
|
+
const [hovered, setHovered] = useState(false);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!autoplay || !multiple || dragging) return;
|
|
51
|
+
if (pauseOnHover && hovered) return;
|
|
52
|
+
if (typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const id = window.setTimeout(
|
|
56
|
+
() => commit(active + 1 >= count ? 0 : active + 1),
|
|
57
|
+
autoplayInterval
|
|
58
|
+
);
|
|
59
|
+
return () => window.clearTimeout(id);
|
|
60
|
+
}, [
|
|
61
|
+
autoplay,
|
|
62
|
+
autoplayInterval,
|
|
63
|
+
pauseOnHover,
|
|
64
|
+
hovered,
|
|
65
|
+
dragging,
|
|
66
|
+
multiple,
|
|
67
|
+
active,
|
|
68
|
+
count,
|
|
69
|
+
commit
|
|
70
|
+
]);
|
|
71
|
+
const onPointerDown = (e) => {
|
|
72
|
+
if (!draggable || !multiple || e.button !== 0) return;
|
|
73
|
+
widthRef.current = viewportRef.current?.offsetWidth ?? 0;
|
|
74
|
+
startX.current = e.clientX;
|
|
75
|
+
setDragging(true);
|
|
76
|
+
viewportRef.current?.setPointerCapture(e.pointerId);
|
|
77
|
+
};
|
|
78
|
+
const onPointerMove = (e) => {
|
|
79
|
+
if (!dragging) return;
|
|
80
|
+
let dx = e.clientX - startX.current;
|
|
81
|
+
const atStart = active === 0 && dx > 0;
|
|
82
|
+
const atEnd = active === count - 1 && dx < 0;
|
|
83
|
+
if (atStart || atEnd) dx *= RUBBER_BAND;
|
|
84
|
+
setDrag(dx);
|
|
85
|
+
};
|
|
86
|
+
const endDrag = (e) => {
|
|
87
|
+
if (!dragging) return;
|
|
88
|
+
setDragging(false);
|
|
89
|
+
viewportRef.current?.releasePointerCapture?.(e.pointerId);
|
|
90
|
+
const threshold = (widthRef.current || 1) * COMMIT_THRESHOLD;
|
|
91
|
+
if (drag <= -threshold) commit(active + 1);
|
|
92
|
+
else if (drag >= threshold) commit(active - 1);
|
|
93
|
+
setDrag(0);
|
|
94
|
+
};
|
|
95
|
+
const onKeyDown = (e) => {
|
|
96
|
+
if (!multiple) return;
|
|
97
|
+
if (e.key === "ArrowRight") {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
commit(active + 1);
|
|
100
|
+
} else if (e.key === "ArrowLeft") {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
commit(active - 1);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const trackStyle = {
|
|
106
|
+
transform: `translate3d(calc(${-active * 100}% + ${drag}px), 0, 0)`
|
|
107
|
+
};
|
|
108
|
+
const classes = [
|
|
109
|
+
"royui-carousel",
|
|
110
|
+
showDots && multiple ? "royui-carousel--has-dots" : "",
|
|
111
|
+
className
|
|
112
|
+
].filter(Boolean).join(" ");
|
|
113
|
+
return /* @__PURE__ */ jsxs(
|
|
114
|
+
"div",
|
|
115
|
+
{
|
|
116
|
+
ref,
|
|
117
|
+
className: classes,
|
|
118
|
+
style: { ["--royui-carousel-ratio"]: ratio, ...style },
|
|
119
|
+
role: "group",
|
|
120
|
+
"aria-roledescription": "carousel",
|
|
121
|
+
"aria-label": "Property images",
|
|
122
|
+
...rest,
|
|
123
|
+
children: [
|
|
124
|
+
/* @__PURE__ */ jsx(
|
|
125
|
+
"div",
|
|
126
|
+
{
|
|
127
|
+
ref: viewportRef,
|
|
128
|
+
className: "royui-carousel__viewport",
|
|
129
|
+
tabIndex: multiple ? 0 : void 0,
|
|
130
|
+
onPointerDown,
|
|
131
|
+
onPointerMove,
|
|
132
|
+
onPointerUp: endDrag,
|
|
133
|
+
onPointerCancel: endDrag,
|
|
134
|
+
onKeyDown,
|
|
135
|
+
onMouseEnter: autoplay && pauseOnHover ? () => setHovered(true) : void 0,
|
|
136
|
+
onMouseLeave: autoplay && pauseOnHover ? () => setHovered(false) : void 0,
|
|
137
|
+
children: /* @__PURE__ */ jsx(
|
|
138
|
+
"div",
|
|
139
|
+
{
|
|
140
|
+
className: [
|
|
141
|
+
"royui-carousel__track",
|
|
142
|
+
dragging ? "royui-carousel__track--dragging" : ""
|
|
143
|
+
].filter(Boolean).join(" "),
|
|
144
|
+
style: trackStyle,
|
|
145
|
+
children: images.map((img, i) => /* @__PURE__ */ jsx("div", { className: "royui-carousel__slide", children: /* @__PURE__ */ jsx(
|
|
146
|
+
"img",
|
|
147
|
+
{
|
|
148
|
+
className: "royui-carousel__img",
|
|
149
|
+
src: img.src,
|
|
150
|
+
alt: img.alt ?? "",
|
|
151
|
+
draggable: false,
|
|
152
|
+
loading: i === 0 ? "eager" : "lazy",
|
|
153
|
+
"aria-hidden": i !== active
|
|
154
|
+
}
|
|
155
|
+
) }, `${img.src}-${i}`))
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
/* @__PURE__ */ jsx("div", { className: "royui-carousel__scrim", "aria-hidden": "true" }),
|
|
161
|
+
overlay != null && /* @__PURE__ */ jsx("div", { className: "royui-carousel__overlay", children: overlay }),
|
|
162
|
+
showDots && multiple && /* @__PURE__ */ jsx(
|
|
163
|
+
"div",
|
|
164
|
+
{
|
|
165
|
+
className: "royui-carousel__dots",
|
|
166
|
+
role: "tablist",
|
|
167
|
+
"aria-label": "Choose image",
|
|
168
|
+
children: images.map((_, i) => /* @__PURE__ */ jsx(
|
|
169
|
+
"button",
|
|
170
|
+
{
|
|
171
|
+
type: "button",
|
|
172
|
+
role: "tab",
|
|
173
|
+
"aria-selected": i === active,
|
|
174
|
+
"aria-label": `Image ${i + 1} of ${count}`,
|
|
175
|
+
className: [
|
|
176
|
+
"royui-carousel__dot",
|
|
177
|
+
i === active ? "royui-carousel__dot--active" : ""
|
|
178
|
+
].filter(Boolean).join(" "),
|
|
179
|
+
onClick: () => commit(i)
|
|
180
|
+
},
|
|
181
|
+
i
|
|
182
|
+
))
|
|
183
|
+
}
|
|
184
|
+
),
|
|
185
|
+
/* @__PURE__ */ jsx(
|
|
186
|
+
"span",
|
|
187
|
+
{
|
|
188
|
+
id: labelId,
|
|
189
|
+
className: "royui-carousel__status",
|
|
190
|
+
role: "status",
|
|
191
|
+
"aria-live": "polite",
|
|
192
|
+
children: multiple ? `Image ${active + 1} of ${count}` : ""
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
ImageCarousel.displayName = "ImageCarousel";
|
|
201
|
+
var StarIcon = () => /* @__PURE__ */ jsx("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "M12 2.5l2.7 5.9 6.4.7-4.8 4.3 1.3 6.3L12 16.9 6.4 19.7l1.3-6.3L2.9 9.1l6.4-.7L12 2.5z" }) });
|
|
202
|
+
var Card = forwardRef(
|
|
203
|
+
({
|
|
204
|
+
images,
|
|
205
|
+
badge,
|
|
206
|
+
badgeIcon,
|
|
207
|
+
price,
|
|
208
|
+
priceLabel,
|
|
209
|
+
subtitle,
|
|
210
|
+
stats,
|
|
211
|
+
author,
|
|
212
|
+
authorHref,
|
|
213
|
+
authorProps,
|
|
214
|
+
meta,
|
|
215
|
+
actionLabel = "View Details",
|
|
216
|
+
onAction,
|
|
217
|
+
actionProps,
|
|
218
|
+
ratio = "4 / 3",
|
|
219
|
+
autoplay = false,
|
|
220
|
+
autoplayInterval = 2500,
|
|
221
|
+
defaultIndex,
|
|
222
|
+
onIndexChange,
|
|
223
|
+
hoverEffect = true,
|
|
224
|
+
theme = "auto",
|
|
225
|
+
className = "",
|
|
226
|
+
...rest
|
|
227
|
+
}, ref) => {
|
|
228
|
+
const classes = [
|
|
229
|
+
"royui-card",
|
|
230
|
+
hoverEffect ? "royui-card--interactive" : "",
|
|
231
|
+
theme === "dark" ? "royui-card--dark" : "",
|
|
232
|
+
theme === "auto" ? "royui-card--auto" : "",
|
|
233
|
+
className
|
|
234
|
+
].filter(Boolean).join(" ");
|
|
235
|
+
const overlay = badge != null ? /* @__PURE__ */ jsxs("span", { className: "royui-card__badge", children: [
|
|
236
|
+
/* @__PURE__ */ jsx("span", { className: "royui-card__badge-icon", children: badgeIcon ?? /* @__PURE__ */ jsx(StarIcon, {}) }),
|
|
237
|
+
badge
|
|
238
|
+
] }) : void 0;
|
|
239
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: classes, ...rest, children: [
|
|
240
|
+
/* @__PURE__ */ jsx(
|
|
241
|
+
ImageCarousel,
|
|
242
|
+
{
|
|
243
|
+
images,
|
|
244
|
+
ratio,
|
|
245
|
+
autoplay,
|
|
246
|
+
autoplayInterval,
|
|
247
|
+
defaultIndex,
|
|
248
|
+
onIndexChange,
|
|
249
|
+
overlay
|
|
250
|
+
}
|
|
251
|
+
),
|
|
252
|
+
/* @__PURE__ */ jsxs("div", { className: "royui-card__body", children: [
|
|
253
|
+
/* @__PURE__ */ jsxs("div", { className: "royui-card__price-row", children: [
|
|
254
|
+
/* @__PURE__ */ jsx("span", { className: "royui-card__price", children: price }),
|
|
255
|
+
priceLabel != null && /* @__PURE__ */ jsx("span", { className: "royui-card__price-label", children: priceLabel })
|
|
256
|
+
] }),
|
|
257
|
+
subtitle != null && /* @__PURE__ */ jsx("p", { className: "royui-card__subtitle", children: subtitle }),
|
|
258
|
+
stats && stats.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
259
|
+
/* @__PURE__ */ jsx("div", { className: "royui-card__divider" }),
|
|
260
|
+
/* @__PURE__ */ jsx("div", { className: "royui-card__stats", children: stats.map((stat, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
261
|
+
i > 0 && /* @__PURE__ */ jsx(
|
|
262
|
+
"span",
|
|
263
|
+
{
|
|
264
|
+
className: "royui-card__stat-sep",
|
|
265
|
+
"aria-hidden": "true"
|
|
266
|
+
}
|
|
267
|
+
),
|
|
268
|
+
/* @__PURE__ */ jsxs("span", { className: "royui-card__stat", children: [
|
|
269
|
+
stat.icon && /* @__PURE__ */ jsx(
|
|
270
|
+
"span",
|
|
271
|
+
{
|
|
272
|
+
className: "royui-card__stat-icon",
|
|
273
|
+
"aria-hidden": "true",
|
|
274
|
+
children: stat.icon
|
|
275
|
+
}
|
|
276
|
+
),
|
|
277
|
+
/* @__PURE__ */ jsx("span", { className: "royui-card__stat-value", children: stat.value }),
|
|
278
|
+
stat.label != null && /* @__PURE__ */ jsx("span", { className: "royui-card__stat-label", children: stat.label })
|
|
279
|
+
] })
|
|
280
|
+
] }, i)) })
|
|
281
|
+
] }),
|
|
282
|
+
(author != null || meta != null) && /* @__PURE__ */ jsxs("div", { className: "royui-card__footer", children: [
|
|
283
|
+
author != null && /* @__PURE__ */ jsxs("span", { className: "royui-card__author", children: [
|
|
284
|
+
"By",
|
|
285
|
+
" ",
|
|
286
|
+
authorHref ? /* @__PURE__ */ jsx(
|
|
287
|
+
"a",
|
|
288
|
+
{
|
|
289
|
+
href: authorHref,
|
|
290
|
+
className: "royui-card__author-link",
|
|
291
|
+
...authorProps,
|
|
292
|
+
children: author
|
|
293
|
+
}
|
|
294
|
+
) : /* @__PURE__ */ jsx("span", { className: "royui-card__author-link", children: author })
|
|
295
|
+
] }),
|
|
296
|
+
meta != null && /* @__PURE__ */ jsx("span", { className: "royui-card__meta", children: meta })
|
|
297
|
+
] })
|
|
298
|
+
] }),
|
|
299
|
+
actionLabel != null && /* @__PURE__ */ jsx("div", { className: "royui-card__action", children: /* @__PURE__ */ jsx(Button, { fullWidth: true, onClick: onAction, ...actionProps, children: actionLabel }) })
|
|
300
|
+
] });
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
Card.displayName = "Card";
|
|
304
|
+
|
|
305
|
+
export { Card, ImageCarousel };
|
|
306
|
+
//# sourceMappingURL=chunk-B7QN2JTN.js.map
|
|
307
|
+
//# sourceMappingURL=chunk-B7QN2JTN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/card/ImageCarousel.tsx","../src/components/card/Card.tsx"],"names":["jsx","forwardRef","jsxs","Fragment"],"mappings":"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","file":"chunk-B7QN2JTN.js","sourcesContent":["'use client';\n\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type CSSProperties,\n type HTMLAttributes,\n type PointerEvent as ReactPointerEvent,\n type ReactNode,\n} from 'react';\nimport './ImageCarousel.css';\n\nexport interface CarouselImage {\n src: string;\n alt?: string;\n}\n\nexport interface ImageCarouselProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {\n /** Slides, in order. A single image renders without dots or drag. */\n images: CarouselImage[];\n /** Controlled active index. Leave undefined for uncontrolled. */\n index?: number;\n /** Starting index when uncontrolled. Default 0. */\n defaultIndex?: number;\n /** Fires with the new index on every change (drag, dot, or keyboard). */\n onIndexChange?: (index: number) => void;\n /** Show the pagination dots. Auto-hidden for a single image. Default true. */\n showDots?: boolean;\n /** Allow pointer drag / swipe between slides. Default true. */\n draggable?: boolean;\n /** Aspect ratio of the media box, e.g. \"4 / 3\" or \"1 / 1\". Default \"4 / 3\". */\n ratio?: string;\n /** Overlay slot painted on top of the image — a badge, a gradient, anything. */\n overlay?: ReactNode;\n /** Advance to the next slide on a timer, looping back to the first. Default false. */\n autoplay?: boolean;\n /** Milliseconds between auto-advances. Default 2500. */\n autoplayInterval?: number;\n /** Pause autoplay while the pointer is over the gallery. Default true. */\n pauseOnHover?: boolean;\n}\n\n/** How far you have to drag, as a fraction of the width, before it commits. */\nconst COMMIT_THRESHOLD = 0.18;\n/** Resistance applied when dragging past the first or last slide. */\nconst RUBBER_BAND = 0.35;\n\nexport const ImageCarousel = forwardRef<HTMLDivElement, ImageCarouselProps>(\n (\n {\n images,\n index,\n defaultIndex = 0,\n onIndexChange,\n showDots = true,\n draggable = true,\n ratio = '4 / 3',\n overlay,\n autoplay = false,\n autoplayInterval = 2500,\n pauseOnHover = true,\n className = '',\n style,\n ...rest\n },\n ref,\n ) => {\n const count = images.length;\n const multiple = count > 1;\n const labelId = useId();\n\n const [internal, setInternal] = useState(() =>\n Math.max(0, Math.min(count - 1, defaultIndex)),\n );\n const active = index ?? internal;\n\n const commit = useCallback(\n (next: number) => {\n const clamped = Math.max(0, Math.min(count - 1, next));\n if (clamped === active) return;\n if (index === undefined) setInternal(clamped);\n onIndexChange?.(clamped);\n },\n [active, count, index, onIndexChange],\n );\n\n // Drag state. `drag` is the live pixel offset of the current gesture.\n const viewportRef = useRef<HTMLDivElement>(null);\n const startX = useRef(0);\n const widthRef = useRef(0);\n const [drag, setDrag] = useState(0);\n const [dragging, setDragging] = useState(false);\n const [hovered, setHovered] = useState(false);\n\n // Autoplay — one timeout, re-armed whenever `active` changes (so a manual\n // dot tap or swipe also restarts the clock). Paused on hover and drag, and\n // skipped entirely under reduced-motion.\n useEffect(() => {\n if (!autoplay || !multiple || dragging) return;\n if (pauseOnHover && hovered) return;\n if (\n typeof window !== 'undefined' &&\n window.matchMedia?.('(prefers-reduced-motion: reduce)').matches\n ) {\n return;\n }\n const id = window.setTimeout(\n () => commit(active + 1 >= count ? 0 : active + 1),\n autoplayInterval,\n );\n return () => window.clearTimeout(id);\n }, [\n autoplay,\n autoplayInterval,\n pauseOnHover,\n hovered,\n dragging,\n multiple,\n active,\n count,\n commit,\n ]);\n\n const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {\n if (!draggable || !multiple || e.button !== 0) return;\n widthRef.current = viewportRef.current?.offsetWidth ?? 0;\n startX.current = e.clientX;\n setDragging(true);\n viewportRef.current?.setPointerCapture(e.pointerId);\n };\n\n const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {\n if (!dragging) return;\n let dx = e.clientX - startX.current;\n const atStart = active === 0 && dx > 0;\n const atEnd = active === count - 1 && dx < 0;\n if (atStart || atEnd) dx *= RUBBER_BAND;\n setDrag(dx);\n };\n\n const endDrag = (e: ReactPointerEvent<HTMLDivElement>) => {\n if (!dragging) return;\n setDragging(false);\n viewportRef.current?.releasePointerCapture?.(e.pointerId);\n const threshold = (widthRef.current || 1) * COMMIT_THRESHOLD;\n if (drag <= -threshold) commit(active + 1);\n else if (drag >= threshold) commit(active - 1);\n setDrag(0);\n };\n\n const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (!multiple) return;\n if (e.key === 'ArrowRight') {\n e.preventDefault();\n commit(active + 1);\n } else if (e.key === 'ArrowLeft') {\n e.preventDefault();\n commit(active - 1);\n }\n };\n\n // Slide the track by whole slides, then add the in-flight drag offset.\n const trackStyle: CSSProperties = {\n transform: `translate3d(calc(${-active * 100}% + ${drag}px), 0, 0)`,\n };\n\n const classes = [\n 'royui-carousel',\n showDots && multiple ? 'royui-carousel--has-dots' : '',\n className,\n ]\n .filter(Boolean)\n .join(' ');\n\n return (\n <div\n ref={ref}\n className={classes}\n style={{ ['--royui-carousel-ratio' as string]: ratio, ...style }}\n role=\"group\"\n aria-roledescription=\"carousel\"\n aria-label=\"Property images\"\n {...rest}\n >\n <div\n ref={viewportRef}\n className=\"royui-carousel__viewport\"\n tabIndex={multiple ? 0 : undefined}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={endDrag}\n onPointerCancel={endDrag}\n onKeyDown={onKeyDown}\n onMouseEnter={\n autoplay && pauseOnHover ? () => setHovered(true) : undefined\n }\n onMouseLeave={\n autoplay && pauseOnHover ? () => setHovered(false) : undefined\n }\n >\n <div\n className={[\n 'royui-carousel__track',\n dragging ? 'royui-carousel__track--dragging' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n style={trackStyle}\n >\n {images.map((img, i) => (\n <div className=\"royui-carousel__slide\" key={`${img.src}-${i}`}>\n <img\n className=\"royui-carousel__img\"\n src={img.src}\n alt={img.alt ?? ''}\n draggable={false}\n loading={i === 0 ? 'eager' : 'lazy'}\n aria-hidden={i !== active}\n />\n </div>\n ))}\n </div>\n </div>\n\n <div className=\"royui-carousel__scrim\" aria-hidden=\"true\" />\n\n {overlay != null && (\n <div className=\"royui-carousel__overlay\">{overlay}</div>\n )}\n\n {showDots && multiple && (\n <div\n className=\"royui-carousel__dots\"\n role=\"tablist\"\n aria-label=\"Choose image\"\n >\n {images.map((_, i) => (\n <button\n key={i}\n type=\"button\"\n role=\"tab\"\n aria-selected={i === active}\n aria-label={`Image ${i + 1} of ${count}`}\n className={[\n 'royui-carousel__dot',\n i === active ? 'royui-carousel__dot--active' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onClick={() => commit(i)}\n />\n ))}\n </div>\n )}\n\n <span\n id={labelId}\n className=\"royui-carousel__status\"\n role=\"status\"\n aria-live=\"polite\"\n >\n {multiple ? `Image ${active + 1} of ${count}` : ''}\n </span>\n </div>\n );\n },\n);\n\nImageCarousel.displayName = 'ImageCarousel';\n","'use client';\n\nimport {\n Fragment,\n forwardRef,\n type AnchorHTMLAttributes,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\nimport { Button, type ButtonProps } from '../button';\nimport { ImageCarousel, type CarouselImage } from './ImageCarousel';\nimport './Card.css';\n\nexport interface CardStat {\n /** Small leading glyph. Inherits the muted stat color via currentColor. */\n icon?: ReactNode;\n /** The figure, full-strength — e.g. \"264 m²\" or \"4\". */\n value: ReactNode;\n /** Dimmed descriptor after the figure — e.g. \"Living\" or \"Bedrooms\". */\n label?: ReactNode;\n}\n\nexport interface CardProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'onChange'> {\n /** Gallery shown at the top, with swipe + dot pagination. */\n images: CarouselImage[];\n /** Pill label over the image, e.g. \"Prime Pick\". Hidden when omitted. */\n badge?: ReactNode;\n /** Glyph inside the badge. Defaults to a gold star. */\n badgeIcon?: ReactNode;\n /** Headline figure, e.g. \"$250,000\". */\n price: ReactNode;\n /** Muted qualifier next to the price, e.g. \"List price\". */\n priceLabel?: ReactNode;\n /** Secondary line under the price — owner, address, etc. */\n subtitle?: ReactNode;\n /** Inline figures separated by hairline dividers, e.g. living area and rooms. */\n stats?: CardStat[];\n /** Footer attribution. Becomes a link when authorHref is set. */\n author?: ReactNode;\n /** Makes the author name a link. */\n authorHref?: string;\n /** Extra attributes for the author link — target, rel, onClick, etc. */\n authorProps?: AnchorHTMLAttributes<HTMLAnchorElement>;\n /** Right-aligned footer note, e.g. \"2 days ago\". */\n meta?: ReactNode;\n /** Label for the action button. Pass null to drop the button entirely. */\n actionLabel?: ReactNode;\n /** Click handler for the action button. */\n onAction?: () => void;\n /** Escape hatch onto the underlying Button (variant, color, loading, …). */\n actionProps?: Partial<ButtonProps>;\n /** Aspect ratio of the gallery. Default \"4 / 3\". */\n ratio?: string;\n /** Auto-advance the gallery on a timer, looping. Default false. */\n autoplay?: boolean;\n /** Milliseconds between auto-advances. Default 2500. */\n autoplayInterval?: number;\n /** Starting gallery index. */\n defaultIndex?: number;\n /** Fires when the gallery moves. */\n onIndexChange?: (index: number) => void;\n /** Lift + image-zoom on hover. Default true. */\n hoverEffect?: boolean;\n /**\n * Colour scheme. \"auto\" (default) follows the system via prefers-color-scheme;\n * \"light\" is a premium off-white surface, \"dark\" a near-black one.\n */\n theme?: 'light' | 'dark' | 'auto';\n}\n\nconst StarIcon = () => (\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden>\n <path d=\"M12 2.5l2.7 5.9 6.4.7-4.8 4.3 1.3 6.3L12 16.9 6.4 19.7l1.3-6.3L2.9 9.1l6.4-.7L12 2.5z\" />\n </svg>\n);\n\nexport const Card = forwardRef<HTMLDivElement, CardProps>(\n (\n {\n images,\n badge,\n badgeIcon,\n price,\n priceLabel,\n subtitle,\n stats,\n author,\n authorHref,\n authorProps,\n meta,\n actionLabel = 'View Details',\n onAction,\n actionProps,\n ratio = '4 / 3',\n autoplay = false,\n autoplayInterval = 2500,\n defaultIndex,\n onIndexChange,\n hoverEffect = true,\n theme = 'auto',\n className = '',\n ...rest\n },\n ref,\n ) => {\n const classes = [\n 'royui-card',\n hoverEffect ? 'royui-card--interactive' : '',\n theme === 'dark' ? 'royui-card--dark' : '',\n theme === 'auto' ? 'royui-card--auto' : '',\n className,\n ]\n .filter(Boolean)\n .join(' ');\n\n const overlay =\n badge != null ? (\n <span className=\"royui-card__badge\">\n <span className=\"royui-card__badge-icon\">\n {badgeIcon ?? <StarIcon />}\n </span>\n {badge}\n </span>\n ) : undefined;\n\n return (\n <div ref={ref} className={classes} {...rest}>\n <ImageCarousel\n images={images}\n ratio={ratio}\n autoplay={autoplay}\n autoplayInterval={autoplayInterval}\n defaultIndex={defaultIndex}\n onIndexChange={onIndexChange}\n overlay={overlay}\n />\n\n <div className=\"royui-card__body\">\n <div className=\"royui-card__price-row\">\n <span className=\"royui-card__price\">{price}</span>\n {priceLabel != null && (\n <span className=\"royui-card__price-label\">{priceLabel}</span>\n )}\n </div>\n\n {subtitle != null && (\n <p className=\"royui-card__subtitle\">{subtitle}</p>\n )}\n\n {stats && stats.length > 0 && (\n <>\n <div className=\"royui-card__divider\" />\n <div className=\"royui-card__stats\">\n {stats.map((stat, i) => (\n <Fragment key={i}>\n {i > 0 && (\n <span\n className=\"royui-card__stat-sep\"\n aria-hidden=\"true\"\n />\n )}\n <span className=\"royui-card__stat\">\n {stat.icon && (\n <span\n className=\"royui-card__stat-icon\"\n aria-hidden=\"true\"\n >\n {stat.icon}\n </span>\n )}\n <span className=\"royui-card__stat-value\">{stat.value}</span>\n {stat.label != null && (\n <span className=\"royui-card__stat-label\">\n {stat.label}\n </span>\n )}\n </span>\n </Fragment>\n ))}\n </div>\n </>\n )}\n\n {(author != null || meta != null) && (\n <div className=\"royui-card__footer\">\n {author != null && (\n <span className=\"royui-card__author\">\n By{' '}\n {authorHref ? (\n <a\n href={authorHref}\n className=\"royui-card__author-link\"\n {...authorProps}\n >\n {author}\n </a>\n ) : (\n <span className=\"royui-card__author-link\">{author}</span>\n )}\n </span>\n )}\n {meta != null && (\n <span className=\"royui-card__meta\">{meta}</span>\n )}\n </div>\n )}\n </div>\n\n {actionLabel != null && (\n <div className=\"royui-card__action\">\n <Button fullWidth onClick={onAction} {...actionProps}>\n {actionLabel}\n </Button>\n </div>\n )}\n </div>\n );\n },\n);\n\nCard.displayName = 'Card';\n"]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { HTMLAttributes, ReactNode, AnchorHTMLAttributes } from 'react';
|
|
3
|
+
import { ButtonProps } from '../button/index.js';
|
|
4
|
+
|
|
5
|
+
interface CarouselImage {
|
|
6
|
+
src: string;
|
|
7
|
+
alt?: string;
|
|
8
|
+
}
|
|
9
|
+
interface ImageCarouselProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
10
|
+
/** Slides, in order. A single image renders without dots or drag. */
|
|
11
|
+
images: CarouselImage[];
|
|
12
|
+
/** Controlled active index. Leave undefined for uncontrolled. */
|
|
13
|
+
index?: number;
|
|
14
|
+
/** Starting index when uncontrolled. Default 0. */
|
|
15
|
+
defaultIndex?: number;
|
|
16
|
+
/** Fires with the new index on every change (drag, dot, or keyboard). */
|
|
17
|
+
onIndexChange?: (index: number) => void;
|
|
18
|
+
/** Show the pagination dots. Auto-hidden for a single image. Default true. */
|
|
19
|
+
showDots?: boolean;
|
|
20
|
+
/** Allow pointer drag / swipe between slides. Default true. */
|
|
21
|
+
draggable?: boolean;
|
|
22
|
+
/** Aspect ratio of the media box, e.g. "4 / 3" or "1 / 1". Default "4 / 3". */
|
|
23
|
+
ratio?: string;
|
|
24
|
+
/** Overlay slot painted on top of the image — a badge, a gradient, anything. */
|
|
25
|
+
overlay?: ReactNode;
|
|
26
|
+
/** Advance to the next slide on a timer, looping back to the first. Default false. */
|
|
27
|
+
autoplay?: boolean;
|
|
28
|
+
/** Milliseconds between auto-advances. Default 2500. */
|
|
29
|
+
autoplayInterval?: number;
|
|
30
|
+
/** Pause autoplay while the pointer is over the gallery. Default true. */
|
|
31
|
+
pauseOnHover?: boolean;
|
|
32
|
+
}
|
|
33
|
+
declare const ImageCarousel: react.ForwardRefExoticComponent<ImageCarouselProps & react.RefAttributes<HTMLDivElement>>;
|
|
34
|
+
|
|
35
|
+
interface CardStat {
|
|
36
|
+
/** Small leading glyph. Inherits the muted stat color via currentColor. */
|
|
37
|
+
icon?: ReactNode;
|
|
38
|
+
/** The figure, full-strength — e.g. "264 m²" or "4". */
|
|
39
|
+
value: ReactNode;
|
|
40
|
+
/** Dimmed descriptor after the figure — e.g. "Living" or "Bedrooms". */
|
|
41
|
+
label?: ReactNode;
|
|
42
|
+
}
|
|
43
|
+
interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'onChange'> {
|
|
44
|
+
/** Gallery shown at the top, with swipe + dot pagination. */
|
|
45
|
+
images: CarouselImage[];
|
|
46
|
+
/** Pill label over the image, e.g. "Prime Pick". Hidden when omitted. */
|
|
47
|
+
badge?: ReactNode;
|
|
48
|
+
/** Glyph inside the badge. Defaults to a gold star. */
|
|
49
|
+
badgeIcon?: ReactNode;
|
|
50
|
+
/** Headline figure, e.g. "$250,000". */
|
|
51
|
+
price: ReactNode;
|
|
52
|
+
/** Muted qualifier next to the price, e.g. "List price". */
|
|
53
|
+
priceLabel?: ReactNode;
|
|
54
|
+
/** Secondary line under the price — owner, address, etc. */
|
|
55
|
+
subtitle?: ReactNode;
|
|
56
|
+
/** Inline figures separated by hairline dividers, e.g. living area and rooms. */
|
|
57
|
+
stats?: CardStat[];
|
|
58
|
+
/** Footer attribution. Becomes a link when authorHref is set. */
|
|
59
|
+
author?: ReactNode;
|
|
60
|
+
/** Makes the author name a link. */
|
|
61
|
+
authorHref?: string;
|
|
62
|
+
/** Extra attributes for the author link — target, rel, onClick, etc. */
|
|
63
|
+
authorProps?: AnchorHTMLAttributes<HTMLAnchorElement>;
|
|
64
|
+
/** Right-aligned footer note, e.g. "2 days ago". */
|
|
65
|
+
meta?: ReactNode;
|
|
66
|
+
/** Label for the action button. Pass null to drop the button entirely. */
|
|
67
|
+
actionLabel?: ReactNode;
|
|
68
|
+
/** Click handler for the action button. */
|
|
69
|
+
onAction?: () => void;
|
|
70
|
+
/** Escape hatch onto the underlying Button (variant, color, loading, …). */
|
|
71
|
+
actionProps?: Partial<ButtonProps>;
|
|
72
|
+
/** Aspect ratio of the gallery. Default "4 / 3". */
|
|
73
|
+
ratio?: string;
|
|
74
|
+
/** Auto-advance the gallery on a timer, looping. Default false. */
|
|
75
|
+
autoplay?: boolean;
|
|
76
|
+
/** Milliseconds between auto-advances. Default 2500. */
|
|
77
|
+
autoplayInterval?: number;
|
|
78
|
+
/** Starting gallery index. */
|
|
79
|
+
defaultIndex?: number;
|
|
80
|
+
/** Fires when the gallery moves. */
|
|
81
|
+
onIndexChange?: (index: number) => void;
|
|
82
|
+
/** Lift + image-zoom on hover. Default true. */
|
|
83
|
+
hoverEffect?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Colour scheme. "auto" (default) follows the system via prefers-color-scheme;
|
|
86
|
+
* "light" is a premium off-white surface, "dark" a near-black one.
|
|
87
|
+
*/
|
|
88
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
89
|
+
}
|
|
90
|
+
declare const Card: react.ForwardRefExoticComponent<CardProps & react.RefAttributes<HTMLDivElement>>;
|
|
91
|
+
|
|
92
|
+
export { Card, type CardProps, type CardStat, type CarouselImage, ImageCarousel, type ImageCarouselProps };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,5 +13,6 @@ export { D as DateRange, a as addDays, b as addMonths, f as formatMonthYear, c a
|
|
|
13
13
|
export { A as AnalogClock, a as AnalogClockProps, T as TimePicker, b as TimePickerProps, c as TimePickerVariant, d as TimeRangePicker, e as TimeRangePickerProps, f as TimeRangeValue, g as TimeValue, h as formatTime, i as formatTimeRange } from './TimeRangePicker-CgkOBnk6.js';
|
|
14
14
|
export { ClockSwitch, ClockSwitchProps, DigitalClock, DigitalClockProps } from './components/time-picker/index.js';
|
|
15
15
|
export { Column, ColumnMenu, ColumnType, DataIO, DataTable, DataTableProps, FilterState, SortDir, TableLayout, downloadString, fromCsv, fromJson, toCsv, toJson } from './components/data-table/index.js';
|
|
16
|
+
export { Card, CardProps, CardStat, CarouselImage, ImageCarousel, ImageCarouselProps } from './components/card/index.js';
|
|
16
17
|
import 'react';
|
|
17
18
|
import 'react/jsx-runtime';
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
export { Popover } from './chunk-C5X3TE5U.js';
|
|
2
3
|
export { TextMorph } from './chunk-PGV55XSZ.js';
|
|
3
4
|
export { TreeNav, TreeNavItem } from './chunk-M6HB6BMA.js';
|
|
4
|
-
export { Button } from './chunk-4SGMAZBG.js';
|
|
5
5
|
export { ColumnMenu, DataTable, downloadString, fromCsv, fromJson, toCsv, toJson } from './chunk-PDUQROG2.js';
|
|
6
6
|
export { Spinner, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './chunk-XERZVDIT.js';
|
|
7
7
|
export { TableSearch } from './chunk-KSHKVSNK.js';
|
|
8
8
|
export { AnalogClock, ClockSwitch, DigitalClock, TimePicker, TimeRangePicker, formatTime, formatTimeRange } from './chunk-QOSMU4DV.js';
|
|
9
9
|
export { DEFAULT_PRESETS, DateRangePicker, addDays, addMonths, formatMonthYear, formatRange, formatShort, isBetween, isSameDay, startOfDay } from './chunk-SFENGB5N.js';
|
|
10
|
+
export { Card, ImageCarousel } from './chunk-B7QN2JTN.js';
|
|
11
|
+
export { Button } from './chunk-4SGMAZBG.js';
|
|
10
12
|
export { GradientButton } from './chunk-RLBVY3DG.js';
|
|
11
13
|
export { MadeBy } from './chunk-MDPMEW4K.js';
|
|
12
14
|
export { Pagination } from './chunk-5CIBIH7R.js';
|
|
13
|
-
export { Popover } from './chunk-C5X3TE5U.js';
|
|
14
15
|
//# sourceMappingURL=index.js.map
|
|
15
16
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roy-ui/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Free, animated React components built with TypeScript. Zero config, RSC-safe, sub-12 KB. Gradient button, popover, text morph and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -210,6 +210,10 @@
|
|
|
210
210
|
"types": "./dist/components/data-table/index.d.ts",
|
|
211
211
|
"import": "./dist/components/data-table/index.js"
|
|
212
212
|
},
|
|
213
|
+
"./card": {
|
|
214
|
+
"types": "./dist/components/card/index.d.ts",
|
|
215
|
+
"import": "./dist/components/card/index.js"
|
|
216
|
+
},
|
|
213
217
|
"./package.json": "./package.json"
|
|
214
218
|
},
|
|
215
219
|
"scripts": {
|