@officexapp/catalogs-cli 0.2.7 → 0.2.9

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 (2) hide show
  1. package/dist/index.js +170 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1427,6 +1427,110 @@ function buildPreviewHtml(schema, port) {
1427
1427
  );
1428
1428
  }
1429
1429
 
1430
+ // --- Cart Components ---
1431
+ const cartIconPath = 'M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z';
1432
+
1433
+ function CartButton({ itemCount, onClick }) {
1434
+ if (itemCount === 0) return null;
1435
+ return h('button', {
1436
+ onClick,
1437
+ className: 'fixed bottom-6 right-6 z-[90] flex items-center gap-2 rounded-full px-5 py-3.5 text-white font-semibold shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl active:scale-95',
1438
+ style: { backgroundColor: themeColor, fontFamily: 'var(--font-display)' },
1439
+ },
1440
+ h('svg', { className: 'w-5 h-5', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2 },
1441
+ h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: cartIconPath })
1442
+ ),
1443
+ h('span', null, itemCount),
1444
+ h('span', { className: 'text-sm opacity-80' }, itemCount === 1 ? 'item' : 'items')
1445
+ );
1446
+ }
1447
+
1448
+ function CartDrawer({ items, isOpen, onToggle, onRemove }) {
1449
+ React.useEffect(() => {
1450
+ if (!isOpen) return;
1451
+ const handleKey = (e) => { if (e.key === 'Escape') onToggle(); };
1452
+ window.addEventListener('keydown', handleKey);
1453
+ return () => window.removeEventListener('keydown', handleKey);
1454
+ }, [isOpen, onToggle]);
1455
+
1456
+ return h(React.Fragment, null,
1457
+ // Backdrop
1458
+ h('div', {
1459
+ className: 'fixed inset-0 z-[95] bg-black/30 backdrop-blur-sm transition-opacity duration-300 ' + (isOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'),
1460
+ onClick: onToggle,
1461
+ }),
1462
+ // Drawer
1463
+ h('div', {
1464
+ className: 'fixed top-0 right-0 bottom-0 z-[96] w-full max-w-md bg-white shadow-2xl transition-transform duration-300 ease-out flex flex-col ' + (isOpen ? 'translate-x-0' : 'translate-x-full'),
1465
+ style: { fontFamily: 'var(--font-display)' },
1466
+ },
1467
+ // Header
1468
+ h('div', { className: 'flex items-center justify-between px-6 py-5 border-b border-gray-100' },
1469
+ h('div', { className: 'flex items-center gap-3' },
1470
+ h('div', { className: 'w-9 h-9 rounded-xl flex items-center justify-center', style: { backgroundColor: themeColor + '12' } },
1471
+ h('svg', { className: 'w-5 h-5', style: { color: themeColor }, fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2 },
1472
+ h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: cartIconPath })
1473
+ )
1474
+ ),
1475
+ h('div', null,
1476
+ h('h2', { className: 'text-lg font-bold text-gray-900' }, 'Your Cart'),
1477
+ h('p', { className: 'text-xs text-gray-400' }, items.length + ' ' + (items.length === 1 ? 'item' : 'items') + ' selected')
1478
+ )
1479
+ ),
1480
+ h('button', { onClick: onToggle, className: 'w-9 h-9 flex items-center justify-center rounded-xl text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-all' },
1481
+ h('svg', { className: 'w-5 h-5', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2 },
1482
+ h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: 'M6 18L18 6M6 6l12 12' })
1483
+ )
1484
+ )
1485
+ ),
1486
+ // Items
1487
+ h('div', { className: 'flex-1 overflow-y-auto px-6 py-4' },
1488
+ items.length === 0
1489
+ ? h('div', { className: 'flex flex-col items-center justify-center h-full text-center py-16' },
1490
+ h('div', { className: 'w-16 h-16 rounded-2xl flex items-center justify-center mb-4', style: { backgroundColor: themeColor + '08' } },
1491
+ h('svg', { className: 'w-8 h-8 text-gray-300', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 1.5 },
1492
+ h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: cartIconPath })
1493
+ )
1494
+ ),
1495
+ h('p', { className: 'text-gray-400 text-sm font-medium' }, 'No offers accepted yet'),
1496
+ h('p', { className: 'text-gray-300 text-xs mt-1' }, 'Accept offers as you browse to add them here')
1497
+ )
1498
+ : h('div', { className: 'space-y-3' },
1499
+ ...items.map(item => h('div', { key: item.offer_id, className: 'group flex items-start gap-4 p-4 rounded-2xl border border-gray-100 bg-gray-50/50 hover:bg-white hover:border-gray-200 hover:shadow-sm transition-all duration-200' },
1500
+ item.image ? h('img', { src: item.image, alt: item.title, className: 'w-14 h-14 rounded-xl object-cover flex-shrink-0' }) : null,
1501
+ h('div', { className: 'flex-1 min-w-0' },
1502
+ h('h3', { className: 'text-sm font-semibold text-gray-900 truncate' }, item.title),
1503
+ item.price_display ? h('p', { className: 'text-sm font-bold mt-0.5', style: { color: themeColor } }, item.price_display) : null,
1504
+ item.price_subtext ? h('p', { className: 'text-xs text-gray-400 mt-0.5' }, item.price_subtext) : null
1505
+ ),
1506
+ h('button', { onClick: () => onRemove(item.offer_id), className: 'w-7 h-7 flex-shrink-0 flex items-center justify-center rounded-lg text-gray-300 hover:text-red-500 hover:bg-red-50 transition-all opacity-0 group-hover:opacity-100' },
1507
+ h('svg', { className: 'w-4 h-4', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2 },
1508
+ h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: 'M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0' })
1509
+ )
1510
+ )
1511
+ ))
1512
+ )
1513
+ ),
1514
+ // Footer
1515
+ items.length > 0 ? h('div', { className: 'border-t border-gray-100 px-6 py-5 space-y-4' },
1516
+ h('div', { className: 'flex items-center justify-between' },
1517
+ h('span', { className: 'text-sm font-medium text-gray-500' }, items.length + ' ' + (items.length === 1 ? 'offer' : 'offers') + ' selected'),
1518
+ h('div', { className: 'flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold', style: { backgroundColor: themeColor + '10', color: themeColor } },
1519
+ h('svg', { className: 'w-3.5 h-3.5', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2 },
1520
+ h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: 'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z' })
1521
+ ),
1522
+ 'Added to order'
1523
+ )
1524
+ ),
1525
+ h('div', { className: 'checkout-stub' },
1526
+ h('h3', null, 'Checkout (Dev Stub)'),
1527
+ h('p', null, 'Payment processing is disabled in local dev mode.')
1528
+ )
1529
+ ) : null
1530
+ )
1531
+ );
1532
+ }
1533
+
1430
1534
  // --- Main App ---
1431
1535
  function CatalogPreview({ catalog }) {
1432
1536
  const pages = catalog.pages || {};
@@ -1435,6 +1539,53 @@ function buildPreviewHtml(schema, port) {
1435
1539
  const [currentPageId, setCurrentPageId] = React.useState(routing.entry || pageKeys[0] || null);
1436
1540
  const [formState, setFormState] = React.useState({});
1437
1541
  const [history, setHistory] = React.useState([]);
1542
+ const [cartItems, setCartItems] = React.useState([]);
1543
+ const [cartOpen, setCartOpen] = React.useState(false);
1544
+
1545
+ // --- Cart logic ---
1546
+ const addToCart = React.useCallback((pageId) => {
1547
+ const pg = pages[pageId];
1548
+ if (!pg?.offer) return;
1549
+ const offer = pg.offer;
1550
+ setCartItems(prev => {
1551
+ if (prev.some(item => item.offer_id === offer.id)) return prev;
1552
+ return [...prev, {
1553
+ offer_id: offer.id,
1554
+ page_id: pageId,
1555
+ title: offer.title || pageId,
1556
+ price_display: offer.price_display,
1557
+ price_subtext: offer.price_subtext,
1558
+ image: offer.image,
1559
+ }];
1560
+ });
1561
+ }, [pages]);
1562
+
1563
+ const removeFromCart = React.useCallback((offerId) => {
1564
+ setCartItems(prev => prev.filter(item => item.offer_id !== offerId));
1565
+ // Clear accept_field so it doesn't re-add
1566
+ for (const [pid, pg] of Object.entries(pages)) {
1567
+ if (pg.offer?.id === offerId && pg.offer.accept_field) {
1568
+ setFormState(prev => { const next = { ...prev }; delete next[pg.offer.accept_field]; return next; });
1569
+ }
1570
+ }
1571
+ }, [pages]);
1572
+
1573
+ const toggleCart = React.useCallback(() => setCartOpen(prev => !prev), []);
1574
+
1575
+ // Detect offer acceptance from field changes
1576
+ React.useEffect(() => {
1577
+ for (const [pageId, pg] of Object.entries(pages)) {
1578
+ const offer = pg.offer;
1579
+ if (!offer?.accept_field) continue;
1580
+ const fieldValue = formState[offer.accept_field];
1581
+ const acceptValue = offer.accept_value || 'accept';
1582
+ if (fieldValue === acceptValue) {
1583
+ if (!cartItems.some(item => item.offer_id === offer.id)) addToCart(pageId);
1584
+ } else {
1585
+ if (cartItems.some(item => item.offer_id === offer.id) && fieldValue !== undefined) removeFromCart(offer.id);
1586
+ }
1587
+ }
1588
+ }, [formState, pages, cartItems, addToCart, removeFromCart]);
1438
1589
 
1439
1590
  // Expose navigation for mindmap
1440
1591
  React.useEffect(() => {
@@ -1454,13 +1605,22 @@ function buildPreviewHtml(schema, port) {
1454
1605
  }, []);
1455
1606
 
1456
1607
  const handleNext = React.useCallback(() => {
1608
+ // Check if page has an offer \u2014 treat "Next" as an accept action
1609
+ const currentPage = pages[currentPageId];
1610
+ if (currentPage?.offer) {
1611
+ const acceptValue = currentPage.offer.accept_value || 'accept';
1612
+ // If there's no accept_field, the CTA button itself is the accept trigger
1613
+ if (!currentPage.offer.accept_field) {
1614
+ addToCart(currentPageId);
1615
+ }
1616
+ }
1457
1617
  const nextId = getNextPageId(currentPageId, routing, formState);
1458
1618
  if (nextId && pages[nextId]) {
1459
1619
  setHistory(prev => [...prev, currentPageId]);
1460
1620
  setCurrentPageId(nextId);
1461
1621
  window.scrollTo({ top: 0, behavior: 'smooth' });
1462
1622
  }
1463
- }, [currentPageId, routing, formState, pages]);
1623
+ }, [currentPageId, routing, formState, pages, addToCart]);
1464
1624
 
1465
1625
  const handleBack = React.useCallback(() => {
1466
1626
  if (history.length > 0) {
@@ -1477,12 +1637,19 @@ function buildPreviewHtml(schema, port) {
1477
1637
  );
1478
1638
  }
1479
1639
 
1480
- const components = page.components || [];
1640
+ const components = (page.components || []).filter(c => !c.hidden && !c.props?.hidden);
1481
1641
  const bgImage = page.background_image || catalog.settings?.theme?.background_image;
1482
1642
 
1643
+ // Cart UI (shared between cover and standard)
1644
+ const cartUI = h(React.Fragment, null,
1645
+ h(CartButton, { itemCount: cartItems.length, onClick: toggleCart }),
1646
+ h(CartDrawer, { items: cartItems, isOpen: cartOpen, onToggle: toggleCart, onRemove: removeFromCart })
1647
+ );
1648
+
1483
1649
  // Cover page layout
1484
1650
  if (isCover) {
1485
1651
  return h('div', { 'data-page-id': currentPageId },
1652
+ cartUI,
1486
1653
  h('div', {
1487
1654
  className: 'cf-page cf-noise min-h-screen flex items-center justify-center relative overflow-hidden',
1488
1655
  style: {
@@ -1516,6 +1683,7 @@ function buildPreviewHtml(schema, port) {
1516
1683
  const topBarEnabled = topBar?.enabled !== false && catalog.settings?.top_bar;
1517
1684
 
1518
1685
  return h('div', { 'data-page-id': currentPageId },
1686
+ cartUI,
1519
1687
  h('div', {
1520
1688
  className: 'cf-page min-h-screen',
1521
1689
  style: { background: 'linear-gradient(180deg, #f8f9fc 0%, #f0f2f7 100%)' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@officexapp/catalogs-cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "CLI for Catalog Kit — upload videos, push catalogs, manage assets",
5
5
  "type": "module",
6
6
  "bin": {