@pixelated-tech/components 3.9.16 → 3.9.17

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.
@@ -19,6 +19,10 @@
19
19
  margin: 10px;
20
20
  }
21
21
 
22
+ .tile-image.clickable {
23
+ cursor: pointer;
24
+ }
25
+
22
26
  .tile-image img {
23
27
  width: 100%;
24
28
  height: 100% !important;
@@ -44,6 +48,12 @@
44
48
  }
45
49
  }
46
50
 
51
+ .tile-image-overlay,
52
+ .tile-image-overlay * {
53
+ pointer-events: none;
54
+ cursor: pointer;
55
+ }
56
+
47
57
  .tile-image:hover .tile-image-overlay {
48
58
  visibility: visible;
49
59
  width: 100% !important;
@@ -67,7 +77,7 @@
67
77
  transition-delay: 0.25s;
68
78
  }
69
79
 
70
- .tile-image-overlay-title{
80
+ .tile-image-overlay-title {
71
81
  margin-bottom: 10px;
72
82
  }
73
83
 
@@ -9,11 +9,12 @@ import "./tiles.css";
9
9
  Tiles.propTypes = {
10
10
  cards: PropTypes.array.isRequired,
11
11
  rowCount: PropTypes.number,
12
+ imgClick: PropTypes.func,
12
13
  };
13
14
  export function Tiles(props) {
14
15
  const rowCount = props.rowCount ?? 2;
15
16
  if (props.cards && props.cards.length > 0) {
16
- return (_jsx("div", { className: "tiles-container", children: _jsx("div", { className: `tile-container row-${rowCount}col`, children: props.cards.map((card, i) => (_jsx("div", { className: "gridItem", children: _jsx(Tile, { index: i, cardLength: props.cards.length, link: card.link, image: card.image, imageAlt: card.imageAlt, bodyText: card.bodyText }) }, i))) }) }));
17
+ return (_jsx("div", { className: "tiles-container", children: _jsx("div", { className: `tile-container row-${rowCount}col`, children: props.cards.map((card, i) => (_jsx("div", { className: "gridItem", children: _jsx(Tile, { index: i, cardLength: props.cards.length, link: card.link, image: card.image, imageAlt: card.imageAlt, bodyText: card.bodyText, imgClick: props.imgClick }) }, i))) }) }));
17
18
  }
18
19
  else {
19
20
  return (_jsx(Loading, {}));
@@ -27,10 +28,12 @@ Tile.propTypes = {
27
28
  image: PropTypes.string.isRequired,
28
29
  imageAlt: PropTypes.string,
29
30
  bodyText: PropTypes.string,
31
+ imgClick: PropTypes.func,
30
32
  };
31
33
  function Tile(props) {
32
34
  const config = usePixelatedConfig();
33
- const tileBody = _jsxs("div", { className: "tile-image", children: [_jsx(SmartImage, { src: props.image, title: props?.imageAlt ?? undefined, alt: props?.imageAlt ?? "", cloudinaryEnv: config?.cloudinary?.product_env ?? undefined }), _jsx("div", { className: "tile-image-overlay", children: _jsxs("div", { className: "tile-image-overlay-text", children: [_jsx("div", { className: "tile-image-overlay-title", children: props.imageAlt }), _jsx("div", { className: "tile-image-overlay-body", children: props.bodyText })] }) })] });
35
+ const imgClick = props.imgClick;
36
+ const tileBody = _jsxs("div", { className: "tile-image" + (imgClick ? " clickable" : ""), children: [_jsx(SmartImage, { src: props.image, title: props?.imageAlt ?? undefined, alt: props?.imageAlt ?? "", onClick: imgClick ? (event) => imgClick(event, props.image) : undefined, cloudinaryEnv: config?.cloudinary?.product_env ?? undefined }), _jsx("div", { className: "tile-image-overlay", children: _jsxs("div", { className: "tile-image-overlay-text", children: [_jsx("div", { className: "tile-image-overlay-title", children: props.imageAlt }), _jsx("div", { className: "tile-image-overlay-body", children: props.bodyText })] }) })] });
34
37
  return (_jsx("div", { className: "tile", id: 'tile-' + props.index, children: props.link ?
35
38
  _jsx("a", { href: props.link, className: "tileLink", children: tileBody })
36
39
  :
@@ -151,14 +151,6 @@ export const CLIENT_ONLY_PATTERNS = [
151
151
  /\bwindow\./,
152
152
  /["']use client["']/ // Client directive
153
153
  ];
154
- /**
155
- * Determines if a component file contains client-only code that requires browser execution
156
- * @param fileContent - The source code content of the file
157
- * @returns true if the file contains client-only patterns
158
- */
159
- export function isClientComponent(fileContent) {
160
- return CLIENT_ONLY_PATTERNS.some(pattern => pattern.test(fileContent));
161
- }
162
154
  /* ===== COMPONENT FILE DETECTION ===== */
163
155
  /**
164
156
  * Glob patterns for finding component files
@@ -1 +1 @@
1
- pxl:v1:1cf1822b5a08925a84ce42d5:4978c2e76935bd83c257792b8af5ff03:daf4d881dfb135ee031bde2792f3ce62a76e91c8c84c831ada3c6295607b07e78052e37637cfc792ff87d4e9b24d4fa1210319a7a238a92a5735d1bace6f153e4e844eec10353010b65cadd2eb5c926619744b341acc8e879bc926f2d60172b682e7eae401198175d2c0b39dfcfd1c6bf6aa95eaea1a131cdda6559a62191ee085a9cf3b3c35495fc29f6697cef26ca9a32ed80145875465a554db7035d1fdba56963074ee5c0e0069b963c9b314f9ed955111c93cb5e2594ae09e44a9c4a8e9686b01909c32dbf997e195ddaa142d554cc3d77fbf2a7bbdad9dfcbcc4e0a2af4efd4855b6619c06d1c5aac21e6bd7ebf13ae4a5e7a847dee7af073087225d4e34899177bde3d5b52d136505c6cf7b109e7e085aa8f96359537b33beea1f222c9d6744d91bd4ad72b3ea6a9477b3b8c7480c4128813cde65a8fc251ff2ea7268819dd45682afb67be2dc7b99c5ffd8d2f566c465fd3aa30e64378a2d5866bc980a247594fb8e494870cd8bd9f5738637180db9e582a3ba4ccf48c55403dedcc02cb2596ba2290f5753f38503225c89fcf2346147ed85355bcb940d0131eb5ca377a1efddff4772faef613752dcb3b1b5f4d06d03ded0ac1a2ae2a78035e98fcff0a38e0df2249028be3a39b86f63616cc5560efd3fc80517780e5524175d06e07cbf0631e0570c929a56fc3b7b8ac85807ae7f2d83673ff11e70273bd9d124853f3c97658819391d387377d40a11407e0a517a6cf40b1e401d88428a86620829062341a38921ee0adf03e182c573e542200be18d40045e8f229e85607936ed9bbebd0f164d1b78a5b732d4befc7aa51f4cf21775e31170022d84cb9d4ef51b8474f5a40117769853d125a69d23d418f419ad7dc21a740caa006a2ba50390973d7d62eab5d979deb12531286b92f35a1c48e479e7222637961dcadc7bdbcf4608199d01e3d13f6a682bf82896edddaefc8aaf181ad28fa514fea84fabb7469d48026f9642258836097ba8577d218b4aebaaa4bd64eac5e6e342980d2e486e482f6986ad39cbc5769a5847ef9cdf3565d2742030a78db96f27f5e40169d1a6b565f972e8772641955e9160a0413bb38ab1b99305dcd037a754115e516ed66c77f011532fcfa187d8e6400459afe8d4aef99a636a2e1a4eb02cd7fb3445198c2963368c3b3091046e768d5bce238a335e66ae300cb341e15dfbd32dde80e0d7ccd54e42776ba0d5bab7fd7756a77fd8cc1bf18d11e5e973a0808d57bda74b61a884303460b1069001731bc5bc15d0513cb2e3d32baffbdce05b56979f13dd9d9fb3d7a0dc2318f5759d299d3224b54c13043e3805b2561732616c8f0d65d0e8ef703ccf0a5ce3d9f6ca1c9451e534f281d48bd94f9f71cd1aa8c752cc2415fe1b311d91babd1dbdc0aa5f879e2aeb850fdcb9079665fa221f70144edc773640de4b50cd3d9244a1860a4241c24531f6c17cc475db50c1e42f80bcd3554716c19839c91d87be5c6431df3ce2af2a155bafe73e10d69e3c1c1fce5afe2b68e0f5a1d61baaa872e4afd701acb2e2a51ffbe44efae010e947b60f974dd32403b39133b29e63581d72b6b6585322e8e2ec6a3865a27bb360f3b61587188606b66dcaa3591ec6cd29b240421a469e966d9347bffbe3789c26ace9b7a8f4b701da7f33c21c63ff70f077c6d66b69282aa2800d29734353982d2b584dafd8dd62fad6c6e6ccc69ec5eca171a290f0cb2010b953683a7764eac31d1d5ff77eda7ce5a12895ad046acb5f77a89b1aea2574039b0bd93cbed60fa350cbfd3c0dacf035dd07b97ad4a9afbca984fc0e92569b5c7894b4c9fe5473a10debc8e4d8be933fab114ada62f4ac8fc09cab343bb883b59f7d40b03e204ba1583487addab5b22b0f81a6f3b99c7d7ec06a426b621c30c5c93c86a5a592e6b886dd1edb28c1a21a3f7f483e0b8ad39e674c13c85df960f761b5fb793415b315c1001a0df06dac8b4183bd00360d3fd94624cdb6c0a0d5f3b4c0a6f6c3c1a87ca55a17288b86bf50a80a960e9f69de45d54a8a85eef935adc2d4563be491b4357c52a82d3dc7972c8253195f062cf4ada45c790cbb8f9eead86fd6bcd400ce1a829ae83ff124f58ea1bbbe32f34ef2b6954e870f9fa3f8a9d2bc1e305ccd51d1be958ec039b4004242f08562214f8bb2e668913b3d643ddacbd02e9535a16844861046141174629e969e992c1e53e58923b0d9f306b6c622f009b31f90510d78f9bfa319ea547d83bb5aeef79f67b2202db3d1e88c83822ee3e14f953d4908024e83b08367fd4468288d7b2e843f00bb5042d45ed9a9183721a6ac806e587178c220cf5c46fb40cd5b1f9de4106fdd9d4649f56b3092075b8e10e98c2cf743e0350e55e1d50d42191d89b2e5354f8c92d3962a9d93ae6f3222d346c90269c14a560ef0f723dba47049b49a47a23b721dc9ad48d7175bd587c9ab35159d532f529113f27481c76958147c576905dec0aabf1092867bafab53e65aca7d480672f52022bbf4eb28f48ae993d80c0934295f11753c71cf751e21cb619fe0657be41b2bb06e22f7c9c55f2eaf25eb99ae47091b4239e01d7fe6d42d5e4f6de01bf3a3bc5e79b4f0862be0fedba3ffb19b0417e326a750b75ba04804d1a7799dbb2ec7fce5d58805019f4b6ed287ae5f68ec348a607624e5f2ff213f45a5bdb3ecfe10a3aaf47044b1baa1160f21e01050fe2c8a9840e07288458f70550476abbdb53ecd4f2080f002265b5d94033ee097cc2bb85cc2d4a152b93992329817dd0e4ecff51972507090a25bd7689087cdf941d6fdb7ef6622859a90032b316ea5d498cd9048eeebfa8936ba3d114f60a4658069ef6cdc41e10ba951766cd8a5672f36f34f0f2d5de67f8a5c0ab55519e0ab542f0d34c7a96c4f6c703b1dc91a54073cc75d9ae3b8855637dfe3cb195cbc628e3b811e4fe74c8b46282b93b42d89edeccec2593ab0070899a366cb6dd14d295a98d04e3113e670745a7f63e0535c4ea61df3feb26acfe0afdd7ebd59414fc42f3499990b6a232d536f88fbff656303d35daab4b6a30309f63a2ebae8ce633b9314176034cc824ec9b5e26d422bbb0f5696092fb6bfe4cb2c8681fcd96b701528f009ba19e9a9479656e674be5da5767d9b399eeba8db84b8e3d47a2352f00ead76e3f36b947804a6812051d475ea086c4c0bde7bc08da2a711ee7c64a8e8173898981b1f9159804ac52a81e786316bc70341d60109499d6234ad699a081a5738e70d686aa5a9bd3d4a8c79d8e163cd55bccbf872ffdbcbbbe61530f5d74b261a4f9adcf2bf75c129972e10dddc5e579cf55d76da4cd380283c4834e0411296f5046be815306a0984a17b33f46fca8e154b05f816c20387afc3fc8058b51865db734d8f6ab4a6e4ae0fdc01630ed9952f1bd2a9df904df26881970f14dbcc87e6f47e8a6229c0279f4d1570282134e6f8d741f9a50686f6fb3e6b20f8c566fb97689fd1361468ed4993c1f8ab28005b6e3ca86964d23288e51b3ba3beed2e7ce073f84f0086a2ae30fcf1f0f6588c21b3e41d5a4ae9e3cb084280ef7c82efa691b1602604356d69953453a33b61abedc621f61a37f3aff58ac845af7e1ae620f007975275d5a9bdb9c7c116fa5c9e4956d8ed1e68bb61f8325592b93556ba7142e27d3440f973ac31c34cd627f11983b9fe2d383565ecb2596c59c83032e6f6fb69c278534cc5d7ce7c1546e398e430c42aa522e994cd05cb9db674fe56d9da0d2551ed2e94f106071f6ea93db72b1000f4417b764bd0cac5fc3ca4536cd53caeaa49a8bc28c068385df109fdd346e02a53526f025b14d5353ca160a7d0b193c9fc63246db45d8959f28fdd4b73c3c590f35f93b6935ee66fb73860dea49b144fd3255197b4bc1d60c78f1c8fa3ddd4a4524c62d7a2678911c37c7e94ee2ceaa752010de696cd82f7be3602b395b2a11ab6109d0080e962a461ff82145b56d1fbbba430a877f2f8d38341c36fa240b725f592b7f248bb0fc1e6cd18154338a859da42035d86fbccaf67337758162315e9f6d9eb5554a93da23d20509ddbb873cce49a681bc31261947f4b7df5dbe4064f2917d702727e8be3331fad7131c3ce7f79f8146d8f084dc1a1462257e86ac7b6f046ea9b53be3307c6da26d18a33db328203884798768486f54ae098baf0de9ab5e325f3370d6927ebc223d2cfbe7496a24837de741c8469948d0ad460d4989cf6bda7732a4bab7accec4ad82e7b8ffb9ccdc54147637778e9007f3316c7dd99cad1af2c841607e06dd5128d53b0c5f1f5f6fa2ce224d215f5707fc39b50d1c3e28c9ccbd5f882f0b8a0b61e699012fa515dbea7eac6b6fad8bed7f4dcdf057485c2de87e1973fb8ad622702d0809e57094d53520ab66b7426d5eb640266dd67fcd195961a46d3a5ac252e20966692461e3a0837eb5a730afeeae10d453d423a4651c3c3d609e16477d7922367aceca2fab82ddeb771bc64daaa1beb65993f527881ef3f827b04e79bebf10cf35f9a28bae73fb18a969c5cb1c925ae52b66486a2a9031b820038f8352f7a273824c5b115463f624933828d6f310cc64116d71034a3094e4473d41102b35a146734dbad78867000d687464b4ec5baa55766b792324fe79dee6ea040ef515abcdd5a959ce73daed9be635ece268b813ef813c8bdc4d7df2303807d678f2a7e673f823a51e267da74c55070d980cf7068923c6311f62b6042c1ba2ace601fd029e069e7864b8629ff339bb373cab71b8c32f112ed70f8b006f7457a653174fe5279fce100fc6f17d6845ee4726f1a1cbfa648d03e703b5e3ab279d22b08f1e62542cb2fb0571c7c1e75d574e682bfcc90142a3b948f791a15809fc713b9babde9f23cad1e4aca8698e5b25dbc3686903e302a351cabd3e8f58ebfe28bcc706e28051d606dfe9771f522d3717862ecb8d65c9b99861c1f2401d7d3853c124b7291e6aa0ae065a2a33f4991acc5c7ee7528d53d22e73d6569c5bba3cb8282b45ad3999cee5f30c2c78296372e433e220e2a7ab193c54f25753b89bc415111b17b7cb5f2e61f3bd1a6fa8c70784d22b45d437a1b2df7b59eda471be3880f7537ce202dbc5cbae84e230d53fdb648325f05d1c74d93fa50bddea19124988837ada0a686f809cd28acb048551ee7c944
1
+ pxl:v1:c70cbe09dc837373c6f97231:63d645cc86fe3d8ab9ed584e12567337:de5401e0f619e7bc50e6333ad1dce44aa01813cf21d55ad9a607b7492a46295e65915a988c3809c1cef0048c3a6a08d6ac73e35b951bfd442a4d8a0db29f0e004d244f7a01dfce7e92fc3b099f5d9f16cb1783ce383d9b4e20b4ec1dfe28761ccc1dde660d0e53be13c7adc4421b9ba775be16ad44368d4f45f821e9b03823c41a49434823e33a0b61e2886b85fa68f73bf2f1789e8096796d32438a116051d99e121795aae13365f27a68ea07f4261bb63ba5e3ce52033efc2a64d2681962bbe5ae76e5f0b1c265815ef651557727ec362c9fd0f92eeda070c1468578e0b30c23d764dd9ca0be56ffc28d61834702aa601dfb5df093d25f71e4255c9f34cae0f8a09512220468642249e477d53bfa1cd22b2c5b83fc5f995812bdf5f18056e87551e6f29f72a8c37fe119f3cf895cd88f4eda02c509a1ee469c48043a00f01e69320454262165aeb499c981cf765ecebc40a5a0b99d9dbd991e11fbc1a1c4140e1fa21652edeaaa569e064bfe063e72521b4bb43d6603be1f1ead52536a52beeac985d91ab3b6e20b1d0e0bfefbe1a56340801736cf3a82fe6dc7b663d9cd70d68669894cc79ff0b992809a0910f7f2d42abfb86b64e4cf2e126a4ebab8976308ff61ea414cc33153c5feaf40f68cb7b61952f048761d75878cb57aeaf3a4ef04e754c560e268192749c6dc07b4488caafd5e1dcd09ac94523f7e9a0242a4ae85de3d045d98ba5a96af9d5068a8054728cd99f37cda73ccf8b46a5cb3978444e1e7d2a5b9a2ba90bb855c7e0ab9f13d78e9b234777109061565185775d369037c84505ab5bd224e91eea59ad32b64fd56d5451c9ac490c6cc479f4e6d76a02484a1aa9735979134a3b8b966817cd518e1d930d668f0f7fdfdda39e577bdb9a8332fd9dc1c60de418e51842965ab88d48469e6122c96e46a4302916a59cfae10957c91a7ac45f059fdb9429b993ac1705ab1a6fc43218f46c1b968fd00e41e11e7e2e4b7b69df78156cad91d30e7ab0d332fd99d1a6883d6a83d0cd713fa39cac6483ffef4f8603b8f9522d4f06968dd04eda944b152e056b173bed76609d1a9894bf6e35c1368f17e76ecc794792ed4b4444b924b6867599da45b208fe50ef540f99ac1fbe22b8ac4ceaee2ad069e96726c4740bb15fd12d50b9718cfed9a6eefc5db4bf86c78ab847d00043033f9991dc978ca5c5f1555e77ef04ff00d83da505bc8141ae1539b0e1dd3f42730fee6ba5d6fc02788df8a0489ba4f1b2542f6fd87b76c2bbb93330c165230d7c767bd51387ec8730bae8d435187da55cfb1deb10be60a80be7d29aa27772b2ecbb4340c0901574c8fee44d2c23b4c9d5f012671a260ad62dde2f1f95d6d6892c90a5fd57b20f3c36e10ad28083981685aea41774f828f5b379a0ae5041ea0d29aebf09e62f0ffaa789b3e6616270a3f6e74a5edd7ed18716f157af8af96c959021ba8553be53db638e3663c21a40d4932702163dbfd5834fb4a7f113bdc4d29346b7238e4461324ed2b614ee4a6e434d1207a824864aea955b24090f8e82320b395dc724defa313b3711ca8dedb1ede348367c8879a5c46aaa90bb57581b7643012fa19ca0c6d8956d4c8b8b50b1a077d4c3e3f5ee802084939b5c7a0c9113bcc06d787411d0c3e4a9f8177f0ec89bba4d0d7ff0a30751daadf6e762ac910f0298e4d3ca0d278f27dfaf16ec0d13533aeab259aa3f896f5018b4fd6b7a8b8b4002c3008e4cf8fb7f550e228579cb459c167095da7eb1930a1504325ad30b08de63df8854f7b47becf4e82d19629ba5ee2ae0a43d74f4af216b38db702b4d7325abc47dc9e9ead359f5f892dee204ec12a4dba509c2fe4892d2fe6fcac1911116fd01a5901af1c60b107ae53a588fb2bfadfa88fdcf2b7ddbd14c4d658ad2b580004cf888379c3339a95e93b7e03c41068b6b2c27c8bc29cf8c075f794f33ee5cff0ad08320c06c28b43308015c3ec4084062714aee486db9e0aa492637917f8a68d544515ec38546f9535f24d4fe44190a4cd7b75a8986f789fa083975f031339af222e6ad84da67d633c4afc7bfcc88c01ba16dc3d9f81952ebd4f2e940d427213fbe7628d1fb53c280c14599cfc6fa500e67abd3a989a275c605c0f2993d15a21a887df3e1d508cc4e5089eb3f5ab9f8a0f8e6b04f9f630025adb8986367f14a2523ddd12e46a9baf761a161dd1e28037d701e7f452784ceaf6e804d021ff29ab1043c6d088fc2942c8fa8c66bcc0c76ba52c4ec527d3327f598992a30390d3dcefc64d626b4a60c5320a7e02f10e6d55a685153cf677d519e2c521c36e35a5f64dc3a4bd05fc5a4201e2f04354646b8e539723eaaf112899f0d9b34a1b3370e6e65a41b474503d85f1052064430939732e7b1212df608011d9e220965ab48c7c4925f81437d7d3c93995f0d6f9f92ec74d4faf4ffdf3a25c8eb4df5140e1afa3f5409edf52eb2a2c14e0b65d9d0ccf9f3c435fcd51676d0db022705a517a732634b2869e01ad3a2669aa84c9ced95dbba2203bc0134944b568a0d9dce086b7dfbd11b4066e2572d1d4c00854cd476fcb27a7b8dd262ba9da2de4da1db90f47ad3053967e24de047fde2e7dbd8933176cf47b2cd51297d5aafbbfd801b71df15768d159a69556acf580f012b6230891b6b5fd61c9c80ef7f7a76a12b4018cad6e2d419b072e9622811253cfa73cc780a310cedd439797bf2aceb7d9855c8ad06e6e2b7daacc0c93d89cdc265b27d8d46d1101ba1518bfad0d5dc869b0c1d8879ee8ba85ff41aae3adb7a3a4c85b939b15591b022ffa43b89ec305668f755a230e59362007a035421f4ddd0b1ee4a4d4f18db66d758a9bfb8d26610f0e340153d551dfc11c48ae816de02e2aaae4d4b2b6a2aaa2c5c11d4d2ef5410bd0b230c8e9d0bacce1906fffdb0e28d5ccdfb54682845bbe59906d3e50b896f888a5eb91120c1192046228a27e9e8a13d525c7d1df963d947a71bb95cd07a4526bf61c8b3986e8963871e94bc8e7db723cc9646fed49b5f823c601aa12dc2c61c2f4035e6a2f68793d31285f1d54b195b8a3126677ebcba699ba055beef1adae9c29213faf38a6e7cd889bb7ff37e6cda3e0ce5bc1274dd2b373da6fff9e9cae18e714b9a43698b1e3ebd31b3a2abd8bdcf500834b5aad1ec8220e1375b558cf53459b4f77d1746abfb6311c9b0f12035642bd307ca926622c07ab5b68487a41cb90f503303b33f828c48dabc4076b0a72e4aee69e367e4a8e145ad5955afa4a901ba32acd46f75b7f7cf9f9e8f0557a5687f8097ca8f0af694cee2f2adff0d31683c0526f99c29e6bf54c62f71f61315b566fd9aa15131722e07a27b014632a82b9f6746ab8c9e2826d37056aff5658303f0c6e5d3a08616a3c49be7ddc1f6b7e0f2bed7a9d17b5fdbc9e99aae9e3ccf683da49b5123ea90cc25887751c2bbc3b931b0fa2891cbd2619aad145898a71105e393284311f39a642570540c7d8936e0284c269b178f1aca07a7d378caeabb7273ac2b114120caf05080d27485f11515313561b4b7cc573da43745b623b56ac4facaa6e970f2f191422ba45d12744754d290034694b2f7eb950c225e0b53907ea68985e3b2c27eaba4ecf774a24aba842826d73c2fc5b1e2671c80a392bbe72c45c0bdca3e62d3a87fddf6684187b577e9d3da21833747e6d25d15b06e035d892acf1242e4a739e844c9b1a62592ba83eaacdcef19cbeda4c9f5fa152a8219f735b8c3d43553b799fbedf1013883f9aee7b3069650f53b117790261d2b48e1fe289061aaca736dbaa8118a836b5b1045bfe4e2707829ee2d9fb978814f79fd2be61f36c3d490e149201731fc3c38685de1620306340a5259d62550560c9e36f1b60c0264b5a64e865f5cf21756fdda21270111d02e13947a651b14056296d268c1b9b89edfc983233541797a70c7831a361b94d830c96f02765ed2bd27cb15c4f06228d723db97c61ef679074960db5bcd533d88485589224412cbd02676d63a70588c4d864c9eb20703d8413749d4ec2195a118524ff63097750c68e991b6a53971ced2ad22d22c13300af131a8133b64c68095e4f4a52689a9368d339e0ec0b769f0a83e7002fa02ad4c954499680fd93c42d0e8aa7864eac7bad8aea8c560815ba620e55f6157b20e937db6d02a6abc3f04588c07e1f02445a0c01230316def1f7f4fb7bedecf8d49034dcb27aeabf3a733e8c02160ef1a91909edc20a49266a38eae88e83f66a6809a0a9edefc8feeba2bced621cbba28a3a400df83f152292e3a9ad0074599a90387fbd67bbe9a9c64611176ee81623283839e3959304b5addca8e5875b32f84a0b446bbbe391c323c41f7aeee5a992419da68f5fd47da1b3a227c8532eebe46fd3491f02ea9e11ab4932a74a0770800301c311afb2af8918511fe6d2051a4af053e34629543e23870b064173b0b8bde9ef6745b62b6f6ff9f6a54d138ff3fcc6fd710d3f136a3780269d2d92babd145319aa64e441c516426640dfde81d70a07f24c8956d512a9db47c1d3d928d56bf183206b8f67f2fb91dba8bab52f23e477db35d633e5a63e22daa78b33d28dcceb7820bb43ec99cc8a1a30164c5a0a090bc2065df37f184ab3075618917222cc4f2bbf8680e881507124b5f0b7460a1a590ee142d9fd4097cd9bd375cf4f14b9b7b21243456752f100e07d25818e7e7383637fc6c96ecd60c91ac6bd8ad82b3efcf60ad0f874ae681f896ea249478e1fc1eda90417d7352bad6b06a67cfa70a0b2d6f5063da403c182eebd05896e20e4dd13f409f34a6afc8c378bb42966c0ade4368275feb11b642d5038e8cf01a9738bd09f10a49fb4daf9d84bdcf5703bf575125369b15e59a980511187a9a5365fe5aa43fd68a012d673f6899e919d0e7ef5223bd594c1b0035ed1bed609f2e38e46e8311b58b9706c7dbeec8e0d3887e6f5dfb611df3ee8a67a122cda70d8dbf5dcf299048c85408d32765c0fb9fd2febb8c1d067f6d2fe8d82ce499197943f22d375f8cc4c5546a23136b08bc90b288abea36e22b87e8bd37bf3aa9dbeb1f643da7a42fb01a245a892db4ae375365d3eda2a0ffe83fe953d0cdfbfd46a6869dd13b5953d4343c452f0f66b2825cb6a819e586fd53ea7c3b8dc7a8bf2e09c28747e7e9883be9a50ae
@@ -8,14 +8,8 @@
8
8
  * - optionally initializes a fresh git repo and adds a remote
9
9
  *
10
10
  * TODOs (placeholders for later work):
11
- * - Create/patch pixelated.config.json and optionally run config:encrypt
12
11
  * - Run `npm ci` / `npm run lint` / `npm test` and optionally build
13
- * - Optionally create GitHub repo using API (with token from secure vault)
14
12
  *
15
- *
16
- 1) Recommended approach (short) ✅
17
- Build a small Node-based CLI (easier cross-platform than a Bash script) called e.g. scripts/new-site.js or an npm create script.
18
- Make it interactive with sensible CLI flags (--name, --domain, --repo, --git, --encrypt, --no-install) and a non-interactive mode for CI.
19
13
 
20
14
  2) High-level workflow the script should perform 🔁
21
15
  Validate inputs (target path, site slug, repo name).
@@ -39,31 +33,13 @@ certificates/ if using TLS — template may include placeholder paths.
39
33
  FEATURE_CHECKLIST.md — optionally update default checklist for new site.
40
34
  .gitignore — ensure pixelated.config.json is ignored if plaintext during setup.
41
35
 
42
- 4) Template hardening (recommended changes in pixelated-template) ⚠️
43
- Add placeholder tokens for the things above (e.g., {{PACKAGE_NAME}}, {{SITE_DOMAIN}}) rather than literal values.
44
- Add a template.json or template.meta that lists files & placeholders to auto-replace.
45
- Ensure the template does not include real secrets. Provide pixelated.config.json.example with placeholders and an example .env.local.example.
46
- Add a scripts/prepare-template.sh or test job that ensures no plaintext sensitive values remain.
47
- Document the creation process in TEMPLATE_README.md for maintainers.
48
-
49
- 5) Security & operational notes 🔐
50
- Do not commit plaintext pixelated.config.json. If the CLI accepts secret values, optionally run npm run config:encrypt immediately and only commit the .enc if needed (prefer not to commit it; store in site secrets store).
51
- Add an option to write the encrypted file directly into dist/config when building a deployment bundle.
52
- Provide an audit step: scan resulting repo for secret patterns before final commit/push.
53
-
54
36
  6) Helpful CLI implementation details (tools & libs) 🛠️
55
- Use Node + these packages: fs-extra (copy), replace-in-file or simple .replace() for template tokens, inquirer (prompt), execa (run commands), simple-git (git ops), node-fetch or @octokit/rest (optional GitHub repo creation).
56
- Use safe file writes and atomic renames for config operations.
57
37
  Provide a --dry-run and --preview mode so users can verify changes before creating repo or pushing.
58
38
 
59
39
  7) Validation & post-creation steps ✅
60
40
  npm run lint && npm test && npm run build — fail fast and show errors for the new site.
61
41
  Optional: run npm run config:decrypt locally (with provided key) to confirm decryption works in your deploy workflow (BUT DO NOT store the key in the repo).
62
42
 
63
- 8) Example minimal CLI flow (pseudo)
64
- Prompt: site name, package name, repo URL (optional), domain, author, contentful tokens, aws keys (optional), encrypt?
65
- Copy template → replace tokens → create pixelated.config.json from pixelated.config.json.example → encrypt if requested → init git → run install/build → push to remote.
66
-
67
43
  */
68
44
 
69
45
  import fs from 'fs/promises';
@@ -74,10 +50,13 @@ import readline from 'readline/promises';
74
50
  import { stdin as input, stdout as output } from 'process';
75
51
  import { fileURLToPath } from 'url';
76
52
  import { loadManifest, findTemplateForSlug, pruneTemplateDirs, printAvailableTemplates } from './create-pixelated-app-template-mapper.js';
53
+ import { AmplifyClient, CreateAppCommand, CreateBranchCommand, UpdateAppCommand, UpdateBranchCommand } from '@aws-sdk/client-amplify';
77
54
 
78
55
  const __filename = fileURLToPath(import.meta.url);
79
56
  const __dirname = path.dirname(__filename);
80
57
 
58
+ let stepNumber = 1;
59
+
81
60
  const exec = promisify(execCb);
82
61
  // Exportable exec wrapper so tests can stub it.
83
62
  export let _exec = exec;
@@ -334,27 +313,194 @@ export async function createAndPushRemote(destPath, siteName, defaultOwner) {
334
313
  throw new Error('Invalid GitHub response');
335
314
  }
336
315
 
337
- // Add remote and push
338
- await _exec(`git remote add origin ${cloneUrl}`, { cwd: destPath });
316
+ // Add remote and push using repo-name as remote
317
+ const remoteName = repoName;
318
+ await _exec(`git remote add ${remoteName} ${cloneUrl}`, { cwd: destPath });
339
319
  await _exec('git branch --show-current || git branch -M main', { cwd: destPath });
340
- await _exec('git push -u origin main', { cwd: destPath });
341
- console.log(`✅ Remote created and initial commit pushed: ${cloneUrl}`);
320
+ try {
321
+ // If we have a github token available in the decrypted config, use it for an authenticated push (avoids relying on local credential helper)
322
+ if (token) {
323
+ await _exec(`git -c credential.helper= -c http.extraheader="Authorization: token ${token}" push -u ${remoteName} main`, { cwd: destPath });
324
+ await _exec('git branch -f dev main', { cwd: destPath });
325
+ await _exec(`git -c credential.helper= -c http.extraheader="Authorization: token ${token}" push -u ${remoteName} dev`, { cwd: destPath });
326
+ } else {
327
+ await _exec(`git push -u ${remoteName} main`, { cwd: destPath });
328
+ await _exec('git branch -f dev main', { cwd: destPath });
329
+ await _exec(`git push -u ${remoteName} dev`, { cwd: destPath });
330
+ }
331
+ console.log(`✅ Remote '${remoteName}' created and pushed (main, dev): ${cloneUrl}`);
332
+ // Return useful values for downstream steps (e.g., Amplify app creation)
333
+ return { cloneUrl, remoteName, token };
334
+ } catch (e) {
335
+ console.warn('⚠️ Failed to push branches automatically. The repo was created on GitHub, but you may need to push manually or configure your git credentials. Error:', e?.message || e);
336
+ // Still return partial info so caller can decide next steps
337
+ return { cloneUrl, remoteName, token };
338
+ }
339
+ }
340
+
341
+ // Create an AWS Amplify app and connect repository branches (best-effort via AWS CLI).
342
+ // This uses the local AWS CLI configuration (credentials/profile) and optionally a GitHub
343
+ // personal access token to allow Amplify to connect to the repo automatically.
344
+ export async function createAmplifyApp(rl, siteName, cloneUrl, sitePath) {
345
+ // Use AWS region from components config if available; skip interactive region prompt
346
+ const componentsCfgPath = path.resolve(__dirname, '..', 'config', 'pixelated.config.json');
347
+ let regionToUse = 'us-east-2';
348
+ let creds = null;
349
+ try {
350
+ if (await exists(componentsCfgPath)) {
351
+ const cfgText = await fs.readFile(componentsCfgPath, 'utf8');
352
+ const cfg = JSON.parse(cfgText);
353
+ if (cfg?.aws?.region) {
354
+ regionToUse = cfg.aws.region;
355
+ console.log(`✅ Using AWS region from components config: ${regionToUse}`);
356
+ }
357
+ if (cfg?.aws?.access_key_id && cfg?.aws?.secret_access_key) {
358
+ creds = { accessKeyId: cfg.aws.access_key_id, secretAccessKey: cfg.aws.secret_access_key };
359
+ console.log('✅ Found AWS credentials in components config; they will be used for Amplify SDK operations.');
360
+ }
361
+ }
362
+ } catch (e) {
363
+ // ignore and continue without config values
364
+ }
365
+
366
+ // Prompt only for GitHub PAT (do not prompt for region)
367
+ const githubToken = (await rl.question('GitHub personal access token (PAT) to connect repo [leave blank to skip]: ')) || '';
368
+
369
+ console.log('Creating Amplify app (this may take a few seconds)...');
370
+ // Use AWS SDK Amplify client
371
+ const client = new AmplifyClient({ region: regionToUse, credentials: creds || undefined });
372
+ let createResp;
373
+ try {
374
+ createResp = await client.send(new CreateAppCommand({ name: siteName, platform: 'WEB_DYNAMIC', repository: cloneUrl || undefined, accessToken: githubToken || undefined }));
375
+ } catch (e) {
376
+ throw new Error('Failed to create Amplify app via SDK: ' + (e?.message || e));
377
+ }
378
+
379
+ const appId = createResp?.app?.appId || createResp?.appId || createResp?.id;
380
+ if (!appId) {
381
+ console.log('Amplify create app response:', createResp);
382
+ throw new Error('Unable to determine Amplify appId from SDK response');
383
+ }
384
+ console.log(`✅ Created Amplify app: ${appId}`);
385
+ console.log(`🔗 Open in console: https://${regionToUse}.console.aws.amazon.com/amplify/home?region=${regionToUse}#/d/${appId}`);
386
+
387
+ // Optionally: read site config (if sitePath provided) and set environment variables for the app & branches
388
+ let envVars = {};
389
+ if (sitePath) {
390
+ // Primary: look for a local .env.local and prefer variables defined there
391
+ const envLocalPath = path.join(sitePath, '.env.local');
392
+ if (await exists(envLocalPath)) {
393
+ try {
394
+ const envText = await fs.readFile(envLocalPath, 'utf8');
395
+ for (const line of envText.split(/\r?\n/)) {
396
+ const m = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.*)$/);
397
+ if (!m) continue;
398
+ const k = m[1];
399
+ let v = m[2] || '';
400
+ // remove optional quotes
401
+ v = v.replace(/^"|"$/g, '');
402
+ if (k === 'PIXELATED_CONFIG_KEY' || k.startsWith('PIXELATED_CONFIG')) {
403
+ envVars[k] = v;
404
+ }
405
+ }
406
+ console.log(`✅ Loaded environment values from ${envLocalPath} and will set matching Amplify environment variables.`);
407
+ } catch (e) {
408
+ console.warn('⚠️ Failed to read .env.local for env var population:', e?.message || e);
409
+ }
410
+ }
411
+
412
+ // Secondary: if no PIXELATED_CONFIG_* vars were found in .env.local, look for pixelated.config.json
413
+ if (!envVars.PIXELATED_CONFIG_JSON && !envVars.PIXELATED_CONFIG_B64) {
414
+ const candidates = [
415
+ path.join(sitePath, 'src', 'app', 'config', 'pixelated.config.json'),
416
+ path.join(sitePath, 'src', 'config', 'pixelated.config.json'),
417
+ path.join(sitePath, 'src', 'pixelated.config.json'),
418
+ path.join(sitePath, 'pixelated.config.json')
419
+ ];
420
+ for (const c of candidates) {
421
+ if (await exists(c)) {
422
+ try {
423
+ const raw = await fs.readFile(c, 'utf8');
424
+ envVars.PIXELATED_CONFIG_JSON = raw;
425
+ envVars.PIXELATED_CONFIG_B64 = Buffer.from(raw, 'utf8').toString('base64');
426
+ console.log(`✅ Loaded site pixelated.config.json from ${c} and will set PIXELATED_CONFIG_* environment variables in Amplify.`);
427
+ break;
428
+ } catch (e) {
429
+ console.warn('⚠️ Failed to read site config for env var population:', e?.message || e);
430
+ }
431
+ }
432
+ }
433
+ }
434
+ }
435
+
436
+ // Attempt to resolve the ARN for the 'amplify-role' role (best-effort)
437
+ let iamRoleArn = null;
438
+ try {
439
+ const { IAMClient, GetRoleCommand } = await import('@aws-sdk/client-iam');
440
+ const iam = new IAMClient({ region: regionToUse, credentials: creds || undefined });
441
+ try {
442
+ const roleResp = await iam.send(new GetRoleCommand({ RoleName: 'amplify-role' }));
443
+ iamRoleArn = roleResp?.Role?.Arn;
444
+ if (iamRoleArn) console.log(`✅ Found amplify-role ARN: ${iamRoleArn}`);
445
+ } catch (e) {
446
+ // ignore; role may not exist or insufficient perms
447
+ console.warn('⚠️ Could not resolve amplify-role ARN; skipping automatic service-role assignment.');
448
+ }
449
+ } catch (e) {
450
+ // ignore if IAM client not available
451
+ }
452
+
453
+ // If we have envVars or iamRoleArn, update the app and branches accordingly
454
+ if (Object.keys(envVars).length || iamRoleArn) {
455
+ try {
456
+ const updateParams = {};
457
+ if (Object.keys(envVars).length) updateParams.environmentVariables = envVars;
458
+ if (iamRoleArn) updateParams.iamServiceRoleArn = iamRoleArn;
459
+ await client.send(new UpdateAppCommand({ appId, ...updateParams }));
460
+ console.log('✅ Amplify app updated with environment variables and IAM service role (if available).');
461
+
462
+ for (const branch of ['dev','main']) {
463
+ try {
464
+ await client.send(new UpdateBranchCommand({ appId, branchName: branch, environmentVariables: envVars }));
465
+ console.log(`✅ Updated branch '${branch}' with environment variables.`);
466
+ } catch (e) {
467
+ console.warn(`⚠️ Failed to update branch ${branch} env vars:`, e?.message || e);
468
+ }
469
+ }
470
+ } catch (e) {
471
+ console.warn('⚠️ Failed to update Amplify app/branches with env vars or service role:', e?.message || e);
472
+ }
473
+ }
474
+
475
+ console.log('ℹ️ Amplify app creation attempt finished. Verify the app in the AWS Console to ensure webhooks and branch connections are correct.');
342
476
  }
477
+
343
478
  async function main() {
344
479
  const rl = readline.createInterface({ input, output });
345
480
  try {
346
481
  console.log('\n📦 Pixelated site creator — scaffold a new site from pixelated-template\n');
482
+ console.log('================================================================================\n');
347
483
 
484
+
485
+ // Prompt for basic site info
486
+ console.log(`\nStep ${stepNumber++}: Site Information`);
487
+ console.log('================================================================================\n');
348
488
  const defaultName = 'my-site';
349
489
  const siteName = (await rl.question(`Root directory name (kebab-case) [${defaultName}]: `)) || defaultName;
350
-
351
490
  // Display name used in route titles: convert kebab to Title Case
352
491
  const rootDisplayName = siteName.split(/[-_]+/).map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');
353
492
 
493
+
354
494
  // Additional site metadata for placeholder substitution
495
+ console.log(`\nStep ${stepNumber++}: Site Metadata`);
496
+ console.log('================================================================================\n');
355
497
  const siteUrl = (await rl.question('Site URL (e.g. https://example.com) [leave blank to skip]: ')).trim();
356
498
  const emailAddress = (await rl.question('Contact email address [leave blank to skip]: ')).trim();
357
499
 
500
+
501
+ // Create a copy of pixelated-template inside the current workspace
502
+ console.log(`\nStep ${stepNumber++}: Template Copy`);
503
+ console.log('================================================================================\n');
358
504
  const workspaceRoot = path.resolve(__dirname, '..', '..', '..');
359
505
  const templatePath = path.resolve(workspaceRoot, 'pixelated-template');
360
506
  if (!(await exists(templatePath))) {
@@ -366,7 +512,6 @@ async function main() {
366
512
  const manifest = await loadManifest(__dirname);
367
513
  // Note: available templates will be printed later just before prompting for pages
368
514
 
369
-
370
515
  // Destination is implicitly the top-level Git folder + site name to avoid prompting for it
371
516
  const destPath = path.resolve(workspaceRoot, siteName);
372
517
  console.log(`\nThe new site will be created at: ${destPath}`);
@@ -404,7 +549,10 @@ async function main() {
404
549
  await fs.rm(gitDir, { recursive: true, force: true });
405
550
  }
406
551
 
552
+
407
553
  // Pages prompt: show available templates and ask which pages to create (comma-separated)
554
+ console.log(`\nStep ${stepNumber++}: Page Creation`);
555
+ console.log('================================================================================\n');
408
556
  if (manifest && Array.isArray(manifest.templates) && manifest.templates.length) {
409
557
  printAvailableTemplates(manifest);
410
558
  }
@@ -519,7 +667,11 @@ async function main() {
519
667
  console.log('Skipping page creation.');
520
668
  }
521
669
  }
670
+
671
+
522
672
  // Automatically replace double-underscore template placeholders (e.g., __SITE_NAME__) with provided values
673
+ console.log(`\nStep ${stepNumber++}: Placeholder Tokens Replacement`);
674
+ console.log('================================================================================\n');
523
675
  const replacements = {};
524
676
  if (rootDisplayName) replacements.SITE_NAME = rootDisplayName;
525
677
  if (siteUrl) replacements.SITE_URL = siteUrl;
@@ -543,7 +695,12 @@ async function main() {
543
695
  console.warn('⚠️ Failed to replace placeholders in site copy:', e?.message || e);
544
696
  }
545
697
  }
698
+
699
+
700
+
546
701
  // Prompt about creating a new GitHub repository. Default owner is read from components config `github.defaultOwner` (fallback: 'brianwhaley')
702
+ console.log(`\nStep ${stepNumber++}: GitHub Repository Creation`);
703
+ console.log('================================================================================\n');
547
704
  const componentsCfgPath = path.resolve(__dirname, '..', 'config', 'pixelated.config.json');
548
705
  let defaultOwner = 'brianwhaley';
549
706
  try {
@@ -556,20 +713,45 @@ async function main() {
556
713
  // ignore and use fallback
557
714
  }
558
715
  const createRemoteAnswer = (await rl.question(`Create a new GitHub repository in '${defaultOwner}' and push the initial commit? (Y/n): `)) || 'y';
716
+ let remoteInfo = null;
559
717
  if (createRemoteAnswer.toLowerCase() === 'y' || createRemoteAnswer.toLowerCase() === 'yes') {
560
718
  try {
561
- await createAndPushRemote(destPath, siteName, defaultOwner);
719
+ remoteInfo = await createAndPushRemote(destPath, siteName, defaultOwner);
562
720
  } catch (e) {
563
721
  console.warn('⚠️ Repo creation or git push failed. Your local repository is still available at:', destPath);
564
722
  console.warn(e?.stderr || e?.message || e);
565
723
  }
566
724
  }
725
+ // Optionally create an AWS Amplify app and connect branches (main, dev)
726
+ console.log(`\nStep ${stepNumber++}: AWS Amplify App Creation`);
727
+ console.log('================================================================================\n'); // Inform user what region will be used (config-backed)
728
+ try {
729
+ const cfgPath = path.resolve(__dirname, '..', 'config', 'pixelated.config.json');
730
+ if (await exists(cfgPath)) {
731
+ const cfgText = await fs.readFile(cfgPath, 'utf8');
732
+ const cfg = JSON.parse(cfgText);
733
+ if (cfg?.aws?.region) {
734
+ console.log(`ℹ️ Note: Amplify will use AWS region from config: ${cfg.aws.region}`);
735
+ }
736
+ }
737
+ } catch (e) {
738
+ // ignore errors reading config; nothing to do
739
+ } const createAmplifyAnswer = (await rl.question(`Create an AWS Amplify app for this repository and connect 'main' and 'dev' branches? (y/N): `)) || 'n';
740
+ if (createAmplifyAnswer.toLowerCase() === 'y' || createAmplifyAnswer.toLowerCase() === 'yes') {
741
+ try {
742
+ await createAmplifyApp(rl, siteName, remoteInfo?.cloneUrl);
743
+ } catch (e) {
744
+ console.warn('⚠️ Amplify app creation failed or was incomplete. You can create an app manually via the AWS Console or AWS CLI.');
745
+ console.warn(e?.stderr || e?.message || e);
746
+ }
747
+ }
567
748
 
568
-
569
-
570
- console.log('\n🎉 Done. Summary:');
749
+ console.log('================================================================================\n');
750
+ console.log('🎉 Done.');
751
+ console.log('================================================================================\n');
752
+ console.log('Summary:');
571
753
  console.log(` - Site copied to: ${destPath}`);
572
- console.log('\nNote: A git remote was not set by this script. You can add one later with `git remote add origin <url>` if desired.');
754
+ console.log('\nNote: A git remote was not set by this script. You can add one later with `git remote add <repo-name> <url>` (use the repository name as the remote) if desired.');
573
755
  console.log('\nNext recommended steps (manual or to be automated in future):');
574
756
  console.log(' - Update pixelated.config.json for this site and encrypt it with your config tool');
575
757
  console.log(' - Run `npm run lint`, `npm test`, and `npm run build` inside the new site and fix any issues');
@@ -6,8 +6,9 @@ import path from 'path';
6
6
  * Enforces workspace standards for SEO, performance, and project structure.
7
7
  */
8
8
 
9
- /* ===== CLIENT COMPONENT DETECTION (Copied from functions.ts for standalone usage) ===== */
10
- const CLIENT_ONLY_PATTERNS = [
9
+
10
+ // DUPLICATE FROM components/general/utilities.ts --- KEEP IN SYNC ---
11
+ export const CLIENT_ONLY_PATTERNS = [
11
12
  /\baddEventListener\b/,
12
13
  /\bcreateContext\b/,
13
14
  /\bdocument\./,
@@ -35,10 +36,11 @@ const CLIENT_ONLY_PATTERNS = [
35
36
  /["']use client["']/ // Client directive
36
37
  ];
37
38
 
38
- function isClientComponent(fileContent) {
39
+ export function isClientComponent(fileContent) {
39
40
  return CLIENT_ONLY_PATTERNS.some(pattern => pattern.test(fileContent));
40
41
  }
41
42
 
43
+
42
44
  const propTypesInferPropsRule = {
43
45
  meta: {
44
46
  type: 'problem',
@@ -372,6 +374,51 @@ const noRawImgRule = {
372
374
  },
373
375
  };
374
376
 
377
+ /* ===== RULE: require-section-ids ===== */
378
+ const requireSectionIdsRule = {
379
+ meta: {
380
+ type: 'suggestion',
381
+ docs: {
382
+ description: 'Require `id` attributes on every <section> and <PageSection> for jump links and SEO',
383
+ category: 'Accessibility',
384
+ recommended: false,
385
+ },
386
+ messages: {
387
+ missingId: '`section` and `PageSection` elements must have an `id` attribute for jump-link support and SEO hierarchy.',
388
+ },
389
+ schema: [],
390
+ },
391
+ create(context) {
392
+ /*
393
+ * Helper: get a string name for a JSX element. Supports
394
+ * `JSXIdentifier` and `JSXMemberExpression` (e.g. `UI.PageSection`).
395
+ */
396
+ function getJSXElementName(node) {
397
+ if (!node) return null;
398
+ if (node.type === 'JSXIdentifier') return node.name;
399
+ if (node.type === 'JSXMemberExpression') return node.property?.name || null;
400
+ return null;
401
+ }
402
+
403
+ return {
404
+ JSXOpeningElement(node) {
405
+ try {
406
+ const name = getJSXElementName(node.name); if (!name || !['section','PageSection'].includes(name)) return;
407
+
408
+ const hasId = (node.attributes || []).some(attr => (
409
+ attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'id' && attr.value != null
410
+ ));
411
+ if (!hasId) {
412
+ context.report({ node, messageId: 'missingId' });
413
+ }
414
+ } catch (e) {
415
+ // defensive: don't crash lint
416
+ }
417
+ },
418
+ };
419
+ },
420
+ };
421
+
375
422
  const requiredFaqRule = {
376
423
  meta: {
377
424
  type: 'suggestion',
@@ -423,6 +470,7 @@ export default {
423
470
  'required-schemas': requiredSchemasRule,
424
471
  'required-files': requiredFilesRule,
425
472
  'no-raw-img': noRawImgRule,
473
+ 'require-section-ids': requireSectionIdsRule,
426
474
  'required-faq': requiredFaqRule,
427
475
  },
428
476
  configs: {
@@ -432,6 +480,7 @@ export default {
432
480
  'pixelated/required-schemas': 'warn',
433
481
  'pixelated/required-files': 'warn',
434
482
  'pixelated/no-raw-img': 'warn',
483
+ 'pixelated/require-section-ids': 'warn',
435
484
  'pixelated/required-faq': 'warn',
436
485
  },
437
486
  },
@@ -38,20 +38,58 @@ fi
38
38
  echo ""
39
39
  echo "🔑 Step $((STEP_COUNT++)): Choose the git Remote to release:"
40
40
  echo "================================================="
41
- # Function to prompt for remote selection
41
+ # Helper: derive a sensible default remote (remote whose repo name matches local folder)
42
+ derive_default_remote() {
43
+ local remotes=($(git remote))
44
+ local local_repo
45
+ local_repo=$(basename "$(git rev-parse --show-toplevel)")
46
+ for remote in "${remotes[@]}"; do
47
+ url=$(git remote get-url "$remote" 2>/dev/null || true)
48
+ repo=$(basename -s .git "${url##*/}")
49
+ if [ -n "$repo" ] && [ "$repo" = "$local_repo" ]; then
50
+ echo "$remote"
51
+ return
52
+ fi
53
+ done
54
+ # Fallback to first remote if no match
55
+ echo "${remotes[0]}"
56
+ }
57
+
58
+ # Function to prompt for remote selection, showing a default derived from local repo name
42
59
  prompt_remote_selection() {
43
60
  echo "Available git remotes:" >&2
44
61
  local remotes=($(git remote))
62
+ local count=${#remotes[@]}
45
63
  local i=1
64
+
46
65
  for remote in "${remotes[@]}"; do
47
66
  echo "$i) $remote" >&2
48
67
  ((i++))
49
68
  done
69
+
70
+ local default_remote
71
+ default_remote=$(derive_default_remote)
72
+ # find index of default_remote for user prompt
73
+ local default_index=1
74
+ for idx in "${!remotes[@]}"; do
75
+ if [ "${remotes[$idx]}" = "$default_remote" ]; then
76
+ default_index=$((idx+1))
77
+ break
78
+ fi
79
+ done
80
+
81
+ local prompt="Select remote to use (1-$count) [default $default_index - $default_remote]: "
50
82
  local choice
51
- read -p "Select remote to use (1-${#remotes[@]}): " choice >&2
83
+ read -p "$prompt" choice >&2
84
+
85
+ if [ -z "$choice" ]; then
86
+ echo "$default_remote"
87
+ return
88
+ fi
89
+
52
90
  case $choice in
53
91
  [1-9]|[1-9][0-9])
54
- if [ "$choice" -le "${#remotes[@]}" ]; then
92
+ if [ "$choice" -le "$count" ]; then
55
93
  echo "${remotes[$((choice-1))]}"
56
94
  else
57
95
  echo "${remotes[0]}" # Default to first if invalid
@@ -213,7 +251,13 @@ prompt_version_type() {
213
251
  echo "3) major (1.x.x)" >&2
214
252
  echo "4) custom version" >&2
215
253
  echo "5) no version bump" >&2
216
- read -p "Enter choice (1-5): " choice >&2
254
+ read -p "Enter choice (1-5) [default 1]: " choice >&2
255
+
256
+ # Default to 1 (patch) when user presses Enter
257
+ if [ -z "$choice" ]; then
258
+ choice=1
259
+ fi
260
+
217
261
  case $choice in
218
262
  1) version_type="patch" ;;
219
263
  2) version_type="minor" ;;
@@ -223,7 +267,7 @@ prompt_version_type() {
223
267
  version_type="$custom_version"
224
268
  ;;
225
269
  5) version_type="none" ;;
226
- *) version_type="patch" ;; # default
270
+ *) version_type="patch" ;; # fallback default
227
271
  esac
228
272
  }
229
273
  prompt_version_type