@cimplify/sdk 0.11.0 → 0.12.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/dist/styles.css CHANGED
@@ -1,2 +1,2 @@
1
1
  /*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */
2
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.visible{visibility:visible}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.container{width:100%}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.w-full{width:100%}.flex-1{flex:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.\[appearance\:textfield\]{appearance:textfield}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-border{border-color:var(--color-border,oklch(90% 0 0))}.border-primary{border-color:var(--color-primary,oklch(50% .1 35))}.border-transparent{border-color:#0000}.bg-primary{background-color:var(--color-primary,oklch(50% .1 35))}.bg-primary\/5{background-color:#934c3a0d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 5%, transparent)}}.bg-primary\/10{background-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.bg-transparent{background-color:#0000}.text-center{text-align:center}.text-left{text-align:left}.text-\[10px\]{font-size:10px}.text-destructive{color:var(--color-destructive,oklch(50% .2 25))}.text-muted-foreground{color:var(--color-muted-foreground,oklch(50% 0 0))}.text-muted-foreground\/60{color:#63636399}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/60{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 60%, transparent)}}.text-muted-foreground\/70{color:#636363b3}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/70{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 70%, transparent)}}.text-primary{color:var(--color-primary,oklch(50% .1 35))}.text-primary-foreground{color:var(--color-primary-foreground,oklch(99% 0 0))}.uppercase{text-transform:uppercase}.opacity-70{opacity:.7}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.outline-none{--tw-outline-style:none;outline-style:none}.\[cimplify\:checkout\]{cimplify:checkout}@media (hover:hover){.hover\:border-primary\/50:hover{border-color:#934c3a80}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 50%, transparent)}}.hover\:bg-muted:hover{background-color:var(--color-muted,oklch(95% 0 0))}.hover\:bg-muted\/50:hover{background-color:#eeeeee80}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 50%, transparent)}}.hover\:bg-primary\/90:hover{background-color:#934c3ae6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 90%, transparent)}}.hover\:text-primary:hover{color:var(--color-primary,oklch(50% .1 35))}}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-50:disabled{opacity:.5}.\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{appearance:none}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.container{width:100%}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.w-full{width:100%}.flex-1{flex:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.\[appearance\:textfield\]{appearance:textfield}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-border{border-color:var(--color-border,oklch(90% 0 0))}.border-primary{border-color:var(--color-primary,oklch(50% .1 35))}.border-transparent{border-color:#0000}.bg-primary{background-color:var(--color-primary,oklch(50% .1 35))}.bg-primary\/5{background-color:#934c3a0d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 5%, transparent)}}.bg-primary\/10{background-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.bg-transparent{background-color:#0000}.text-center{text-align:center}.text-left{text-align:left}.text-\[10px\]{font-size:10px}.text-destructive{color:var(--color-destructive,oklch(50% .2 25))}.text-muted-foreground{color:var(--color-muted-foreground,oklch(50% 0 0))}.text-muted-foreground\/60{color:#63636399}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/60{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 60%, transparent)}}.text-muted-foreground\/70{color:#636363b3}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/70{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 70%, transparent)}}.text-primary{color:var(--color-primary,oklch(50% .1 35))}.text-primary-foreground{color:var(--color-primary-foreground,oklch(99% 0 0))}.uppercase{text-transform:uppercase}.opacity-70{opacity:.7}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.outline-none{--tw-outline-style:none;outline-style:none}.\[cimplify\:checkout\]{cimplify:checkout}@media (hover:hover){.hover\:border-primary\/50:hover{border-color:#934c3a80}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 50%, transparent)}}.hover\:bg-muted:hover{background-color:var(--color-muted,oklch(95% 0 0))}.hover\:bg-muted\/50:hover{background-color:#eeeeee80}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 50%, transparent)}}.hover\:bg-primary\/90:hover{background-color:#934c3ae6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 90%, transparent)}}.hover\:text-primary:hover{color:var(--color-primary,oklch(50% .1 35))}}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-50:disabled{opacity:.5}.\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{appearance:none}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/sdk",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Cimplify Commerce SDK for storefronts",
5
5
  "keywords": [
6
6
  "cimplify",
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  {
13
13
  "path": "bookings-page.tsx",
14
- "content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport type { CustomerBooking } from \"@cimplify/sdk\";\nimport { BookingList } from \"@cimplify/sdk/react\";\nimport { BookingCard } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface BookingsPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n filters?: string;\n filterButton?: string;\n list?: string;\n detail?: string;\n backButton?: string;\n}\n\nexport interface BookingsPageProps {\n /** Page title. */\n title?: string;\n /** Pre-fetched bookings for SSR. */\n bookings?: CustomerBooking[];\n /** Called when navigating to a booking detail (e.g. for routing). */\n onBookingNavigate?: (booking: CustomerBooking) => void;\n /** Called when cancel is clicked. */\n onCancel?: (booking: CustomerBooking) => void;\n /** Called when reschedule is clicked. */\n onReschedule?: (booking: CustomerBooking) => void;\n /** Show filter tabs. Default: true. */\n showFilters?: boolean;\n /** Custom booking renderer. */\n renderBooking?: (booking: CustomerBooking) => React.ReactNode;\n className?: string;\n classNames?: BookingsPageClassNames;\n}\n\nconst BOOKING_FILTERS: { label: string; value: \"all\" | \"upcoming\" | \"past\" }[] = [\n { label: \"All\", value: \"all\" },\n { label: \"Upcoming\", value: \"upcoming\" },\n { label: \"Past\", value: \"past\" },\n];\n\nexport function BookingsPage({\n title = \"My Bookings\",\n bookings: bookingsProp,\n onBookingNavigate,\n onCancel,\n onReschedule,\n showFilters = true,\n renderBooking,\n className,\n classNames,\n}: BookingsPageProps): React.ReactElement {\n const [filter, setFilter] = useState<\"all\" | \"upcoming\" | \"past\">(\"all\");\n const [selectedBooking, setSelectedBooking] = useState<CustomerBooking | null>(null);\n\n const handleBookingClick = useCallback(\n (booking: CustomerBooking) => {\n if (onBookingNavigate) {\n onBookingNavigate(booking);\n } else {\n setSelectedBooking(booking);\n }\n },\n [onBookingNavigate],\n );\n\n const handleBack = useCallback(() => {\n setSelectedBooking(null);\n }, []);\n\n if (selectedBooking && !onBookingNavigate) {\n return (\n <div data-cimplify-bookings-page className={cn(className, classNames?.root)}>\n <div data-cimplify-bookings-detail className={classNames?.detail}>\n <button\n type=\"button\"\n onClick={handleBack}\n data-cimplify-bookings-back\n className={classNames?.backButton}\n >\n Back to bookings\n </button>\n <BookingCard\n booking={selectedBooking}\n onCancel={onCancel}\n onReschedule={onReschedule}\n />\n </div>\n </div>\n );\n }\n\n return (\n <div data-cimplify-bookings-page className={cn(className, classNames?.root)}>\n <div data-cimplify-bookings-header className={classNames?.header}>\n <h1 data-cimplify-bookings-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {showFilters && (\n <div data-cimplify-bookings-filters className={classNames?.filters} role=\"tablist\">\n {BOOKING_FILTERS.map((f) => (\n <button\n key={f.value}\n type=\"button\"\n role=\"tab\"\n aria-selected={filter === f.value}\n onClick={() => setFilter(f.value)}\n data-cimplify-booking-filter\n data-selected={filter === f.value || undefined}\n className={classNames?.filterButton}\n >\n {f.label}\n </button>\n ))}\n </div>\n )}\n\n <div data-cimplify-bookings-list className={classNames?.list}>\n <BookingList\n bookings={bookingsProp}\n filter={filter}\n onCancel={onCancel}\n onReschedule={onReschedule}\n onBookingClick={handleBookingClick}\n renderBooking={renderBooking}\n />\n </div>\n </div>\n );\n}\n"
14
+ "content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport { Tabs } from \"@base-ui/react/tabs\";\nimport type { CustomerBooking } from \"@cimplify/sdk\";\nimport { BookingList } from \"@cimplify/sdk/react\";\nimport { BookingCard } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface BookingsPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n filters?: string;\n filterButton?: string;\n list?: string;\n detail?: string;\n backButton?: string;\n}\n\nexport interface BookingsPageProps {\n /** Page title. */\n title?: string;\n /** Pre-fetched bookings for SSR. */\n bookings?: CustomerBooking[];\n /** Called when navigating to a booking detail (e.g. for routing). */\n onBookingNavigate?: (booking: CustomerBooking) => void;\n /** Called when cancel is clicked. */\n onCancel?: (booking: CustomerBooking) => void;\n /** Called when reschedule is clicked. */\n onReschedule?: (booking: CustomerBooking) => void;\n /** Show filter tabs. Default: true. */\n showFilters?: boolean;\n /** Custom booking renderer. */\n renderBooking?: (booking: CustomerBooking) => React.ReactNode;\n className?: string;\n classNames?: BookingsPageClassNames;\n}\n\nconst BOOKING_FILTERS: { label: string; value: \"all\" | \"upcoming\" | \"past\" }[] = [\n { label: \"All\", value: \"all\" },\n { label: \"Upcoming\", value: \"upcoming\" },\n { label: \"Past\", value: \"past\" },\n];\n\nexport function BookingsPage({\n title = \"My Bookings\",\n bookings: bookingsProp,\n onBookingNavigate,\n onCancel,\n onReschedule,\n showFilters = true,\n renderBooking,\n className,\n classNames,\n}: BookingsPageProps): React.ReactElement {\n const [filter, setFilter] = useState<\"all\" | \"upcoming\" | \"past\">(\"all\");\n const [selectedBooking, setSelectedBooking] = useState<CustomerBooking | null>(null);\n\n const handleBookingClick = useCallback(\n (booking: CustomerBooking) => {\n if (onBookingNavigate) {\n onBookingNavigate(booking);\n } else {\n setSelectedBooking(booking);\n }\n },\n [onBookingNavigate],\n );\n\n const handleBack = useCallback(() => {\n setSelectedBooking(null);\n }, []);\n\n if (selectedBooking && !onBookingNavigate) {\n return (\n <div data-cimplify-bookings-page className={cn(className, classNames?.root)}>\n <div data-cimplify-bookings-detail className={classNames?.detail}>\n <button\n type=\"button\"\n onClick={handleBack}\n data-cimplify-bookings-back\n className={classNames?.backButton}\n >\n Back to bookings\n </button>\n <BookingCard\n booking={selectedBooking}\n onCancel={onCancel}\n onReschedule={onReschedule}\n />\n </div>\n </div>\n );\n }\n\n return (\n <div data-cimplify-bookings-page className={cn(className, classNames?.root)}>\n <div data-cimplify-bookings-header className={classNames?.header}>\n <h1 data-cimplify-bookings-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {showFilters && (\n <Tabs.Root\n value={filter}\n onValueChange={(value) => setFilter(value as \"all\" | \"upcoming\" | \"past\")}\n >\n <Tabs.List data-cimplify-bookings-filters className={classNames?.filters}>\n {BOOKING_FILTERS.map((f) => (\n <Tabs.Tab\n key={f.value}\n value={f.value}\n data-cimplify-booking-filter\n data-selected={filter === f.value || undefined}\n className={classNames?.filterButton}\n >\n {f.label}\n </Tabs.Tab>\n ))}\n </Tabs.List>\n </Tabs.Root>\n )}\n\n <div data-cimplify-bookings-list className={classNames?.list}>\n <BookingList\n bookings={bookingsProp}\n filter={filter}\n onCancel={onCancel}\n onReschedule={onReschedule}\n onBookingClick={handleBookingClick}\n renderBooking={renderBooking}\n />\n </div>\n </div>\n );\n}\n"
15
15
  }
16
16
  ]
17
17
  }
@@ -9,7 +9,7 @@
9
9
  "files": [
10
10
  {
11
11
  "path": "category-filter.tsx",
12
- "content": "\"use client\";\n\nimport React, { useCallback } from \"react\";\nimport type { Category } from \"@cimplify/sdk\";\nimport { useCategories } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface CategoryFilterClassNames {\n root?: string;\n item?: string;\n allButton?: string;\n count?: string;\n}\n\nexport interface CategoryFilterProps {\n /** Currently selected category ID. Null means \"all\". */\n selectedId?: string | null;\n /** Called when a category is selected. Null means \"all\". */\n onSelect: (categoryId: string | null) => void;\n /** Label for the \"all\" option. Default: \"All\". */\n allLabel?: string;\n /** Show product counts per category. Default: true. */\n showCounts?: boolean;\n className?: string;\n classNames?: CategoryFilterClassNames;\n}\n\n/**\n * CategoryFilter — horizontal or vertical list of category chips.\n *\n * Fetches categories via `useCategories` and renders selectable buttons.\n * The parent controls selection state via `selectedId` + `onSelect`.\n */\nexport function CategoryFilter({\n selectedId = null,\n onSelect,\n allLabel = \"All\",\n showCounts = true,\n className,\n classNames,\n}: CategoryFilterProps): React.ReactElement {\n const { categories, isLoading } = useCategories();\n\n const handleSelect = useCallback(\n (id: string | null) => {\n onSelect(id);\n },\n [onSelect],\n );\n\n if (isLoading) {\n return (\n <div\n data-cimplify-category-filter\n aria-busy=\"true\"\n className={cn(className, classNames?.root)}\n />\n );\n }\n\n return (\n <div\n data-cimplify-category-filter\n className={cn(className, classNames?.root)}\n role=\"tablist\"\n aria-label=\"Filter by category\"\n >\n <button\n type=\"button\"\n role=\"tab\"\n aria-selected={selectedId === null}\n onClick={() => handleSelect(null)}\n data-cimplify-category-filter-item\n data-selected={selectedId === null || undefined}\n className={cn(classNames?.item, classNames?.allButton)}\n >\n {allLabel}\n </button>\n\n {categories.map((category: Category) => (\n <button\n key={category.id}\n type=\"button\"\n role=\"tab\"\n aria-selected={selectedId === category.id}\n onClick={() => handleSelect(category.id)}\n data-cimplify-category-filter-item\n data-selected={selectedId === category.id || undefined}\n className={classNames?.item}\n >\n {category.name}\n {showCounts && category.product_count != null && (\n <span data-cimplify-category-count className={classNames?.count}>\n {category.product_count}\n </span>\n )}\n </button>\n ))}\n </div>\n );\n}\n"
12
+ "content": "\"use client\";\n\nimport React, { useCallback } from \"react\";\nimport { Tabs } from \"@base-ui/react/tabs\";\nimport type { Category } from \"@cimplify/sdk\";\nimport { useCategories } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface CategoryFilterClassNames {\n root?: string;\n item?: string;\n allButton?: string;\n count?: string;\n}\n\nexport interface CategoryFilterProps {\n /** Currently selected category ID. Null means \"all\". */\n selectedId?: string | null;\n /** Called when a category is selected. Null means \"all\". */\n onSelect: (categoryId: string | null) => void;\n /** Label for the \"all\" option. Default: \"All\". */\n allLabel?: string;\n /** Show product counts per category. Default: true. */\n showCounts?: boolean;\n className?: string;\n classNames?: CategoryFilterClassNames;\n}\n\n/** Sentinel value representing the \"all\" tab (no category filter). */\nconst ALL_VALUE = \"__all__\";\n\n/**\n * CategoryFilter — horizontal or vertical list of category chips.\n *\n * Fetches categories via `useCategories` and renders selectable buttons.\n * The parent controls selection state via `selectedId` + `onSelect`.\n *\n * Built on Base UI Tabs for accessible keyboard navigation and ARIA roles.\n */\nexport function CategoryFilter({\n selectedId = null,\n onSelect,\n allLabel = \"All\",\n showCounts = true,\n className,\n classNames,\n}: CategoryFilterProps): React.ReactElement {\n const { categories, isLoading } = useCategories();\n\n const handleValueChange = useCallback(\n (value: string | number | null) => {\n onSelect(value === ALL_VALUE ? null : String(value));\n },\n [onSelect],\n );\n\n if (isLoading) {\n return (\n <div\n data-cimplify-category-filter\n aria-busy=\"true\"\n className={cn(className, classNames?.root)}\n />\n );\n }\n\n return (\n <Tabs.Root\n value={selectedId ?? ALL_VALUE}\n onValueChange={handleValueChange}\n >\n <Tabs.List\n data-cimplify-category-filter\n aria-label=\"Filter by category\"\n className={cn(className, classNames?.root)}\n >\n <Tabs.Tab\n value={ALL_VALUE}\n data-cimplify-category-filter-item\n data-selected={selectedId === null || undefined}\n className={cn(classNames?.item, classNames?.allButton)}\n >\n {allLabel}\n </Tabs.Tab>\n\n {categories.map((category: Category) => (\n <Tabs.Tab\n key={category.id}\n value={category.id}\n data-cimplify-category-filter-item\n data-selected={selectedId === category.id || undefined}\n className={classNames?.item}\n >\n {category.name}\n {showCounts && category.product_count != null && (\n <span data-cimplify-category-count className={classNames?.count}>\n {category.product_count}\n </span>\n )}\n </Tabs.Tab>\n ))}\n </Tabs.List>\n </Tabs.Root>\n );\n}\n"
13
13
  }
14
14
  ]
15
15
  }
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "date-slot-picker.tsx",
13
- "content": "\"use client\";\n\nimport React, { useState, useMemo, useCallback } from \"react\";\nimport type { AvailableSlot, DayAvailability } from \"@cimplify/sdk\";\nimport { useServiceAvailability } from \"@cimplify/sdk/react\";\nimport { SlotPicker } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface DateSlotPickerClassNames {\n root?: string;\n dateStrip?: string;\n dateButton?: string;\n nav?: string;\n navButton?: string;\n slots?: string;\n loading?: string;\n}\n\nexport interface DateSlotPickerProps {\n /** Service ID to fetch availability and slots for. */\n serviceId: string;\n /** Number of days to show in the date strip. Default: 7. */\n daysToShow?: number;\n /** Number of participants. */\n participantCount?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot, date: string) => void;\n /** Pre-fetched availability data (skips fetch). */\n availability?: DayAvailability[];\n /** Show price on slots. Default: true. */\n showPrice?: boolean;\n className?: string;\n classNames?: DateSlotPickerClassNames;\n}\n\nfunction formatDate(dateStr: string): string {\n const date = new Date(dateStr + \"T00:00:00\");\n return date.toLocaleDateString(undefined, { weekday: \"short\", month: \"short\", day: \"numeric\" });\n}\n\nfunction toDateString(date: Date): string {\n return date.toISOString().split(\"T\")[0];\n}\n\nfunction addDays(date: Date, days: number): Date {\n const result = new Date(date);\n result.setDate(result.getDate() + days);\n return result;\n}\n\nexport function DateSlotPicker({\n serviceId,\n daysToShow = 7,\n participantCount,\n selectedSlot,\n onSlotSelect,\n availability: availabilityProp,\n showPrice = true,\n className,\n classNames,\n}: DateSlotPickerProps): React.ReactElement {\n const [offset, setOffset] = useState(0);\n const [selectedDate, setSelectedDate] = useState<string>(toDateString(new Date()));\n\n const dateRange = useMemo(() => {\n const today = new Date();\n const start = addDays(today, offset);\n const dates: string[] = [];\n for (let i = 0; i < daysToShow; i++) {\n dates.push(toDateString(addDays(start, i)));\n }\n return {\n dates,\n startDate: dates[0],\n endDate: dates[dates.length - 1],\n };\n }, [offset, daysToShow]);\n\n const { days: fetchedDays, isLoading: availabilityLoading } = useServiceAvailability(\n serviceId,\n dateRange.startDate,\n dateRange.endDate,\n {\n participantCount,\n enabled: availabilityProp === undefined,\n },\n );\n\n const days = availabilityProp ?? fetchedDays;\n\n const availabilityMap = useMemo(() => {\n const map = new Map<string, DayAvailability>();\n for (const day of days) {\n map.set(day.date, day);\n }\n return map;\n }, [days]);\n\n const handlePrev = useCallback(() => {\n setOffset((prev) => Math.max(0, prev - daysToShow));\n }, [daysToShow]);\n\n const handleNext = useCallback(() => {\n setOffset((prev) => prev + daysToShow);\n }, [daysToShow]);\n\n const handleDateSelect = useCallback((date: string) => {\n setSelectedDate(date);\n }, []);\n\n const handleSlotSelect = useCallback(\n (slot: AvailableSlot) => {\n onSlotSelect?.(slot, selectedDate);\n },\n [onSlotSelect, selectedDate],\n );\n\n return (\n <div data-cimplify-date-slot-picker className={cn(className, classNames?.root)}>\n <div data-cimplify-date-nav className={classNames?.nav}>\n <button\n type=\"button\"\n onClick={handlePrev}\n disabled={offset === 0}\n data-cimplify-date-nav-prev\n className={classNames?.navButton}\n >\n &larr;\n </button>\n <button\n type=\"button\"\n onClick={handleNext}\n data-cimplify-date-nav-next\n className={classNames?.navButton}\n >\n &rarr;\n </button>\n </div>\n\n <div data-cimplify-date-strip className={classNames?.dateStrip} role=\"tablist\">\n {dateRange.dates.map((date) => {\n const dayInfo = availabilityMap.get(date);\n const hasAvailability = dayInfo?.has_availability !== false;\n const isSelected = selectedDate === date;\n return (\n <button\n key={date}\n type=\"button\"\n role=\"tab\"\n aria-selected={isSelected}\n onClick={() => handleDateSelect(date)}\n data-cimplify-date-button\n data-selected={isSelected || undefined}\n data-available={hasAvailability || undefined}\n data-fully-booked={(!hasAvailability) || undefined}\n className={classNames?.dateButton}\n >\n {formatDate(date)}\n </button>\n );\n })}\n </div>\n\n {availabilityLoading && (\n <div\n data-cimplify-date-slot-loading\n aria-busy=\"true\"\n className={classNames?.loading}\n />\n )}\n\n <div data-cimplify-date-slots className={classNames?.slots}>\n <SlotPicker\n serviceId={serviceId}\n date={selectedDate}\n participantCount={participantCount}\n selectedSlot={selectedSlot}\n onSlotSelect={handleSlotSelect}\n showPrice={showPrice}\n />\n </div>\n </div>\n );\n}\n"
13
+ "content": "\"use client\";\n\nimport React, { useState, useMemo, useCallback } from \"react\";\nimport { Tabs } from \"@base-ui/react/tabs\";\nimport type { AvailableSlot, DayAvailability } from \"@cimplify/sdk\";\nimport { useServiceAvailability } from \"@cimplify/sdk/react\";\nimport { SlotPicker } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface DateSlotPickerClassNames {\n root?: string;\n dateStrip?: string;\n dateButton?: string;\n nav?: string;\n navButton?: string;\n slots?: string;\n loading?: string;\n}\n\nexport interface DateSlotPickerProps {\n /** Service ID to fetch availability and slots for. */\n serviceId: string;\n /** Number of days to show in the date strip. Default: 7. */\n daysToShow?: number;\n /** Number of participants. */\n participantCount?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot, date: string) => void;\n /** Pre-fetched availability data (skips fetch). */\n availability?: DayAvailability[];\n /** Show price on slots. Default: true. */\n showPrice?: boolean;\n className?: string;\n classNames?: DateSlotPickerClassNames;\n}\n\nfunction formatDate(dateStr: string): string {\n const date = new Date(dateStr + \"T00:00:00\");\n return date.toLocaleDateString(undefined, { weekday: \"short\", month: \"short\", day: \"numeric\" });\n}\n\nfunction toDateString(date: Date): string {\n return date.toISOString().split(\"T\")[0];\n}\n\nfunction addDays(date: Date, days: number): Date {\n const result = new Date(date);\n result.setDate(result.getDate() + days);\n return result;\n}\n\nexport function DateSlotPicker({\n serviceId,\n daysToShow = 7,\n participantCount,\n selectedSlot,\n onSlotSelect,\n availability: availabilityProp,\n showPrice = true,\n className,\n classNames,\n}: DateSlotPickerProps): React.ReactElement {\n const [offset, setOffset] = useState(0);\n const [selectedDate, setSelectedDate] = useState<string>(toDateString(new Date()));\n\n const dateRange = useMemo(() => {\n const today = new Date();\n const start = addDays(today, offset);\n const dates: string[] = [];\n for (let i = 0; i < daysToShow; i++) {\n dates.push(toDateString(addDays(start, i)));\n }\n return {\n dates,\n startDate: dates[0],\n endDate: dates[dates.length - 1],\n };\n }, [offset, daysToShow]);\n\n const { days: fetchedDays, isLoading: availabilityLoading } = useServiceAvailability(\n serviceId,\n dateRange.startDate,\n dateRange.endDate,\n {\n participantCount,\n enabled: availabilityProp === undefined,\n },\n );\n\n const days = availabilityProp ?? fetchedDays;\n\n const availabilityMap = useMemo(() => {\n const map = new Map<string, DayAvailability>();\n for (const day of days) {\n map.set(day.date, day);\n }\n return map;\n }, [days]);\n\n const handlePrev = useCallback(() => {\n setOffset((prev) => Math.max(0, prev - daysToShow));\n }, [daysToShow]);\n\n const handleNext = useCallback(() => {\n setOffset((prev) => prev + daysToShow);\n }, [daysToShow]);\n\n const handleDateChange = useCallback((value: string | number | null) => {\n if (typeof value === \"string\") {\n setSelectedDate(value);\n }\n }, []);\n\n const handleSlotSelect = useCallback(\n (slot: AvailableSlot) => {\n onSlotSelect?.(slot, selectedDate);\n },\n [onSlotSelect, selectedDate],\n );\n\n return (\n <Tabs.Root\n value={selectedDate}\n onValueChange={handleDateChange}\n data-cimplify-date-slot-picker\n className={cn(className, classNames?.root)}\n >\n <div data-cimplify-date-nav className={classNames?.nav}>\n <button\n type=\"button\"\n onClick={handlePrev}\n disabled={offset === 0}\n data-cimplify-date-nav-prev\n className={classNames?.navButton}\n >\n &larr;\n </button>\n <button\n type=\"button\"\n onClick={handleNext}\n data-cimplify-date-nav-next\n className={classNames?.navButton}\n >\n &rarr;\n </button>\n </div>\n\n <Tabs.List data-cimplify-date-strip className={classNames?.dateStrip}>\n {dateRange.dates.map((date) => {\n const dayInfo = availabilityMap.get(date);\n const hasAvailability = dayInfo?.has_availability !== false;\n const isSelected = selectedDate === date;\n return (\n <Tabs.Tab\n key={date}\n value={date}\n data-cimplify-date-button\n data-selected={isSelected || undefined}\n data-available={hasAvailability || undefined}\n data-fully-booked={(!hasAvailability) || undefined}\n className={classNames?.dateButton}\n >\n {formatDate(date)}\n </Tabs.Tab>\n );\n })}\n </Tabs.List>\n\n {availabilityLoading && (\n <div\n data-cimplify-date-slot-loading\n aria-busy=\"true\"\n className={classNames?.loading}\n />\n )}\n\n <div data-cimplify-date-slots className={classNames?.slots}>\n <SlotPicker\n serviceId={serviceId}\n date={selectedDate}\n participantCount={participantCount}\n selectedSlot={selectedSlot}\n onSlotSelect={handleSlotSelect}\n showPrice={showPrice}\n />\n </div>\n </Tabs.Root>\n );\n}\n"
14
14
  }
15
15
  ]
16
16
  }
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "discount-input.tsx",
13
- "content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport type { DiscountValidation } from \"@cimplify/sdk\";\nimport { useValidateDiscount } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface DiscountInputClassNames {\n root?: string;\n input?: string;\n button?: string;\n result?: string;\n error?: string;\n success?: string;\n}\n\nexport interface DiscountInputProps {\n /** Current order subtotal for validation. */\n orderSubtotal: string;\n /** Location ID for location-specific discounts. */\n locationId?: string;\n /** Called when a valid discount is applied. */\n onApply?: (validation: DiscountValidation) => void;\n /** Called when discount is cleared. */\n onClear?: () => void;\n /** Placeholder text. */\n placeholder?: string;\n className?: string;\n classNames?: DiscountInputClassNames;\n}\n\n/**\n * DiscountInput — discount code input with inline validation.\n *\n * Wraps `useValidateDiscount` with a text input and apply button.\n * Shows validation result inline (success with amount, or error).\n */\nexport function DiscountInput({\n orderSubtotal,\n locationId,\n onApply,\n onClear,\n placeholder = \"Discount code\",\n className,\n classNames,\n}: DiscountInputProps): React.ReactElement {\n const [code, setCode] = useState(\"\");\n const [appliedValidation, setAppliedValidation] = useState<DiscountValidation | null>(null);\n const { validate, isValidating, error } = useValidateDiscount();\n\n const handleApply = useCallback(async () => {\n const trimmed = code.trim();\n if (!trimmed) return;\n\n const result = await validate(trimmed, orderSubtotal, locationId);\n if (result) {\n if (result.is_eligible) {\n setAppliedValidation(result);\n onApply?.(result);\n } else {\n setAppliedValidation(result);\n }\n }\n }, [code, validate, orderSubtotal, locationId, onApply]);\n\n const handleClear = useCallback(() => {\n setCode(\"\");\n setAppliedValidation(null);\n onClear?.();\n }, [onClear]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\") {\n void handleApply();\n }\n },\n [handleApply],\n );\n\n const isApplied = appliedValidation?.is_eligible === true;\n\n return (\n <div data-cimplify-discount className={cn(className, classNames?.root)}>\n <div data-cimplify-discount-form>\n <input\n type=\"text\"\n value={code}\n onChange={(e) => setCode(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={isApplied}\n data-cimplify-discount-input\n className={classNames?.input}\n aria-label=\"Discount code\"\n />\n {isApplied ? (\n <button\n type=\"button\"\n onClick={handleClear}\n data-cimplify-discount-clear\n className={classNames?.button}\n >\n Remove\n </button>\n ) : (\n <button\n type=\"button\"\n onClick={handleApply}\n disabled={isValidating || code.trim().length === 0}\n data-cimplify-discount-apply\n className={classNames?.button}\n >\n {isValidating ? \"Checking...\" : \"Apply\"}\n </button>\n )}\n </div>\n\n {error && (\n <div data-cimplify-discount-error className={classNames?.error}>\n {error.message}\n </div>\n )}\n\n {appliedValidation && !appliedValidation.is_eligible && (\n <div data-cimplify-discount-error className={classNames?.error}>\n {appliedValidation.ineligibility_reason ?? \"This code is not valid.\"}\n </div>\n )}\n\n {isApplied && appliedValidation.discount_amount && (\n <div data-cimplify-discount-success className={classNames?.success}>\n <span>Discount applied</span>\n <Price amount={appliedValidation.discount_amount} prefix=\"-\" />\n </div>\n )}\n </div>\n );\n}\n"
13
+ "content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport { Field } from \"@base-ui/react/field\";\nimport { Input } from \"@base-ui/react/input\";\nimport type { DiscountValidation } from \"@cimplify/sdk\";\nimport { useValidateDiscount } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface DiscountInputClassNames {\n root?: string;\n input?: string;\n button?: string;\n result?: string;\n error?: string;\n success?: string;\n}\n\nexport interface DiscountInputProps {\n /** Current order subtotal for validation. */\n orderSubtotal: string;\n /** Location ID for location-specific discounts. */\n locationId?: string;\n /** Called when a valid discount is applied. */\n onApply?: (validation: DiscountValidation) => void;\n /** Called when discount is cleared. */\n onClear?: () => void;\n /** Placeholder text. */\n placeholder?: string;\n className?: string;\n classNames?: DiscountInputClassNames;\n}\n\n/**\n * DiscountInput — discount code input with inline validation.\n *\n * Wraps `useValidateDiscount` with a Base UI Field + Input and apply button.\n * Shows validation result inline (success with amount, or error via Field.Error).\n */\nexport function DiscountInput({\n orderSubtotal,\n locationId,\n onApply,\n onClear,\n placeholder = \"Discount code\",\n className,\n classNames,\n}: DiscountInputProps): React.ReactElement {\n const [code, setCode] = useState(\"\");\n const [appliedValidation, setAppliedValidation] =\n useState<DiscountValidation | null>(null);\n const { validate, isValidating, error } = useValidateDiscount();\n\n const handleApply = useCallback(async () => {\n const trimmed = code.trim();\n if (!trimmed) return;\n\n const result = await validate(trimmed, orderSubtotal, locationId);\n if (result) {\n if (result.is_eligible) {\n setAppliedValidation(result);\n onApply?.(result);\n } else {\n setAppliedValidation(result);\n }\n }\n }, [code, validate, orderSubtotal, locationId, onApply]);\n\n const handleClear = useCallback(() => {\n setCode(\"\");\n setAppliedValidation(null);\n onClear?.();\n }, [onClear]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\") {\n void handleApply();\n }\n },\n [handleApply],\n );\n\n const isApplied = appliedValidation?.is_eligible === true;\n const hasError =\n !!error || (!!appliedValidation && !appliedValidation.is_eligible);\n\n const errorMessage = error\n ? error.message\n : appliedValidation && !appliedValidation.is_eligible\n ? (appliedValidation.ineligibility_reason ?? \"This code is not valid.\")\n : undefined;\n\n return (\n <Field.Root\n data-cimplify-discount\n invalid={hasError}\n disabled={isApplied}\n className={cn(className, classNames?.root)}\n >\n <div data-cimplify-discount-form>\n <Input\n type=\"text\"\n value={code}\n onValueChange={(value) => setCode(value)}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n data-cimplify-discount-input\n className={classNames?.input}\n aria-label=\"Discount code\"\n />\n {isApplied ? (\n <button\n type=\"button\"\n onClick={handleClear}\n data-cimplify-discount-clear\n className={classNames?.button}\n >\n Remove\n </button>\n ) : (\n <button\n type=\"button\"\n onClick={handleApply}\n disabled={isValidating || code.trim().length === 0}\n data-cimplify-discount-apply\n className={classNames?.button}\n >\n {isValidating ? \"Checking...\" : \"Apply\"}\n </button>\n )}\n </div>\n\n {hasError && errorMessage && (\n <Field.Error\n match={true}\n data-cimplify-discount-error\n className={classNames?.error}\n >\n {errorMessage}\n </Field.Error>\n )}\n\n {isApplied && appliedValidation.discount_amount && (\n <Field.Description\n data-cimplify-discount-success\n className={classNames?.success}\n >\n <span>Discount applied</span>\n <Price amount={appliedValidation.discount_amount} prefix=\"-\" />\n </Field.Description>\n )}\n </Field.Root>\n );\n}\n"
14
14
  }
15
15
  ]
16
16
  }
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  {
13
13
  "path": "order-history-page.tsx",
14
- "content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport type { Order, OrderStatus } from \"@cimplify/sdk\";\nimport { OrderHistory } from \"@cimplify/sdk/react\";\nimport { OrderSummary } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderHistoryPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n filters?: string;\n filterButton?: string;\n list?: string;\n detail?: string;\n backButton?: string;\n}\n\nexport interface OrderHistoryPageProps {\n /** Page title. */\n title?: string;\n /** Pre-fetched orders for SSR. */\n orders?: Order[];\n /** Max orders to show per page. */\n limit?: number;\n /** Called when navigating to an order detail (e.g. for routing). */\n onOrderNavigate?: (order: Order) => void;\n /** Show status filter tabs. Default: true. */\n showFilters?: boolean;\n /** Custom order row renderer. */\n renderOrder?: (order: Order) => React.ReactNode;\n className?: string;\n classNames?: OrderHistoryPageClassNames;\n}\n\nconst STATUS_FILTERS: { label: string; value: OrderStatus | undefined }[] = [\n { label: \"All\", value: undefined },\n { label: \"Active\", value: \"confirmed\" },\n { label: \"Completed\", value: \"completed\" },\n { label: \"Cancelled\", value: \"cancelled\" },\n];\n\n/**\n * OrderHistoryPage — order list with inline detail view and status filtering.\n *\n * SSR-friendly: pass `orders` prop for server rendering.\n * Supports both inline detail view and external navigation via `onOrderNavigate`.\n */\nexport function OrderHistoryPage({\n title = \"Order History\",\n orders: ordersProp,\n limit = 20,\n onOrderNavigate,\n showFilters = true,\n renderOrder,\n className,\n classNames,\n}: OrderHistoryPageProps): React.ReactElement {\n const [statusFilter, setStatusFilter] = useState<OrderStatus | undefined>(undefined);\n const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);\n\n const handleOrderClick = useCallback(\n (order: Order) => {\n if (onOrderNavigate) {\n onOrderNavigate(order);\n } else {\n setSelectedOrder(order);\n }\n },\n [onOrderNavigate],\n );\n\n const handleBack = useCallback(() => {\n setSelectedOrder(null);\n }, []);\n\n // Inline detail view\n if (selectedOrder && !onOrderNavigate) {\n return (\n <div data-cimplify-order-history-page className={cn(className, classNames?.root)}>\n <div data-cimplify-order-history-detail className={classNames?.detail}>\n <button\n type=\"button\"\n onClick={handleBack}\n data-cimplify-order-history-back\n className={classNames?.backButton}\n >\n Back to orders\n </button>\n <OrderSummary order={selectedOrder} />\n </div>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-history-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-history-header className={classNames?.header}>\n <h1 data-cimplify-order-history-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {/* Status filter */}\n {showFilters && (\n <div data-cimplify-order-history-filters className={classNames?.filters} role=\"tablist\">\n {STATUS_FILTERS.map((filter) => (\n <button\n key={filter.label}\n type=\"button\"\n role=\"tab\"\n aria-selected={statusFilter === filter.value}\n onClick={() => setStatusFilter(filter.value)}\n data-cimplify-order-filter\n data-selected={statusFilter === filter.value || undefined}\n className={classNames?.filterButton}\n >\n {filter.label}\n </button>\n ))}\n </div>\n )}\n\n {/* Order list */}\n <div data-cimplify-order-history-list className={classNames?.list}>\n <OrderHistory\n orders={ordersProp}\n status={statusFilter}\n limit={limit}\n onOrderClick={handleOrderClick}\n renderOrder={renderOrder}\n />\n </div>\n </div>\n );\n}\n"
14
+ "content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport { Tabs } from \"@base-ui/react/tabs\";\nimport type { Order, OrderStatus } from \"@cimplify/sdk\";\nimport { OrderHistory } from \"@cimplify/sdk/react\";\nimport { OrderSummary } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderHistoryPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n filters?: string;\n filterButton?: string;\n list?: string;\n detail?: string;\n backButton?: string;\n}\n\nexport interface OrderHistoryPageProps {\n /** Page title. */\n title?: string;\n /** Pre-fetched orders for SSR. */\n orders?: Order[];\n /** Max orders to show per page. */\n limit?: number;\n /** Called when navigating to an order detail (e.g. for routing). */\n onOrderNavigate?: (order: Order) => void;\n /** Show status filter tabs. Default: true. */\n showFilters?: boolean;\n /** Custom order row renderer. */\n renderOrder?: (order: Order) => React.ReactNode;\n className?: string;\n classNames?: OrderHistoryPageClassNames;\n}\n\nconst STATUS_FILTERS: { label: string; value: OrderStatus | undefined; tabValue: string }[] = [\n { label: \"All\", value: undefined, tabValue: \"all\" },\n { label: \"Active\", value: \"confirmed\", tabValue: \"confirmed\" },\n { label: \"Completed\", value: \"completed\", tabValue: \"completed\" },\n { label: \"Cancelled\", value: \"cancelled\", tabValue: \"cancelled\" },\n];\n\n/**\n * OrderHistoryPage — order list with inline detail view and status filtering.\n *\n * SSR-friendly: pass `orders` prop for server rendering.\n * Supports both inline detail view and external navigation via `onOrderNavigate`.\n */\nexport function OrderHistoryPage({\n title = \"Order History\",\n orders: ordersProp,\n limit = 20,\n onOrderNavigate,\n showFilters = true,\n renderOrder,\n className,\n classNames,\n}: OrderHistoryPageProps): React.ReactElement {\n const [statusFilter, setStatusFilter] = useState<OrderStatus | undefined>(undefined);\n const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);\n\n const handleTabChange = useCallback((value: string | number | null) => {\n const filter = STATUS_FILTERS.find((f) => f.tabValue === value);\n setStatusFilter(filter?.value);\n }, []);\n\n const handleOrderClick = useCallback(\n (order: Order) => {\n if (onOrderNavigate) {\n onOrderNavigate(order);\n } else {\n setSelectedOrder(order);\n }\n },\n [onOrderNavigate],\n );\n\n const handleBack = useCallback(() => {\n setSelectedOrder(null);\n }, []);\n\n const activeTabValue = STATUS_FILTERS.find((f) => f.value === statusFilter)?.tabValue ?? \"all\";\n\n // Inline detail view\n if (selectedOrder && !onOrderNavigate) {\n return (\n <div data-cimplify-order-history-page className={cn(className, classNames?.root)}>\n <div data-cimplify-order-history-detail className={classNames?.detail}>\n <button\n type=\"button\"\n onClick={handleBack}\n data-cimplify-order-history-back\n className={classNames?.backButton}\n >\n Back to orders\n </button>\n <OrderSummary order={selectedOrder} />\n </div>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-history-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-history-header className={classNames?.header}>\n <h1 data-cimplify-order-history-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {/* Status filter */}\n {showFilters && (\n <Tabs.Root value={activeTabValue} onValueChange={handleTabChange}>\n <Tabs.List data-cimplify-order-history-filters className={classNames?.filters}>\n {STATUS_FILTERS.map((filter) => (\n <Tabs.Tab\n key={filter.tabValue}\n value={filter.tabValue}\n data-cimplify-order-filter\n data-selected={statusFilter === filter.value || undefined}\n className={classNames?.filterButton}\n >\n {filter.label}\n </Tabs.Tab>\n ))}\n </Tabs.List>\n </Tabs.Root>\n )}\n\n {/* Order list */}\n <div data-cimplify-order-history-list className={classNames?.list}>\n <OrderHistory\n orders={ordersProp}\n status={statusFilter}\n limit={limit}\n onOrderClick={handleOrderClick}\n renderOrder={renderOrder}\n />\n </div>\n </div>\n );\n}\n"
15
15
  }
16
16
  ]
17
17
  }
@@ -7,7 +7,7 @@
7
7
  "files": [
8
8
  {
9
9
  "path": "product-image-gallery.tsx",
10
- "content": "\"use client\";\n\nimport React, { useEffect, useMemo, useState } from \"react\";\n\nexport interface ProductImageGalleryProps {\n images: string[];\n productName: string;\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n}\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\n/**\n * ProductImageGallery — main image + thumbnail strip.\n *\n * Uses plain `<img>` for framework-agnostic rendering (not Next.js Image).\n */\nexport function ProductImageGallery({\n images,\n productName,\n aspectRatio = \"4/3\",\n className,\n}: ProductImageGalleryProps): React.ReactElement | null {\n const normalizedImages = useMemo(\n () =>\n images.filter(\n (image): image is string =>\n typeof image === \"string\" && image.trim().length > 0,\n ),\n [images],\n );\n\n const [selectedImage, setSelectedImage] = useState(0);\n\n useEffect(() => {\n setSelectedImage(0);\n }, [normalizedImages.length, productName]);\n\n if (normalizedImages.length === 0) {\n return null;\n }\n\n const activeImage = normalizedImages[selectedImage] || normalizedImages[0];\n\n return (\n <div data-cimplify-image-gallery className={className}>\n <div\n data-cimplify-image-gallery-main\n style={{ position: \"relative\", overflow: \"hidden\", ...ASPECT_STYLES[aspectRatio] }}\n >\n <img\n src={activeImage}\n alt={productName}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n data-cimplify-image-gallery-active\n />\n </div>\n\n {normalizedImages.length > 1 && (\n <div data-cimplify-image-gallery-thumbnails style={{ display: \"flex\", gap: \"0.5rem\", marginTop: \"0.75rem\" }}>\n {normalizedImages.map((image, index) => (\n <button\n key={`${image}-${index}`}\n type=\"button\"\n onClick={() => setSelectedImage(index)}\n aria-selected={selectedImage === index}\n data-cimplify-image-gallery-thumb\n data-selected={selectedImage === index || undefined}\n style={{\n width: \"4rem\",\n height: \"4rem\",\n overflow: \"hidden\",\n padding: 0,\n border: \"none\",\n cursor: \"pointer\",\n }}\n >\n <img\n src={image}\n alt=\"\"\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n />\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n"
10
+ "content": "\"use client\";\n\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\n\nexport interface ProductImageGalleryProps {\n images: string[];\n productName: string;\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n}\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\n/**\n * ProductImageGallery — main image + thumbnail strip.\n *\n * Uses plain `<img>` for framework-agnostic rendering (not Next.js Image).\n */\nexport function ProductImageGallery({\n images,\n productName,\n aspectRatio = \"4/3\",\n className,\n}: ProductImageGalleryProps): React.ReactElement | null {\n const normalizedImages = useMemo(\n () =>\n images.filter(\n (image): image is string =>\n typeof image === \"string\" && image.trim().length > 0,\n ),\n [images],\n );\n\n const [selectedImage, setSelectedImage] = useState(0);\n\n useEffect(() => {\n setSelectedImage(0);\n }, [normalizedImages.length, productName]);\n\n if (normalizedImages.length === 0) {\n return null;\n }\n\n const activeImage = normalizedImages[selectedImage] || normalizedImages[0];\n\n return (\n <div data-cimplify-image-gallery className={className}>\n <div\n data-cimplify-image-gallery-main\n style={{ position: \"relative\", overflow: \"hidden\", ...ASPECT_STYLES[aspectRatio] }}\n >\n <img\n src={activeImage}\n alt={productName}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n data-cimplify-image-gallery-active\n />\n </div>\n\n {normalizedImages.length > 1 && (\n <RadioGroup\n aria-label={`${productName} image thumbnails`}\n value={String(selectedImage)}\n onValueChange={(value) => setSelectedImage(Number(value))}\n data-cimplify-image-gallery-thumbnails\n style={{ display: \"flex\", gap: \"0.5rem\", marginTop: \"0.75rem\" }}\n >\n {normalizedImages.map((image, index) => {\n const isSelected = selectedImage === index;\n return (\n <Radio.Root\n key={`${image}-${index}`}\n value={String(index)}\n render={<button type=\"button\" />}\n data-cimplify-image-gallery-thumb\n data-selected={isSelected || undefined}\n style={{\n width: \"4rem\",\n height: \"4rem\",\n overflow: \"hidden\",\n padding: 0,\n border: \"none\",\n cursor: \"pointer\",\n }}\n >\n <img\n src={image}\n alt=\"\"\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n />\n </Radio.Root>\n );\n })}\n </RadioGroup>\n )}\n </div>\n );\n}\n"
11
11
  }
12
12
  ]
13
13
  }
@@ -9,7 +9,7 @@
9
9
  "files": [
10
10
  {
11
11
  "path": "search-input.tsx",
12
- "content": "\"use client\";\n\nimport React, { useCallback } from \"react\";\nimport { useSearch } from \"@cimplify/sdk/react\";\nimport type { UseSearchOptions } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SearchInputClassNames {\n root?: string;\n input?: string;\n clearButton?: string;\n results?: string;\n resultItem?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface SearchInputProps {\n /** Placeholder text for the input. */\n placeholder?: string;\n /** Search options forwarded to useSearch. */\n searchOptions?: UseSearchOptions;\n /** Called when a product result is clicked. */\n onResultClick?: (product: import(\"../types/product\").Product) => void;\n /** Custom result item renderer. */\n renderResult?: (product: import(\"../types/product\").Product) => React.ReactNode;\n /** Show inline results dropdown. Default: true. */\n showResults?: boolean;\n className?: string;\n classNames?: SearchInputClassNames;\n}\n\n/**\n * SearchInput — search bar with debounced results dropdown.\n *\n * Wraps `useSearch` with a controlled input and optional inline results list.\n */\nexport function SearchInput({\n placeholder = \"Search products...\",\n searchOptions,\n onResultClick,\n renderResult,\n showResults = true,\n className,\n classNames,\n}: SearchInputProps): React.ReactElement {\n const { results, isLoading, query, setQuery, clear } = useSearch(searchOptions);\n\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n setQuery(e.target.value);\n },\n [setQuery],\n );\n\n return (\n <div\n data-cimplify-search\n className={cn(className, classNames?.root)}\n style={{ position: \"relative\" }}\n >\n <input\n type=\"search\"\n value={query}\n onChange={handleChange}\n placeholder={placeholder}\n data-cimplify-search-input\n className={classNames?.input}\n aria-label=\"Search products\"\n />\n\n {query.length > 0 && (\n <button\n type=\"button\"\n onClick={clear}\n data-cimplify-search-clear\n className={classNames?.clearButton}\n aria-label=\"Clear search\"\n >\n &times;\n </button>\n )}\n\n {showResults && query.length > 0 && (\n <div data-cimplify-search-results className={classNames?.results}>\n {isLoading && (\n <div data-cimplify-search-loading className={classNames?.loading} aria-busy=\"true\">\n Searching...\n </div>\n )}\n\n {!isLoading && results.length === 0 && query.length >= 2 && (\n <div data-cimplify-search-empty className={classNames?.empty}>\n No results found\n </div>\n )}\n\n {!isLoading &&\n results.map((product) => (\n <button\n key={product.id}\n type=\"button\"\n onClick={() => onResultClick?.(product)}\n data-cimplify-search-result\n className={classNames?.resultItem}\n >\n {renderResult ? (\n renderResult(product)\n ) : (\n <>\n <span data-cimplify-search-result-name>{product.name}</span>\n </>\n )}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n"
12
+ "content": "\"use client\";\n\nimport React, { useCallback } from \"react\";\nimport { Field } from \"@base-ui/react/field\";\nimport { Input } from \"@base-ui/react/input\";\nimport { useSearch } from \"@cimplify/sdk/react\";\nimport type { UseSearchOptions } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SearchInputClassNames {\n root?: string;\n input?: string;\n clearButton?: string;\n results?: string;\n resultItem?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface SearchInputProps {\n /** Placeholder text for the input. */\n placeholder?: string;\n /** Search options forwarded to useSearch. */\n searchOptions?: UseSearchOptions;\n /** Called when a product result is clicked. */\n onResultClick?: (product: import(\"../types/product\").Product) => void;\n /** Custom result item renderer. */\n renderResult?: (product: import(\"../types/product\").Product) => React.ReactNode;\n /** Show inline results dropdown. Default: true. */\n showResults?: boolean;\n className?: string;\n classNames?: SearchInputClassNames;\n}\n\nconst SearchIcon = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n data-cimplify-search-icon\n >\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\" />\n </svg>\n);\n\n/**\n * SearchInput — search bar with debounced results dropdown.\n *\n * Wraps `useSearch` with a controlled input and optional inline results list.\n * Uses Base UI Field.Root + Input for accessible form semantics.\n */\nexport function SearchInput({\n placeholder = \"Search products...\",\n searchOptions,\n onResultClick,\n renderResult,\n showResults = true,\n className,\n classNames,\n}: SearchInputProps): React.ReactElement {\n const { results, isLoading, query, setQuery, clear } = useSearch(searchOptions);\n\n const handleValueChange = useCallback(\n (value: string) => {\n setQuery(value);\n },\n [setQuery],\n );\n\n return (\n <Field.Root\n data-cimplify-search\n className={cn(className, classNames?.root)}\n style={{ position: \"relative\" }}\n >\n <Field.Label className=\"sr-only\">Search products</Field.Label>\n\n <div style={{ position: \"relative\", display: \"flex\", alignItems: \"center\" }}>\n <span\n data-cimplify-search-icon-wrapper\n style={{\n position: \"absolute\",\n left: \"0.5rem\",\n pointerEvents: \"none\",\n display: \"flex\",\n alignItems: \"center\",\n }}\n >\n <SearchIcon />\n </span>\n\n <Input\n type=\"search\"\n value={query}\n onValueChange={handleValueChange}\n placeholder={placeholder}\n data-cimplify-search-input\n className={classNames?.input}\n />\n\n {query.length > 0 && (\n <button\n type=\"button\"\n onClick={clear}\n data-cimplify-search-clear\n className={classNames?.clearButton}\n aria-label=\"Clear search\"\n >\n &times;\n </button>\n )}\n </div>\n\n {showResults && query.length > 0 && (\n <div data-cimplify-search-results className={classNames?.results}>\n {isLoading && (\n <div data-cimplify-search-loading className={classNames?.loading} aria-busy=\"true\">\n Searching...\n </div>\n )}\n\n {!isLoading && results.length === 0 && query.length >= 2 && (\n <div data-cimplify-search-empty className={classNames?.empty}>\n No results found\n </div>\n )}\n\n {!isLoading &&\n results.map((product) => (\n <button\n key={product.id}\n type=\"button\"\n onClick={() => onResultClick?.(product)}\n data-cimplify-search-result\n className={classNames?.resultItem}\n >\n {renderResult ? (\n renderResult(product)\n ) : (\n <>\n <span data-cimplify-search-result-name>{product.name}</span>\n </>\n )}\n </button>\n ))}\n </div>\n )}\n </Field.Root>\n );\n}\n"
13
13
  }
14
14
  ]
15
15
  }
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "search-page.tsx",
13
- "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport { useSearch } from \"@cimplify/sdk/react\";\nimport { ProductGrid } from \"./product-grid\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SearchPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n inputContainer?: string;\n input?: string;\n clearButton?: string;\n resultCount?: string;\n grid?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface SearchPageProps {\n /** Page title. */\n title?: string;\n /** Placeholder text. */\n placeholder?: string;\n /** Called when a product is clicked in results. */\n onProductClick?: (product: Product) => void;\n /** Custom card renderer. */\n renderCard?: (product: Product) => React.ReactNode;\n /** Custom image renderer. */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Grid column config. */\n columns?: { sm?: number; md?: number; lg?: number; xl?: number };\n className?: string;\n classNames?: SearchPageClassNames;\n}\n\n/**\n * SearchPage — dedicated search page with input and results grid.\n *\n * Uses `useSearch` for debounced full-text search.\n */\nexport function SearchPage({\n title = \"Search\",\n placeholder = \"What are you looking for?\",\n onProductClick,\n renderCard,\n renderImage,\n columns,\n className,\n classNames,\n}: SearchPageProps): React.ReactElement {\n const { results, isLoading, query, setQuery, clear } = useSearch();\n\n return (\n <div data-cimplify-search-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-search-page-header className={classNames?.header}>\n <h1 data-cimplify-search-page-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {/* Search input */}\n <div data-cimplify-search-page-input className={classNames?.inputContainer}>\n <input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={placeholder}\n data-cimplify-search-page-field\n className={classNames?.input}\n aria-label=\"Search products\"\n autoFocus\n />\n {query.length > 0 && (\n <button\n type=\"button\"\n onClick={clear}\n data-cimplify-search-page-clear\n className={classNames?.clearButton}\n aria-label=\"Clear search\"\n >\n &times;\n </button>\n )}\n </div>\n\n {/* Result count */}\n {query.length >= 2 && !isLoading && (\n <div data-cimplify-search-page-count className={classNames?.resultCount}>\n {results.length} {results.length === 1 ? \"result\" : \"results\"} for &ldquo;{query}&rdquo;\n </div>\n )}\n\n {/* Results */}\n <div data-cimplify-search-page-grid className={classNames?.grid}>\n {isLoading ? (\n <div data-cimplify-search-page-loading aria-busy=\"true\" className={classNames?.loading}>\n Searching...\n </div>\n ) : query.length >= 2 && results.length === 0 ? (\n <div data-cimplify-search-page-empty className={classNames?.empty}>\n <p>No products found for &ldquo;{query}&rdquo;</p>\n </div>\n ) : (\n results.length > 0 && (\n <ProductGrid\n products={results}\n renderCard={renderCard}\n renderImage={renderImage}\n columns={columns}\n />\n )\n )}\n </div>\n </div>\n );\n}\n"
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport { Input } from \"@base-ui/react/input\";\nimport type { Product } from \"@cimplify/sdk\";\nimport { useSearch } from \"@cimplify/sdk/react\";\nimport { ProductGrid } from \"./product-grid\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SearchPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n inputContainer?: string;\n input?: string;\n clearButton?: string;\n resultCount?: string;\n grid?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface SearchPageProps {\n /** Page title. */\n title?: string;\n /** Placeholder text. */\n placeholder?: string;\n /** Called when a product is clicked in results. */\n onProductClick?: (product: Product) => void;\n /** Custom card renderer. */\n renderCard?: (product: Product) => React.ReactNode;\n /** Custom image renderer. */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Grid column config. */\n columns?: { sm?: number; md?: number; lg?: number; xl?: number };\n className?: string;\n classNames?: SearchPageClassNames;\n}\n\n/**\n * SearchPage — dedicated search page with input and results grid.\n *\n * Uses `useSearch` for debounced full-text search.\n * The search input uses Base UI's `Input` primitive, which provides\n * automatic `data-filled`, `data-focused`, `data-dirty`, and\n * `data-touched` attributes for styling hooks.\n */\nexport function SearchPage({\n title = \"Search\",\n placeholder = \"What are you looking for?\",\n onProductClick,\n renderCard,\n renderImage,\n columns,\n className,\n classNames,\n}: SearchPageProps): React.ReactElement {\n const { results, isLoading, query, setQuery, clear } = useSearch();\n\n return (\n <div data-cimplify-search-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-search-page-header className={classNames?.header}>\n <h1 data-cimplify-search-page-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {/* Search input */}\n <div data-cimplify-search-page-input className={classNames?.inputContainer}>\n <Input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={placeholder}\n data-cimplify-search-page-field\n className={classNames?.input}\n aria-label=\"Search products\"\n autoFocus\n />\n {query.length > 0 && (\n <button\n type=\"button\"\n onClick={clear}\n data-cimplify-search-page-clear\n className={classNames?.clearButton}\n aria-label=\"Clear search\"\n >\n &times;\n </button>\n )}\n </div>\n\n {/* Result count */}\n {query.length >= 2 && !isLoading && (\n <div data-cimplify-search-page-count className={classNames?.resultCount}>\n {results.length} {results.length === 1 ? \"result\" : \"results\"} for &ldquo;{query}&rdquo;\n </div>\n )}\n\n {/* Results */}\n <div data-cimplify-search-page-grid className={classNames?.grid}>\n {isLoading ? (\n <div data-cimplify-search-page-loading aria-busy=\"true\" className={classNames?.loading}>\n Searching...\n </div>\n ) : query.length >= 2 && results.length === 0 ? (\n <div data-cimplify-search-page-empty className={classNames?.empty}>\n <p>No products found for &ldquo;{query}&rdquo;</p>\n </div>\n ) : (\n results.length > 0 && (\n <ProductGrid\n products={results}\n renderCard={renderCard}\n renderImage={renderImage}\n columns={columns}\n />\n )\n )}\n </div>\n </div>\n );\n}\n"
14
14
  }
15
15
  ]
16
16
  }
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "slot-picker.tsx",
13
- "content": "\"use client\";\n\nimport React from \"react\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport { useAvailableSlots } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SlotPickerClassNames {\n root?: string;\n group?: string;\n groupLabel?: string;\n slot?: string;\n slotTime?: string;\n slotPrice?: string;\n loading?: string;\n empty?: string;\n}\n\nexport interface SlotPickerProps {\n /** Pre-fetched slots (skips fetch). */\n slots?: AvailableSlot[];\n /** Service ID — used to fetch slots when `slots` prop is not provided. */\n serviceId?: string;\n /** Date string (YYYY-MM-DD) — used to fetch slots when `slots` prop is not provided. */\n date?: string;\n /** Number of participants for capacity-based availability. */\n participantCount?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot) => void;\n /** Whether to group slots by time of day. Default: true. */\n groupByTimeOfDay?: boolean;\n /** Show price on each slot. Default: true. */\n showPrice?: boolean;\n /** Text shown when no slots available. */\n emptyMessage?: string;\n className?: string;\n classNames?: SlotPickerClassNames;\n}\n\ninterface SlotGroup {\n label: string;\n slots: AvailableSlot[];\n}\n\nfunction getTimeOfDay(timeStr: string): \"morning\" | \"afternoon\" | \"evening\" {\n const hour = parseInt(timeStr.split(\"T\").pop()?.split(\":\")[0] ?? timeStr.split(\":\")[0], 10);\n if (hour < 12) return \"morning\";\n if (hour < 17) return \"afternoon\";\n return \"evening\";\n}\n\nconst TIME_OF_DAY_LABELS: Record<string, string> = {\n morning: \"Morning\",\n afternoon: \"Afternoon\",\n evening: \"Evening\",\n};\n\nfunction groupSlots(slots: AvailableSlot[]): SlotGroup[] {\n const groups: Record<string, AvailableSlot[]> = {};\n for (const slot of slots) {\n const tod = getTimeOfDay(slot.start_time);\n if (!groups[tod]) groups[tod] = [];\n groups[tod].push(slot);\n }\n return ([\"morning\", \"afternoon\", \"evening\"] as const)\n .filter((tod) => groups[tod]?.length)\n .map((tod) => ({ label: TIME_OF_DAY_LABELS[tod], slots: groups[tod] }));\n}\n\nfunction formatTime(timeStr: string): string {\n try {\n const date = new Date(timeStr);\n if (!isNaN(date.getTime())) {\n return date.toLocaleTimeString(undefined, { hour: \"numeric\", minute: \"2-digit\" });\n }\n } catch {\n // noop\n }\n\n const parts = timeStr.split(\":\");\n if (parts.length >= 2) {\n const hour = parseInt(parts[0], 10);\n const minute = parts[1];\n const ampm = hour >= 12 ? \"PM\" : \"AM\";\n const displayHour = hour % 12 || 12;\n return `${displayHour}:${minute} ${ampm}`;\n }\n return timeStr;\n}\n\nexport function SlotPicker({\n slots: slotsProp,\n serviceId,\n date,\n participantCount,\n selectedSlot,\n onSlotSelect,\n groupByTimeOfDay = true,\n showPrice = true,\n emptyMessage = \"No available slots\",\n className,\n classNames,\n}: SlotPickerProps): React.ReactElement {\n const { slots: fetched, isLoading } = useAvailableSlots(\n serviceId ?? null,\n date ?? null,\n {\n participantCount,\n enabled: slotsProp === undefined && !!serviceId && !!date,\n },\n );\n\n const slots = slotsProp ?? fetched;\n\n if (isLoading && slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n const groups = groupByTimeOfDay ? groupSlots(slots) : [{ label: \"\", slots }];\n\n return (\n <div data-cimplify-slot-picker className={cn(className, classNames?.root)}>\n {groups.map((group) => (\n <div key={group.label || \"all\"} data-cimplify-slot-group className={classNames?.group}>\n {group.label && (\n <div data-cimplify-slot-group-label className={classNames?.groupLabel}>\n {group.label}\n </div>\n )}\n {group.slots.map((slot) => {\n const isSelected =\n selectedSlot?.start_time === slot.start_time &&\n selectedSlot?.end_time === slot.end_time;\n return (\n <button\n key={`${slot.start_time}-${slot.end_time}`}\n type=\"button\"\n disabled={!slot.is_available}\n onClick={() => slot.is_available && onSlotSelect?.(slot)}\n data-cimplify-slot\n data-selected={isSelected || undefined}\n data-unavailable={!slot.is_available || undefined}\n className={classNames?.slot}\n >\n <span data-cimplify-slot-time className={classNames?.slotTime}>\n {formatTime(slot.start_time)}\n </span>\n {showPrice && slot.price && (\n <span data-cimplify-slot-price className={classNames?.slotPrice}>\n <Price amount={slot.price} />\n </span>\n )}\n </button>\n );\n })}\n </div>\n ))}\n </div>\n );\n}\n"
13
+ "content": "\"use client\";\n\nimport { Radio } from \"@base-ui/react/radio\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport React from \"react\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport { useAvailableSlots } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SlotPickerClassNames {\n root?: string;\n group?: string;\n groupLabel?: string;\n slot?: string;\n slotTime?: string;\n slotPrice?: string;\n loading?: string;\n empty?: string;\n}\n\nexport interface SlotPickerProps {\n /** Pre-fetched slots (skips fetch). */\n slots?: AvailableSlot[];\n /** Service ID — used to fetch slots when `slots` prop is not provided. */\n serviceId?: string;\n /** Date string (YYYY-MM-DD) — used to fetch slots when `slots` prop is not provided. */\n date?: string;\n /** Number of participants for capacity-based availability. */\n participantCount?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot) => void;\n /** Whether to group slots by time of day. Default: true. */\n groupByTimeOfDay?: boolean;\n /** Show price on each slot. Default: true. */\n showPrice?: boolean;\n /** Text shown when no slots available. */\n emptyMessage?: string;\n className?: string;\n classNames?: SlotPickerClassNames;\n}\n\ninterface SlotGroup {\n label: string;\n slots: AvailableSlot[];\n}\n\nfunction getTimeOfDay(timeStr: string): \"morning\" | \"afternoon\" | \"evening\" {\n const hour = parseInt(timeStr.split(\"T\").pop()?.split(\":\")[0] ?? timeStr.split(\":\")[0], 10);\n if (hour < 12) return \"morning\";\n if (hour < 17) return \"afternoon\";\n return \"evening\";\n}\n\nconst TIME_OF_DAY_LABELS: Record<string, string> = {\n morning: \"Morning\",\n afternoon: \"Afternoon\",\n evening: \"Evening\",\n};\n\nfunction groupSlots(slots: AvailableSlot[]): SlotGroup[] {\n const groups: Record<string, AvailableSlot[]> = {};\n for (const slot of slots) {\n const tod = getTimeOfDay(slot.start_time);\n if (!groups[tod]) groups[tod] = [];\n groups[tod].push(slot);\n }\n return ([\"morning\", \"afternoon\", \"evening\"] as const)\n .filter((tod) => groups[tod]?.length)\n .map((tod) => ({ label: TIME_OF_DAY_LABELS[tod], slots: groups[tod] }));\n}\n\nfunction formatTime(timeStr: string): string {\n try {\n const date = new Date(timeStr);\n if (!isNaN(date.getTime())) {\n return date.toLocaleTimeString(undefined, { hour: \"numeric\", minute: \"2-digit\" });\n }\n } catch {\n // noop\n }\n\n const parts = timeStr.split(\":\");\n if (parts.length >= 2) {\n const hour = parseInt(parts[0], 10);\n const minute = parts[1];\n const ampm = hour >= 12 ? \"PM\" : \"AM\";\n const displayHour = hour % 12 || 12;\n return `${displayHour}:${minute} ${ampm}`;\n }\n return timeStr;\n}\n\nfunction slotToValue(slot: AvailableSlot): string {\n return `${slot.start_time}|${slot.end_time}`;\n}\n\nexport function SlotPicker({\n slots: slotsProp,\n serviceId,\n date,\n participantCount,\n selectedSlot,\n onSlotSelect,\n groupByTimeOfDay = true,\n showPrice = true,\n emptyMessage = \"No available slots\",\n className,\n classNames,\n}: SlotPickerProps): React.ReactElement {\n const { slots: fetched, isLoading } = useAvailableSlots(\n serviceId ?? null,\n date ?? null,\n {\n participantCount,\n enabled: slotsProp === undefined && !!serviceId && !!date,\n },\n );\n\n const slots = slotsProp ?? fetched;\n\n if (isLoading && slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n const groups = groupByTimeOfDay ? groupSlots(slots) : [{ label: \"\", slots }];\n\n const slotsByValue = new Map<string, AvailableSlot>();\n for (const slot of slots) {\n slotsByValue.set(slotToValue(slot), slot);\n }\n\n const selectedValue = selectedSlot ? slotToValue(selectedSlot) : \"\";\n\n return (\n <RadioGroup\n data-cimplify-slot-picker\n className={cn(className, classNames?.root)}\n value={selectedValue}\n onValueChange={(value: string) => {\n const slot = slotsByValue.get(value);\n if (slot?.is_available) {\n onSlotSelect?.(slot);\n }\n }}\n >\n {groups.map((group) => (\n <div key={group.label || \"all\"} data-cimplify-slot-group className={classNames?.group}>\n {group.label && (\n <div data-cimplify-slot-group-label className={classNames?.groupLabel}>\n {group.label}\n </div>\n )}\n {group.slots.map((slot) => {\n const value = slotToValue(slot);\n const isSelected =\n selectedSlot?.start_time === slot.start_time &&\n selectedSlot?.end_time === slot.end_time;\n return (\n <Radio.Root\n key={value}\n value={value}\n disabled={!slot.is_available}\n render={<button type=\"button\" />}\n data-cimplify-slot\n data-selected={isSelected || undefined}\n data-unavailable={!slot.is_available || undefined}\n className={classNames?.slot}\n >\n <span data-cimplify-slot-time className={classNames?.slotTime}>\n {formatTime(slot.start_time)}\n </span>\n {showPrice && slot.price && (\n <span data-cimplify-slot-price className={classNames?.slotPrice}>\n <Price amount={slot.price} />\n </span>\n )}\n </Radio.Root>\n );\n })}\n </div>\n ))}\n </RadioGroup>\n );\n}\n"
14
14
  }
15
15
  ]
16
16
  }
@@ -9,7 +9,7 @@
9
9
  "files": [
10
10
  {
11
11
  "path": "staff-picker.tsx",
12
- "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Staff } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface StaffPickerClassNames {\n root?: string;\n option?: string;\n avatar?: string;\n name?: string;\n bio?: string;\n}\n\nexport interface StaffPickerProps {\n /** List of available staff members. */\n staff: Staff[];\n /** Currently selected staff ID, or null for \"Any available\". */\n selectedStaffId?: string | null;\n /** Called when a staff member is selected. Passes null for \"Any available\". */\n onStaffSelect?: (staffId: string | null) => void;\n /** Show \"Any available\" option. Default: true. */\n showAnyOption?: boolean;\n /** Label for the \"Any available\" option. */\n anyLabel?: string;\n className?: string;\n classNames?: StaffPickerClassNames;\n}\n\nexport function StaffPicker({\n staff,\n selectedStaffId,\n onStaffSelect,\n showAnyOption = true,\n anyLabel = \"Any available\",\n className,\n classNames,\n}: StaffPickerProps): React.ReactElement {\n return (\n <div data-cimplify-staff-picker className={cn(className, classNames?.root)}>\n {showAnyOption && (\n <button\n type=\"button\"\n onClick={() => onStaffSelect?.(null)}\n data-cimplify-staff-option\n data-selected={selectedStaffId === null || undefined}\n data-any\n className={classNames?.option}\n >\n <span data-cimplify-staff-name className={classNames?.name}>\n {anyLabel}\n </span>\n </button>\n )}\n {staff.map((member) => (\n <button\n key={member.id}\n type=\"button\"\n onClick={() => onStaffSelect?.(member.id)}\n data-cimplify-staff-option\n data-selected={selectedStaffId === member.id || undefined}\n className={classNames?.option}\n >\n {member.avatar_url && (\n <img\n src={member.avatar_url}\n alt={member.name}\n data-cimplify-staff-avatar\n className={classNames?.avatar}\n />\n )}\n <span data-cimplify-staff-name className={classNames?.name}>\n {member.name}\n </span>\n {member.bio && (\n <span data-cimplify-staff-bio className={classNames?.bio}>\n {member.bio}\n </span>\n )}\n </button>\n ))}\n </div>\n );\n}\n"
12
+ "content": "\"use client\";\n\nimport { Radio } from \"@base-ui/react/radio\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport React from \"react\";\nimport type { Staff } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst ANY_VALUE = \"__any__\";\n\nexport interface StaffPickerClassNames {\n root?: string;\n option?: string;\n avatar?: string;\n name?: string;\n bio?: string;\n}\n\nexport interface StaffPickerProps {\n /** List of available staff members. */\n staff: Staff[];\n /** Currently selected staff ID, or null for \"Any available\". */\n selectedStaffId?: string | null;\n /** Called when a staff member is selected. Passes null for \"Any available\". */\n onStaffSelect?: (staffId: string | null) => void;\n /** Show \"Any available\" option. Default: true. */\n showAnyOption?: boolean;\n /** Label for the \"Any available\" option. */\n anyLabel?: string;\n className?: string;\n classNames?: StaffPickerClassNames;\n}\n\nexport function StaffPicker({\n staff,\n selectedStaffId,\n onStaffSelect,\n showAnyOption = true,\n anyLabel = \"Any available\",\n className,\n classNames,\n}: StaffPickerProps): React.ReactElement {\n const groupValue =\n selectedStaffId === null ? ANY_VALUE : (selectedStaffId ?? \"\");\n\n return (\n <RadioGroup\n data-cimplify-staff-picker\n className={cn(className, classNames?.root)}\n value={groupValue}\n onValueChange={(value) => {\n onStaffSelect?.(value === ANY_VALUE ? null : value);\n }}\n >\n {showAnyOption && (\n <Radio.Root\n value={ANY_VALUE}\n render={<button type=\"button\" />}\n data-cimplify-staff-option\n data-selected={selectedStaffId === null || undefined}\n data-any\n className={classNames?.option}\n >\n <span data-cimplify-staff-name className={classNames?.name}>\n {anyLabel}\n </span>\n </Radio.Root>\n )}\n {staff.map((member) => (\n <Radio.Root\n key={member.id}\n value={member.id}\n render={<button type=\"button\" />}\n data-cimplify-staff-option\n data-selected={selectedStaffId === member.id || undefined}\n className={classNames?.option}\n >\n {member.avatar_url && (\n <img\n src={member.avatar_url}\n alt={member.name}\n data-cimplify-staff-avatar\n className={classNames?.avatar}\n />\n )}\n <span data-cimplify-staff-name className={classNames?.name}>\n {member.name}\n </span>\n {member.bio && (\n <span data-cimplify-staff-bio className={classNames?.bio}>\n {member.bio}\n </span>\n )}\n </Radio.Root>\n ))}\n </RadioGroup>\n );\n}\n"
13
13
  }
14
14
  ]
15
15
  }