@stapel/listings-react 0.3.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/MODULE.md +12 -6
  3. package/README.md +33 -17
  4. package/dist/api/generated/schema.d.ts +129 -3
  5. package/dist/api/generated/schema.d.ts.map +1 -1
  6. package/dist/api/listingsApi.d.ts +35 -22
  7. package/dist/api/listingsApi.d.ts.map +1 -1
  8. package/dist/api/listingsApi.js +17 -0
  9. package/dist/api/listingsApi.js.map +1 -1
  10. package/dist/api/types.d.ts +43 -2
  11. package/dist/api/types.d.ts.map +1 -1
  12. package/dist/api/types.js.map +1 -1
  13. package/dist/default/FavoritesPane.d.ts +40 -3
  14. package/dist/default/FavoritesPane.d.ts.map +1 -1
  15. package/dist/default/FavoritesPane.js +30 -18
  16. package/dist/default/FavoritesPane.js.map +1 -1
  17. package/dist/default/ListingCard.d.ts +25 -4
  18. package/dist/default/ListingCard.d.ts.map +1 -1
  19. package/dist/default/ListingCard.js +19 -12
  20. package/dist/default/ListingCard.js.map +1 -1
  21. package/dist/default/ListingComposerPage.d.ts +163 -23
  22. package/dist/default/ListingComposerPage.d.ts.map +1 -1
  23. package/dist/default/ListingComposerPage.js +85 -43
  24. package/dist/default/ListingComposerPage.js.map +1 -1
  25. package/dist/default/ListingDetailPane.d.ts +41 -4
  26. package/dist/default/ListingDetailPane.d.ts.map +1 -1
  27. package/dist/default/ListingDetailPane.js +38 -25
  28. package/dist/default/ListingDetailPane.js.map +1 -1
  29. package/dist/default/ListingPhoto.d.ts +13 -1
  30. package/dist/default/ListingPhoto.d.ts.map +1 -1
  31. package/dist/default/ListingPhoto.js +47 -9
  32. package/dist/default/ListingPhoto.js.map +1 -1
  33. package/dist/default/ListingPrice.d.ts +13 -0
  34. package/dist/default/ListingPrice.d.ts.map +1 -0
  35. package/dist/default/ListingPrice.js +60 -0
  36. package/dist/default/ListingPrice.js.map +1 -0
  37. package/dist/default/MyListingsPane.d.ts +9 -17
  38. package/dist/default/MyListingsPane.d.ts.map +1 -1
  39. package/dist/default/MyListingsPane.js +148 -63
  40. package/dist/default/MyListingsPane.js.map +1 -1
  41. package/dist/default/SignInLink.js +8 -7
  42. package/dist/default/SignInLink.js.map +1 -1
  43. package/dist/default/StatusTags.d.ts +16 -5
  44. package/dist/default/StatusTags.d.ts.map +1 -1
  45. package/dist/default/StatusTags.js +19 -13
  46. package/dist/default/StatusTags.js.map +1 -1
  47. package/dist/default/index.d.ts +15 -9
  48. package/dist/default/index.d.ts.map +1 -1
  49. package/dist/default/index.js +13 -6
  50. package/dist/default/index.js.map +1 -1
  51. package/dist/headless/Favorites.d.ts +6 -0
  52. package/dist/headless/Favorites.d.ts.map +1 -1
  53. package/dist/headless/Favorites.js +4 -0
  54. package/dist/headless/Favorites.js.map +1 -1
  55. package/dist/headless/ListingActions.d.ts +12 -0
  56. package/dist/headless/ListingActions.d.ts.map +1 -1
  57. package/dist/headless/ListingActions.js +3 -0
  58. package/dist/headless/ListingActions.js.map +1 -1
  59. package/dist/headless/ListingComposer.d.ts +23 -0
  60. package/dist/headless/ListingComposer.d.ts.map +1 -1
  61. package/dist/headless/ListingComposer.js +33 -7
  62. package/dist/headless/ListingComposer.js.map +1 -1
  63. package/dist/headless/MyListings.d.ts +27 -12
  64. package/dist/headless/MyListings.d.ts.map +1 -1
  65. package/dist/headless/MyListings.js +29 -7
  66. package/dist/headless/MyListings.js.map +1 -1
  67. package/dist/i18n/es.d.ts +6 -1
  68. package/dist/i18n/es.d.ts.map +1 -1
  69. package/dist/i18n/es.js +28 -6
  70. package/dist/i18n/es.js.map +1 -1
  71. package/dist/i18n/generated/errors.es.gen.d.ts +1 -1
  72. package/dist/i18n/generated/errors.es.gen.js +1 -1
  73. package/dist/i18n/generated/errors.gen.d.ts +6 -0
  74. package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
  75. package/dist/i18n/generated/errors.gen.js +3 -0
  76. package/dist/i18n/generated/errors.gen.js.map +1 -1
  77. package/dist/i18n/generated/errors.ru.gen.d.ts +1 -1
  78. package/dist/i18n/generated/errors.ru.gen.js +1 -1
  79. package/dist/i18n/keys.d.ts +28 -4
  80. package/dist/i18n/keys.d.ts.map +1 -1
  81. package/dist/i18n/keys.js +49 -8
  82. package/dist/i18n/keys.js.map +1 -1
  83. package/dist/i18n/ru.d.ts +16 -1
  84. package/dist/i18n/ru.d.ts.map +1 -1
  85. package/dist/i18n/ru.js +40 -6
  86. package/dist/i18n/ru.js.map +1 -1
  87. package/dist/index.d.ts +8 -6
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +8 -6
  90. package/dist/index.js.map +1 -1
  91. package/dist/model/draft.d.ts +9 -6
  92. package/dist/model/draft.d.ts.map +1 -1
  93. package/dist/model/draft.js +3 -1
  94. package/dist/model/draft.js.map +1 -1
  95. package/dist/model/features.d.ts +21 -7
  96. package/dist/model/features.d.ts.map +1 -1
  97. package/dist/model/features.js +26 -0
  98. package/dist/model/features.js.map +1 -1
  99. package/dist/model/mine.d.ts +44 -0
  100. package/dist/model/mine.d.ts.map +1 -0
  101. package/dist/model/mine.js +52 -0
  102. package/dist/model/mine.js.map +1 -0
  103. package/dist/model/mineSource.d.ts +39 -34
  104. package/dist/model/mineSource.d.ts.map +1 -1
  105. package/dist/model/mineSource.js +10 -41
  106. package/dist/model/mineSource.js.map +1 -1
  107. package/dist/model/mutations.d.ts.map +1 -1
  108. package/dist/model/mutations.js +11 -4
  109. package/dist/model/mutations.js.map +1 -1
  110. package/dist/model/queryKeys.d.ts +7 -2
  111. package/dist/model/queryKeys.d.ts.map +1 -1
  112. package/dist/model/queryKeys.js +2 -0
  113. package/dist/model/queryKeys.js.map +1 -1
  114. package/dist/model/status.d.ts +12 -31
  115. package/dist/model/status.d.ts.map +1 -1
  116. package/dist/model/status.js +44 -0
  117. package/dist/model/status.js.map +1 -1
  118. package/llms.txt +11 -8
  119. package/manifest.json +88 -14
  120. package/nav-manifest.json +1 -1
  121. package/package.json +17 -12
  122. package/src/analytics/generated/events.json +1 -1
  123. package/src/api/generated/schema.ts +129 -3
  124. package/src/api/listingsApi.ts +60 -21
  125. package/src/api/types.ts +46 -2
  126. package/src/default/FavoritesPane.tsx +156 -82
  127. package/src/default/ListingCard.tsx +88 -46
  128. package/src/default/ListingComposerPage.tsx +403 -187
  129. package/src/default/ListingDetailPane.tsx +200 -98
  130. package/src/default/ListingPhoto.tsx +89 -16
  131. package/src/default/ListingPrice.tsx +77 -0
  132. package/src/default/MyListingsPane.tsx +344 -222
  133. package/src/default/SignInLink.tsx +4 -4
  134. package/src/default/StatusTags.tsx +36 -18
  135. package/src/default/index.ts +18 -7
  136. package/src/headless/Favorites.tsx +10 -0
  137. package/src/headless/ListingActions.tsx +19 -0
  138. package/src/headless/ListingComposer.tsx +56 -5
  139. package/src/headless/MyListings.tsx +62 -22
  140. package/src/i18n/es.ts +36 -7
  141. package/src/i18n/generated/errors.es.gen.ts +1 -1
  142. package/src/i18n/generated/errors.gen.ts +3 -0
  143. package/src/i18n/generated/errors.json +9 -0
  144. package/src/i18n/generated/errors.ru.gen.ts +1 -1
  145. package/src/i18n/keys.ts +58 -9
  146. package/src/i18n/ru.ts +48 -7
  147. package/src/index.ts +16 -5
  148. package/src/model/draft.ts +12 -7
  149. package/src/model/features.ts +48 -7
  150. package/src/model/mine.ts +70 -0
  151. package/src/model/mineSource.ts +46 -41
  152. package/src/model/mutations.ts +11 -4
  153. package/src/model/queryKeys.ts +9 -2
  154. package/src/model/status.ts +15 -0
  155. package/dist/default/ErrorAlert.d.ts +0 -9
  156. package/dist/default/ErrorAlert.d.ts.map +0 -1
  157. package/dist/default/ErrorAlert.js +0 -27
  158. package/dist/default/ErrorAlert.js.map +0 -1
  159. package/dist/default/theme.d.ts +0 -25
  160. package/dist/default/theme.d.ts.map +0 -1
  161. package/dist/default/theme.js +0 -40
  162. package/dist/default/theme.js.map +0 -1
  163. package/src/default/ErrorAlert.tsx +0 -46
  164. package/src/default/theme.tsx +0 -67
@@ -22,19 +22,24 @@
22
22
  * card. That property is the whole reason the projection exists, and it is
23
23
  * what makes a grid of forty cards cost one query.
24
24
  *
25
- * ── The heart is never hidden ──────────────────────────────────────────────
25
+ * ── The heart is never hidden, and never explained by hover ────────────────
26
26
  *
27
27
  * A visitor sees it, blocked, with the reason IN WORDS beside it and the
28
28
  * sign-in link the container supplies (`signIn`, typically `?next=<current>`).
29
29
  * Hiding it would teach nobody that favourites exist (private-space canon
30
- * §6.3, spec §6.2 item 6) — and until 0.3.0 the reason lived only in a tooltip
31
- * on a disabled button, which receives no pointer events in any browser: a
32
- * reason nobody could read, and no door to walk through.
30
+ * §6.3, spec §6.2 item 6). The reason itself goes through the shared
31
+ * `<GatedControl>`, which renders it as text linked by `aria-describedby`
32
+ * a disabled antd button receives no pointer events and is not focusable, so
33
+ * the `Tooltip` this component used to offer as a "quieter" volume was a
34
+ * reason nobody could read on any device. That arm is gone; `blockedReason`
35
+ * is now "text" (reason + door) or "line" (reason alone, for a grid).
33
36
  */
34
37
  import type { ReactElement, ReactNode } from "react";
35
- import { Button, Card, Flex, Tooltip, Typography } from "antd";
38
+ import { Button, Card, Flex, Typography } from "antd";
39
+ import { GatedControl, SkinTheme } from "@stapel/tokens-antd/skin";
36
40
  import { useActionGate, useT } from "@stapel/core";
37
41
  import type { LinkComponent, SignInCtaProp } from "@stapel/core";
42
+ import { spacing } from "@stapel/tokens";
38
43
  import { FeatureBadges } from "@stapel/attributes-react/default";
39
44
  import type { ListingCard as ListingCardData } from "../api/types.js";
40
45
  import { asFeatureDaoList, featuresDtoFromDaoList, featuresFromDaoList } from "../model/features.js";
@@ -44,7 +49,7 @@ import { LISTINGS_I18N_KEYS } from "../i18n/keys.js";
44
49
  import { HeartIcon } from "./icons.js";
45
50
  import { SignInLink } from "./SignInLink.js";
46
51
  import { ListingPhoto } from "./ListingPhoto.js";
47
- import { ListingsSkinTheme } from "./theme.js";
52
+ import { ListingPrice } from "./ListingPrice.js";
48
53
  import type { ThemeModeProp } from "./types.js";
49
54
 
50
55
  /**
@@ -87,8 +92,27 @@ export type ListingCardOpenProps =
87
92
  readonly linkComponent?: undefined;
88
93
  };
89
94
 
95
+ /**
96
+ * How loudly a blocked favourite states its reason on THIS surface.
97
+ *
98
+ * `"text"` (the default) is the sentence under the controls, with the sign-in
99
+ * door beside it — the right volume for a card that stands alone.
100
+ *
101
+ * `"line"` collapses it to the reason alone, no door: a grid of forty cards
102
+ * repeating "sign in to do this — sign in" forty times is not forty pieces of
103
+ * help, it is the loudest thing on the page, and every one of those doors
104
+ * leads where the header's own sign-in button already leads.
105
+ *
106
+ * There is no third setting. The reason is on screen in both, because the only
107
+ * way to make it quieter than a line of text is to hide it behind hover, and
108
+ * hover does not exist on the device most of these cards are read on.
109
+ */
110
+ export type ListingCardBlockedReason = "text" | "line";
111
+
90
112
  export interface ListingCardBaseProps extends ThemeModeProp, SignInCtaProp {
91
113
  readonly listing: ListingCardData;
114
+ /** See {@link ListingCardBlockedReason}. Default `"text"`. */
115
+ readonly blockedReason?: ListingCardBlockedReason;
92
116
  /** Extra chrome the container adds (a `promoted` tag from search, say —
93
117
  * DSA Art. 26 marking belongs to the pair that receives it). */
94
118
  readonly badge?: ReactNode;
@@ -105,6 +129,11 @@ export type ListingCardProps = ListingCardBaseProps & ListingCardOpenProps;
105
129
  * Exactly one of the three renders, so exactly one navigation happens per
106
130
  * click. That is the whole fix — the branch below has no arm in which both a
107
131
  * handler and an `href` reach the DOM.
132
+ *
133
+ * It is the card's PRIMARY action and is drawn as one: a full-width button,
134
+ * which on a phone is 44px tall because `SkinTheme` sets antd's
135
+ * `controlHeight` there. It was a bare text link beside a 40px icon button,
136
+ * with no hierarchy between them and neither one a real touch target.
108
137
  */
109
138
  function OpenControl(
110
139
  props: ListingCardOpenProps & { readonly listingId: number }
@@ -131,8 +160,8 @@ function OpenControl(
131
160
  </Link>
132
161
  ) : (
133
162
  <Button
134
- size="small"
135
- type="link"
163
+ type="primary"
164
+ block
136
165
  href={props.href}
137
166
  data-testid="listings-card-open"
138
167
  data-analytics="none"
@@ -147,8 +176,8 @@ function OpenControl(
147
176
  const onOpen = props.onOpen;
148
177
  return (
149
178
  <Button
150
- size="small"
151
- type="link"
179
+ type="primary"
180
+ block
152
181
  data-testid="listings-card-open"
153
182
  data-analytics="none"
154
183
  data-analytics-reason="business action — host app wraps with its own tracked()"
@@ -175,12 +204,9 @@ export function ListingCard(props: ListingCardProps): ReactElement {
175
204
  const titleDaos = asFeatureDaoList(listing.features_title);
176
205
 
177
206
  const status = listing.status === undefined ? undefined : lifecycleCaption(listing.status);
178
- const price =
179
- listing.price !== undefined && listing.price.length > 0
180
- ? `${listing.price} ${listing.currency ?? ""}`.trim()
181
- : t(LISTINGS_I18N_KEYS.cardPriceAbsent);
182
207
 
183
208
  const favoriteGate = useActionGate(favorite.gate);
209
+ const blockedReason = props.blockedReason ?? "text";
184
210
 
185
211
  const favoriteLabel = t(
186
212
  favorite.favorited
@@ -189,7 +215,10 @@ export function ListingCard(props: ListingCardProps): ReactElement {
189
215
  );
190
216
 
191
217
  return (
192
- <ListingsSkinTheme {...(props.mode !== undefined ? { mode: props.mode } : {})}>
218
+ <SkinTheme
219
+ surface="bare"
220
+ {...(props.mode !== undefined ? { mode: props.mode } : {})}
221
+ >
193
222
  <Card
194
223
  size="small"
195
224
  data-testid="listings-card"
@@ -197,19 +226,22 @@ export function ListingCard(props: ListingCardProps): ReactElement {
197
226
  {...(status !== undefined
198
227
  ? { "data-listing-status": status.status }
199
228
  : {})}
229
+ styles={{ body: { minWidth: 0 } }}
200
230
  cover={
201
231
  <ListingPhoto
202
232
  imageRef={listing.images?.[0]}
203
233
  alt={listing.title ?? String(listing.id)}
204
- style={{ aspectRatio: "4 / 3" }}
205
234
  />
206
235
  }
207
236
  >
208
- <Flex vertical gap={4}>
237
+ <Flex vertical gap={spacing[1]} style={{ minWidth: 0 }}>
209
238
  {props.badge}
210
239
 
211
240
  <Typography.Text strong data-testid="listings-card-price">
212
- {price}
241
+ <ListingPrice
242
+ amount={listing.price}
243
+ {...(listing.currency !== undefined ? { currency: listing.currency } : {})}
244
+ />
213
245
  </Typography.Text>
214
246
 
215
247
  <Typography.Text ellipsis data-testid="listings-card-title">
@@ -242,24 +274,32 @@ export function ListingCard(props: ListingCardProps): ReactElement {
242
274
  </Typography.Text>
243
275
  ) : null}
244
276
 
245
- <Flex gap={8} align="center">
277
+ {/* The action row is wrapped in ONE `GatedControl`, so the reason a
278
+ blocked heart carries lands full width UNDER both controls
279
+ rather than in the two-centimetre column beside the icon. */}
280
+ {props.showFavorite === false ? (
246
281
  <OpenControl {...props} listingId={listing.id} />
247
-
248
- {props.showFavorite === false ? null : (
249
- <Tooltip
250
- title={
251
- favorite.gate.available
252
- ? favoriteLabel
253
- : t(favorite.gate.block.code, favorite.gate.block.params)
254
- }
255
- >
256
- {/* A disabled antd Button swallows pointer events, so the
257
- tooltip needs the wrapper to hear them — which is also
258
- what makes the REASON reachable by keyboard. */}
259
- <span data-testid="listings-card-favorite-wrap">
282
+ ) : (
283
+ <GatedControl
284
+ gate={favorite.gate}
285
+ testId="listings-card-actions"
286
+ style={{ width: "100%" }}
287
+ >
288
+ {(bind) => (
289
+ <Flex
290
+ gap={spacing[2]}
291
+ align="center"
292
+ style={{ width: "100%", minWidth: 0 }}
293
+ >
294
+ <div style={{ flex: "1 1 auto", minWidth: 0 }}>
295
+ <OpenControl {...props} listingId={listing.id} />
296
+ </div>
260
297
  <Button
261
- size="small"
262
- disabled={!favorite.gate.available}
298
+ disabled={bind.disabled}
299
+ data-disabled-reason="the enclosing <GatedControl> renders the gate's reason beside this button"
300
+ {...(bind["aria-describedby"] !== undefined
301
+ ? { "aria-describedby": bind["aria-describedby"] }
302
+ : {})}
263
303
  aria-label={favoriteLabel}
264
304
  aria-pressed={favorite.favorited}
265
305
  data-testid="listings-card-favorite"
@@ -269,27 +309,29 @@ export function ListingCard(props: ListingCardProps): ReactElement {
269
309
  onClick={favorite.toggle}
270
310
  icon={<HeartIcon filled={favorite.favorited} />}
271
311
  />
272
- </span>
273
- </Tooltip>
274
- )}
275
- </Flex>
312
+ </Flex>
313
+ )}
314
+ </GatedControl>
315
+ )}
276
316
 
277
- {/* The reason IN WORDS, plus the door. A tooltip on a disabled
278
- button is a reason nobody can read (core's actionGate.ts says so
279
- in as many words), and a reason with no next action leaves the
280
- visitor hunting for the header which is what the storefront
281
- had to write a paragraph about instead of shipping the screen. */}
282
- {props.showFavorite === false || favoriteGate.reason === undefined ? null : (
317
+ {/* The door. `GatedControl` above already prints the reason and
318
+ wires `aria-describedby` to it; what it cannot know is WHERE a
319
+ visitor signs in, which is the container's business and arrives
320
+ as `signIn`. On a grid `blockedReason="line"` drops the door and
321
+ keeps the sentence twenty-four doors to one place is not
322
+ twenty-four pieces of help. */}
323
+ {props.showFavorite === false ||
324
+ favoriteGate.reason === undefined ||
325
+ blockedReason === "line" ? null : (
283
326
  <Typography.Text
284
327
  type="secondary"
285
328
  data-testid="listings-card-favorite-blocked"
286
329
  >
287
- {favoriteGate.reason}
288
330
  <SignInLink cta={props.signIn} testId="listings-card-sign-in" />
289
331
  </Typography.Text>
290
332
  )}
291
333
  </Flex>
292
334
  </Card>
293
- </ListingsSkinTheme>
335
+ </SkinTheme>
294
336
  );
295
337
  }