@cartbase/storefront 0.17.0 → 0.18.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 (107) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +258 -256
  3. package/src/api/auth.ts +108 -108
  4. package/src/api/carts.ts +523 -523
  5. package/src/api/categories.ts +184 -184
  6. package/src/api/checkout.ts +526 -526
  7. package/src/api/collections.ts +130 -130
  8. package/src/api/consent.ts +75 -75
  9. package/src/api/content.ts +125 -125
  10. package/src/api/customers.ts +303 -303
  11. package/src/api/gift-cards.ts +112 -112
  12. package/src/api/http.ts +180 -180
  13. package/src/api/index.ts +30 -30
  14. package/src/api/menus.ts +77 -77
  15. package/src/api/metaobjects.ts +136 -136
  16. package/src/api/orders.ts +290 -290
  17. package/src/api/products.ts +43 -1
  18. package/src/api/redirects.ts +37 -37
  19. package/src/api/regions.ts +200 -200
  20. package/src/api/reviews.ts +259 -259
  21. package/src/api/search.ts +163 -163
  22. package/src/api/store.ts +35 -35
  23. package/src/api/types.ts +91 -91
  24. package/src/cart-drawer/context.tsx +277 -164
  25. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  26. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  27. package/src/cart-drawer/gift-wrap.tsx +82 -82
  28. package/src/cart-drawer/index.ts +71 -68
  29. package/src/cart-drawer/item/index.tsx +162 -162
  30. package/src/cart-drawer/item/quantity.tsx +8 -14
  31. package/src/cart-drawer/item/upsell.tsx +110 -110
  32. package/src/cart-drawer/labels.ts +123 -123
  33. package/src/cart-drawer/mutation-queue.ts +78 -0
  34. package/src/cart-drawer/notes.tsx +131 -131
  35. package/src/cart-drawer/sticky-footer.tsx +73 -73
  36. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  37. package/src/checkout/address-error-copy.ts +117 -117
  38. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  39. package/src/checkout/compare-addresses.ts +40 -40
  40. package/src/checkout/discount-section.tsx +218 -218
  41. package/src/checkout/error-copy-codes.ts +63 -63
  42. package/src/checkout/geocode.ts +154 -154
  43. package/src/checkout/gift-card-section.tsx +224 -224
  44. package/src/checkout/index.ts +72 -72
  45. package/src/checkout/labels.ts +495 -495
  46. package/src/checkout/payment-button.tsx +372 -372
  47. package/src/checkout/payment-error-copy.ts +154 -154
  48. package/src/checkout/promotion-error-copy.ts +91 -91
  49. package/src/checkout/use-checkout-orchestration.ts +1592 -1592
  50. package/src/common/country-flag.tsx +52 -52
  51. package/src/common/country-select.tsx +11 -11
  52. package/src/common/index.ts +20 -20
  53. package/src/common/market-select.tsx +57 -57
  54. package/src/index.ts +12 -12
  55. package/src/lib/country-name.ts +59 -59
  56. package/src/lib/get-product-price.ts +133 -133
  57. package/src/lib/media-image.tsx +39 -39
  58. package/src/lib/payment-constants.ts +53 -53
  59. package/src/lib/platform.ts +13 -13
  60. package/src/lib/price.tsx +39 -39
  61. package/src/lib/store-api-error.ts +36 -36
  62. package/src/lib/variant-caption.ts +32 -32
  63. package/src/locales/bg.ts +467 -467
  64. package/src/locales/context.ts +37 -37
  65. package/src/locales/en.ts +26 -26
  66. package/src/locales/es.ts +466 -466
  67. package/src/locales/index.ts +19 -19
  68. package/src/locales/provider.tsx +59 -59
  69. package/src/locales/types.ts +77 -77
  70. package/src/order/index.ts +62 -62
  71. package/src/order/labels.ts +79 -79
  72. package/src/order/order-totals.tsx +250 -250
  73. package/src/primitives/select-field.tsx +93 -93
  74. package/src/products/image-gallery.tsx +43 -43
  75. package/src/products/index.ts +68 -59
  76. package/src/products/option-select.tsx +67 -67
  77. package/src/products/product-actions-wrapper.tsx +63 -62
  78. package/src/products/product-actions.tsx +151 -150
  79. package/src/products/product-price.tsx +69 -69
  80. package/src/products/product-promises.tsx +61 -61
  81. package/src/products/product-specs.tsx +65 -65
  82. package/src/products/product-tabs.tsx +123 -123
  83. package/src/products/product-template.tsx +163 -162
  84. package/src/products/purchase-options.tsx +130 -130
  85. package/src/products/sets.ts +110 -0
  86. package/src/products/use-product-actions.ts +199 -164
  87. package/src/products/variant-matching.ts +71 -71
  88. package/src/products/variant-url.ts +74 -74
  89. package/src/reviews-ui/index.ts +73 -73
  90. package/src/store/category-template.tsx +136 -136
  91. package/src/store/index.ts +40 -40
  92. package/src/tracking/chatgpt-pixel.tsx +99 -99
  93. package/src/tracking/consent-init.tsx +62 -62
  94. package/src/tracking/events.ts +348 -348
  95. package/src/tracking/ga4.tsx +93 -93
  96. package/src/tracking/google-ads.ts +84 -84
  97. package/src/tracking/gtm.tsx +60 -60
  98. package/src/tracking/inline-script.ts +49 -49
  99. package/src/tracking/oaiq.ts +206 -206
  100. package/src/tracking/tiktok-pixel.tsx +91 -91
  101. package/src/tracking/track-init.tsx +56 -56
  102. package/src/tracking/track-order-purchase.tsx +122 -122
  103. package/src/tracking/ttq.ts +180 -180
  104. package/src/tracking/use-tracking-config.ts +54 -54
  105. package/theme/index.css +25 -25
  106. package/theme/theme.css +150 -150
  107. package/theme/tokens.css +106 -106
@@ -1,123 +1,123 @@
1
- /**
2
- * Cart drawer labels — default English copy.
3
- *
4
- * Every user-facing string in the cart drawer comes from a `CartDrawerLabels`
5
- * object. Stores override strings by passing a partial labels object to
6
- * `CartDrawerProvider`. English is the baseline; stores wanting Bulgarian,
7
- * German, etc. pass their own translations.
8
- *
9
- * Ported from `@1click/ui/src/cart-drawer/labels.ts` (v2.3.1). Cartbase
10
- * additions: the summary-breakdown row labels (`discount`, `tax`, `paymentMethodFee`,
11
- * `giftCard`, `remainingToPay`) — the decorated cart carries
12
- * discount/tax/COD-fee/gift-card tender fields that the breakdown renders,
13
- * so the copy for those rows lives here too.
14
- */
15
-
16
- export type CartDrawerLabels = {
17
- // Header
18
- yourCart: string
19
- item: string
20
- items: string
21
- closeCart: string
22
-
23
- // Empty state
24
- emptyCartTitle: string
25
- emptyCartMessage: string
26
- browseProducts: string
27
-
28
- // Tiered progress
29
- awayFromFreeShipping: string
30
- allRewardsUnlocked: string
31
-
32
- // Gift wrap / notes
33
- addGiftWrap: string
34
- giftNote: string
35
- orderNote: string
36
- giftNotePlaceholder: string
37
- orderNotePlaceholder: string
38
-
39
- // Rewards
40
- estimatedRewardPoints: string
41
-
42
- // Cross-sell
43
- pairsWellWith: string
44
- youllLoveThis: string
45
- youMightAlsoLike: string
46
- addToCart: string
47
-
48
- // Summary / footer
49
- subtotal: string
50
- taxAndShipping: string
51
- shipping: string
52
- free: string
53
- calculatedAtCheckout: string
54
- total: string
55
- secureCheckout: string
56
- shippingAndTaxCalculatedAtCheckout: string
57
-
58
- // Summary breakdown rows (Cartbase decorated-cart fields)
59
- discount: string
60
- tax: string
61
- /** Fallback only — the server's `payment_method_fee_label` wins when present. */
62
- paymentMethodFee: string
63
- /** Prefix for an applied gift-card row; rendered as `<giftCard> ••1234`. */
64
- giftCard: string
65
- /** Label for `gift_card_remainder` — what the remainder provider charges. */
66
- remainingToPay: string
67
-
68
- // Continue shopping
69
- continueShopping: string
70
-
71
- // A11y / buttons
72
- remove: string
73
- }
74
-
75
- export const defaultCartDrawerLabels: CartDrawerLabels = {
76
- yourCart: "Your cart",
77
- item: "item",
78
- items: "items",
79
- closeCart: "Close cart",
80
-
81
- emptyCartTitle: "Your cart is empty",
82
- emptyCartMessage: "You haven't added any products yet.",
83
- browseProducts: "Browse products",
84
-
85
- awayFromFreeShipping: "away from free shipping",
86
- allRewardsUnlocked: "All rewards unlocked",
87
-
88
- addGiftWrap: "Add gift wrapping",
89
- giftNote: "Gift note",
90
- orderNote: "Order note",
91
- giftNotePlaceholder: "Add a personal message...",
92
- orderNotePlaceholder: "Special instructions for this order...",
93
-
94
- estimatedRewardPoints: "Estimated reward points",
95
-
96
- pairsWellWith: "Pairs well with",
97
- youllLoveThis: "You'll love this",
98
- youMightAlsoLike: "You might also like",
99
- addToCart: "Add",
100
-
101
- subtotal: "Subtotal",
102
- // Neutral and true on every store: it states WHEN the numbers are worked
103
- // out, not which tax regime applies. It read "VAT included. Shipping
104
- // calculated at checkout." until 2026-09-13, and the first half is false
105
- // wherever tax is added at checkout instead of sitting in the price.
106
- taxAndShipping: "Taxes and shipping calculated at checkout.",
107
- shipping: "Shipping",
108
- free: "FREE",
109
- calculatedAtCheckout: "Calculated at checkout",
110
- total: "Total",
111
- secureCheckout: "Checkout",
112
- shippingAndTaxCalculatedAtCheckout: "Shipping and tax calculated at checkout",
113
-
114
- discount: "Discount",
115
- tax: "VAT",
116
- paymentMethodFee: "Payment method fee",
117
- giftCard: "Gift card",
118
- remainingToPay: "Remaining to pay",
119
-
120
- continueShopping: "Continue shopping",
121
-
122
- remove: "Remove",
123
- }
1
+ /**
2
+ * Cart drawer labels — default English copy.
3
+ *
4
+ * Every user-facing string in the cart drawer comes from a `CartDrawerLabels`
5
+ * object. Stores override strings by passing a partial labels object to
6
+ * `CartDrawerProvider`. English is the baseline; stores wanting Bulgarian,
7
+ * German, etc. pass their own translations.
8
+ *
9
+ * Ported from `@1click/ui/src/cart-drawer/labels.ts` (v2.3.1). Cartbase
10
+ * additions: the summary-breakdown row labels (`discount`, `tax`, `paymentMethodFee`,
11
+ * `giftCard`, `remainingToPay`) — the decorated cart carries
12
+ * discount/tax/COD-fee/gift-card tender fields that the breakdown renders,
13
+ * so the copy for those rows lives here too.
14
+ */
15
+
16
+ export type CartDrawerLabels = {
17
+ // Header
18
+ yourCart: string
19
+ item: string
20
+ items: string
21
+ closeCart: string
22
+
23
+ // Empty state
24
+ emptyCartTitle: string
25
+ emptyCartMessage: string
26
+ browseProducts: string
27
+
28
+ // Tiered progress
29
+ awayFromFreeShipping: string
30
+ allRewardsUnlocked: string
31
+
32
+ // Gift wrap / notes
33
+ addGiftWrap: string
34
+ giftNote: string
35
+ orderNote: string
36
+ giftNotePlaceholder: string
37
+ orderNotePlaceholder: string
38
+
39
+ // Rewards
40
+ estimatedRewardPoints: string
41
+
42
+ // Cross-sell
43
+ pairsWellWith: string
44
+ youllLoveThis: string
45
+ youMightAlsoLike: string
46
+ addToCart: string
47
+
48
+ // Summary / footer
49
+ subtotal: string
50
+ taxAndShipping: string
51
+ shipping: string
52
+ free: string
53
+ calculatedAtCheckout: string
54
+ total: string
55
+ secureCheckout: string
56
+ shippingAndTaxCalculatedAtCheckout: string
57
+
58
+ // Summary breakdown rows (Cartbase decorated-cart fields)
59
+ discount: string
60
+ tax: string
61
+ /** Fallback only — the server's `payment_method_fee_label` wins when present. */
62
+ paymentMethodFee: string
63
+ /** Prefix for an applied gift-card row; rendered as `<giftCard> ••1234`. */
64
+ giftCard: string
65
+ /** Label for `gift_card_remainder` — what the remainder provider charges. */
66
+ remainingToPay: string
67
+
68
+ // Continue shopping
69
+ continueShopping: string
70
+
71
+ // A11y / buttons
72
+ remove: string
73
+ }
74
+
75
+ export const defaultCartDrawerLabels: CartDrawerLabels = {
76
+ yourCart: "Your cart",
77
+ item: "item",
78
+ items: "items",
79
+ closeCart: "Close cart",
80
+
81
+ emptyCartTitle: "Your cart is empty",
82
+ emptyCartMessage: "You haven't added any products yet.",
83
+ browseProducts: "Browse products",
84
+
85
+ awayFromFreeShipping: "away from free shipping",
86
+ allRewardsUnlocked: "All rewards unlocked",
87
+
88
+ addGiftWrap: "Add gift wrapping",
89
+ giftNote: "Gift note",
90
+ orderNote: "Order note",
91
+ giftNotePlaceholder: "Add a personal message...",
92
+ orderNotePlaceholder: "Special instructions for this order...",
93
+
94
+ estimatedRewardPoints: "Estimated reward points",
95
+
96
+ pairsWellWith: "Pairs well with",
97
+ youllLoveThis: "You'll love this",
98
+ youMightAlsoLike: "You might also like",
99
+ addToCart: "Add",
100
+
101
+ subtotal: "Subtotal",
102
+ // Neutral and true on every store: it states WHEN the numbers are worked
103
+ // out, not which tax regime applies. It read "VAT included. Shipping
104
+ // calculated at checkout." until 2026-09-13, and the first half is false
105
+ // wherever tax is added at checkout instead of sitting in the price.
106
+ taxAndShipping: "Taxes and shipping calculated at checkout.",
107
+ shipping: "Shipping",
108
+ free: "FREE",
109
+ calculatedAtCheckout: "Calculated at checkout",
110
+ total: "Total",
111
+ secureCheckout: "Checkout",
112
+ shippingAndTaxCalculatedAtCheckout: "Shipping and tax calculated at checkout",
113
+
114
+ discount: "Discount",
115
+ tax: "VAT",
116
+ paymentMethodFee: "Payment method fee",
117
+ giftCard: "Gift card",
118
+ remainingToPay: "Remaining to pay",
119
+
120
+ continueShopping: "Continue shopping",
121
+
122
+ remove: "Remove",
123
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * THE CART'S CHANGES, ONE AT A TIME (the instant cart, 2026-09-16).
3
+ *
4
+ * The drawer shows every change at once and the platform confirms behind
5
+ * it. The platform's answers are whole carts, so two changes in flight
6
+ * together can answer out of order and the older cart would overwrite the
7
+ * newer one; and a second add before the first has created the cart would
8
+ * create a second cart. So the provider sends its changes through this
9
+ * queue: one call at a time, each starting after the one before has
10
+ * answered, each reading the cart as the answers left it.
11
+ *
12
+ * Quantity clicks on one line are combined: while a change for a line waits
13
+ * its turn, a new click only moves the number it will send, so ten fast
14
+ * clicks send at most two calls (the one already in flight and the last
15
+ * number). Pure module, no React.
16
+ */
17
+
18
+ export interface CartMutationQueue {
19
+ /** Run a change after every change before it has answered. A failure never blocks the next. */
20
+ run<T>(job: () => Promise<T>): Promise<T>
21
+ /**
22
+ * A quantity change for one line. While an earlier change for the same
23
+ * line still waits its turn, only its number moves and its promise is
24
+ * returned; otherwise `send` is queued and called with the latest number
25
+ * when its turn comes.
26
+ */
27
+ quantity<T>(lineKey: string, quantity: number, send: (quantity: number) => Promise<T>): Promise<T>
28
+ /** A waiting quantity change for this line is dropped (the line is being removed). */
29
+ forget(lineKey: string): void
30
+ /** True while any change is queued or in flight. */
31
+ busy(): boolean
32
+ }
33
+
34
+ export function createCartMutationQueue(): CartMutationQueue {
35
+ let tail: Promise<unknown> = Promise.resolve()
36
+ let pending = 0
37
+ const waiting = new Map<string, { quantity: number; dropped: boolean; job: Promise<unknown> }>()
38
+
39
+ function run<T>(job: () => Promise<T>): Promise<T> {
40
+ pending++
41
+ const result = tail.then(job, job)
42
+ const settled = result.then(
43
+ () => undefined,
44
+ () => undefined
45
+ )
46
+ tail = settled
47
+ void settled.then(() => {
48
+ pending--
49
+ })
50
+ return result
51
+ }
52
+
53
+ function quantity<T>(lineKey: string, quantity: number, send: (quantity: number) => Promise<T>): Promise<T> {
54
+ const queued = waiting.get(lineKey)
55
+ if (queued && !queued.dropped) {
56
+ queued.quantity = quantity
57
+ return queued.job as Promise<T>
58
+ }
59
+ const entry = { quantity, dropped: false, job: Promise.resolve() as Promise<unknown> }
60
+ entry.job = run(async () => {
61
+ // The turn has come: later clicks start a new change from here on.
62
+ if (waiting.get(lineKey) === entry) waiting.delete(lineKey)
63
+ if (entry.dropped) return undefined as T
64
+ return send(entry.quantity)
65
+ })
66
+ waiting.set(lineKey, entry)
67
+ return entry.job as Promise<T>
68
+ }
69
+
70
+ function forget(lineKey: string): void {
71
+ const queued = waiting.get(lineKey)
72
+ if (!queued) return
73
+ queued.dropped = true
74
+ waiting.delete(lineKey)
75
+ }
76
+
77
+ return { run, quantity, forget, busy: () => pending > 0 }
78
+ }
@@ -1,131 +1,131 @@
1
- "use client"
2
-
3
- import { useState } from "react"
4
-
5
- import { cn } from "../lib/utils"
6
- import { useCartDrawer } from "./context"
7
-
8
- /**
9
- * CartNotes — pill buttons to open gift/order note textareas.
10
- *
11
- * Ported from `@1click/ui/src/cart-drawer/notes.tsx` (v2.3.1);
12
- * originally extracted from mindpages-storefront
13
- * src/modules/cart-drawer/cart-notes.tsx. Tokens applied. Icons +
14
- * placeholders pulled from `labels` context. Persist notes via `onSave`
15
- * — e.g. `updateCart(client, cartId, { metadata: { ...cart.metadata,
16
- * gift_note: value } })` (`@cartbase/storefront/api/carts`). NOTE: the update
17
- * route REPLACES the metadata object wholesale (no server-side merge) —
18
- * always spread the current `cart.metadata` or you clobber other keys
19
- * (UTM attribution, gift-wrap flags…). Cart metadata is copied onto the
20
- * order at complete.
21
- */
22
-
23
- type NoteType = "gift" | "order"
24
-
25
- type CartNotesProps = {
26
- types?: NoteType[]
27
- onSave?: (type: NoteType, value: string) => void
28
- }
29
-
30
- const iconGift = (
31
- <svg
32
- width="14"
33
- height="14"
34
- viewBox="0 0 14 14"
35
- fill="none"
36
- stroke="currentColor"
37
- strokeWidth="1.3"
38
- strokeLinecap="round"
39
- strokeLinejoin="round"
40
- >
41
- <rect x="1.5" y="5" width="11" height="7.5" rx="1" />
42
- <rect x="0.5" y="3" width="13" height="2.5" rx="0.5" />
43
- <path d="M7 3v9.5" />
44
- </svg>
45
- )
46
-
47
- const iconOrder = (
48
- <svg
49
- width="14"
50
- height="14"
51
- viewBox="0 0 14 14"
52
- fill="none"
53
- stroke="currentColor"
54
- strokeWidth="1.3"
55
- strokeLinecap="round"
56
- strokeLinejoin="round"
57
- >
58
- <path d="M8.5 1.5H4a1 1 0 00-1 1v9a1 1 0 001 1h6a1 1 0 001-1V4L8.5 1.5z" />
59
- <path d="M8.5 1.5V4H11M5.5 7h3M5.5 9.5h3" />
60
- </svg>
61
- )
62
-
63
- export function CartNotes({
64
- types = ["gift", "order"],
65
- onSave,
66
- }: CartNotesProps) {
67
- const { labels } = useCartDrawer()
68
- const [activeNote, setActiveNote] = useState<NoteType | null>(null)
69
- const [values, setValues] = useState<Record<NoteType, string>>({
70
- gift: "",
71
- order: "",
72
- })
73
-
74
- const noteConfig: Record<
75
- NoteType,
76
- { label: string; placeholder: string; icon: React.ReactNode }
77
- > = {
78
- gift: {
79
- label: labels.giftNote,
80
- placeholder: labels.giftNotePlaceholder,
81
- icon: iconGift,
82
- },
83
- order: {
84
- label: labels.orderNote,
85
- placeholder: labels.orderNotePlaceholder,
86
- icon: iconOrder,
87
- },
88
- }
89
-
90
- return (
91
- <div className="px-5 sm:px-6 py-3">
92
- <div className="flex gap-2">
93
- {types.map((type) => {
94
- const config = noteConfig[type]
95
- const active = activeNote === type
96
- return (
97
- <button
98
- key={type}
99
- type="button"
100
- onClick={() => setActiveNote(active ? null : type)}
101
- className={cn(
102
- "flex items-center gap-1.5 text-xs font-medium px-4 py-2.5 min-h-[44px] rounded-xl border transition-colors active:scale-[0.97]",
103
- active
104
- ? "border-text-base bg-foreground text-card"
105
- : "border-border text-muted-foreground hover:border-text-muted active:bg-muted"
106
- )}
107
- >
108
- {config.icon}
109
- {config.label}
110
- </button>
111
- )
112
- })}
113
- </div>
114
-
115
- {activeNote && (
116
- <div className="mt-3">
117
- <textarea
118
- value={values[activeNote]}
119
- onChange={(e) =>
120
- setValues({ ...values, [activeNote]: e.target.value })
121
- }
122
- onBlur={() => onSave?.(activeNote, values[activeNote])}
123
- placeholder={noteConfig[activeNote].placeholder}
124
- rows={3}
125
- className="w-full text-[14px] sm:text-sm border border-border rounded-xl px-4 py-3 resize-none focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary placeholder:text-muted-foreground text-foreground bg-card"
126
- />
127
- </div>
128
- )}
129
- </div>
130
- )
131
- }
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+
5
+ import { cn } from "../lib/utils"
6
+ import { useCartDrawer } from "./context"
7
+
8
+ /**
9
+ * CartNotes — pill buttons to open gift/order note textareas.
10
+ *
11
+ * Ported from `@1click/ui/src/cart-drawer/notes.tsx` (v2.3.1);
12
+ * originally extracted from mindpages-storefront
13
+ * src/modules/cart-drawer/cart-notes.tsx. Tokens applied. Icons +
14
+ * placeholders pulled from `labels` context. Persist notes via `onSave`
15
+ * — e.g. `updateCart(client, cartId, { metadata: { ...cart.metadata,
16
+ * gift_note: value } })` (`@cartbase/storefront/api/carts`). NOTE: the update
17
+ * route REPLACES the metadata object wholesale (no server-side merge) —
18
+ * always spread the current `cart.metadata` or you clobber other keys
19
+ * (UTM attribution, gift-wrap flags…). Cart metadata is copied onto the
20
+ * order at complete.
21
+ */
22
+
23
+ type NoteType = "gift" | "order"
24
+
25
+ type CartNotesProps = {
26
+ types?: NoteType[]
27
+ onSave?: (type: NoteType, value: string) => void
28
+ }
29
+
30
+ const iconGift = (
31
+ <svg
32
+ width="14"
33
+ height="14"
34
+ viewBox="0 0 14 14"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ strokeWidth="1.3"
38
+ strokeLinecap="round"
39
+ strokeLinejoin="round"
40
+ >
41
+ <rect x="1.5" y="5" width="11" height="7.5" rx="1" />
42
+ <rect x="0.5" y="3" width="13" height="2.5" rx="0.5" />
43
+ <path d="M7 3v9.5" />
44
+ </svg>
45
+ )
46
+
47
+ const iconOrder = (
48
+ <svg
49
+ width="14"
50
+ height="14"
51
+ viewBox="0 0 14 14"
52
+ fill="none"
53
+ stroke="currentColor"
54
+ strokeWidth="1.3"
55
+ strokeLinecap="round"
56
+ strokeLinejoin="round"
57
+ >
58
+ <path d="M8.5 1.5H4a1 1 0 00-1 1v9a1 1 0 001 1h6a1 1 0 001-1V4L8.5 1.5z" />
59
+ <path d="M8.5 1.5V4H11M5.5 7h3M5.5 9.5h3" />
60
+ </svg>
61
+ )
62
+
63
+ export function CartNotes({
64
+ types = ["gift", "order"],
65
+ onSave,
66
+ }: CartNotesProps) {
67
+ const { labels } = useCartDrawer()
68
+ const [activeNote, setActiveNote] = useState<NoteType | null>(null)
69
+ const [values, setValues] = useState<Record<NoteType, string>>({
70
+ gift: "",
71
+ order: "",
72
+ })
73
+
74
+ const noteConfig: Record<
75
+ NoteType,
76
+ { label: string; placeholder: string; icon: React.ReactNode }
77
+ > = {
78
+ gift: {
79
+ label: labels.giftNote,
80
+ placeholder: labels.giftNotePlaceholder,
81
+ icon: iconGift,
82
+ },
83
+ order: {
84
+ label: labels.orderNote,
85
+ placeholder: labels.orderNotePlaceholder,
86
+ icon: iconOrder,
87
+ },
88
+ }
89
+
90
+ return (
91
+ <div className="px-5 sm:px-6 py-3">
92
+ <div className="flex gap-2">
93
+ {types.map((type) => {
94
+ const config = noteConfig[type]
95
+ const active = activeNote === type
96
+ return (
97
+ <button
98
+ key={type}
99
+ type="button"
100
+ onClick={() => setActiveNote(active ? null : type)}
101
+ className={cn(
102
+ "flex items-center gap-1.5 text-xs font-medium px-4 py-2.5 min-h-[44px] rounded-xl border transition-colors active:scale-[0.97]",
103
+ active
104
+ ? "border-text-base bg-foreground text-card"
105
+ : "border-border text-muted-foreground hover:border-text-muted active:bg-muted"
106
+ )}
107
+ >
108
+ {config.icon}
109
+ {config.label}
110
+ </button>
111
+ )
112
+ })}
113
+ </div>
114
+
115
+ {activeNote && (
116
+ <div className="mt-3">
117
+ <textarea
118
+ value={values[activeNote]}
119
+ onChange={(e) =>
120
+ setValues({ ...values, [activeNote]: e.target.value })
121
+ }
122
+ onBlur={() => onSave?.(activeNote, values[activeNote])}
123
+ placeholder={noteConfig[activeNote].placeholder}
124
+ rows={3}
125
+ className="w-full text-[14px] sm:text-sm border border-border rounded-xl px-4 py-3 resize-none focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary placeholder:text-muted-foreground text-foreground bg-card"
126
+ />
127
+ </div>
128
+ )}
129
+ </div>
130
+ )
131
+ }