@stapel/listings-react 0.25.10 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +259 -0
- package/MODULE.md +3 -1
- package/README.md +122 -9
- package/dist/api/generated/schema.d.ts +22 -12
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/default/CardBadges.d.ts.map +1 -1
- package/dist/default/CardBadges.js +3 -1
- package/dist/default/CardBadges.js.map +1 -1
- package/dist/default/ListingActions.d.ts +108 -0
- package/dist/default/ListingActions.d.ts.map +1 -0
- package/dist/default/ListingActions.js +29 -0
- package/dist/default/ListingActions.js.map +1 -0
- package/dist/default/ListingCard.d.ts.map +1 -1
- package/dist/default/ListingCard.js +15 -8
- package/dist/default/ListingCard.js.map +1 -1
- package/dist/default/ListingDetailPane.d.ts +170 -62
- package/dist/default/ListingDetailPane.d.ts.map +1 -1
- package/dist/default/ListingDetailPane.js +284 -28
- package/dist/default/ListingDetailPane.js.map +1 -1
- package/dist/default/ListingFeedCard.d.ts.map +1 -1
- package/dist/default/ListingFeedCard.js +2 -1
- package/dist/default/ListingFeedCard.js.map +1 -1
- package/dist/default/ListingPhoto.d.ts.map +1 -1
- package/dist/default/ListingPhoto.js +5 -2
- package/dist/default/ListingPhoto.js.map +1 -1
- package/dist/default/ListingSerpCard.d.ts.map +1 -1
- package/dist/default/ListingSerpCard.js +18 -4
- package/dist/default/ListingSerpCard.js.map +1 -1
- package/dist/default/MyListingsPane.d.ts.map +1 -1
- package/dist/default/MyListingsPane.js +49 -21
- package/dist/default/MyListingsPane.js.map +1 -1
- package/dist/default/ShareAction.d.ts +32 -0
- package/dist/default/ShareAction.d.ts.map +1 -0
- package/dist/default/ShareAction.js +116 -0
- package/dist/default/ShareAction.js.map +1 -0
- package/dist/default/actionRow.d.ts +58 -0
- package/dist/default/actionRow.d.ts.map +1 -0
- package/dist/default/actionRow.js +128 -0
- package/dist/default/actionRow.js.map +1 -0
- package/dist/default/cardGallery.d.ts +2 -0
- package/dist/default/cardGallery.d.ts.map +1 -1
- package/dist/default/cardGallery.js +31 -0
- package/dist/default/cardGallery.js.map +1 -1
- package/dist/default/detailGallery.d.ts +77 -0
- package/dist/default/detailGallery.d.ts.map +1 -0
- package/dist/default/detailGallery.js +91 -0
- package/dist/default/detailGallery.js.map +1 -0
- package/dist/default/favorite.d.ts +25 -0
- package/dist/default/favorite.d.ts.map +1 -1
- package/dist/default/favorite.js +29 -3
- package/dist/default/favorite.js.map +1 -1
- package/dist/default/icons.d.ts +19 -0
- package/dist/default/icons.d.ts.map +1 -1
- package/dist/default/icons.js +23 -0
- package/dist/default/icons.js.map +1 -1
- package/dist/default/index.d.ts +11 -2
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +10 -1
- package/dist/default/index.js.map +1 -1
- package/dist/default/movableCluster.d.ts +18 -0
- package/dist/default/movableCluster.d.ts.map +1 -0
- package/dist/default/movableCluster.js +110 -0
- package/dist/default/movableCluster.js.map +1 -0
- package/dist/default/notice.d.ts +14 -0
- package/dist/default/notice.d.ts.map +1 -0
- package/dist/default/notice.js +62 -0
- package/dist/default/notice.js.map +1 -0
- package/dist/headless/MyListings.d.ts +61 -19
- package/dist/headless/MyListings.d.ts.map +1 -1
- package/dist/headless/MyListings.js +58 -19
- package/dist/headless/MyListings.js.map +1 -1
- package/dist/headless/Share.d.ts +103 -0
- package/dist/headless/Share.d.ts.map +1 -0
- package/dist/headless/Share.js +190 -0
- package/dist/headless/Share.js.map +1 -0
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +21 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/generated/errors.es.gen.d.ts +1 -1
- package/dist/i18n/generated/errors.es.gen.js +1 -1
- package/dist/i18n/generated/errors.gen.d.ts +18 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.gen.js +9 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -1
- package/dist/i18n/generated/errors.ru.gen.d.ts +1 -1
- package/dist/i18n/generated/errors.ru.gen.js +1 -1
- package/dist/i18n/keys.d.ts +45 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +60 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +21 -0
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/model/cardBadges.d.ts +52 -3
- package/dist/model/cardBadges.d.ts.map +1 -1
- package/dist/model/cardBadges.js +74 -12
- package/dist/model/cardBadges.js.map +1 -1
- package/dist/model/mineSource.d.ts +9 -3
- package/dist/model/mineSource.d.ts.map +1 -1
- package/dist/model/mineSource.js.map +1 -1
- package/dist/model/status.d.ts +79 -15
- package/dist/model/status.d.ts.map +1 -1
- package/dist/model/status.js +86 -17
- package/dist/model/status.js.map +1 -1
- package/llms.txt +8 -3
- package/manifest.json +96 -4
- package/nav-manifest.json +1 -1
- package/package.json +10 -10
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +22 -12
- package/src/default/CardBadges.tsx +3 -1
- package/src/default/ListingActions.tsx +196 -0
- package/src/default/ListingCard.tsx +79 -43
- package/src/default/ListingDetailPane.tsx +520 -89
- package/src/default/ListingFeedCard.tsx +2 -0
- package/src/default/ListingPhoto.tsx +28 -0
- package/src/default/ListingSerpCard.tsx +39 -12
- package/src/default/MyListingsPane.tsx +81 -44
- package/src/default/ShareAction.tsx +260 -0
- package/src/default/actionRow.ts +131 -0
- package/src/default/cardGallery.ts +31 -0
- package/src/default/detailGallery.ts +97 -0
- package/src/default/favorite.tsx +68 -1
- package/src/default/icons.tsx +60 -0
- package/src/default/index.ts +37 -1
- package/src/default/movableCluster.tsx +137 -0
- package/src/default/notice.ts +68 -0
- package/src/headless/MyListings.tsx +130 -39
- package/src/headless/Share.tsx +278 -0
- package/src/i18n/es.ts +28 -0
- package/src/i18n/generated/errors.es.gen.ts +1 -1
- package/src/i18n/generated/errors.gen.ts +9 -0
- package/src/i18n/generated/errors.json +31 -0
- package/src/i18n/generated/errors.ru.gen.ts +1 -1
- package/src/i18n/keys.ts +63 -0
- package/src/i18n/ru.ts +28 -0
- package/src/index.ts +22 -0
- package/src/model/cardBadges.ts +124 -12
- package/src/model/mineSource.ts +9 -3
- package/src/model/status.ts +102 -20
package/manifest.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$generated": "by scripts/gen-manifest.mjs — do not edit; drift-gated (pnpm gen:manifest:check)",
|
|
3
3
|
"package": "@stapel/listings-react",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.27.0",
|
|
5
5
|
"backend": {
|
|
6
6
|
"module": "stapel-listings",
|
|
7
|
-
"contract": ">=0.
|
|
7
|
+
"contract": ">=0.22 <0.23"
|
|
8
8
|
},
|
|
9
9
|
"layers": [
|
|
10
10
|
"api",
|
|
@@ -522,6 +522,31 @@
|
|
|
522
522
|
],
|
|
523
523
|
"remediation": "fix_input"
|
|
524
524
|
},
|
|
525
|
+
"error.400.listing_features_draft_shape": {
|
|
526
|
+
"status": 400,
|
|
527
|
+
"params": [
|
|
528
|
+
"got_type",
|
|
529
|
+
"example"
|
|
530
|
+
],
|
|
531
|
+
"remediation": "fix_input"
|
|
532
|
+
},
|
|
533
|
+
"error.400.listing_features_draft_unknown_slug": {
|
|
534
|
+
"status": 400,
|
|
535
|
+
"params": [
|
|
536
|
+
"index",
|
|
537
|
+
"example"
|
|
538
|
+
],
|
|
539
|
+
"remediation": "fix_input"
|
|
540
|
+
},
|
|
541
|
+
"error.400.listing_features_draft_value_shape": {
|
|
542
|
+
"status": 400,
|
|
543
|
+
"params": [
|
|
544
|
+
"slug",
|
|
545
|
+
"got_type",
|
|
546
|
+
"example"
|
|
547
|
+
],
|
|
548
|
+
"remediation": "fix_input"
|
|
549
|
+
},
|
|
525
550
|
"error.400.listing_invalid_status_filter": {
|
|
526
551
|
"status": 400,
|
|
527
552
|
"params": [
|
|
@@ -714,6 +739,20 @@
|
|
|
714
739
|
"flows": []
|
|
715
740
|
},
|
|
716
741
|
"demos": [
|
|
742
|
+
{
|
|
743
|
+
"id": "listings.actions",
|
|
744
|
+
"title": "Save and share cluster",
|
|
745
|
+
"description": "The reader's two actions in one cluster, so the pair can be given a hit-target tier, a corner and an order ONCE rather than twice. `placement=\"overlay\"` pins it to the trailing-top corner of a card's media — the one corner a gallery leaves free, because the dots own the bottom centre and the photo counter owns the bottom trailing corner. Not to be confused with `useListingActions`, which is the SELLER's lifecycle moves; neither is exported from the other's barrel.",
|
|
746
|
+
"component": "ListingActions",
|
|
747
|
+
"tokens": [
|
|
748
|
+
"surface-raised"
|
|
749
|
+
],
|
|
750
|
+
"variants": [
|
|
751
|
+
"default",
|
|
752
|
+
"blocked"
|
|
753
|
+
],
|
|
754
|
+
"source": "demo/ListingActions.demo.tsx"
|
|
755
|
+
},
|
|
717
756
|
{
|
|
718
757
|
"id": "listings.card",
|
|
719
758
|
"title": "Listing card",
|
|
@@ -837,7 +876,7 @@
|
|
|
837
876
|
{
|
|
838
877
|
"id": "listings.mine",
|
|
839
878
|
"title": "My listings",
|
|
840
|
-
"description": "stapel-listings 0.7.0 gave the owner's own listings a route (GET my/listings/, every status, ?status= for a tab's set), so the rows here are the contract's own. Four things the pane refuses to smooth over: both axes on every row (a LIVE listing whose edit is under review says so — status alone cannot); a moderation takedown, which my/counters counts in no tab at all,
|
|
879
|
+
"description": "stapel-listings 0.7.0 gave the owner's own listings a route (GET my/listings/, every status, ?status= for a tab's set), so the rows here are the contract's own. Four things the pane refuses to smooth over: both axes on every row (a LIVE listing whose edit is under review says so — status alone cannot); a moderation takedown, which my/counters counts in no tab at all, gets a fourth tab and a count of its own, announced by a line above the tab strip (D407); every switched-off action prints its reason beside itself instead of hiding it in a hover a phone cannot open; and Delete asks first, through a bottom sheet — and is drawn only on the rows that HAVE a delete route, never as a switched-off button on a listing that is on sale. The row is a thumbnail plus a min-width:0 column, so four actions wrap at 390px instead of clipping.",
|
|
841
880
|
"component": "MyListingsPane",
|
|
842
881
|
"covers": [
|
|
843
882
|
"MyListings"
|
|
@@ -915,6 +954,24 @@
|
|
|
915
954
|
],
|
|
916
955
|
"source": "demo/ListingSerpCard.demo.tsx"
|
|
917
956
|
},
|
|
957
|
+
{
|
|
958
|
+
"id": "listings.share",
|
|
959
|
+
"title": "Share a listing",
|
|
960
|
+
"description": "One control, two arms, and the device decides: the platform's own sheet where `navigator.share` exists, and a small menu (copy the link, Telegram, WhatsApp, VK) where it does not. The address shared is the route the CONTAINER built — handed in as `url` and used verbatim — never `window.location`, because the address bar on a SERP carries the query, the page and whatever tracking parameters the visitor arrived with, and none of that belongs in a link somebody sends to a friend.",
|
|
961
|
+
"component": "ShareAction",
|
|
962
|
+
"covers": [
|
|
963
|
+
"Share"
|
|
964
|
+
],
|
|
965
|
+
"tokens": [
|
|
966
|
+
"surface-sunken",
|
|
967
|
+
"surface-raised"
|
|
968
|
+
],
|
|
969
|
+
"variants": [
|
|
970
|
+
"default",
|
|
971
|
+
"menu"
|
|
972
|
+
],
|
|
973
|
+
"source": "demo/Share.demo.tsx"
|
|
974
|
+
},
|
|
918
975
|
{
|
|
919
976
|
"id": "listings.spec-parts",
|
|
920
977
|
"title": "Badges and specs",
|
|
@@ -971,6 +1028,9 @@
|
|
|
971
1028
|
"error.400.invalid_ad_id",
|
|
972
1029
|
"error.400.listing_draft_meta_too_large",
|
|
973
1030
|
"error.400.listing_feature_not_allowed",
|
|
1031
|
+
"error.400.listing_features_draft_shape",
|
|
1032
|
+
"error.400.listing_features_draft_unknown_slug",
|
|
1033
|
+
"error.400.listing_features_draft_value_shape",
|
|
974
1034
|
"error.400.listing_invalid_status_filter",
|
|
975
1035
|
"error.400.listing_location_required",
|
|
976
1036
|
"error.400.listing_zero_price_not_allowed",
|
|
@@ -1017,6 +1077,7 @@
|
|
|
1017
1077
|
"listings.card.favorite_add",
|
|
1018
1078
|
"listings.card.favorite_remove",
|
|
1019
1079
|
"listings.card.no_photo",
|
|
1080
|
+
"listings.card.photo_counter",
|
|
1020
1081
|
"listings.card.photo_unavailable",
|
|
1021
1082
|
"listings.card.photos",
|
|
1022
1083
|
"listings.card.price_absent",
|
|
@@ -1091,6 +1152,9 @@
|
|
|
1091
1152
|
"listings.detail.views",
|
|
1092
1153
|
"listings.detail.withdrawn",
|
|
1093
1154
|
"listings.error.unknown",
|
|
1155
|
+
"listings.favorite.added",
|
|
1156
|
+
"listings.favorite.count",
|
|
1157
|
+
"listings.favorite.removed",
|
|
1094
1158
|
"listings.favorites.empty",
|
|
1095
1159
|
"listings.favorites.empty_hint",
|
|
1096
1160
|
"listings.favorites.load_failed",
|
|
@@ -1106,12 +1170,14 @@
|
|
|
1106
1170
|
"listings.mine.counters_failed",
|
|
1107
1171
|
"listings.mine.delete",
|
|
1108
1172
|
"listings.mine.delete_confirm_body",
|
|
1173
|
+
"listings.mine.delete_confirm_body.final",
|
|
1109
1174
|
"listings.mine.delete_confirm_title",
|
|
1110
1175
|
"listings.mine.edit",
|
|
1111
1176
|
"listings.mine.empty",
|
|
1112
1177
|
"listings.mine.empty.active",
|
|
1113
1178
|
"listings.mine.empty.archived",
|
|
1114
1179
|
"listings.mine.empty.drafts",
|
|
1180
|
+
"listings.mine.empty.removed",
|
|
1115
1181
|
"listings.mine.live_under_review",
|
|
1116
1182
|
"listings.mine.load_failed",
|
|
1117
1183
|
"listings.mine.loading",
|
|
@@ -1124,6 +1190,7 @@
|
|
|
1124
1190
|
"listings.mine.tab.active",
|
|
1125
1191
|
"listings.mine.tab.archived",
|
|
1126
1192
|
"listings.mine.tab.drafts",
|
|
1193
|
+
"listings.mine.tab.removed",
|
|
1127
1194
|
"listings.mine.title",
|
|
1128
1195
|
"listings.mine.view",
|
|
1129
1196
|
"listings.moderation.first_review",
|
|
@@ -1142,6 +1209,13 @@
|
|
|
1142
1209
|
"listings.page.indicator",
|
|
1143
1210
|
"listings.page.next",
|
|
1144
1211
|
"listings.page.prev",
|
|
1212
|
+
"listings.share.action",
|
|
1213
|
+
"listings.share.copied",
|
|
1214
|
+
"listings.share.copy",
|
|
1215
|
+
"listings.share.copy_failed",
|
|
1216
|
+
"listings.share.telegram",
|
|
1217
|
+
"listings.share.vk",
|
|
1218
|
+
"listings.share.whatsapp",
|
|
1145
1219
|
"listings.status.archived",
|
|
1146
1220
|
"listings.status.blocked",
|
|
1147
1221
|
"listings.status.draft",
|
|
@@ -1182,6 +1256,8 @@
|
|
|
1182
1256
|
"ListingsProvider",
|
|
1183
1257
|
"ListingsRuntimeContext",
|
|
1184
1258
|
"MODERATION_STATUSES",
|
|
1259
|
+
"MY_LISTINGS_COUNTED_TABS",
|
|
1260
|
+
"MY_LISTINGS_REMOVED_TAB",
|
|
1185
1261
|
"MY_LISTINGS_TABS",
|
|
1186
1262
|
"MY_LISTINGS_TAB_PARAM",
|
|
1187
1263
|
"MY_LISTINGS_TAB_STATUSES",
|
|
@@ -1190,6 +1266,9 @@
|
|
|
1190
1266
|
"NO_ADDRESS",
|
|
1191
1267
|
"OWNER_TRANSITIONS",
|
|
1192
1268
|
"PRICE_FIELD",
|
|
1269
|
+
"SHARE_COPIED_MS",
|
|
1270
|
+
"SHARE_NETWORKS",
|
|
1271
|
+
"Share",
|
|
1193
1272
|
"TITLE_FIELD",
|
|
1194
1273
|
"asFeatureDaoList",
|
|
1195
1274
|
"badgePresentation",
|
|
@@ -1199,6 +1278,7 @@
|
|
|
1199
1278
|
"canTransition",
|
|
1200
1279
|
"cardBadgeText",
|
|
1201
1280
|
"cardBadgeTexts",
|
|
1281
|
+
"countedTabOf",
|
|
1202
1282
|
"createDraftBody",
|
|
1203
1283
|
"createListingsApi",
|
|
1204
1284
|
"createListingsRuntime",
|
|
@@ -1221,6 +1301,7 @@
|
|
|
1221
1301
|
"flowEndpoints",
|
|
1222
1302
|
"formatSpecValue",
|
|
1223
1303
|
"hasCardBadgeContract",
|
|
1304
|
+
"hasNativeShare",
|
|
1224
1305
|
"isBatchValid",
|
|
1225
1306
|
"isListingViewed",
|
|
1226
1307
|
"isPubliclyVisible",
|
|
@@ -1242,8 +1323,10 @@
|
|
|
1242
1323
|
"pageKey",
|
|
1243
1324
|
"publishRefusal",
|
|
1244
1325
|
"registerListingsI18n",
|
|
1326
|
+
"resolveShareUrl",
|
|
1245
1327
|
"retainKnownFeatureValues",
|
|
1246
1328
|
"searchWithTab",
|
|
1329
|
+
"shareLinks",
|
|
1247
1330
|
"showsDraft",
|
|
1248
1331
|
"tabFromSearch",
|
|
1249
1332
|
"tabOf",
|
|
@@ -1277,6 +1360,7 @@
|
|
|
1277
1360
|
"useMyListings",
|
|
1278
1361
|
"usePublishListing",
|
|
1279
1362
|
"useSaveDraft",
|
|
1363
|
+
"useShare",
|
|
1280
1364
|
"useTransitionListing",
|
|
1281
1365
|
"useValidateDraft",
|
|
1282
1366
|
"withEngagement"
|
|
@@ -1285,6 +1369,7 @@
|
|
|
1285
1369
|
"CardBadgeContract",
|
|
1286
1370
|
"CardBadgePresentation",
|
|
1287
1371
|
"CardBadgeRow",
|
|
1372
|
+
"CardBadgeStyle",
|
|
1288
1373
|
"ComposeStage",
|
|
1289
1374
|
"CreateListingsRuntimeOptions",
|
|
1290
1375
|
"DeleteResponse",
|
|
@@ -1343,6 +1428,7 @@
|
|
|
1343
1428
|
"MyListingCard",
|
|
1344
1429
|
"MyListingsAddress",
|
|
1345
1430
|
"MyListingsBag",
|
|
1431
|
+
"MyListingsCountedTab",
|
|
1346
1432
|
"MyListingsParams",
|
|
1347
1433
|
"MyListingsSource",
|
|
1348
1434
|
"MyListingsTab",
|
|
@@ -1354,12 +1440,18 @@
|
|
|
1354
1440
|
"Remediation",
|
|
1355
1441
|
"SaveDraftInput",
|
|
1356
1442
|
"Schemas",
|
|
1443
|
+
"ShareBag",
|
|
1444
|
+
"ShareChannel",
|
|
1445
|
+
"ShareLink",
|
|
1446
|
+
"ShareNetwork",
|
|
1447
|
+
"ShareTarget",
|
|
1357
1448
|
"UseFavoritesOptions",
|
|
1358
1449
|
"UseListingActionsOptions",
|
|
1359
1450
|
"UseListingComposerOptions",
|
|
1360
1451
|
"UseListingDetailOptions",
|
|
1361
1452
|
"UseListingEngagementOverlayOptions",
|
|
1362
|
-
"UseMyListingsOptions"
|
|
1453
|
+
"UseMyListingsOptions",
|
|
1454
|
+
"UseShareOptions"
|
|
1363
1455
|
]
|
|
1364
1456
|
}
|
|
1365
1457
|
}
|
package/nav-manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stapel/listings-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Headless React pair for stapel-listings: a typed client over the draft twin, the composer that turns a category schema into a submittable listing, and the owner's dashboard. Two independent axes are rendered as two — lifecycle `status` decides visibility and `moderation_status` decides nothing about it, so an edit to a live listing stays live and says 'under review' instead of vanishing. Feature values are drawn and mirrored by @stapel/attributes-react; photos arrive as an injected upload bag whose `refs` ARE `images_draft` and whose `settled` gates the submit; a publish refusal is routed onto the control that caused it by slug. Zero visual opinion in the main entry; an opt-in /default subpath ships the antd skin.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,24 +48,24 @@
|
|
|
48
48
|
],
|
|
49
49
|
"size-limit": [
|
|
50
50
|
{
|
|
51
|
-
"name": "index — the headless pair (client + status axes + draft model + hooks): no antd. Raised 11 -> 12 KB for the elevation gate (useElevatableMandateGate + core's useElevation): the anonymous arm can now mint an identity for a named action instead of refusing, and that is a state hook the gate did not previously carry. Raised 11.5 -> 12 KB for envelopeFieldErrors and its API-field -> control table: a save-draft/create 400 names its field in the envelope rather than in a publish batch, and without the table the refusal had nowhere to land — which is how one over-precise coordinate painted two unreadable banners and left every control clean. Thirty-six bytes of routing table is what a legible refusal costs. Raised 12 -> 13 KB for two seams the mobile wave needed in the HEADLESS half: the category-feature copy source in model/features.ts (the option table a stored select does not carry, so a published listing prints \"Second-hand\" and not \"b-u\"), and firstUnsatisfied (the field a \"10 required details are still empty\" count actually points at, in the order the form asks). Both are model, not skin: a headless host renders its own controls and needs the same two answers. Raised 13 -> 14 KB (measured 12.91) for the engagement overlay: a batch read, its normalized key, and the scope that runs it once for a page and hands every card its own entry. It is a whole new READ rather than a flag — and it is the read that makes the viewed/saved state visible at all on a feed and a SERP, which come from the search index and carry no per-viewer field. Landing at 12.91 of 13 would have been a budget passed by rounding. Raised 14 -> 15 KB (measured 14.39) for the two model modules the listing-page wave added to the HEADLESS half: model/featureText.ts, which typesets a numeric value instead of stringifying it (its unit resolved from the definition, its digits through Intl.NumberFormat — the live page read 'Power 173' and 'Mileage 20000'), and model/cardBadges.ts, the card badge contract's four presentations plus the detector that falls back to the old rendering on an older backend. Both are answers a headless host needs as much as the skin does: a container drawing its own spec rows has the same value and the same missing unit.",
|
|
51
|
+
"name": "index — the headless pair (client + status axes + draft model + hooks): no antd. Raised 11 -> 12 KB for the elevation gate (useElevatableMandateGate + core's useElevation): the anonymous arm can now mint an identity for a named action instead of refusing, and that is a state hook the gate did not previously carry. Raised 11.5 -> 12 KB for envelopeFieldErrors and its API-field -> control table: a save-draft/create 400 names its field in the envelope rather than in a publish batch, and without the table the refusal had nowhere to land — which is how one over-precise coordinate painted two unreadable banners and left every control clean. Thirty-six bytes of routing table is what a legible refusal costs. Raised 12 -> 13 KB for two seams the mobile wave needed in the HEADLESS half: the category-feature copy source in model/features.ts (the option table a stored select does not carry, so a published listing prints \"Second-hand\" and not \"b-u\"), and firstUnsatisfied (the field a \"10 required details are still empty\" count actually points at, in the order the form asks). Both are model, not skin: a headless host renders its own controls and needs the same two answers. Raised 13 -> 14 KB (measured 12.91) for the engagement overlay: a batch read, its normalized key, and the scope that runs it once for a page and hands every card its own entry. It is a whole new READ rather than a flag — and it is the read that makes the viewed/saved state visible at all on a feed and a SERP, which come from the search index and carry no per-viewer field. Landing at 12.91 of 13 would have been a budget passed by rounding. Raised 14 -> 15 KB (measured 14.39) for the two model modules the listing-page wave added to the HEADLESS half: model/featureText.ts, which typesets a numeric value instead of stringifying it (its unit resolved from the definition, its digits through Intl.NumberFormat — the live page read 'Power 173' and 'Mileage 20000'), and model/cardBadges.ts, the card badge contract's four presentations plus the detector that falls back to the old rendering on an older backend. Both are answers a headless host needs as much as the skin does: a container drawing its own spec rows has the same value and the same missing unit. Raised 15 -> 16 KB (measured 15.27) for the two model answers the desktop/mobile wave needed in the HEADLESS half: the fourth dashboard tab (D407 — `MY_LISTINGS_COUNTED_TABS` beside `MY_LISTINGS_TABS`, `countedTabOf`, the removed tab's own status set, and the tab count that comes from the takedowns' own read because `my/counters` has three integers and no fourth), and the spec-line disambiguation in model/cardBadges.ts (the caption/answer punctuation per surface, plus the pass that tells two axes wearing one unit apart). Both are answers a headless host needs as much as the skin does: a container drawing its own tab strip has the same missing count, and one drawing its own spec line has the same two axes. Raised 16 -> 17 KB (measured 15.99) for `useShare`: the canonical-URL resolution, the three networks' encoded endpoints, the clipboard write and the native-sheet capability probe. The previous ceiling was reached EXACTLY — 15.99 of 16 — and a budget passed by rounding is a budget the next sentence fails. It is in the headless half deliberately: a host drawing its own share menu needs the same three hrefs and the same rule that a canonical route beats the address bar.",
|
|
52
52
|
"path": "dist/index.js",
|
|
53
|
-
"limit": "
|
|
53
|
+
"limit": "17 KB"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
|
-
"name": "default — the antd skin (three cards, detail, composer, dashboard, the named slots and the delete confirmation) must stay out of the main bundle. Raised 17 -> 19 KB for the mobile wave: <ListingSerpCard> (photo strip + price-first block + action rail), <ListingFeedCard> and <FeedGrid> are two more card surfaces on the same shared target/favourite/photo parts, not three copies of one Raised 19 -> 20 KB for the desktop wave of the detail page: layout=\"split\" is a second ASSEMBLY of the money page (the sticky buy column, the two-column spec table, the widened measure) over the same parts, and GateReasonPopover is the blocked heart's third volume — the reason and the door disclosed on interaction, with the visually-hidden copy that keeps the refusal in the accessibility tree. Both are geometry and wiring, not new widgets. Raised 20 -> 21 KB (measured 20.22) for the engagement wave, which is four small things and no new component: the disclosure became a controlled open/close so a pointer that hovers and then clicks does not shut the only explanation the control has; the heart draws a filled accent when saved and states a failed save through the shared ErrorAlert; an already-seen card dims through one rule in the sheet all three cards already hoist; and the listing page gained a view-count row. 216 bytes is what a control that reports its own outcome costs. Raised 21 -> 22 KB for the seller's board becoming a door in both directions: the transition route and useTransitionListing, OWNER_TRANSITIONS/ownerMoves as the fallback mirror, ListingActionsBag.moves drawn in a stable order, listingHref/linkComponent on the cabinet pane, the refusal that stops quoting the wire, and the row that fits a phone. Measured on a clean tree, this package's src reverted to the previous commit with dependencies held constant: 20.82 -> 21.60 KB. The old ceiling had 180 B of room and the work needs 780; none of it is copy, and a fourth axis of owner state is not a rounding error. Raised 22 -> 25 KB (measured 23.94) for the three surfaces of the same wave: <ListingSpecList>/<ListingSpecColumns>, which is a REPLACEMENT of antd's <Descriptions> with a paragraph row (an inline muted label and the value in one text flow, so a long answer wraps at the measure instead of stacking inside a third-of-a-page cell) and therefore carries its own hoisted sheet; <CardBadges>/<CardSpecLine>, one badge renderer for all three card surfaces with the pre-contract path kept whole beside it; and cardGallery.ts, the hover-scrub and swipe gestures — a segment map, a horizontal-intent threshold, the pointer-capability gate and the touch-action rule that leaves the page's vertical scroll to the browser. A gallery a card did not have and a spec list rebuilt from a table are not a rounding error.",
|
|
56
|
+
"name": "default — the antd skin (three cards, detail, composer, dashboard, the named slots and the delete confirmation) must stay out of the main bundle. Raised 17 -> 19 KB for the mobile wave: <ListingSerpCard> (photo strip + price-first block + action rail), <ListingFeedCard> and <FeedGrid> are two more card surfaces on the same shared target/favourite/photo parts, not three copies of one Raised 19 -> 20 KB for the desktop wave of the detail page: layout=\"split\" is a second ASSEMBLY of the money page (the sticky buy column, the two-column spec table, the widened measure) over the same parts, and GateReasonPopover is the blocked heart's third volume — the reason and the door disclosed on interaction, with the visually-hidden copy that keeps the refusal in the accessibility tree. Both are geometry and wiring, not new widgets. Raised 20 -> 21 KB (measured 20.22) for the engagement wave, which is four small things and no new component: the disclosure became a controlled open/close so a pointer that hovers and then clicks does not shut the only explanation the control has; the heart draws a filled accent when saved and states a failed save through the shared ErrorAlert; an already-seen card dims through one rule in the sheet all three cards already hoist; and the listing page gained a view-count row. 216 bytes is what a control that reports its own outcome costs. Raised 21 -> 22 KB for the seller's board becoming a door in both directions: the transition route and useTransitionListing, OWNER_TRANSITIONS/ownerMoves as the fallback mirror, ListingActionsBag.moves drawn in a stable order, listingHref/linkComponent on the cabinet pane, the refusal that stops quoting the wire, and the row that fits a phone. Measured on a clean tree, this package's src reverted to the previous commit with dependencies held constant: 20.82 -> 21.60 KB. The old ceiling had 180 B of room and the work needs 780; none of it is copy, and a fourth axis of owner state is not a rounding error. Raised 22 -> 25 KB (measured 23.94) for the three surfaces of the same wave: <ListingSpecList>/<ListingSpecColumns>, which is a REPLACEMENT of antd's <Descriptions> with a paragraph row (an inline muted label and the value in one text flow, so a long answer wraps at the measure instead of stacking inside a third-of-a-page cell) and therefore carries its own hoisted sheet; <CardBadges>/<CardSpecLine>, one badge renderer for all three card surfaces with the pre-contract path kept whole beside it; and cardGallery.ts, the hover-scrub and swipe gestures — a segment map, a horizontal-intent threshold, the pointer-capability gate and the touch-action rule that leaves the page's vertical scroll to the browser. A gallery a card did not have and a spec list rebuilt from a table are not a rounding error. Raised 25 -> 26 KB (measured 25.34) for three small things and no new component: the dashboard's fourth tab and the takedown notice that shrank to one line above it (D407), the delete dialog choosing its sentence from the row's own remaining moves rather than printing the archive at somebody standing in the archive, and the photo strip's \"1 of 16\" counter with its two CSS rules (D418's neighbour on the card). None of it is copy; the counter is the one thing on a phone card that says a swipe is worth making. Raised 26 -> 29 KB (measured 27.56) for the reader's two actions: <ShareAction> (the native-sheet arm, the desktop menu with its three outbound links and the clipboard row, and the copy confirmation that stands in the menu rather than only in a toast), <ListingActions> (the cluster the listing page and the cards lay out through), actionRow.ts (two hit-target tiers as a class contract — 44px on the page, 36/44 on a card — plus the overlay pin and the label's phone arm) and notice.ts (the message seam, which is antd's own App/static message and no new dependency). Measured with dependencies held constant: 25.34 -> 27.56 KB. A verb the product did not have at all is not a rounding error. Raised 29 -> 31 KB (measured 29.92) for the two seams a live container was working around with a second component and an `!important`: the MOVABLE CLUSTER (movableCluster.tsx — the portal, the slot registry, the settle that appends the container into the highest-priority mount on screen, and the pane's two slots plus the bar layer) and the GALLERY LAYOUT (detailGallery.ts — the class, the two arms and the strip's child rule, hoisted because an inline style cannot reach a child), together with `useTitleVisibility`'s IntersectionObserver. Measured with dependencies held constant, this package's src at HEAD built into a scratch tree and then this one: 27.72 -> 29.92 KB. 30 would have left 80 B, which is the budget-passed-by-rounding this entry has warned about four times; 31 leaves 1.08 KB. What was bought: a page that mounts ONE `useFavoriteToggle` instead of two, and a host stylesheet that no longer needs `!important` over a pair's own geometry.",
|
|
57
57
|
"path": "dist/default/index.js",
|
|
58
|
-
"limit": "
|
|
58
|
+
"limit": "31 KB"
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
|
-
"name": "i18n/ru — the Russian bundle is opt-in, not carried by the main entry",
|
|
61
|
+
"name": "i18n/ru — the Russian bundle is opt-in, not carried by the main entry. Raised 5 -> 5.5 KB (measured 5.02): three sentences the wave added — the fourth tab and its empty state, and the delete warning that does not promise the archive to a listing already in it. A bundle sitting exactly ON its limit is a limit the next sentence breaks. Raised 5.5 -> 6 KB (measured 5.48) for the three features_draft refusals the stapel-listings 0.22.3 pin brought with it: the module is locale-exempt upstream, so a code that arrives without a ru sentence renders its own key on a Russian host, and each of these three has to NAME the shape it wanted rather than say \"try again\". 5.48 of 5.5 is twenty bytes of headroom — the rounding this entry already warns about.",
|
|
62
62
|
"path": "dist/i18n/ru.js",
|
|
63
|
-
"limit": "
|
|
63
|
+
"limit": "6 KB"
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
|
-
"name": "i18n/es — the Spanish bundle is opt-in, not carried by the main entry. Raised 3.5 -> 4 KB: the mobile wave's four card keys landed the bundle exactly ON the old limit, and a budget a release passes by rounding is a budget that fails the next one",
|
|
66
|
+
"name": "i18n/es — the Spanish bundle is opt-in, not carried by the main entry. Raised 3.5 -> 4 KB: the mobile wave's four card keys landed the bundle exactly ON the old limit, and a budget a release passes by rounding is a budget that fails the next one. Raised 4 -> 4.5 KB (measured 3.94) for the share wave's ten sentences, which landed it on that limit again by the same 60 bytes",
|
|
67
67
|
"path": "dist/i18n/es.js",
|
|
68
|
-
"limit": "4 KB"
|
|
68
|
+
"limit": "4.5 KB"
|
|
69
69
|
}
|
|
70
70
|
],
|
|
71
71
|
"peerDependencies": {
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"size-limit": "^11.2.0",
|
|
103
103
|
"typescript": "^5.8.3",
|
|
104
104
|
"vitest": "^3.2.4",
|
|
105
|
-
"@stapel/attributes-react": "^0.16.
|
|
105
|
+
"@stapel/attributes-react": "^0.16.5",
|
|
106
106
|
"@stapel/core": "^0.25.4",
|
|
107
107
|
"@stapel/currencies-react": "^0.4.0",
|
|
108
108
|
"@stapel/image": "^0.4.2",
|
|
@@ -439,15 +439,18 @@ export interface paths {
|
|
|
439
439
|
cookie?: never;
|
|
440
440
|
};
|
|
441
441
|
/**
|
|
442
|
-
* @description
|
|
442
|
+
* @description One integer per cabinet tab, over the caller's own listings.
|
|
443
443
|
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
444
|
+
* The tab groupings are the SERVER's — ``my/listings?status=`` takes
|
|
445
|
+
* the same sets — so a cabinet never has to re-derive which statuses
|
|
446
|
+
* make up "active". Every lifecycle status is in exactly one group:
|
|
447
|
+
* ``blocked`` (a moderation takedown) is its own count rather than
|
|
448
|
+
* being folded into ``archived``, because the two tell the seller
|
|
449
|
+
* different things and only one of them is theirs to undo.
|
|
450
|
+
*
|
|
451
|
+
* Owner-scoped at the queryset via ``owned_by``, soft-deleted rows
|
|
452
|
+
* excluded by the default manager — the same scope as
|
|
453
|
+
* ``my/listings``, so a tab's rows and its count always agree.
|
|
451
454
|
*
|
|
452
455
|
* **Permissions:** `IsAuthenticated`
|
|
453
456
|
*/
|
|
@@ -500,7 +503,7 @@ export interface paths {
|
|
|
500
503
|
* @description The caller's OWN listings, in every status.
|
|
501
504
|
*
|
|
502
505
|
* The counterpart of ``my/counters``: the same owner scope and the same
|
|
503
|
-
* status grouping, but the rows behind the
|
|
506
|
+
* status grouping, but the rows behind the four numbers. ``list`` is
|
|
504
507
|
* the shop window (``published()``, narrowable to nobody), so this is
|
|
505
508
|
* the only route by which a person can be shown their own drafts.
|
|
506
509
|
*
|
|
@@ -1056,6 +1059,7 @@ export interface components {
|
|
|
1056
1059
|
lat_draft?: string | null;
|
|
1057
1060
|
/** Format: decimal */
|
|
1058
1061
|
lon_draft?: string | null;
|
|
1062
|
+
/** @description The WRITE shape of features_draft: an object keyed by feature slug, each value a FeatureDto — e.g. {"mileage":{"type":"int","value":42000}}. A listing READ hands back features as a *list* of decorated DAOs instead (`features` / `features_title` / `features_badges` — `{slug, name, label, presentation, …}`); posting that same list back here is also accepted, e.g. [{"slug":"mileage","type":"int","value":42000}] — only `slug`, `type` and `value` are read back out, every other (decoration) key is ignored. A malformed payload of either shape gets a features_draft_shape / features_draft_value_shape / features_draft_unknown_slug 400 naming the shape that would have worked. */
|
|
1059
1063
|
features_draft?: {
|
|
1060
1064
|
[key: string]: components["schemas"]["FeatureDto"];
|
|
1061
1065
|
} | null;
|
|
@@ -1128,11 +1132,18 @@ export interface components {
|
|
|
1128
1132
|
* @enum {string}
|
|
1129
1133
|
*/
|
|
1130
1134
|
ModerationStatusEnum: "not_submitted" | "pending" | "approved" | "rejected" | "needs_review";
|
|
1131
|
-
/**
|
|
1135
|
+
/**
|
|
1136
|
+
* @description Listing counts by tab for the current user.
|
|
1137
|
+
*
|
|
1138
|
+
* One integer per cabinet tab, and every lifecycle status belongs to
|
|
1139
|
+
* exactly one of them — ``blocked`` included, so a listing a moderator
|
|
1140
|
+
* took down is counted somewhere instead of vanishing from the totals.
|
|
1141
|
+
*/
|
|
1132
1142
|
MyCountersResponse: {
|
|
1133
1143
|
active: number;
|
|
1134
1144
|
archived: number;
|
|
1135
1145
|
drafts: number;
|
|
1146
|
+
blocked: number;
|
|
1136
1147
|
};
|
|
1137
1148
|
/**
|
|
1138
1149
|
* @description The owner's own card — the public card plus what only an owner sees.
|
|
@@ -1286,6 +1297,7 @@ export interface components {
|
|
|
1286
1297
|
lat_draft?: string | null;
|
|
1287
1298
|
/** Format: decimal */
|
|
1288
1299
|
lon_draft?: string | null;
|
|
1300
|
+
/** @description The WRITE shape of features_draft: an object keyed by feature slug, each value a FeatureDto — e.g. {"mileage":{"type":"int","value":42000}}. A listing READ hands back features as a *list* of decorated DAOs instead (`features` / `features_title` / `features_badges` — `{slug, name, label, presentation, …}`); posting that same list back here is also accepted, e.g. [{"slug":"mileage","type":"int","value":42000}] — only `slug`, `type` and `value` are read back out, every other (decoration) key is ignored. A malformed payload of either shape gets a features_draft_shape / features_draft_value_shape / features_draft_unknown_slug 400 naming the shape that would have worked. */
|
|
1289
1301
|
features_draft?: {
|
|
1290
1302
|
[key: string]: components["schemas"]["FeatureDto"];
|
|
1291
1303
|
} | null;
|
|
@@ -1357,8 +1369,6 @@ export interface components {
|
|
|
1357
1369
|
labels?: string[];
|
|
1358
1370
|
vocabulary?: string | null;
|
|
1359
1371
|
level?: string | null;
|
|
1360
|
-
prefix?: string | null;
|
|
1361
|
-
postfix?: string | null;
|
|
1362
1372
|
};
|
|
1363
1373
|
/** @description Serializer for the ref_select feature DTO. */
|
|
1364
1374
|
RefSelectDto: {
|
|
@@ -56,7 +56,9 @@ export function CardBadges(props: CardBadgesProps): ReactElement | null {
|
|
|
56
56
|
if (rows.length === 0) return null;
|
|
57
57
|
|
|
58
58
|
if (hasCardBadgeContract(rows)) {
|
|
59
|
-
|
|
59
|
+
// The variant is not only a layout: it decides how a caption is joined to
|
|
60
|
+
// its answer, because a chip has a border and a line has a dot (D421).
|
|
61
|
+
const printed = cardBadgeTexts(rows, locale, props.variant === "line" ? "line" : "badge");
|
|
60
62
|
if (printed.length === 0) return null;
|
|
61
63
|
if (props.variant === "line") {
|
|
62
64
|
return (
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<ListingActions>` — save it, or send it to somebody. The two verbs a
|
|
3
|
+
* classified puts beside a listing's title, in one cluster.
|
|
4
|
+
*
|
|
5
|
+
* ── Why they are one component and not two buttons in a row ───────────────
|
|
6
|
+
*
|
|
7
|
+
* Because everything that makes them usable is a property of the PAIR, not of
|
|
8
|
+
* either control: they share a hit-target tier, they share the corner of the
|
|
9
|
+
* photograph they are pinned to on a phone, and the reason they are icon-only
|
|
10
|
+
* on the listing page is that there are two of them and a price on one line.
|
|
11
|
+
* Measured against the reference (§23): our listing page drew a 152px
|
|
12
|
+
* "Save to favourites" button with a word in it at every width, where the
|
|
13
|
+
* reference draws a 44×44 glyph on a desktop and 36×36 on a phone and puts
|
|
14
|
+
* the share glyph beside it. Two components could not have been given that
|
|
15
|
+
* ruling once.
|
|
16
|
+
*
|
|
17
|
+
* ── The NAME of `<ListingActions>` and of `useListingActions` ─────────────
|
|
18
|
+
*
|
|
19
|
+
* They are different things and the collision is worth stating rather than
|
|
20
|
+
* renaming around. `useListingActions` (the headless entry) is the SELLER's
|
|
21
|
+
* lifecycle moves — archive, mark sold, delete — the owner's half of the
|
|
22
|
+
* state machine. This is the READER's two actions, and it lives in
|
|
23
|
+
* `/default` because it is a layout. Neither is exported from the other's
|
|
24
|
+
* barrel.
|
|
25
|
+
*
|
|
26
|
+
* ── The heart arrives as a node when the surface already has one ──────────
|
|
27
|
+
*
|
|
28
|
+
* The listing page's heart is driven by `useListingDetail`'s own optimistic
|
|
29
|
+
* bag (it holds the whole listing, so it flips the row it already has),
|
|
30
|
+
* while a card's is driven by `useFavoriteToggle` against a card row. Two
|
|
31
|
+
* hooks, one control — so the cluster takes {@link ListingActionsProps.favorite}
|
|
32
|
+
* as a node for a surface that already built one, and mounts the shared
|
|
33
|
+
* `<FavoriteHeart>` for everyone else. What it never does is let the two
|
|
34
|
+
* surfaces disagree about SIZE, PLACEMENT or ORDER, which is the whole job.
|
|
35
|
+
*
|
|
36
|
+
* ── There is no favourite count on the wire, and this does not invent one ─
|
|
37
|
+
*
|
|
38
|
+
* `ListingCard`/`ListingDetail` carry `is_favorited` — a per-reader boolean —
|
|
39
|
+
* and no aggregate anywhere (asserted in `test/engagementState`). So
|
|
40
|
+
* {@link ListingActionsProps.favoriteCount} is a number the HOST was given by
|
|
41
|
+
* something else, rendered when it is there and absent when it is not. Never
|
|
42
|
+
* a zero standing in for "nobody counted".
|
|
43
|
+
*/
|
|
44
|
+
import type { CSSProperties, ReactElement, ReactNode } from "react";
|
|
45
|
+
import { Flex, Typography } from "antd";
|
|
46
|
+
import type { SignInCta } from "@stapel/core";
|
|
47
|
+
import { useT } from "@stapel/core";
|
|
48
|
+
import { LISTINGS_I18N_KEYS } from "../i18n/keys.js";
|
|
49
|
+
import type { ShareChannel } from "../headless/Share.js";
|
|
50
|
+
import { FavoriteHeart } from "./favorite.js";
|
|
51
|
+
import { ShareAction } from "./ShareAction.js";
|
|
52
|
+
import {
|
|
53
|
+
LISTING_ACTIONS_CLASS,
|
|
54
|
+
LISTING_ACTIONS_OVERLAY_CLASS,
|
|
55
|
+
LISTING_ACTIONS_STYLE_HREF,
|
|
56
|
+
LISTING_ACTION_CLASS,
|
|
57
|
+
actionRowCss,
|
|
58
|
+
} from "./actionRow.js";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Which of the two the host wants drawn.
|
|
62
|
+
*
|
|
63
|
+
* Both default to `true`: a pair whose share button had to be switched ON
|
|
64
|
+
* would have shipped a storefront with no share button, which is the exact
|
|
65
|
+
* defect this wave answers. `{ share: false }` is for a deployment that has
|
|
66
|
+
* decided against it — a private marketplace, an internal catalogue — and
|
|
67
|
+
* `{ favorite: false }` for a surface where saving makes no sense (the
|
|
68
|
+
* owner's own listing).
|
|
69
|
+
*/
|
|
70
|
+
export interface ListingActionsConfig {
|
|
71
|
+
readonly favorite?: boolean;
|
|
72
|
+
readonly share?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ListingActionsProps {
|
|
76
|
+
readonly listingId: number;
|
|
77
|
+
/** `is_favorited` off the row — ignored when {@link favorite} is given. */
|
|
78
|
+
readonly favorited?: boolean | null | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* The surface's own heart, when it has one. See the file header: the
|
|
81
|
+
* listing page's heart is driven by a different hook from a card's, and
|
|
82
|
+
* this cluster's job is the geometry, not the hook.
|
|
83
|
+
*/
|
|
84
|
+
readonly favorite?: ReactNode;
|
|
85
|
+
/** How many people saved it, when the host knows. Absent draws nothing —
|
|
86
|
+
* the wire carries no such aggregate. */
|
|
87
|
+
readonly favoriteCount?: number;
|
|
88
|
+
/** The listing's canonical address — a path or an absolute URL. Given, it
|
|
89
|
+
* is what is shared and `window.location` is never read. */
|
|
90
|
+
readonly shareUrl?: string;
|
|
91
|
+
/** The listing's title: the share sheet's heading. */
|
|
92
|
+
readonly shareTitle?: string;
|
|
93
|
+
/** A line under it in the sheet — the price, typically. */
|
|
94
|
+
readonly shareText?: string;
|
|
95
|
+
readonly onShared?: (channel: ShareChannel) => void;
|
|
96
|
+
/** The container's sign-in door for the heart. */
|
|
97
|
+
readonly signIn?: SignInCta;
|
|
98
|
+
/** How loudly a blocked heart speaks — only consulted when this cluster
|
|
99
|
+
* mounts the heart itself. */
|
|
100
|
+
readonly blockedReason?: "text" | "popover";
|
|
101
|
+
/** A short toast on each successful favourite press. */
|
|
102
|
+
readonly announce?: boolean;
|
|
103
|
+
/** See {@link ListingActionsConfig}. */
|
|
104
|
+
readonly actions?: ListingActionsConfig;
|
|
105
|
+
/**
|
|
106
|
+
* `"inline"` (default) is a row wherever the surface put it.
|
|
107
|
+
*
|
|
108
|
+
* `"overlay"` pins the cluster to the TRAILING TOP corner of the media it
|
|
109
|
+
* is drawn over — the one corner of a card gallery that is free, because
|
|
110
|
+
* the dots own the bottom centre and the "3 of 16" counter owns the bottom
|
|
111
|
+
* trailing corner. The element it is pinned inside must be a containing
|
|
112
|
+
* block; every media well in this package already is.
|
|
113
|
+
*/
|
|
114
|
+
readonly placement?: "inline" | "overlay";
|
|
115
|
+
/** This cluster's test id. Default `listings-actions`. */
|
|
116
|
+
readonly testId?: string;
|
|
117
|
+
/** The heart's test id, when this cluster mounts it. */
|
|
118
|
+
readonly favoriteTestId?: string;
|
|
119
|
+
readonly style?: CSSProperties;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function ListingActions(props: ListingActionsProps): ReactElement | null {
|
|
123
|
+
const t = useT();
|
|
124
|
+
const testId = props.testId ?? "listings-actions";
|
|
125
|
+
const showFavorite = props.actions?.favorite !== false;
|
|
126
|
+
const showShare = props.actions?.share !== false;
|
|
127
|
+
const overlay = props.placement === "overlay";
|
|
128
|
+
|
|
129
|
+
// Nothing asked for is nothing drawn — not an empty box with a gap in it.
|
|
130
|
+
if (!showFavorite && !showShare) return null;
|
|
131
|
+
|
|
132
|
+
const heart = !showFavorite ? null : (
|
|
133
|
+
props.favorite ?? (
|
|
134
|
+
<FavoriteHeart
|
|
135
|
+
listingId={props.listingId}
|
|
136
|
+
favorited={props.favorited}
|
|
137
|
+
testId={props.favoriteTestId ?? `${testId}-favorite`}
|
|
138
|
+
className={LISTING_ACTION_CLASS}
|
|
139
|
+
{...(props.announce === true ? { announce: true } : {})}
|
|
140
|
+
{...(props.blockedReason !== undefined
|
|
141
|
+
? { blockedReason: props.blockedReason }
|
|
142
|
+
: {})}
|
|
143
|
+
{...(props.signIn !== undefined ? { signIn: props.signIn } : {})}
|
|
144
|
+
/>
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<>
|
|
150
|
+
<style href={LISTING_ACTIONS_STYLE_HREF} precedence="default">
|
|
151
|
+
{actionRowCss()}
|
|
152
|
+
</style>
|
|
153
|
+
<Flex
|
|
154
|
+
align="flex-end"
|
|
155
|
+
className={
|
|
156
|
+
overlay
|
|
157
|
+
? `${LISTING_ACTIONS_CLASS} ${LISTING_ACTIONS_OVERLAY_CLASS}`
|
|
158
|
+
: LISTING_ACTIONS_CLASS
|
|
159
|
+
}
|
|
160
|
+
data-testid={testId}
|
|
161
|
+
data-placement={overlay ? "overlay" : "inline"}
|
|
162
|
+
{...(props.style !== undefined ? { style: props.style } : {})}
|
|
163
|
+
>
|
|
164
|
+
{heart}
|
|
165
|
+
|
|
166
|
+
{/* The count, when a host was given one. Beside the heart and not
|
|
167
|
+
inside it: the number is a fact about the listing, and a button
|
|
168
|
+
whose accessible name changed with a counter would be announced
|
|
169
|
+
differently to every reader. */}
|
|
170
|
+
{showFavorite && props.favoriteCount !== undefined ? (
|
|
171
|
+
<Typography.Text
|
|
172
|
+
type="secondary"
|
|
173
|
+
data-testid={`${testId}-favorite-count`}
|
|
174
|
+
>
|
|
175
|
+
{t(LISTINGS_I18N_KEYS.favoriteCount, { count: props.favoriteCount })}
|
|
176
|
+
</Typography.Text>
|
|
177
|
+
) : null}
|
|
178
|
+
|
|
179
|
+
{showShare ? (
|
|
180
|
+
<ShareAction
|
|
181
|
+
testId={`${testId}-share`}
|
|
182
|
+
// Icon-only in the cluster, at both placements: two glyphs and a
|
|
183
|
+
// price share one line on a phone, and the reference draws the
|
|
184
|
+
// page's actions as glyphs on a desktop too (§23). The verb is
|
|
185
|
+
// still the control's accessible name in every arm.
|
|
186
|
+
shape="circle"
|
|
187
|
+
{...(props.shareUrl !== undefined ? { url: props.shareUrl } : {})}
|
|
188
|
+
{...(props.shareTitle !== undefined ? { title: props.shareTitle } : {})}
|
|
189
|
+
{...(props.shareText !== undefined ? { text: props.shareText } : {})}
|
|
190
|
+
{...(props.onShared !== undefined ? { onShared: props.onShared } : {})}
|
|
191
|
+
/>
|
|
192
|
+
) : null}
|
|
193
|
+
</Flex>
|
|
194
|
+
</>
|
|
195
|
+
);
|
|
196
|
+
}
|