[{"data":1,"prerenderedAt":14659},["ShallowReactive",2],{"navigation":3,"search-sections":381,"$fP-xBqVoi0N65tNSVHt-lHqg-zjkv0CUsRRMYXkKt23Y":14160,"$fCJDNzULSn1LUYTYDhSIpB-3R_Fb6p5ftzkf3PPvrqNw":14208},[4,28,72,112,136,164,182,251,279,362],{"title":5,"path":6,"stem":7,"children":8,"icon":27},"Getting Started","/getting-started","1.getting-started/1.index",[9,12,17,22],{"title":10,"path":6,"stem":7,"icon":11},"What is Nuxt Crouton?","i-lucide-house",{"title":13,"path":14,"stem":15,"icon":16},"Installation","/getting-started/installation","1.getting-started/2.installation","i-lucide-download",{"title":18,"path":19,"stem":20,"icon":21},"Quick Start","/getting-started/usage","1.getting-started/3.usage","i-lucide-rocket",{"title":23,"path":24,"stem":25,"icon":26},"Adding Modules","/getting-started/adding-modules","1.getting-started/4.adding-modules","i-lucide-puzzle",false,{"title":29,"path":30,"stem":31,"children":32},"Guides","/guides","10.guides",[33,35,39,43,47,51,55,59,63,67],{"title":29,"path":30,"stem":34},"10.guides/index",{"title":36,"path":37,"stem":38},"Troubleshooting","/guides/troubleshooting","10.guides/1.troubleshooting",{"title":40,"path":41,"stem":42},"Migration Guide","/guides/migration","10.guides/2.migration",{"title":44,"path":45,"stem":46},"Best Practices","/guides/best-practices","10.guides/3.best-practices",{"title":48,"path":49,"stem":50},"Pagination Guide","/guides/pagination","10.guides/4.pagination",{"title":52,"path":53,"stem":54},"Asset Management","/guides/asset-management","10.guides/5.asset-management",{"title":56,"path":57,"stem":58},"Future Features","/guides/future-roadmap","10.guides/6.future-roadmap",{"title":60,"path":61,"stem":62},"Rollback & Undo","/guides/rollback","10.guides/7.rollback",{"title":64,"path":65,"stem":66},"Creating Custom CardMini Components","/guides/custom-cardmini","10.guides/8.custom-cardmini",{"title":68,"path":69,"stem":70,"icon":71},"Deployment","/guides/deployment","10.guides/9.deployment","i-lucide-cloud-upload",{"title":73,"path":74,"stem":75,"children":76},"Fundamentals","/fundamentals","2.fundamentals",[77,80,84,88,92,96,100,104,108],{"title":78,"path":74,"stem":79},"Fundamentals Overview","2.fundamentals/index",{"title":81,"path":82,"stem":83},"Collections & Layers","/fundamentals/collections","2.fundamentals/1.collections",{"title":85,"path":86,"stem":87},"Architecture","/fundamentals/architecture","2.fundamentals/2.architecture",{"title":89,"path":90,"stem":91},"Forms & Modals","/fundamentals/forms-modals","2.fundamentals/3.forms-modals",{"title":93,"path":94,"stem":95},"Data Operations (Mutations)","/fundamentals/data-operations","2.fundamentals/4.data-operations",{"title":97,"path":98,"stem":99},"Caching","/fundamentals/caching","2.fundamentals/6.caching",{"title":101,"path":102,"stem":103},"Package Architecture","/fundamentals/packages","2.fundamentals/7.packages",{"title":105,"path":106,"stem":107},"Generated vs Core Code","/fundamentals/generated-code","2.fundamentals/generated-code",{"title":109,"path":110,"stem":111},"Querying Data","/fundamentals/querying","2.fundamentals/querying",{"title":113,"path":114,"stem":115,"children":116},"Generation","/generation","3.generation",[117,120,124,128,132],{"title":118,"path":114,"stem":119},"Generation Overview","3.generation/index",{"title":121,"path":122,"stem":123},"Schema File Format","/generation/schema-format","3.generation/2.schema-format",{"title":125,"path":126,"stem":127},"Multi-Collection Configuration","/generation/multi-collection","3.generation/3.multi-collection",{"title":129,"path":130,"stem":131},"CLI Reference","/generation/cli-reference","3.generation/4.cli-reference",{"title":133,"path":134,"stem":135},"Generator Commands","/generation/cli-commands","3.generation/cli-commands",{"title":137,"path":138,"stem":139,"children":140},"Patterns","/patterns","4.patterns",[141,144,148,152,156,160],{"title":142,"path":138,"stem":143},"Patterns Overview","4.patterns/index",{"title":145,"path":146,"stem":147},"Working with Relations","/patterns/relations","4.patterns/1.relations",{"title":149,"path":150,"stem":151},"Form Patterns","/patterns/forms","4.patterns/2.forms",{"title":153,"path":154,"stem":155},"Table Patterns","/patterns/tables","4.patterns/3.tables",{"title":157,"path":158,"stem":159},"List Layout","/patterns/list-layouts","4.patterns/5.list-layouts",{"title":161,"path":162,"stem":163},"Manual Drizzle Setup","/patterns/drizzle","4.patterns/drizzle",{"title":165,"path":166,"stem":167,"children":168},"Customizing Generated Code","/customization","5.customization/1.index",[169,170,174,178],{"title":165,"path":166,"stem":167},{"title":171,"path":172,"stem":173},"Custom Components","/customization/custom-components","5.customization/3.custom-components",{"title":175,"path":176,"stem":177},"Custom Columns","/customization/custom-columns","5.customization/4.custom-columns",{"title":179,"path":180,"stem":181},"Layout Components","/customization/layouts","5.customization/5.layouts",{"title":183,"path":184,"stem":185,"children":186},"Features","/features","6.features",[187,190,194,198,203,207,211,215,219,223,227,231,235,239,243,247],{"title":188,"path":184,"stem":189},"Features Overview","6.features/index",{"title":191,"path":192,"stem":193},"Internationalization (i18n)","/features/internationalization","6.features/1.internationalization",{"title":195,"path":196,"stem":197},"Maps Integration","/features/maps","6.features/10.maps",{"title":199,"path":200,"stem":201,"icon":202},"DevTools Addon","/features/devtools","6.features/11.devtools","i-lucide-wrench",{"title":204,"path":205,"stem":206},"Flow Visualization","/features/flow","6.features/12.flow",{"title":208,"path":209,"stem":210},"AI Integration","/features/ai","6.features/13.ai",{"title":212,"path":213,"stem":214},"Admin Dashboard","/features/admin","6.features/14.admin",{"title":216,"path":217,"stem":218},"Data Export","/features/export","6.features/15.export",{"title":220,"path":221,"stem":222},"Real-time Collaboration","/features/collaboration","6.features/16.collaboration",{"title":224,"path":225,"stem":226},"Email","/features/email","6.features/17.email",{"title":228,"path":229,"stem":230},"Pages (CMS)","/features/pages","6.features/18.pages",{"title":232,"path":233,"stem":234},"Bookings","/features/bookings","6.features/19.bookings",{"title":236,"path":237,"stem":238},"Sales (POS)","/features/sales","6.features/20.sales",{"title":240,"path":241,"stem":242},"Rich Text Editor","/features/rich-text","6.features/6.rich-text",{"title":244,"path":245,"stem":246},"Assets Package (BETA)","/features/assets","6.features/7.assets",{"title":248,"path":249,"stem":250},"Events Package (BETA)","/features/events","6.features/9.events",{"title":252,"path":253,"stem":254,"children":255},"Advanced","/advanced","7.advanced",[256,259,263,267,271,275],{"title":257,"path":253,"stem":258},"Advanced Topics","7.advanced/index",{"title":260,"path":261,"stem":262},"Team-Based Authentication","/advanced/team-based-auth","7.advanced/2.team-based-auth",{"title":264,"path":265,"stem":266},"Conditional Fields & Dependent Dropdowns","/advanced/conditional-fields","7.advanced/3.conditional-fields",{"title":268,"path":269,"stem":270},"Bulk Operations","/advanced/bulk-operations","7.advanced/4.bulk-operations",{"title":272,"path":273,"stem":274},"Optimistic Updates & Custom Validation","/advanced/optimistic-updates","7.advanced/5.optimistic-updates",{"title":276,"path":277,"stem":278},"Rate Limiting","/advanced/rate-limiting","7.advanced/6.rate-limiting",{"title":280,"path":281,"stem":282,"children":283},"Api Reference","/api-reference","8.api-reference",[284,287,291,295,299,303,332],{"title":285,"path":281,"stem":286},"API Reference","8.api-reference/index",{"title":288,"path":289,"stem":290},"Types & Configuration","/api-reference/types","8.api-reference/3.types",{"title":292,"path":293,"stem":294},"Server Utilities","/api-reference/server","8.api-reference/4.server",{"title":296,"path":297,"stem":298},"Internal API","/api-reference/internal-api","8.api-reference/5.internal-api",{"title":300,"path":301,"stem":302},"useCollectionItem","/api-reference/use-collection-item","8.api-reference/6.use-collection-item",{"title":304,"path":305,"stem":306,"children":307},"Components Reference","/api-reference/components","8.api-reference/components/index",[308,309,313,317,320,324,328],{"title":304,"path":305,"stem":306},{"title":310,"path":311,"stem":312},"Content Components","/api-reference/components/content-components","8.api-reference/components/content-components",{"title":314,"path":315,"stem":316},"Form Components","/api-reference/components/form-components","8.api-reference/components/form-components",{"title":179,"path":318,"stem":319},"/api-reference/components/layout-components","8.api-reference/components/layout-components",{"title":321,"path":322,"stem":323},"Modal Components","/api-reference/components/modal-components","8.api-reference/components/modal-components",{"title":325,"path":326,"stem":327},"Table Components","/api-reference/components/table-components","8.api-reference/components/table-components",{"title":329,"path":330,"stem":331},"Utility Components","/api-reference/components/utility-components","8.api-reference/components/utility-components",{"title":333,"path":334,"stem":335,"children":336},"Composables Reference","/api-reference/composables","8.api-reference/composables/index",[337,338,342,346,350,354,358],{"title":333,"path":334,"stem":335},{"title":339,"path":340,"stem":341},"Data Composables","/api-reference/composables/data-composables","8.api-reference/composables/data-composables",{"title":343,"path":344,"stem":345},"Form Composables","/api-reference/composables/form-composables","8.api-reference/composables/form-composables",{"title":347,"path":348,"stem":349},"Mutation Composables","/api-reference/composables/mutation-composables","8.api-reference/composables/mutation-composables",{"title":351,"path":352,"stem":353},"Query Composables","/api-reference/composables/query-composables","8.api-reference/composables/query-composables",{"title":355,"path":356,"stem":357},"Table Composables","/api-reference/composables/table-composables","8.api-reference/composables/table-composables",{"title":359,"path":360,"stem":361},"Utility Composables","/api-reference/composables/utility-composables","8.api-reference/composables/utility-composables",{"title":363,"path":364,"stem":365,"children":366},"Reference","/reference","9.reference",[367,369,373,377],{"title":363,"path":364,"stem":368},"9.reference/index",{"title":370,"path":371,"stem":372},"Conventions","/reference/conventions","9.reference/1.conventions",{"title":374,"path":375,"stem":376},"Frequently Asked Questions","/reference/faq","9.reference/2.faq",{"title":378,"path":379,"stem":380},"Glossary","/reference/glossary","9.reference/3.glossary",[382,386,392,397,402,407,411,416,421,424,429,434,439,444,450,455,460,465,470,474,477,482,487,492,497,502,507,512,516,519,524,529,534,539,544,549,554,558,563,568,573,578,581,586,591,596,601,606,610,613,617,622,627,632,637,641,645,649,654,659,663,667,672,676,681,686,691,696,700,704,708,712,716,720,724,729,734,739,743,749,754,759,763,767,771,776,781,786,791,795,799,803,808,813,817,821,826,830,834,838,843,848,853,857,861,865,869,874,879,884,888,892,897,902,907,911,915,920,925,930,934,939,944,948,953,958,963,968,973,977,981,985,990,995,1000,1005,1010,1013,1018,1023,1027,1032,1037,1042,1046,1050,1055,1060,1064,1069,1074,1079,1084,1089,1094,1099,1104,1108,1113,1118,1123,1128,1132,1136,1140,1145,1150,1155,1160,1164,1168,1172,1177,1182,1186,1190,1194,1198,1201,1206,1211,1216,1221,1226,1231,1236,1239,1244,1249,1254,1259,1264,1268,1271,1276,1280,1285,1290,1295,1300,1304,1309,1314,1319,1324,1328,1333,1338,1343,1347,1352,1357,1362,1366,1371,1376,1381,1385,1390,1395,1400,1404,1409,1414,1418,1423,1428,1433,1437,1440,1444,1449,1454,1459,1464,1469,1474,1479,1484,1489,1494,1499,1504,1509,1514,1517,1522,1527,1532,1537,1542,1545,1550,1555,1560,1565,1568,1571,1576,1579,1583,1588,1592,1596,1601,1606,1610,1615,1620,1625,1630,1633,1638,1643,1648,1652,1657,1662,1667,1672,1676,1681,1686,1691,1694,1699,1704,1709,1714,1719,1722,1727,1732,1737,1742,1747,1752,1757,1761,1764,1769,1774,1778,1783,1787,1792,1796,1801,1805,1809,1814,1818,1823,1827,1832,1835,1840,1845,1850,1855,1860,1865,1870,1875,1880,1885,1890,1895,1900,1905,1909,1914,1919,1924,1929,1932,1937,1942,1947,1952,1956,1961,1964,1969,1972,1977,1982,1987,1991,1996,2001,2006,2009,2014,2019,2024,2029,2034,2037,2042,2047,2052,2056,2061,2066,2069,2074,2079,2084,2089,2093,2096,2101,2106,2110,2114,2118,2123,2128,2133,2138,2142,2147,2152,2157,2162,2167,2171,2176,2181,2186,2190,2195,2200,2203,2208,2213,2218,2223,2228,2233,2237,2242,2247,2250,2254,2259,2262,2267,2272,2277,2282,2286,2289,2293,2298,2303,2308,2313,2318,2323,2328,2333,2338,2341,2346,2351,2356,2361,2365,2368,2373,2378,2383,2388,2393,2398,2403,2408,2413,2418,2423,2428,2433,2438,2443,2448,2453,2457,2460,2465,2470,2474,2478,2483,2488,2492,2497,2502,2506,2511,2516,2520,2525,2530,2535,2540,2544,2547,2551,2556,2561,2566,2571,2576,2581,2585,2590,2595,2599,2602,2607,2611,2616,2621,2626,2631,2636,2641,2645,2650,2655,2660,2665,2670,2675,2680,2685,2690,2695,2700,2705,2710,2715,2720,2725,2730,2735,2739,2744,2749,2754,2759,2764,2769,2773,2777,2782,2785,2790,2795,2800,2805,2809,2812,2817,2822,2827,2832,2837,2842,2846,2851,2855,2859,2862,2865,2870,2874,2879,2884,2889,2894,2899,2904,2909,2914,2919,2923,2928,2931,2936,2941,2946,2951,2956,2961,2966,2971,2976,2981,2986,2991,2996,3001,3005,3008,3013,3018,3023,3027,3032,3037,3042,3047,3052,3057,3062,3067,3072,3077,3082,3087,3092,3097,3102,3106,3111,3116,3120,3125,3130,3135,3140,3145,3149,3153,3158,3163,3168,3173,3178,3183,3188,3193,3198,3202,3206,3209,3214,3219,3224,3229,3234,3238,3243,3248,3253,3258,3263,3268,3273,3278,3283,3288,3291,3296,3301,3306,3311,3316,3320,3323,3326,3331,3336,3341,3345,3350,3355,3360,3365,3370,3374,3379,3384,3389,3394,3398,3402,3407,3412,3417,3421,3425,3429,3433,3439,3443,3448,3453,3458,3462,3466,3470,3474,3479,3484,3488,3492,3496,3501,3506,3511,3514,3519,3524,3529,3534,3539,3544,3549,3554,3559,3564,3569,3573,3578,3583,3588,3593,3598,3603,3608,3613,3618,3621,3626,3631,3636,3641,3646,3651,3656,3659,3664,3668,3673,3678,3683,3688,3693,3696,3701,3705,3709,3714,3719,3724,3728,3733,3738,3743,3747,3752,3757,3762,3767,3772,3777,3782,3787,3791,3794,3797,3801,3804,3809,3814,3819,3824,3829,3834,3838,3843,3848,3852,3856,3860,3863,3868,3873,3878,3883,3888,3893,3897,3901,3904,3908,3913,3918,3923,3928,3933,3938,3943,3948,3953,3958,3963,3968,3972,3977,3980,3985,3990,3995,3999,4003,4007,4012,4015,4020,4025,4030,4033,4038,4043,4048,4053,4057,4060,4064,4069,4074,4078,4082,4087,4092,4097,4102,4107,4112,4117,4122,4127,4132,4137,4142,4146,4151,4156,4161,4166,4171,4176,4181,4186,4190,4195,4198,4203,4208,4213,4218,4221,4225,4230,4235,4239,4244,4248,4253,4258,4263,4266,4271,4276,4281,4285,4288,4292,4297,4301,4306,4311,4316,4321,4326,4331,4335,4339,4344,4347,4352,4357,4362,4367,4372,4376,4381,4386,4391,4396,4400,4405,4410,4414,4417,4420,4424,4427,4432,4437,4442,4447,4452,4455,4460,4465,4470,4475,4479,4483,4487,4491,4494,4499,4504,4508,4511,4516,4521,4526,4531,4536,4541,4546,4551,4556,4560,4565,4570,4575,4578,4583,4587,4591,4595,4598,4603,4608,4613,4618,4623,4628,4632,4637,4642,4647,4652,4657,4662,4667,4671,4676,4681,4686,4691,4694,4699,4704,4709,4713,4716,4720,4725,4730,4734,4739,4744,4749,4754,4759,4764,4769,4774,4779,4784,4789,4794,4799,4804,4808,4813,4817,4822,4827,4832,4837,4840,4844,4848,4852,4855,4860,4865,4870,4875,4879,4882,4885,4889,4894,4899,4904,4909,4914,4918,4923,4928,4933,4938,4943,4948,4952,4957,4961,4966,4971,4976,4980,4985,4990,4994,4998,5003,5008,5012,5016,5021,5026,5031,5036,5040,5044,5048,5053,5057,5061,5066,5070,5074,5079,5083,5087,5091,5096,5101,5105,5110,5114,5119,5123,5127,5131,5136,5140,5144,5148,5153,5157,5161,5166,5169,5174,5179,5183,5187,5192,5197,5202,5206,5210,5215,5220,5225,5229,5232,5237,5242,5247,5251,5256,5261,5266,5269,5274,5279,5284,5289,5294,5299,5302,5307,5312,5317,5321,5324,5329,5333,5337,5342,5347,5352,5356,5361,5365,5370,5375,5379,5384,5388,5392,5396,5401,5406,5410,5414,5418,5422,5427,5431,5435,5439,5444,5448,5453,5458,5463,5468,5473,5478,5482,5487,5491,5494,5499,5504,5509,5513,5518,5523,5528,5533,5536,5540,5544,5549,5554,5557,5562,5567,5572,5576,5581,5586,5590,5593,5597,5602,5605,5610,5615,5620,5624,5628,5633,5638,5643,5648,5653,5658,5663,5668,5673,5678,5683,5688,5693,5698,5703,5708,5713,5718,5723,5727,5732,5737,5742,5747,5752,5757,5762,5767,5772,5777,5782,5786,5791,5794,5799,5804,5808,5813,5818,5823,5827,5832,5837,5842,5847,5851,5856,5860,5865,5870,5875,5880,5883,5888,5893,5898,5903,5908,5911,5916,5921,5926,5931,5936,5939,5944,5949,5954,5959,5964,5969,5974,5979,5983,5988,5993,5998,6003,6008,6012,6016,6021,6026,6031,6036,6041,6046,6051,6056,6061,6066,6069,6074,6079,6084,6088,6091,6094,6098,6102,6106,6110,6113,6118,6122,6126,6130,6135,6139,6144,6149,6154,6158,6162,6167,6171,6176,6181,6185,6190,6194,6199,6203,6206,6211,6215,6220,6225,6229,6234,6239,6244,6248,6252,6257,6262,6266,6270,6275,6279,6284,6289,6294,6297,6302,6307,6312,6317,6321,6324,6327,6331,6336,6340,6343,6347,6352,6356,6361,6364,6369,6374,6377,6382,6386,6390,6394,6399,6403,6407,6412,6417,6421,6425,6429,6434,6438,6442,6447,6451,6456,6460,6464,6469,6473,6477,6481,6485,6490,6495,6500,6505,6510,6515,6519,6524,6529,6534,6539,6544,6549,6554,6558,6561,6566,6570,6574,6579,6582,6587,6592,6597,6601,6605,6609,6612,6616,6619,6624,6629,6634,6639,6643,6648,6653,6658,6663,6668,6673,6678,6683,6688,6693,6696,6701,6706,6711,6716,6719,6724,6729,6734,6738,6743,6748,6753,6758,6761,6766,6771,6776,6781,6785,6790,6793,6798,6803,6806,6811,6816,6821,6826,6830,6833,6837,6841,6844,6849,6854,6858,6863,6868,6871,6876,6881,6886,6891,6895,6900,6904,6908,6912,6915,6920,6925,6930,6935,6940,6945,6949,6952,6955,6959,6963,6967,6970,6975,6980,6985,6990,6993,6997,7001,7006,7011,7016,7021,7025,7030,7034,7039,7044,7048,7053,7058,7063,7067,7072,7076,7079,7084,7089,7094,7097,7101,7106,7110,7114,7117,7122,7127,7131,7136,7141,7146,7151,7156,7161,7166,7171,7176,7180,7184,7187,7191,7195,7199,7204,7209,7214,7219,7224,7229,7234,7239,7244,7249,7254,7258,7263,7267,7272,7276,7281,7286,7290,7294,7298,7300,7304,7308,7312,7316,7321,7326,7329,7334,7339,7344,7349,7354,7359,7364,7369,7374,7379,7383,7388,7393,7398,7402,7407,7412,7416,7421,7426,7430,7434,7439,7444,7449,7453,7457,7460,7464,7468,7471,7476,7481,7486,7491,7496,7499,7504,7508,7512,7517,7522,7527,7531,7535,7540,7545,7550,7555,7560,7565,7570,7574,7579,7584,7589,7594,7598,7602,7606,7609,7613,7617,7620,7624,7628,7632,7636,7641,7645,7649,7654,7658,7663,7667,7671,7675,7680,7685,7689,7693,7698,7702,7707,7711,7716,7720,7724,7728,7731,7736,7740,7744,7748,7752,7757,7761,7764,7767,7771,7775,7779,7782,7786,7789,7793,7798,7801,7805,7809,7813,7816,7821,7825,7829,7833,7837,7842,7847,7851,7855,7859,7863,7867,7871,7876,7881,7885,7889,7893,7897,7902,7906,7911,7915,7920,7923,7927,7932,7936,7941,7944,7949,7954,7959,7963,7966,7971,7975,7980,7984,7987,7992,7995,8000,8005,8010,8015,8019,8024,8028,8031,8035,8039,8042,8046,8050,8054,8058,8062,8067,8072,8077,8080,8085,8090,8095,8100,8104,8108,8113,8118,8123,8128,8133,8138,8142,8146,8151,8155,8160,8164,8169,8173,8178,8182,8187,8191,8196,8200,8204,8209,8214,8217,8221,8226,8231,8236,8240,8245,8250,8255,8259,8264,8269,8274,8279,8283,8288,8293,8296,8301,8305,8309,8312,8317,8322,8327,8332,8335,8339,8343,8348,8351,8354,8359,8362,8367,8372,8377,8382,8387,8392,8397,8402,8407,8412,8417,8422,8427,8432,8437,8441,8446,8451,8456,8461,8466,8470,8474,8478,8483,8486,8490,8494,8499,8504,8508,8513,8518,8522,8525,8529,8534,8537,8541,8545,8549,8554,8559,8564,8569,8573,8576,8581,8585,8590,8594,8598,8601,8605,8610,8615,8619,8623,8627,8630,8634,8638,8642,8645,8650,8654,8659,8664,8669,8673,8678,8682,8686,8691,8696,8700,8703,8707,8712,8715,8719,8724,8729,8733,8738,8743,8748,8753,8757,8762,8767,8772,8776,8781,8786,8791,8796,8801,8805,8810,8814,8819,8824,8829,8834,8839,8843,8847,8852,8856,8861,8866,8871,8875,8880,8885,8890,8894,8899,8904,8908,8912,8917,8922,8927,8932,8937,8941,8946,8951,8956,8961,8965,8970,8974,8979,8984,8988,8993,8998,9003,9006,9011,9016,9021,9026,9031,9035,9040,9045,9050,9054,9057,9062,9067,9072,9077,9082,9087,9092,9097,9102,9107,9112,9116,9121,9125,9130,9134,9138,9142,9146,9151,9156,9161,9166,9171,9175,9179,9183,9187,9192,9196,9201,9206,9211,9216,9221,9224,9229,9234,9239,9244,9247,9252,9257,9262,9267,9272,9276,9279,9284,9289,9294,9299,9303,9306,9310,9314,9318,9321,9326,9331,9336,9340,9345,9350,9354,9359,9364,9368,9372,9377,9382,9387,9392,9395,9400,9405,9410,9415,9419,9424,9429,9434,9437,9442,9447,9451,9456,9459,9464,9468,9472,9477,9482,9486,9490,9494,9499,9504,9509,9514,9518,9522,9526,9531,9536,9541,9545,9550,9555,9560,9564,9567,9572,9576,9580,9585,9590,9595,9599,9603,9607,9612,9615,9619,9624,9629,9634,9638,9643,9648,9653,9658,9661,9666,9671,9676,9680,9684,9688,9693,9698,9703,9708,9713,9718,9723,9728,9731,9736,9741,9744,9749,9754,9759,9764,9768,9772,9777,9782,9787,9792,9796,9799,9804,9809,9814,9819,9823,9826,9831,9836,9841,9846,9850,9854,9858,9863,9867,9871,9876,9880,9884,9888,9892,9897,9902,9906,9911,9915,9919,9922,9927,9932,9937,9942,9947,9952,9957,9962,9966,9971,9976,9979,9984,9989,9994,9999,10004,10008,10012,10015,10020,10025,10030,10035,10040,10045,10050,10054,10059,10062,10067,10072,10077,10082,10087,10092,10096,10101,10104,10109,10114,10119,10124,10129,10134,10138,10142,10146,10150,10154,10159,10163,10166,10170,10174,10179,10183,10187,10191,10196,10200,10204,10209,10213,10217,10222,10227,10232,10236,10239,10244,10249,10254,10259,10264,10269,10274,10278,10282,10285,10289,10294,10299,10304,10308,10312,10315,10320,10325,10329,10333,10338,10343,10348,10353,10357,10361,10364,10369,10374,10379,10384,10389,10393,10397,10402,10407,10411,10415,10418,10422,10427,10430,10435,10440,10445,10449,10454,10459,10464,10468,10472,10476,10479,10484,10489,10494,10499,10504,10509,10514,10518,10522,10525,10530,10535,10540,10545,10550,10555,10559,10563,10567,10571,10575,10578,10583,10588,10593,10598,10603,10608,10612,10616,10620,10623,10628,10632,10637,10642,10646,10651,10656,10661,10665,10669,10673,10676,10681,10686,10691,10696,10701,10706,10710,10715,10719,10724,10728,10732,10735,10739,10744,10749,10754,10759,10764,10769,10773,10776,10779,10783,10787,10791,10795,10799,10803,10808,10812,10815,10820,10824,10828,10831,10836,10841,10845,10849,10854,10858,10863,10868,10871,10876,10880,10884,10889,10894,10899,10904,10908,10913,10918,10922,10927,10932,10935,10940,10945,10950,10955,10959,10964,10967,10972,10977,10982,10986,10990,10995,11000,11004,11008,11013,11016,11021,11026,11031,11036,11040,11044,11047,11052,11057,11062,11067,11071,11075,11079,11083,11087,11092,11097,11101,11106,11111,11114,11119,11124,11129,11134,11138,11142,11147,11151,11155,11159,11163,11168,11173,11178,11182,11187,11192,11197,11201,11205,11209,11214,11219,11223,11228,11232,11236,11239,11243,11248,11253,11257,11262,11267,11271,11275,11279,11282,11286,11291,11296,11301,11306,11311,11316,11320,11324,11327,11331,11336,11340,11345,11349,11354,11359,11363,11367,11370,11375,11380,11385,11390,11395,11400,11404,11409,11414,11418,11421,11425,11429,11433,11438,11443,11447,11451,11455,11460,11464,11469,11474,11479,11483,11488,11493,11497,11502,11506,11509,11514,11518,11522,11525,11530,11535,11540,11544,11547,11551,11556,11561,11566,11571,11575,11579,11583,11586,11591,11596,11600,11605,11609,11612,11617,11622,11626,11630,11635,11639,11643,11647,11651,11656,11660,11665,11669,11673,11677,11681,11686,11690,11694,11699,11703,11707,11711,11715,11720,11725,11729,11733,11738,11742,11746,11750,11754,11759,11764,11769,11773,11777,11782,11786,11790,11794,11797,11801,11806,11810,11814,11819,11824,11829,11833,11837,11841,11844,11848,11853,11858,11863,11868,11873,11877,11881,11885,11888,11892,11896,11901,11906,11911,11916,11921,11925,11929,11933,11936,11940,11944,11949,11953,11958,11963,11968,11972,11976,11980,11983,11987,11992,11997,12001,12006,12011,12015,12018,12022,12027,12031,12036,12039,12043,12048,12053,12058,12062,12067,12071,12075,12080,12084,12088,12092,12095,12100,12105,12110,12115,12119,12124,12129,12134,12139,12143,12147,12151,12156,12161,12166,12170,12174,12178,12182,12187,12192,12196,12200,12203,12207,12211,12216,12221,12226,12230,12234,12238,12241,12245,12250,12255,12260,12265,12270,12274,12277,12282,12286,12290,12294,12298,12302,12307,12312,12317,12322,12326,12330,12335,12339,12343,12347,12352,12357,12361,12366,12371,12376,12380,12384,12389,12393,12398,12403,12408,12413,12418,12422,12427,12431,12436,12440,12444,12449,12453,12457,12461,12465,12470,12475,12480,12485,12490,12494,12497,12502,12507,12512,12516,12520,12523,12528,12532,12536,12540,12544,12549,12554,12559,12564,12569,12574,12579,12584,12589,12593,12598,12602,12606,12610,12613,12616,12620,12624,12628,12632,12636,12640,12644,12648,12651,12656,12660,12664,12668,12672,12676,12681,12686,12690,12694,12699,12704,12709,12714,12718,12723,12728,12733,12737,12741,12744,12749,12753,12757,12761,12765,12770,12775,12779,12783,12786,12790,12794,12798,12802,12807,12811,12816,12821,12826,12831,12835,12839,12843,12847,12851,12855,12859,12864,12868,12873,12878,12882,12886,12890,12894,12898,12902,12906,12910,12915,12920,12925,12929,12933,12937,12940,12945,12949,12953,12957,12962,12967,12972,12977,12982,12987,12992,12997,13002,13007,13011,13016,13020,13024,13028,13031,13035,13039,13042,13047,13052,13056,13061,13065,13070,13075,13080,13085,13090,13094,13098,13103,13106,13110,13114,13118,13122,13127,13131,13135,13139,13144,13149,13154,13158,13162,13166,13171,13175,13179,13183,13187,13192,13197,13202,13207,13212,13216,13221,13226,13231,13235,13239,13244,13248,13252,13257,13262,13267,13272,13277,13282,13287,13292,13297,13302,13307,13311,13315,13320,13324,13328,13332,13336,13341,13346,13351,13356,13361,13365,13370,13375,13380,13384,13388,13392,13397,13401,13405,13409,13414,13419,13424,13429,13433,13438,13443,13448,13453,13458,13463,13467,13472,13476,13481,13485,13489,13493,13498,13502,13507,13512,13517,13522,13527,13531,13536,13540,13544,13548,13552,13556,13560,13564,13569,13574,13579,13584,13589,13594,13599,13604,13608,13612,13616,13620,13623,13627,13632,13635,13639,13644,13649,13654,13659,13662,13667,13672,13677,13681,13686,13690,13695,13699,13704,13709,13713,13718,13722,13726,13731,13736,13740,13745,13750,13755,13759,13762,13766,13771,13776,13781,13786,13789,13794,13799,13804,13807,13812,13817,13822,13826,13831,13836,13841,13845,13850,13855,13859,13864,13869,13872,13877,13880,13885,13890,13893,13898,13903,13907,13912,13916,13919,13923,13928,13932,13936,13940,13945,13950,13955,13959,13964,13969,13973,13978,13983,13987,13992,13997,14001,14006,14011,14015,14019,14023,14028,14032,14037,14042,14046,14051,14055,14060,14064,14068,14072,14077,14081,14086,14091,14096,14100,14105,14110,14114,14118,14123,14128,14132,14136,14140,14145,14149,14152,14156],{"id":6,"title":10,"titles":383,"content":384,"level":385},[],"Learn what Nuxt Crouton is, what it does, and how it can help you build CRUD applications faster. Nuxt Crouton is a code generator and composable library that helps you build CRUD (Create, Read, Update, Delete) applications in Nuxt 4. It provides an opinionated approach to types and CRUD patterns, with built-in authentication, team management, and admin features to help you ship production-ready applications faster.",1,{"id":387,"title":388,"titles":389,"content":390,"level":391},"/getting-started#what-it-does","What It Does",[10],"Nuxt Crouton generates working CRUD interfaces including list views, forms, TypeScript types, and composables with validation. It also provides optional database migrations and smart data caching built on useFetch. Additional features include modal and slideover management, toast notifications, automatic cache invalidation, translation support, and team-based auth utilities.",2,{"id":393,"title":394,"titles":395,"content":396,"level":391},"/getting-started#what-its-not","What It's Not",[10],"Nuxt Crouton is not a runtime admin panel like Strapi or Directus. It's not a framework but rather works within Nuxt. It doesn't replace your database—instead, it generates code that works with your existing database. And while it generates frontend components and backend stubs, it's not a complete backend solution.",{"id":398,"title":399,"titles":400,"content":401,"level":391},"/getting-started#key-principle","Key Principle",[10],"The philosophy behind Nuxt Crouton is simple: Generate → Customize → Own. Start by generating 80% of your working code in 30 seconds. Then customize the generated code however you need—it's yours to edit freely. Finally, own and maintain it as part of your codebase.",{"id":403,"title":404,"titles":405,"content":406,"level":391},"/getting-started#the-rails-scaffold-approach","The Rails Scaffold Approach",[10],"If you're familiar with Rails scaffolding, Nuxt Crouton works similarly. Both generate starting code that you own immediately and can customize as needed, while the core framework stays stable and receives updates independently. # Rails\nrails generate scaffold Post title:string body:text\n\n# Nuxt Crouton\nnpx crouton-generate blog posts --fields-file post-schema.json",{"id":408,"title":85,"titles":409,"content":410,"level":391},"/getting-started#architecture",[10],"Nuxt Crouton uses a two-layer architecture that separates your customizable generated code from the stable core library. You can also organize collections by domain using Nuxt layers for better maintainability and reusability. Learn more about the Architecture and how to structure your application using domain-driven design.",{"id":412,"title":413,"titles":414,"content":415,"level":391},"/getting-started#built-in-features","Built-In Features",[10],"Nuxt Crouton is a complete solution that builds on Nuxt, Nuxt UI, and NuxtHub—tools you may already be using. The core package includes: Team-based authentication with Better Auth (OAuth, passkeys, 2FA)Super admin dashboard for user and team managementMulti-language support with translation managementNuxtHub integration for database, KV, and blob storageDrizzle ORM ready to go Nuxt Crouton lets you add CRUD functionality and iterate quickly. But since every project has unique requirements, it takes a scaffolding approach rather than being a framework. You generate the code you need, then customize it completely. The core components (like modal management and data operations) are opt-in, just like the internationalization and Tiptap editor support. Everything works with Nuxt layers for easy composition, giving you the best of both worlds: reusability with complete control.",{"id":417,"title":418,"titles":419,"content":420,"level":391},"/getting-started#next-steps","Next Steps",[10],"Ready to get started? Continue to Installation to set up Nuxt Crouton in your project. html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":14,"title":13,"titles":422,"content":423,"level":385},[],"Install and configure Nuxt Crouton in your nuxt 4 project. Here's how to install and configure Nuxt Crouton in your Nuxt 4 project. Nuxt 4 Required: Nuxt Crouton requires Nuxt 4 and Nuxt UI 4. It will not work with Nuxt 3 due to the app/ directory structure convention used by layers. See Troubleshooting - Layer Pages 404 if you encounter routing issues.",{"id":425,"title":426,"titles":427,"content":428,"level":391},"/getting-started/installation#prerequisites","Prerequisites",[13],"You'll need Node.js 18 or 20+, pnpm (recommended) or npm, Nuxt 4.x, and Nuxt UI 4.x.",{"id":430,"title":431,"titles":432,"content":433,"level":391},"/getting-started/installation#install-nuxt-crouton","Install Nuxt Crouton",[13],"# Install main module\npnpm add @fyit/crouton\n\n# Install generator (dev dependency)\npnpm add -D @fyit/crouton-cli",{"id":435,"title":436,"titles":437,"content":438,"level":391},"/getting-started/installation#configure-nuxt","Configure Nuxt",[13],"Add Nuxt Crouton to your nuxt.config.ts: export default defineNuxtConfig({\n  modules: ['@fyit/crouton'],\n\n  // Optional: Enable additional features\n  crouton: {\n    // Core features (enabled by default)\n    auth: true,\n    admin: true,\n    i18n: true,\n\n    // Optional features (enable as needed)\n    editor: false,     // Rich text editing\n    assets: false,     // Asset management\n    ai: false,         // AI integration\n    email: false,      // Email templates\n    pages: false,      // CMS pages\n    // ... see Package Architecture for all options\n  },\n\n  // Recommended: Enable hot reload for development\n  vite: {\n    server: {\n      watch: {\n        ignored: ['!**/node_modules/@fyit/**']\n      }\n    },\n    optimizeDeps: {\n      exclude: ['@fyit/crouton']\n    }\n  }\n}) Auto-included: The module automatically includes @fyit/crouton-core (CRUD), @fyit/crouton-auth (authentication), @fyit/crouton-admin (super admin), and @fyit/crouton-i18n (translations). No separate installation needed for these core features.",{"id":440,"title":441,"titles":442,"content":443,"level":391},"/getting-started/installation#configure-tailwind-css","Configure Tailwind CSS",[13],"Nuxt Crouton layers use Tailwind CSS classes that need to be scanned by the Tailwind compiler. Due to how Tailwind CSS v4 works with Nuxt Layers, you need to add the @source directive to your app's CSS file to ensure layer components are properly styled. Automatic Setup: The collection generator (crouton-generate) and installer (crouton-install) will automatically configure the @source directive for you! If you run either of these commands, you can skip manual configuration.",{"id":445,"title":446,"titles":447,"content":448,"level":449},"/getting-started/installation#automatic-setup-recommended","Automatic Setup (Recommended)",[13,441],"When you run the generator or installer, it will: Detect your existing CSS file (or create app/assets/css/main.css)Add the required @source directive automaticallyUpdate nuxt.config.ts if a new CSS file was created # The generator sets up CSS automatically\nnpx crouton-generate \u003Clayer> \u003Ccollection> --fields-file=./schema.json",3,{"id":451,"title":452,"titles":453,"content":454,"level":449},"/getting-started/installation#manual-setup","Manual Setup",[13,441],"If automatic setup fails or you prefer manual configuration, create or update your main CSS file (e.g., app/assets/css/main.css): @import \"tailwindcss\";\n@import \"@nuxt/ui\";\n\n/* Scan Nuxt Crouton layers - REQUIRED for proper styling */\n@source \"../../../node_modules/@fyit/crouton*/app/**/*.{vue,js,ts}\"; The @source directive path is relative to your CSS file location. Adjust the ../ depth based on where your CSS file is located:If CSS is at app/assets/css/main.css: use \"../../../node_modules/...\"If CSS is at app.css: use \"../node_modules/...\" Why is this needed? Tailwind CSS v4 doesn't automatically scan external layers in node_modules. The @source directive explicitly tells Tailwind where to find classes used in layer components. Without this, hover states and dynamic classes won't work. Learn more",{"id":456,"title":457,"titles":458,"content":459,"level":449},"/getting-started/installation#for-multiple-layers","For Multiple Layers",[13,441],"The wildcard pattern @fyit/crouton*/ automatically scans all installed Nuxt Crouton layers: @fyit/crouton@fyit/crouton-i18n@fyit/crouton-editor@fyit/crouton-assets Alternatively, you can list each layer explicitly: @source \"../../../node_modules/@fyit/crouton/app/**/*.{vue,js,ts}\";\n@source \"../../../node_modules/@fyit/crouton-i18n/app/**/*.{vue,js,ts}\";\n@source \"../../../node_modules/@fyit/crouton-editor/app/**/*.{vue,js,ts}\";",{"id":461,"title":462,"titles":463,"content":464,"level":391},"/getting-started/installation#verify-installation","Verify Installation",[13],"Check that the generator is installed correctly: npx crouton-generate --help You should see: Usage: crouton-generate \u003Clayer> \u003Ccollection> [options]",{"id":466,"title":467,"titles":468,"content":469,"level":391},"/getting-started/installation#pre-generation-checklist","Pre-Generation Checklist",[13],"Before running the generator, ensure you've completed these steps: Install the packagepnpm add @fyit/crouton\npnpm add -D @fyit/crouton-cli\nAdd to nuxt.config.tsThe generator checks that the module is properly configured:modules: ['@fyit/crouton']\nCreate your schema file(s)Create JSON schema files defining your collection fields. See Schema Format.Run the generatornpx crouton-generate \u003Clayer> \u003Ccollection> --fields-file=./schema.json Common issue: If you get a \"Missing Required Dependencies\" error even though the package is installed, make sure you've added @fyit/crouton to the modules array in nuxt.config.ts.",{"id":471,"title":418,"titles":472,"content":473,"level":391},"/getting-started/installation#next-steps",[13],"Now that Nuxt Crouton is installed, continue to Usage to generate your first collection. html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":19,"title":18,"titles":475,"content":476,"level":385},[],"Generate your first CRUD collection in 30 seconds. Let's generate your first working CRUD collection in under a minute.",{"id":478,"title":479,"titles":480,"content":481,"level":391},"/getting-started/usage#_1-create-your-schema","1. Create Your Schema",[18],"Start by creating a JSON file that defines your collection fields: cat > product-schema.json \u003C\u003C 'EOF'\n[\n  { \"name\": \"name\", \"type\": \"string\" },\n  { \"name\": \"description\", \"type\": \"text\" },\n  { \"name\": \"price\", \"type\": \"number\" },\n  { \"name\": \"inStock\", \"type\": \"boolean\" }\n]\nEOF",{"id":483,"title":484,"titles":485,"content":486,"level":391},"/getting-started/usage#_2-generate-the-collection","2. Generate the Collection",[18],"Now run the generator command: npx crouton-generate shop products --fields-file product-schema.json This creates several files in your project: a List.vue component for table and list views, a _Form.vue component for creating and editing items, a useShopProducts.ts composable containing validation, column definitions, and defaults, and a types.ts file with TypeScript types. layers/shop/\n  └── collections/\n      └── products/\n          ├── app/\n          │   ├── components/\n          │   │   ├── List.vue            # Table/list view\n          │   │   └── _Form.vue           # Create/edit form\n          │   └── composables/\n          │       └── useShopProducts.ts  # Validation, columns, defaults\n          └── types.ts                    # TypeScript types",{"id":488,"title":489,"titles":490,"content":491,"level":391},"/getting-started/usage#_3-register-collection","3. Register Collection",[18],"Next, register your collection in app.config.ts: export default defineAppConfig({\n  croutonCollections: {\n    shopProducts: {\n      name: 'shopProducts',\n      layer: 'shop',\n      componentName: 'ShopProductsForm',\n      apiPath: 'shop-products',\n    }\n  }\n})",{"id":493,"title":494,"titles":495,"content":496,"level":391},"/getting-started/usage#_4-add-croutonform-to-app","4. Add CroutonForm to App",[18],"Add the \u003CCroutonForm /> component to your app.vue. This component renders the modal/slideover forms for creating and editing items: \u003Ctemplate>\n  \u003CUApp>\n    \u003CNuxtPage />\n    \u003CCroutonForm />  \u003C!-- Required for inline editing -->\n  \u003C/UApp>\n\u003C/template> Important: Without \u003CCroutonForm />, the inline form editing functionality won't work. Forms won't open when you click create/edit buttons.",{"id":498,"title":499,"titles":500,"content":501,"level":391},"/getting-started/usage#_5-use-in-your-app","5. Use in Your App",[18],"Add the component to a page: \u003Ctemplate>\n  \u003CShopProductsList />\n\u003C/template> Nuxt Crouton provides CRUD endpoints automatically. Your generated components use these out of the box—no custom API routes needed.",{"id":503,"title":504,"titles":505,"content":506,"level":391},"/getting-started/usage#thats-it","That's It!",[18],"You now have a working CRUD interface with modal forms for creating, editing, and deleting items, a sortable table, type-safe code, and validation—all generated and ready to use.",{"id":508,"title":509,"titles":510,"content":511,"level":391},"/getting-started/usage#customize","Customize",[18],"The generated code belongs to your project, so you can edit it however you need. Here's an example of adding a custom field: \u003Ctemplate>\n  \u003CUForm :state=\"state\" :schema=\"schema\" @submit=\"handleSubmit\">\n    \u003C!-- Generated fields -->\n    \u003CUFormField label=\"Name\" name=\"name\">\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n\n    \u003C!-- Add your custom field -->\n    \u003CUFormField label=\"Category\" name=\"categoryId\">\n      \u003CUSelectMenu\n        v-model=\"state.categoryId\"\n        :options=\"categories\"\n        option-attribute=\"name\"\n      />\n    \u003C/UFormField>\n\n    \u003CCroutonFormActionButton\n      :action=\"action\"\n      :collection=\"collection\"\n      :loading=\"loading\"\n    />\n  \u003C/UForm>\n\u003C/template>",{"id":513,"title":418,"titles":514,"content":515,"level":391},"/getting-started/usage#next-steps",[18],"Learn about Collections and how they workUnderstand Data Operations for mutationsExplore Customization options html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}",{"id":24,"title":23,"titles":517,"content":518,"level":385},[],"Add optional Crouton modules like bookings, maps, and more to your Nuxt app. This guide covers how to add optional Crouton modules to your existing Nuxt app. Each module provides domain-specific functionality with its own database tables, components, and APIs. Auto-included packages: When you install @fyit/crouton, the auth, admin, and i18n packages are automatically included. You don't need to install them separately.",{"id":520,"title":521,"titles":522,"content":523,"level":391},"/getting-started/adding-modules#quick-start-with-crouton-add","Quick Start with crouton add",[23],"The fastest way to add modules is using the CLI command: # Add a single module (handles everything automatically)\ncrouton add auth\n\n# Add multiple modules at once\ncrouton add bookings i18n\n\n# Preview what will be done without making changes\ncrouton add auth --dry-run\n\n# List all available modules\ncrouton add --list The crouton add command automatically: Installs the package via your package manager (pnpm/yarn/npm)Updates nuxt.config.ts - adds to extends arrayUpdates server/db/schema.ts - adds schema exports (if applicable)Generates database migrationsApplies migrations to your database That's it! After running crouton add, restart your dev server and the module is ready to use.",{"id":525,"title":526,"titles":527,"content":528,"level":391},"/getting-started/adding-modules#available-modules","Available Modules",[23],"",{"id":530,"title":531,"titles":532,"content":533,"level":449},"/getting-started/adding-modules#auto-included-with-core","Auto-Included (with core)",[23,526],"These packages are automatically included when you install @fyit/crouton: PackageDescriptionAuthBetter Auth integration with teams, passkeys, 2FA, OAuthAdminSuper admin dashboard for user/team managementi18nMulti-language translations with database-backed team overrides",{"id":535,"title":536,"titles":537,"content":538,"level":449},"/getting-started/adding-modules#optional-add-ons","Optional Add-ons",[23,526],"Use crouton add \u003Cmodule> to install these optional packages: ModuleAliasDescriptionBookingsbookingsBooking system with locations, time slots, settingsEditoreditorRich text editor with TipTapAssetsassetsFile uploads and image managementEventseventsEvent tracking and audit trailsFlowflowVue Flow graph visualizationEmailemailEmail integration with Vue Email and ResendMapsmapsMap integration with MapboxAIaiAI integration with Anthropic ClaudeDevtoolsdevtoolsNuxt Devtools integration",{"id":540,"title":541,"titles":542,"content":543,"level":391},"/getting-started/adding-modules#cli-options","CLI Options",[23],"OptionDescription--skip-migrationsSkip running npx nuxt db:generate and db:migrate--skip-installSkip pnpm add (assume already installed)--dry-runPreview what would be done without making changes--forceForce reinstall even if already installed--listList all available modules",{"id":545,"title":546,"titles":547,"content":548,"level":391},"/getting-started/adding-modules#module-dependencies","Module Dependencies",[23],"Some optional modules require the core package (which includes auth). Since auth is auto-included with the core package, you only need to install the optional module: ModuleNotesbookingsWorks out of the box (auth auto-included) The CLI handles dependencies automatically when you use crouton add.",{"id":550,"title":551,"titles":552,"content":553,"level":391},"/getting-started/adding-modules#manual-installation-steps","Manual Installation Steps",[23],"If you prefer to install manually or need more control: Install the packagepnpm add @fyit/crouton-bookings\nAdd to nuxt.config.tsAdd the module to the extends array:export default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',       // Includes auth, admin, i18n\n    '@fyit/crouton-bookings'    // Add new module here\n  ],\n  modules: ['@nuxthub/core', '@nuxt/ui'],\n  hub: { db: 'sqlite' }\n})\nSimplified extends: You no longer need to list nuxt-crouton-auth, nuxt-crouton-admin, or nuxt-crouton-i18n separately—they're automatically included with the core package.Register database schemasUpdate your schema index to include the module's database tables:// Existing exports\nexport * from '@fyit/crouton-auth/server/database/schema/auth'\n\n// Add new module schemas\nexport * from '@fyit/crouton-bookings/server/database/schema'\nGenerate migrationsRun the migration generator to create SQL for the new tables:npx nuxt db:generate\nThis creates migration files in server/db/migrations/sqlite/.Apply migrationsApply the pending migrations to your database:npx nuxt db:migrate",{"id":555,"title":556,"titles":557,"content":528,"level":391},"/getting-started/adding-modules#module-specific-setup","Module-Specific Setup",[23],{"id":559,"title":560,"titles":561,"content":562,"level":449},"/getting-started/adding-modules#auth-module-auto-included","Auth Module (Auto-Included)",[23,556],"The auth module is automatically included with @fyit/crouton. No separate installation needed. Required environment variables: BETTER_AUTH_SECRET=your-secret-key-here\nBETTER_AUTH_URL=http://localhost:3000 Optional OAuth providers: GOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\nGITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET=",{"id":564,"title":565,"titles":566,"content":567,"level":449},"/getting-started/adding-modules#i18n-module-auto-included","i18n Module (Auto-Included)",[23,556],"The i18n module is automatically included with @fyit/crouton. No separate installation needed. Optional locale configuration: export default defineNuxtConfig({\n  i18n: {\n    locales: [\n      { code: 'en', file: 'en.json' },\n      { code: 'nl', file: 'nl.json' }\n    ],\n    langDir: './locales'\n  }\n})",{"id":569,"title":570,"titles":571,"content":572,"level":449},"/getting-started/adding-modules#bookings-module","Bookings Module",[23,556],"The bookings module provides a complete booking system. crouton add bookings The module provides components and composables. You'll typically generate collections using crouton config with a schema that references booking types.",{"id":574,"title":575,"titles":576,"content":577,"level":449},"/getting-started/adding-modules#assets-module","Assets Module",[23,556],"The assets module provides file upload and media library functionality. crouton add assets Required NuxtHub config: export default defineNuxtConfig({\n  hub: {\n    db: 'sqlite',\n    blob: true  // Required for assets\n  }\n})",{"id":579,"title":36,"titles":580,"content":528,"level":391},"/getting-started/adding-modules#troubleshooting",[23],{"id":582,"title":583,"titles":584,"content":585,"level":449},"/getting-started/adding-modules#_500-server-error-on-api-calls","500 Server Error on API calls",[23,36],"Symptom: API endpoints return 500 errors after adding a module. Cause: Database tables weren't created. The module's schema was added to nuxt.config.ts but migrations weren't generated/applied. Solution: # Generate migration for new tables\nnpx nuxt db:generate\n\n# Apply the migration\nnpx nuxt db:migrate If migrations were already generated but tables still don't exist: sqlite3 .data/db/sqlite.db \u003C server/db/migrations/sqlite/XXXX_migration.sql",{"id":587,"title":588,"titles":589,"content":590,"level":449},"/getting-started/adding-modules#cannot-resolve-entry-module-error","\"Cannot resolve entry module\" error",[23,36],"Symptom: Build fails with \"Cannot resolve entry module .nuxt/hub/db/schema.entry.ts\" Cause: Using hub: { database: true } instead of hub: { db: 'sqlite' }. Solution: // Wrong\nhub: { database: true }\n\n// Correct\nhub: { db: 'sqlite' }",{"id":592,"title":593,"titles":594,"content":595,"level":449},"/getting-started/adding-modules#module-components-not-found","Module components not found",[23,36],"Symptom: Components from the module aren't available in templates. Cause: Module not properly added to extends array. Solution: Verify the module is listed in nuxt.config.ts: extends: [\n  '@fyit/crouton',\n  '@fyit/crouton-bookings'  // Must be here\n] Then restart the dev server: pnpm dev",{"id":597,"title":598,"titles":599,"content":600,"level":449},"/getting-started/adding-modules#missing-core-package-error","Missing core package error",[23,36],"Symptom: Module features don't work or components aren't available. Cause: The core package @fyit/crouton isn't installed. Solution: Install the core package first: pnpm add @fyit/crouton The core package automatically includes auth, admin, and i18n—no need to install them separately.",{"id":602,"title":603,"titles":604,"content":605,"level":391},"/getting-started/adding-modules#quick-reference","Quick Reference",[23],"# List available modules\ncrouton add --list\n\n# Add a module (automatic)\ncrouton add bookings\n\n# Add multiple modules\ncrouton add bookings maps\n\n# Preview what will happen\ncrouton add bookings --dry-run\n\n# Skip migrations (configure manually later)\ncrouton add bookings --skip-migrations\n\n# Force reinstall\ncrouton add bookings --force",{"id":607,"title":418,"titles":608,"content":609,"level":391},"/getting-started/adding-modules#next-steps",[23],"Schema Format - Define custom collectionsTroubleshooting - Common issues and solutions html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":37,"title":36,"titles":611,"content":612,"level":385},[],"Common issues and solutions when working with Nuxt Crouton Query Examples: For complete useCollectionQuery patterns (basic, filtering, pagination, sorting, relations), see Querying Data. This guide covers common issues you might encounter when using Nuxt Crouton and how to resolve them.",{"id":614,"title":615,"titles":616,"content":528,"level":391},"/guides/troubleshooting#data-not-updating-after-save","Data Not Updating After Save",[36],{"id":618,"title":619,"titles":620,"content":621,"level":449},"/guides/troubleshooting#problem","Problem",[36,615],"Table or list doesn't refresh after creating, updating, or deleting items.",{"id":623,"title":624,"titles":625,"content":626,"level":449},"/guides/troubleshooting#solution","Solution",[36,615],"Check that cache invalidation is working properly: // In useCollectionMutation\nconst invalidateCache = async () => {\n  await refreshNuxtData(`collection:${collection}:{}`)\n}",{"id":628,"title":629,"titles":630,"content":631,"level":449},"/guides/troubleshooting#debug-steps","Debug Steps",[36,615],"Check the cache key format in your console (should show collection:shopProducts:{})Verify the mutation is calling invalidation: const { create } = useCollectionMutation('shopProducts')\n\n// Should automatically invalidate cache after success\nawait create({ name: 'New Product' }) If using custom query parameters, ensure invalidation matches: // All queries with shopProducts prefix should invalidate\nawait refreshNuxtData((key) => key.startsWith(`collection:shopProducts:`))",{"id":633,"title":634,"titles":635,"content":636,"level":449},"/guides/troubleshooting#common-causes","Common Causes",[36,615],"Collection name mismatch between query and mutationCustom cache keys not being invalidatedAPI endpoint not returning updated data",{"id":638,"title":639,"titles":640,"content":528,"level":391},"/guides/troubleshooting#hot-reload-not-working","Hot Reload Not Working",[36],{"id":642,"title":619,"titles":643,"content":644,"level":449},"/guides/troubleshooting#problem-1",[36,639],"Changes to core library files in node_modules/@fyit/crouton don't trigger hot reload.",{"id":646,"title":624,"titles":647,"content":648,"level":449},"/guides/troubleshooting#solution-1",[36,639],"Configure Vite to watch the core library: // nuxt.config.ts\nexport default defineNuxtConfig({\n  vite: {\n    server: {\n      watch: {\n        ignored: ['!**/node_modules/@fyit/**']\n      }\n    },\n    optimizeDeps: {\n      exclude: ['@fyit/crouton']\n    }\n  }\n})",{"id":650,"title":651,"titles":652,"content":653,"level":449},"/guides/troubleshooting#then-restart","Then Restart",[36,639],"# Kill dev server and restart\npnpm dev",{"id":655,"title":656,"titles":657,"content":658,"level":449},"/guides/troubleshooting#additional-steps","Additional Steps",[36,639],"If the issue persists: Clear Nuxt cache: rm -rf .nuxt Clear Vite cache: rm -rf node_modules/.cache Restart dev server: pnpm dev",{"id":660,"title":661,"titles":662,"content":528,"level":391},"/guides/troubleshooting#tailwind-classes-not-working","Tailwind Classes Not Working",[36],{"id":664,"title":619,"titles":665,"content":666,"level":449},"/guides/troubleshooting#problem-2",[36,661],"Tailwind CSS classes aren't being applied to Nuxt Crouton layer components: Hover states don't work (e.g., hover:bg-primary)Dynamic classes not applyingOnly static classes from your app work, not from layer componentsClasses work when used directly in your app, but not in layer components",{"id":668,"title":669,"titles":670,"content":671,"level":449},"/guides/troubleshooting#cause","Cause",[36,661],"This is expected behavior with Tailwind CSS v4 and Nuxt Layers. Tailwind's JIT compiler doesn't automatically scan components in external layers located in node_modules. This is not a bug—it's how Tailwind v4 is designed to work.",{"id":673,"title":624,"titles":674,"content":675,"level":449},"/guides/troubleshooting#solution-2",[36,661],"Add the @source directive to your app's main CSS file to explicitly tell Tailwind to scan the layer components. 1. Create or update your CSS file (e.g., app/assets/css/tailwind.css): @import \"tailwindcss\";\n@import \"@nuxt/ui\";\n\n/* Scan Nuxt Crouton layers */\n@source \"../../../node_modules/@fyit/crouton*/app/**/*.{vue,js,ts}\"; 2. Adjust the path based on your CSS file location: If CSS is at app/assets/css/tailwind.css: use \"../../../node_modules/...\"If CSS is at app.css: use \"../node_modules/...\" 3. Restart your dev server: pnpm dev",{"id":677,"title":678,"titles":679,"content":680,"level":449},"/guides/troubleshooting#important-notes","Important Notes",[36,661],"Use relative paths only - Nuxt aliases like ~~, ~, or @ do NOT work in @source directivesBe specific - Don't use @source \"../../node_modules/.c12/\" as it's too broad and will cause timeoutsUse wildcards - The pattern nuxt-crouton*/ scans all Nuxt Crouton layers automatically",{"id":682,"title":683,"titles":684,"content":685,"level":449},"/guides/troubleshooting#verify-its-working","Verify It's Working",[36,661],"Test with a simple hover effect: \u003Ctemplate>\n  \u003Cdiv class=\"p-4 bg-gray-100 hover:bg-primary transition-colors\">\n    Hover over me\n  \u003C/div>\n\u003C/template> If the hover effect works after adding @source, the configuration is correct.",{"id":687,"title":688,"titles":689,"content":690,"level":449},"/guides/troubleshooting#alternative-list-layers-explicitly","Alternative: List Layers Explicitly",[36,661],"Instead of the wildcard pattern, you can list each layer: @source \"../../../node_modules/@fyit/crouton/app/**/*.{vue,js,ts}\";\n@source \"../../../node_modules/@fyit/crouton-i18n/app/**/*.{vue,js,ts}\";\n@source \"../../../node_modules/@fyit/crouton-editor/app/**/*.{vue,js,ts}\";\n@source \"../../../node_modules/@fyit/crouton-assets/app/**/*.{vue,js,ts}\";",{"id":692,"title":693,"titles":694,"content":695,"level":449},"/guides/troubleshooting#related-issues","Related Issues",[36,661],"This is a known limitation of Tailwind CSS v4 with Nuxt Layers: Nuxt UI Issue #5104Nuxt UI Issue #5184Nuxt Issue #32575 See Installation Guide - Configure Tailwind CSS for complete setup instructions.",{"id":697,"title":698,"titles":699,"content":528,"level":391},"/guides/troubleshooting#form-not-opening","Form Not Opening",[36],{"id":701,"title":619,"titles":702,"content":703,"level":449},"/guides/troubleshooting#problem-3",[36,698],"Clicking a button to open a form does nothing. No modal or slideover appears.",{"id":705,"title":629,"titles":706,"content":707,"level":449},"/guides/troubleshooting#debug-steps-1",[36,698],"Check if useCrouton state is updating: \u003Cscript setup>\nconst { open, showCrouton } = useCrouton()\n\nconst handleClick = () => {\n  console.log('Before:', showCrouton.value)\n  open('create', 'shopProducts')\n  console.log('After:', showCrouton.value)\n}\n\u003C/script> Verify collection is registered in app.config.ts: // app.config.ts\nexport default defineAppConfig({\n  croutonCollections: {\n    shopProducts: {\n      name: 'shopProducts',\n      layer: 'shop',\n      componentName: 'ShopProductsForm',\n      apiPath: 'shop-products',\n    }\n  }\n}) Check component naming matches: layers/shop/components/products/Form.vue\n↓\nComponent name: ShopProductsForm (PascalCase)",{"id":709,"title":634,"titles":710,"content":711,"level":449},"/guides/troubleshooting#common-causes-1",[36,698],"Collection not registered in app.config.tsTypo in collection name (case-sensitive)Form component doesn't exist or has wrong nameModal container component not imported",{"id":713,"title":624,"titles":714,"content":715,"level":449},"/guides/troubleshooting#solution-3",[36,698],"Verify the full chain: Collection registered: ✅Component exists: ✅ layers/shop/components/products/Form.vueComponent name matches: ✅ ShopProductsFormContainer component imported: ✅ Check app layout",{"id":717,"title":718,"titles":719,"content":528,"level":391},"/guides/troubleshooting#type-errors-after-generation","Type Errors After Generation",[36],{"id":721,"title":619,"titles":722,"content":723,"level":449},"/guides/troubleshooting#problem-4",[36,718],"TypeScript shows errors like \"Cannot find module\" or \"Property does not exist\" after generating a collection.",{"id":725,"title":726,"titles":727,"content":728,"level":449},"/guides/troubleshooting#solution-1-restart-typescript-server","Solution 1: Restart TypeScript Server",[36,718],"In VS Code: Press Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows/Linux)Type \"Restart TS Server\"Select \"TypeScript: Restart TS Server\"",{"id":730,"title":731,"titles":732,"content":733,"level":449},"/guides/troubleshooting#solution-2-clear-nuxt-cache","Solution 2: Clear Nuxt Cache",[36,718],"rm -rf .nuxt\nnpx nuxt prepare",{"id":735,"title":736,"titles":737,"content":738,"level":449},"/guides/troubleshooting#solution-3-regenerate-types","Solution 3: Regenerate Types",[36,718],"npx nuxt typecheck",{"id":740,"title":741,"titles":742,"content":528,"level":449},"/guides/troubleshooting#common-type-errors","Common Type Errors",[36,718],{"id":744,"title":745,"titles":746,"content":747,"level":748},"/guides/troubleshooting#missing-module-error","Missing Module Error",[36,718,741],"Cannot find module '~/layers/shop/types/products' Fix: Ensure the file exists and path is correct ls layers/shop/types/products.ts",4,{"id":750,"title":751,"titles":752,"content":753,"level":748},"/guides/troubleshooting#property-does-not-exist","Property Does Not Exist",[36,718,741],"Property 'name' does not exist on type 'never' Fix: Add type parameter to query // ❌ Bad\nconst { items } = await useCollectionQuery('shopProducts')\n\n// ✅ Good\nimport type { ShopProduct } from '~/layers/shop/types/products'\nconst { items } = await useCollectionQuery\u003CShopProduct>('shopProducts')",{"id":755,"title":756,"titles":757,"content":758,"level":449},"/guides/troubleshooting#full-reset-procedure","Full Reset Procedure",[36,718],"If all else fails: # 1. Clear all caches\nrm -rf .nuxt node_modules/.cache\n\n# 2. Reinstall dependencies\npnpm install\n\n# 3. Regenerate types\nnpx nuxt prepare\n\n# 4. Restart dev server\npnpm dev",{"id":760,"title":761,"titles":762,"content":528,"level":391},"/guides/troubleshooting#duplicate-key-errors-during-build","Duplicate Key Errors During Build",[36],{"id":764,"title":619,"titles":765,"content":766,"level":449},"/guides/troubleshooting#problem-5",[36,761],"Getting duplicate key errors when building or running the development server: [esbuild] (schema.ts:8:2) Duplicate key \"owner\" in object literal\n[esbuild] (schema.ts:9:2) Duplicate key \"teamId\" in object literal\n[esbuild] (schema.ts:10:2) Duplicate key \"createdAt\" in object literal\n[esbuild] (schema.ts:11:2) Duplicate key \"updatedAt\" in object literal",{"id":768,"title":669,"titles":769,"content":770,"level":449},"/guides/troubleshooting#cause-1",[36,761],"You've manually defined auto-generated fields (teamId, owner, createdAt, updatedAt, createdBy, or updatedBy) in your schema JSON files. The generator adds these fields automatically based on your configuration flags, so manual definitions create duplicates.",{"id":772,"title":773,"titles":774,"content":775,"level":449},"/guides/troubleshooting#solution-1-remove-fields-from-schema","Solution 1: Remove Fields from Schema",[36,761],"Remove these fields from your schema JSON files: {\n  \"owner\": {            // ❌ Remove this\n    \"type\": \"string\",\n    \"refTarget\": \"users\"\n  },\n  \"teamId\": {           // ❌ Remove this\n    \"type\": \"string\"\n  },\n  \"createdAt\": {        // ❌ Remove this\n    \"type\": \"date\"\n  },\n  \"updatedAt\": {        // ❌ Remove this\n    \"type\": \"date\"\n  },\n  \"title\": {            // ✅ Keep your custom fields\n    \"type\": \"string\"\n  }\n}",{"id":777,"title":778,"titles":779,"content":780,"level":449},"/guides/troubleshooting#solution-2-adjust-configuration-flags","Solution 2: Adjust Configuration Flags",[36,761],"If you don't need certain auto-generated fields, disable them in your config: // crouton.config.js\nexport default {\n  flags: {\n    useMetadata: false      // Disables createdAt & updatedAt\n  }\n}",{"id":782,"title":783,"titles":784,"content":785,"level":449},"/guides/troubleshooting#understanding-auto-generated-fields","Understanding Auto-Generated Fields",[36,761],"The generator automatically adds fields: Always added: id - Primary keyteamId - Team/organization reference (for team-scoped collections)owner - User who owns the record (for team-scoped collections) When useMetadata: true (default): createdAt - Creation timestampupdatedAt - Last update timestampcreatedBy - User who created the recordupdatedBy - User who last updated the record",{"id":787,"title":788,"titles":789,"content":790,"level":449},"/guides/troubleshooting#after-fixing","After Fixing",[36,761],"Update your schema JSON files to remove auto-generated fieldsRegenerate the collection: npx crouton-generate config ./crouton.config.js --force Restart your dev server: pnpm dev See Schema Format - Auto-Generated Fields for complete details.",{"id":792,"title":793,"titles":794,"content":528,"level":391},"/guides/troubleshooting#delete-button-error","Delete Button Error",[36],{"id":796,"title":619,"titles":797,"content":798,"level":449},"/guides/troubleshooting#problem-6",[36,793],"Error: send is not a function when clicking delete button.",{"id":800,"title":669,"titles":801,"content":802,"level":449},"/guides/troubleshooting#cause-2",[36,793],"Using old generated code with the new core library. The send() method was removed in v2.0.",{"id":804,"title":805,"titles":806,"content":807,"level":449},"/guides/troubleshooting#solution-1-regenerate-form","Solution 1: Regenerate Form",[36,793],"npx crouton-generate shop products --fields-file schema.json --force",{"id":809,"title":810,"titles":811,"content":812,"level":449},"/guides/troubleshooting#solution-2-manual-update","Solution 2: Manual Update",[36,793],"Update your Form.vue manually: Before (v1.x): \u003Cscript setup>\nconst { send } = useCrouton()\n\nconst handleSubmit = async () => {\n  await send('create', 'shopProducts', state.value)\n}\n\u003C/script> After (v2.0): \u003Cscript setup>\nconst { create, update, deleteItems } = useCollectionMutation('shopProducts')\n\nconst handleSubmit = async () => {\n  if (props.action === 'create') {\n    await create(state.value)\n  } else if (props.action === 'update') {\n    await update(state.value.id, state.value)\n  } else if (props.action === 'delete') {\n    await deleteItems(props.items)\n  }\n  close()\n}\n\u003C/script> See Migration Guide for complete migration instructions.",{"id":814,"title":815,"titles":816,"content":528,"level":391},"/guides/troubleshooting#cache-not-invalidating","Cache Not Invalidating",[36],{"id":818,"title":619,"titles":819,"content":820,"level":449},"/guides/troubleshooting#problem-7",[36,815],"Multiple views of the same data, but only one updates after mutation.",{"id":822,"title":823,"titles":824,"content":825,"level":449},"/guides/troubleshooting#debug","Debug",[36,815],"Check if you're using different cache keys: // List view\nconst { items } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({ page: 1 }))  // Cache key: collection:shopProducts:{\"page\":1}\n})\n\n// Detail view\nconst { items } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({ page: 2 }))  // Cache key: collection:shopProducts:{\"page\":2}\n})",{"id":827,"title":624,"titles":828,"content":829,"level":449},"/guides/troubleshooting#solution-4",[36,815],"useCollectionMutation already invalidates all matching queries: // This invalidates ALL shopProducts queries, regardless of parameters\nawait refreshNuxtData((key) => key.startsWith(`collection:shopProducts:`)) If you have custom invalidation, ensure it matches all variants: // ❌ Bad - only invalidates exact match\nawait refreshNuxtData(`collection:shopProducts:{}`)\n\n// ✅ Good - invalidates all variants\nawait refreshNuxtData((key) => key.startsWith(`collection:shopProducts:`))",{"id":831,"title":832,"titles":833,"content":528,"level":391},"/guides/troubleshooting#validation-errors-not-showing","Validation Errors Not Showing",[36],{"id":835,"title":619,"titles":836,"content":837,"level":449},"/guides/troubleshooting#problem-8",[36,832],"Form submits even with invalid data, or validation errors don't display.",{"id":839,"title":840,"titles":841,"content":842,"level":449},"/guides/troubleshooting#check-schema-setup","Check Schema Setup",[36,832],"// composables/useProducts.ts\nimport { z } from 'zod'\n\nexport function useShopProducts() {\n  const schema = z.object({\n    name: z.string().min(1, 'Name is required'),\n    price: z.number().min(0, 'Price must be positive')\n  })\n\n  return { schema }\n}",{"id":844,"title":845,"titles":846,"content":847,"level":449},"/guides/troubleshooting#check-form-usage","Check Form Usage",[36,832],"\u003Ctemplate>\n  \u003CUForm\n    :state=\"state\"\n    :schema=\"schema\"\n    @submit=\"handleSubmit\"\n  >\n    \u003CUFormField label=\"Name\" name=\"name\">\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n  \u003C/UForm>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { schema } = useShopProducts()\nconst state = ref({ name: '', price: 0 })\n\u003C/script>",{"id":849,"title":850,"titles":851,"content":852,"level":449},"/guides/troubleshooting#common-issues","Common Issues",[36,832],"Schema not passed to form:Ensure :schema=\"schema\" prop is setField name mismatch:name=\"name\" must match schema keyCheck spelling and caseState not reactive:Use ref() or reactive() for form state",{"id":854,"title":855,"titles":856,"content":528,"level":391},"/guides/troubleshooting#layer-pages-404-errors","Layer Pages 404 Errors",[36],{"id":858,"title":619,"titles":859,"content":860,"level":449},"/guides/troubleshooting#problem-9",[36,855],"Getting 404 errors when navigating to Nuxt Crouton pages like /admin/[team]/collections or /super-admin: 404 - Page not found: /admin/myteam/collections",{"id":862,"title":669,"titles":863,"content":864,"level":449},"/guides/troubleshooting#cause-3",[36,855],"You're using Nuxt 3 instead of Nuxt 4. Nuxt Crouton uses the app/pages/ directory structure (Nuxt 4 convention). When the consuming app uses Nuxt 3, Nuxt looks for pages in pages/ instead of app/pages/, so layer pages are not found.",{"id":866,"title":624,"titles":867,"content":868,"level":449},"/guides/troubleshooting#solution-5",[36,855],"Upgrade your project to Nuxt 4: {\n  \"dependencies\": {\n    \"nuxt\": \"^4.0.0\",\n    \"@nuxt/ui\": \"^4.3.0\"\n  }\n} Then reinstall dependencies: pnpm install",{"id":870,"title":871,"titles":872,"content":873,"level":449},"/guides/troubleshooting#verification","Verification",[36,855],"After upgrading, verify your project structure follows Nuxt 4 conventions: app/\n  ├── pages/          # Nuxt 4 location\n  ├── components/\n  ├── composables/\n  └── app.vue Nuxt 4 is required. Nuxt Crouton will not work correctly with Nuxt 3 due to directory structure differences. See Installation Requirements for details.",{"id":875,"title":876,"titles":877,"content":878,"level":391},"/guides/troubleshooting#route-structure-reference","Route Structure Reference",[36],"Nuxt Crouton uses a three-tier route architecture: TierRoute PatternPurposeAccessUser/dashboard/[team]/*User-facing featuresAny team memberAdmin/admin/[team]/*Team management, collectionsTeam admins/ownersSuper Admin/super-admin/*System-wide managementApp owner only",{"id":880,"title":881,"titles":882,"content":883,"level":449},"/guides/troubleshooting#common-routes","Common Routes",[36,876],"/auth/login - Login page/auth/register - Registration page/dashboard - Team selection/dashboard/[team] - User dashboard/admin/[team]/collections - Collection management/admin/[team]/members - Team members/admin/[team]/settings - Team settings/super-admin - Super admin dashboard/super-admin/users - User management/super-admin/teams - Team management",{"id":885,"title":886,"titles":887,"content":528,"level":391},"/guides/troubleshooting#api-route-404-errors","API Route 404 Errors",[36],{"id":889,"title":619,"titles":890,"content":891,"level":449},"/guides/troubleshooting#problem-10",[36,886],"Getting 404 errors when trying to fetch or mutate data.",{"id":893,"title":894,"titles":895,"content":896,"level":449},"/guides/troubleshooting#check-api-route-paths","Check API Route Paths",[36,886],"Ensure your API routes match the collection's apiPath: // app.config.ts\nexport default defineAppConfig({\n  croutonCollections: {\n    shopProducts: {\n      apiPath: 'shop-products',  // ← This determines the route\n    }\n  }\n}) Should match: server/api/teams/[team]/shop-products/\n  ├── index.get.ts        # GET /api/teams/:team/shop-products\n  ├── index.post.ts       # POST /api/teams/:team/shop-products\n  ├── [id].patch.ts       # PATCH /api/teams/:team/shop-products/:id\n  └── [id].delete.ts      # DELETE /api/teams/:team/shop-products/:id",{"id":898,"title":899,"titles":900,"content":901,"level":449},"/guides/troubleshooting#check-team-based-routes","Check Team-Based Routes",[36,886],"If using team-based auth: // server/api/teams/[team]/shop-products/index.get.ts\nexport default defineEventHandler(async (event) => {\n  const teamId = getRouterParam(event, 'team')\n\n  if (!teamId) {\n    throw createError({ status: 400, statusText: 'Team ID required' })\n  }\n\n  // Your query logic\n})",{"id":903,"title":904,"titles":905,"content":906,"level":449},"/guides/troubleshooting#debug-api-calls","Debug API Calls",[36,886],"Check browser DevTools Network tab: Look for failed requestsCheck request URL matches expected patternVerify request method (GET, POST, PATCH, DELETE)Check response error messages",{"id":908,"title":909,"titles":910,"content":528,"level":391},"/guides/troubleshooting#translation-issues","Translation Issues",[36],{"id":912,"title":619,"titles":913,"content":914,"level":449},"/guides/troubleshooting#problem-11",[36,909],"Translations not loading or switching locales doesn't update content.",{"id":916,"title":917,"titles":918,"content":919,"level":449},"/guides/troubleshooting#check-i18n-setup","Check i18n Setup",[36,909],"Ensure @fyit/crouton-i18n is included in your nuxt.config.ts extends array.",{"id":921,"title":922,"titles":923,"content":924,"level":449},"/guides/troubleshooting#check-query-locale-binding","Check Query Locale Binding",[36,909],"Make sure your query reactively binds to the i18n locale using a computed query parameter (see Querying with Filters for the pattern).",{"id":926,"title":927,"titles":928,"content":929,"level":449},"/guides/troubleshooting#check-translation-fields","Check Translation Fields",[36,909],"Ensure fields are marked as translatable in config: // crouton.config.js\nexport default {\n  translations: {\n    collections: {\n      products: ['name', 'description']  // ← Mark translatable fields\n    }\n  }\n}",{"id":931,"title":932,"titles":933,"content":528,"level":391},"/guides/troubleshooting#customizing-auto-generated-repeater-components","Customizing Auto-Generated Repeater Components",[36],{"id":935,"title":936,"titles":937,"content":938,"level":449},"/guides/troubleshooting#overview","Overview",[36,932],"When you define a repeater field in your schema, the generator automatically creates a placeholder component for you. This placeholder is fully functional but needs customization to match your specific data structure.",{"id":940,"title":941,"titles":942,"content":943,"level":449},"/guides/troubleshooting#what-you-get","What You Get",[36,932],"The auto-generated placeholder component includes: Proper Vue component structure (props, emits, v-model)TypeScript interface with TODO commentsDefault values in computed getterID field (auto-generated, disabled)Debug section showing raw dataVisual indicators (dashed border, gray background) Example placeholder location: layers/bookings/collections/locations/app/components/Slot.vue",{"id":945,"title":946,"titles":947,"content":528,"level":449},"/guides/troubleshooting#how-to-customize","How to Customize",[36,932],{"id":949,"title":950,"titles":951,"content":952,"level":748},"/guides/troubleshooting#step-1-update-the-interface","Step 1: Update the Interface",[36,932,946],"Define the fields your repeater item needs: // Before (placeholder)\ninterface SlotItem {\n  id: string\n  // TODO: Add your fields here\n}\n\n// After (customized)\ninterface SlotItem {\n  id: string\n  label: string\n  startTime: string\n  endTime: string\n}",{"id":954,"title":955,"titles":956,"content":957,"level":748},"/guides/troubleshooting#step-2-add-default-values","Step 2: Add Default Values",[36,932,946],"Provide sensible defaults for your fields: import { nanoid } from 'nanoid'\n\n// Before\nconst localValue = computed({\n  get: () => props.modelValue || {\n    id: nanoid(),\n    // TODO: Add default values for your fields\n  },\n  set: (val) => emit('update:modelValue', val)\n})\n\n// After\nconst localValue = computed({\n  get: () => props.modelValue || {\n    id: nanoid(),\n    label: '',\n    startTime: '09:00',\n    endTime: '17:00'\n  },\n  set: (val) => emit('update:modelValue', val)\n})",{"id":959,"title":960,"titles":961,"content":962,"level":748},"/guides/troubleshooting#step-3-update-the-template","Step 3: Update the Template",[36,932,946],"Replace the placeholder with your custom form fields: \u003Ctemplate>\n  \u003C!-- Remove the TODO section and placeholder styling -->\n  \u003Cdiv class=\"grid grid-cols-4 gap-4\">\n    \u003CUFormField label=\"ID\" name=\"id\">\n      \u003CUInput v-model=\"localValue.id\" disabled />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Label\" name=\"label\">\n      \u003CUInput v-model=\"localValue.label\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Start Time\" name=\"startTime\">\n      \u003CUInput v-model=\"localValue.startTime\" type=\"time\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"End Time\" name=\"endTime\">\n      \u003CUInput v-model=\"localValue.endTime\" type=\"time\" />\n    \u003C/UFormField>\n  \u003C/div>\n\u003C/template>",{"id":964,"title":965,"titles":966,"content":967,"level":449},"/guides/troubleshooting#complete-customized-example","Complete Customized Example",[36,932],"Before (auto-generated): \u003Cscript setup lang=\"ts\">\nimport { nanoid } from 'nanoid'\n\ninterface SlotItem {\n  id: string\n  // TODO: Add your fields here\n}\n\nconst props = defineProps\u003C{ modelValue: SlotItem }>()\nconst emit = defineEmits\u003C{ 'update:modelValue': [value: SlotItem] }>()\n\nconst localValue = computed({\n  get: () => props.modelValue || { id: nanoid() },\n  set: (val) => emit('update:modelValue', val)\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"space-y-3 p-4 bg-gray-50 rounded-lg border border-dashed\">\n    \u003Cdiv class=\"text-sm text-gray-600\">\n      \u003Cstrong>TODO:\u003C/strong> Customize this component\n    \u003C/div>\n    \u003C!-- Placeholder fields -->\n  \u003C/div>\n\u003C/template> After (customized): \u003Cscript setup lang=\"ts\">\nimport { nanoid } from 'nanoid'\n\ninterface SlotItem {\n  id: string\n  label: string\n  startTime: string\n  endTime: string\n}\n\nconst props = defineProps\u003C{ modelValue: SlotItem }>()\nconst emit = defineEmits\u003C{ 'update:modelValue': [value: SlotItem] }>()\n\nconst localValue = computed({\n  get: () => props.modelValue || {\n    id: nanoid(),\n    label: '',\n    startTime: '09:00',\n    endTime: '17:00'\n  },\n  set: (val) => emit('update:modelValue', val)\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"grid grid-cols-4 gap-4\">\n    \u003CUFormField label=\"ID\" name=\"id\">\n      \u003CUInput v-model=\"localValue.id\" disabled />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Label\" name=\"label\">\n      \u003CUInput v-model=\"localValue.label\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Start Time\" name=\"startTime\">\n      \u003CUInput v-model=\"localValue.startTime\" type=\"time\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"End Time\" name=\"endTime\">\n      \u003CUInput v-model=\"localValue.endTime\" type=\"time\" />\n    \u003C/UFormField>\n  \u003C/div>\n\u003C/template>",{"id":969,"title":970,"titles":971,"content":972,"level":449},"/guides/troubleshooting#tips","Tips",[36,932],"The placeholder works immediately—you can test your repeater before customizingKeep the ID field disabled to prevent accidental modificationThe debug section in the placeholder helps verify data structure during developmentRemove placeholder styling (dashed borders, gray background) when ready for production See Schema Format - Repeater Fields for more details on defining repeater fields.",{"id":974,"title":975,"titles":976,"content":528,"level":391},"/guides/troubleshooting#performance-issues","Performance Issues",[36],{"id":978,"title":619,"titles":979,"content":980,"level":449},"/guides/troubleshooting#problem-12",[36,975],"Slow page loads or laggy UI when working with large datasets.",{"id":982,"title":983,"titles":984,"content":528,"level":449},"/guides/troubleshooting#solutions","Solutions",[36,975],{"id":986,"title":987,"titles":988,"content":989,"level":748},"/guides/troubleshooting#_1-implement-pagination","1. Implement Pagination",[36,975,983],"Use pagination with page and limit query parameters (see Querying with Pagination).",{"id":991,"title":992,"titles":993,"content":994,"level":748},"/guides/troubleshooting#_2-use-server-side-filtering","2. Use Server-Side Filtering",[36,975,983],"// server/api/teams/[team]/products/index.get.ts\nexport default defineEventHandler(async (event) => {\n  const query = getQuery(event)\n  const { page = 1, limit = 50, search } = query\n\n  // Filter in database, not in frontend\n  let dbQuery = db.select().from(products)\n\n  if (search) {\n    dbQuery = dbQuery.where(like(products.name, `%${search}%`))\n  }\n\n  return dbQuery\n    .limit(limit)\n    .offset((page - 1) * limit)\n})",{"id":996,"title":997,"titles":998,"content":999,"level":748},"/guides/troubleshooting#_3-optimize-relations","3. Optimize Relations",[36,975,983],"Use server-side joins instead of multiple queries to avoid N+1 query problems. See Querying with Relations for the recommended pattern.",{"id":1001,"title":1002,"titles":1003,"content":1004,"level":391},"/guides/troubleshooting#getting-more-help","Getting More Help",[36],"If you're still experiencing issues: Check GitHub Issues: Search for similar problems at github.com/pmcp/nuxt-crouton/issuesCreate an Issue: Include:Nuxt Crouton versionNuxt versionNode versionError messagesMinimal reproduction stepsAsk in Discussions: For general questions, use GitHub Discussions",{"id":1006,"title":1007,"titles":1008,"content":1009,"level":391},"/guides/troubleshooting#related-resources","Related Resources",[36],"Migration Guide - Upgrading from older versionsBest Practices - Avoid common pitfallsAPI Reference - Detailed API documentation html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":41,"title":40,"titles":1011,"content":1012,"level":385},[],"Guide for migrating from older versions of Nuxt Crouton This guide helps you migrate your Nuxt Crouton projects from older versions to the latest release.",{"id":1014,"title":1015,"titles":1016,"content":1017,"level":391},"/guides/migration#team-authentication-migration-fyitcrouton-auth","Team Authentication Migration (@fyit/crouton-auth)",[40],"If you're upgrading from a project that used the useTeamUtility flag or the #crouton/team-auth alias, follow this section.",{"id":1019,"title":1020,"titles":1021,"content":1022,"level":449},"/guides/migration#breaking-changes-overview","Breaking Changes Overview",[40,1015],"✅ useTeamUtility flag removed from generator✅ All collections are now team-scoped by default✅ #crouton/team-auth alias replaced with @fyit/crouton-auth/server/utils/team imports✅ @fyit/crouton-auth is now required for team authentication",{"id":1024,"title":1025,"titles":1026,"content":528,"level":449},"/guides/migration#breaking-change-useteamutility-removed","Breaking Change: useTeamUtility Removed",[40,1015],{"id":1028,"title":1029,"titles":1030,"content":1031,"level":748},"/guides/migration#what-changed","What Changed",[40,1015,1025],"The useTeamUtility configuration flag has been removed. All generated collections now use team-scoped authentication by default.",{"id":1033,"title":1034,"titles":1035,"content":1036,"level":748},"/guides/migration#before","Before",[40,1015,1025],"// crouton.config.js\nexport default {\n  flags: {\n    useTeamUtility: true  // or false for non-team collections\n  }\n}",{"id":1038,"title":1039,"titles":1040,"content":1041,"level":748},"/guides/migration#after","After",[40,1015,1025],"// crouton.config.js\nexport default {\n  // No useTeamUtility flag - all collections are team-scoped\n}",{"id":1043,"title":1044,"titles":1045,"content":528,"level":449},"/guides/migration#breaking-change-import-path-changed","Breaking Change: Import Path Changed",[40,1015],{"id":1047,"title":1029,"titles":1048,"content":1049,"level":748},"/guides/migration#what-changed-1",[40,1015,1044],"Generated API endpoints now import team auth utilities from @fyit/crouton-auth/server/utils/team instead of using the #crouton/team-auth alias.",{"id":1051,"title":1052,"titles":1053,"content":1054,"level":748},"/guides/migration#before-generated-code","Before (Generated Code)",[40,1015,1044],"import { resolveTeamAndCheckMembership } from '#crouton/team-auth'",{"id":1056,"title":1057,"titles":1058,"content":1059,"level":748},"/guides/migration#after-generated-code","After (Generated Code)",[40,1015,1044],"import { resolveTeamAndCheckMembership } from '@fyit/crouton-auth/server/utils/team'",{"id":1061,"title":1062,"titles":1063,"content":528,"level":449},"/guides/migration#migration-steps","Migration Steps",[40,1015],{"id":1065,"title":1066,"titles":1067,"content":1068,"level":748},"/guides/migration#step-1-install-fyitcrouton-auth","Step 1: Install @fyit/crouton-auth",[40,1015,1062],"pnpm add @fyit/crouton-auth",{"id":1070,"title":1071,"titles":1072,"content":1073,"level":748},"/guides/migration#step-2-update-nuxtconfigts","Step 2: Update nuxt.config.ts",[40,1015,1062],"// nuxt.config.ts\nexport default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-auth'  // Add this if not already present\n  ]\n})",{"id":1075,"title":1076,"titles":1077,"content":1078,"level":748},"/guides/migration#step-3-export-auth-schema","Step 3: Export Auth Schema",[40,1015,1062],"Ensure your database schema exports the auth tables: // server/database/schema/index.ts\nexport * from '@fyit/crouton-auth/server/database/schema/auth'",{"id":1080,"title":1081,"titles":1082,"content":1083,"level":748},"/guides/migration#step-4-regenerate-collections","Step 4: Regenerate Collections",[40,1015,1062],"Regenerate your collections to update the import paths: # Backup your customizations first\ncp -r layers layers.backup\n\n# Regenerate with new imports\nnpx crouton-generate config ./crouton.config.js --force Then restore any customizations from layers.backup/.",{"id":1085,"title":1086,"titles":1087,"content":1088,"level":748},"/guides/migration#step-5-update-custom-code","Step 5: Update Custom Code",[40,1015,1062],"If you have custom API endpoints using the old import: # Find all old imports\ngrep -r \"#crouton/team-auth\" layers/ Replace them: // Before\nimport { resolveTeamAndCheckMembership } from '#crouton/team-auth'\n\n// After\nimport { resolveTeamAndCheckMembership } from '@fyit/crouton-auth/server/utils/team'",{"id":1090,"title":1091,"titles":1092,"content":1093,"level":748},"/guides/migration#step-6-update-client-side-code","Step 6: Update Client-Side Code",[40,1015,1062],"If you were using route.params.team directly, switch to useTeamContext(): // Before\nconst route = useRoute()\nconst teamId = route.params.team\n\n// After\nconst { teamId, teamSlug } = useTeamContext()",{"id":1095,"title":1096,"titles":1097,"content":1098,"level":748},"/guides/migration#step-7-remove-nitro-alias-if-present","Step 7: Remove Nitro Alias (if present)",[40,1015,1062],"If you had a manual nitro alias, remove it: // nuxt.config.ts - REMOVE this if present\nexport default defineNuxtConfig({\n  nitro: {\n    alias: {\n      '#crouton/team-auth': '@fyit/crouton-auth/server/utils/team-auth'  // Remove\n    }\n  }\n}) The @fyit/crouton-auth/server/utils/team import works directly without aliases.",{"id":1100,"title":1101,"titles":1102,"content":1103,"level":748},"/guides/migration#step-8-verify-and-test","Step 8: Verify and Test",[40,1015,1062],"# Type check\nnpx nuxt typecheck\n\n# Test CRUD operations\npnpm dev",{"id":1105,"title":1106,"titles":1107,"content":528,"level":449},"/guides/migration#common-migration-issues","Common Migration Issues",[40,1015],{"id":1109,"title":1110,"titles":1111,"content":1112,"level":748},"/guides/migration#issue-cannot-find-module-fyitcrouton-authserverutilsteam","Issue: \"Cannot find module '@fyit/crouton-auth/server/utils/team'\"",[40,1015,1106],"Cause: @fyit/crouton-auth not installed or not extending the layer Fix: pnpm add @fyit/crouton-auth And ensure it's in your extends array in nuxt.config.ts.",{"id":1114,"title":1115,"titles":1116,"content":1117,"level":748},"/guides/migration#issue-resolveteamandcheckmembership-is-not-exported","Issue: \"resolveTeamAndCheckMembership is not exported\"",[40,1015,1106],"Cause: Using old alias or package version mismatch Fix: Update to the latest @fyit/crouton-auth: pnpm update @fyit/crouton-auth",{"id":1119,"title":1120,"titles":1121,"content":1122,"level":748},"/guides/migration#issue-team-context-undefined","Issue: Team context undefined",[40,1015,1106],"Cause: Route doesn't have team parameter Fix: Ensure your routes include [team] parameter: pages/\n└── [team]/\n    └── products/\n        └── index.vue",{"id":1124,"title":1125,"titles":1126,"content":1127,"level":391},"/guides/migration#v1x-v20","v1.x → v2.0",[40],"Version 2.0 introduced significant changes to improve performance and developer experience.",{"id":1129,"title":1020,"titles":1130,"content":1131,"level":449},"/guides/migration#breaking-changes-overview-1",[40,1125],"✅ send() method removed✅ Global state management removed✅ Button component API updated✅ Cache invalidation improved",{"id":1133,"title":1134,"titles":1135,"content":528,"level":391},"/guides/migration#breaking-change-1-send-removed","Breaking Change 1: send() Removed",[40],{"id":1137,"title":1029,"titles":1138,"content":1139,"level":449},"/guides/migration#what-changed-2",[40,1134],"The send() method from useCrouton() has been removed and replaced with two new approaches: useCroutonMutate() - For quick, one-off mutationsuseCollectionMutation() - For optimized mutations in forms",{"id":1141,"title":1142,"titles":1143,"content":1144,"level":449},"/guides/migration#before-v1x","Before (v1.x)",[40,1134],"\u003Cscript setup lang=\"ts\">\nconst { send } = useCrouton()\n\nconst handleCreate = async () => {\n  await send('create', 'shopProducts', {\n    name: 'New Product',\n    price: 29.99\n  })\n}\n\nconst handleUpdate = async (id: string) => {\n  await send('update', 'shopProducts', {\n    id,\n    name: 'Updated Name'\n  })\n}\n\nconst handleDelete = async (ids: string[]) => {\n  await send('delete', 'shopProducts', ids)\n}\n\u003C/script>",{"id":1146,"title":1147,"titles":1148,"content":1149,"level":449},"/guides/migration#after-v20-option-1-usecroutonmutate-quick","After v2.0 - Option 1: useCroutonMutate (Quick)",[40,1134],"For quick actions, toggle buttons, and utility functions: \u003Cscript setup lang=\"ts\">\nconst { mutate } = useCroutonMutate()\n\nconst handleCreate = async () => {\n  await mutate('create', 'shopProducts', {\n    name: 'New Product',\n    price: 29.99\n  })\n}\n\nconst handleUpdate = async (id: string) => {\n  await mutate('update', 'shopProducts', {\n    id,\n    name: 'Updated Name'\n  })\n}\n\nconst handleDelete = async (ids: string[]) => {\n  await mutate('delete', 'shopProducts', ids)\n}\n\u003C/script>",{"id":1151,"title":1152,"titles":1153,"content":1154,"level":449},"/guides/migration#after-v20-option-2-usecollectionmutation-optimized","After v2.0 - Option 2: useCollectionMutation (Optimized)",[40,1134],"For forms and repeated operations on the same collection: \u003Cscript setup lang=\"ts\">\nconst { create, update, deleteItems } = useCollectionMutation('shopProducts')\n\nconst handleCreate = async () => {\n  await create({\n    name: 'New Product',\n    price: 29.99\n  })\n}\n\nconst handleUpdate = async (id: string, data: any) => {\n  await update(id, data)\n}\n\nconst handleDelete = async (ids: string[]) => {\n  await deleteItems(ids)\n}\n\u003C/script>",{"id":1156,"title":1157,"titles":1158,"content":1159,"level":449},"/guides/migration#when-to-use-which","When to Use Which?",[40,1134],"Use CaseUse ThisToggle buttonuseCroutonMutate()Quick add/removeuseCroutonMutate()Utility functionuseCroutonMutate()Generated formsuseCollectionMutation()Multi-step wizarduseCollectionMutation()Bulk operationsuseCollectionMutation()",{"id":1161,"title":1162,"titles":1163,"content":528,"level":391},"/guides/migration#breaking-change-2-global-state-removed","Breaking Change 2: Global State Removed",[40],{"id":1165,"title":1029,"titles":1166,"content":1167,"level":449},"/guides/migration#what-changed-3",[40,1162],"The global reactive collections state (useCollections()) has been removed in favor of useCollectionQuery() with proper caching.",{"id":1169,"title":1142,"titles":1170,"content":1171,"level":449},"/guides/migration#before-v1x-1",[40,1162],"\u003Cscript setup lang=\"ts\">\nconst { shopProducts } = useCollections()\n\n// Manual fetch\nconst { data } = await useFetch('/api/products')\nshopProducts.value = data.value\n\n// Manual updates\nwatch(shopProducts, () => {\n  // React to changes\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-for=\"product in shopProducts\" :key=\"product.id\">\n    {{ product.name }}\n  \u003C/div>\n\u003C/template>",{"id":1173,"title":1174,"titles":1175,"content":1176,"level":449},"/guides/migration#after-v20","After v2.0",[40,1162],"Query Examples: For complete useCollectionQuery patterns including filters, pagination, and sorting, see Querying Data. \u003Cscript setup lang=\"ts\">\n// Automatic fetching and caching\nconst { items, pending, refresh } = await useCollectionQuery('shopProducts')\n\n// Automatic refetch after mutations\nconst { create } = useCollectionMutation('shopProducts')\nawait create({ name: 'New Product' })\n// → items automatically updates\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"pending\">Loading...\u003C/div>\n  \u003Cdiv v-else>\n    \u003Cdiv v-for=\"product in items\" :key=\"product.id\">\n      {{ product.name }}\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":1178,"title":1179,"titles":1180,"content":1181,"level":449},"/guides/migration#benefits","Benefits",[40,1162],"✅ Automatic cache management✅ Automatic refetching after mutations✅ Loading and error states built-in✅ No manual state synchronization",{"id":1183,"title":1184,"titles":1185,"content":528,"level":391},"/guides/migration#breaking-change-3-button-component-updated","Breaking Change 3: Button Component Updated",[40],{"id":1187,"title":1029,"titles":1188,"content":1189,"level":449},"/guides/migration#what-changed-4",[40,1184],"CroutonFormActionButton no longer accepts a @submit handler. Form submission is now handled by the UForm component.",{"id":1191,"title":1142,"titles":1192,"content":1193,"level":449},"/guides/migration#before-v1x-2",[40,1184],"\u003Ctemplate>\n  \u003CUForm :state=\"state\" :schema=\"schema\">\n    \u003CUFormField label=\"Name\" name=\"name\">\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n\n    \u003CCroutonFormActionButton\n      :action=\"action\"\n      :collection=\"collection\"\n      @submit=\"send(action, collection, state)\"\n    />\n  \u003C/UForm>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { send } = useCrouton()\nconst state = ref({ name: '' })\n\u003C/script>",{"id":1195,"title":1174,"titles":1196,"content":1197,"level":449},"/guides/migration#after-v20-1",[40,1184],"\u003Ctemplate>\n  \u003CUForm\n    :state=\"state\"\n    :schema=\"schema\"\n    @submit=\"handleSubmit\"\n  >\n    \u003CUFormField label=\"Name\" name=\"name\">\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n\n    \u003CCroutonFormActionButton\n      :action=\"action\"\n      :collection=\"collection\"\n      :loading=\"loading\"\n      type=\"submit\"\n    />\n  \u003C/UForm>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  action: 'create' | 'update' | 'delete'\n  collection: string\n}>()\n\nconst { create, update, deleteItems } = useCollectionMutation(props.collection)\nconst state = ref({ name: '' })\n\nconst handleSubmit = async () => {\n  if (props.action === 'create') {\n    await create(state.value)\n  } else if (props.action === 'update') {\n    await update(state.value.id, state.value)\n  } else if (props.action === 'delete') {\n    await deleteItems(props.items)\n  }\n  close()\n}\n\u003C/script>",{"id":1199,"title":1062,"titles":1200,"content":528,"level":391},"/guides/migration#migration-steps-1",[40],{"id":1202,"title":1203,"titles":1204,"content":1205,"level":449},"/guides/migration#step-1-update-dependencies","Step 1: Update Dependencies",[40,1062],"# Update to latest version\npnpm update @fyit/crouton\npnpm update @fyit/crouton-cli\npnpm update @fyit/crouton-i18n  # If using i18n",{"id":1207,"title":1208,"titles":1209,"content":1210,"level":449},"/guides/migration#step-2-backup-generated-code","Step 2: Backup Generated Code",[40,1062],"# Create backup of your customizations\ncp -r layers layers.backup",{"id":1212,"title":1213,"titles":1214,"content":1215,"level":449},"/guides/migration#step-3-update-generated-forms","Step 3: Update Generated Forms",[40,1062],"Option A: Regenerate (Recommended for standard forms) # Regenerate all collections\nnpx crouton-generate config ./crouton.config.js --force Then restore any customizations from layers.backup/. Option B: Manual Update (If heavily customized) Update each Form.vue manually: Replace useCrouton().send() with useCollectionMutation()Add handleSubmit functionUpdate CroutonFormActionButton props See Breaking Change 1 and Breaking Change 3 for examples.",{"id":1217,"title":1218,"titles":1219,"content":1220,"level":449},"/guides/migration#step-4-update-custom-code","Step 4: Update Custom Code",[40,1062],"Find and replace all instances of send(): # Find all usages\ngrep -r \"const.*send.*=.*useCrouton\" layers/\n\n# Replace with useCroutonMutate or useCollectionMutation Quick actions: // Before\nconst { send } = useCrouton()\nawait send('update', 'products', data)\n\n// After\nconst { mutate } = useCroutonMutate()\nawait mutate('update', 'products', data) Forms: // Before\nconst { send } = useCrouton()\nawait send('create', 'products', data)\n\n// After\nconst { create } = useCollectionMutation('products')\nawait create(data)",{"id":1222,"title":1223,"titles":1224,"content":1225,"level":449},"/guides/migration#step-5-update-global-state-usage","Step 5: Update Global State Usage",[40,1062],"Replace useCollections() with useCollectionQuery(): \u003C!-- Before -->\n\u003Cscript setup>\nconst { shopProducts } = useCollections()\n\u003C/script>\n\n\u003C!-- After -->\n\u003Cscript setup>\nconst { items: shopProducts } = await useCollectionQuery('shopProducts')\n\u003C/script> See Querying Data for advanced query patterns.",{"id":1227,"title":1228,"titles":1229,"content":1230,"level":449},"/guides/migration#step-6-test-thoroughly","Step 6: Test Thoroughly",[40,1062],"Test CRUD operations:Create new itemsUpdate existing itemsDelete itemsVerify data refreshes automaticallyTest forms:Open create formOpen edit formSubmit formsCheck validationTest caching:Verify data loads from cacheVerify cache invalidates after mutationsCheck multiple views update together",{"id":1232,"title":1233,"titles":1234,"content":1235,"level":449},"/guides/migration#step-7-clean-up","Step 7: Clean Up",[40,1062],"# Remove backup if everything works\nrm -rf layers.backup\n\n# Clear caches\nrm -rf .nuxt node_modules/.cache\n\n# Restart dev server\npnpm dev",{"id":1237,"title":1106,"titles":1238,"content":528,"level":391},"/guides/migration#common-migration-issues-1",[40],{"id":1240,"title":1241,"titles":1242,"content":1243,"level":449},"/guides/migration#issue-send-is-not-a-function","Issue: \"send is not a function\"",[40,1106],"Cause: Using old form with new core library Fix: Regenerate form or manually update (see Step 3)",{"id":1245,"title":1246,"titles":1247,"content":1248,"level":449},"/guides/migration#issue-data-not-refreshing","Issue: Data not refreshing",[40,1106],"Cause: Old cache invalidation logic Fix: Ensure using useCollectionMutation() which handles invalidation automatically",{"id":1250,"title":1251,"titles":1252,"content":1253,"level":449},"/guides/migration#issue-type-errors","Issue: Type errors",[40,1106],"Cause: Type definitions changed Fix: rm -rf .nuxt\nnpx nuxt prepare\n# Restart TS server in VS Code",{"id":1255,"title":1256,"titles":1257,"content":1258,"level":449},"/guides/migration#issue-forms-not-submitting","Issue: Forms not submitting",[40,1106],"Cause: Missing @submit handler on UForm Fix: Add @submit=\"handleSubmit\" to UForm component",{"id":1260,"title":1261,"titles":1262,"content":1263,"level":391},"/guides/migration#need-help","Need Help?",[40],"If you encounter issues during migration: Check GitHub Issues: github.com/pmcp/nuxt-crouton/issuesSearch Discussions: github.com/pmcp/nuxt-crouton/discussionsCreate an Issue: Include:\nVersion you're migrating fromVersion you're migrating toError messagesCode samples",{"id":1265,"title":1007,"titles":1266,"content":1267,"level":391},"/guides/migration#related-resources",[40],"API Reference - New API documentationTroubleshooting - Common issues and fixesBest Practices - Recommended patternsWorking with Data - Mutation guide html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":45,"title":44,"titles":1269,"content":1270,"level":385},[],"Recommended patterns and practices for building with Nuxt Crouton Here are the recommended patterns and practices for building maintainable, performant applications with Nuxt Crouton.",{"id":1272,"title":1273,"titles":1274,"content":1275,"level":391},"/guides/best-practices#the-recommended-workflow","The Recommended Workflow",[44],"After generating a collection, the best way to build your app is: Use the built-in endpoints — Nuxt Crouton provides complete CRUD endpoints automatically. Don't create custom API routes unless you need custom logic.Customize the generated components — Edit _Form.vue and List.vue directly. These are designed to work seamlessly with Crouton's composables and endpoints.Extend via composables — Use useCollectionQuery and useCollectionMutation for data operations. They handle caching, team-scoping, and error handling automatically. The generated components + built-in endpoints + Crouton composables form an integrated system. Replacing any part means losing that integration.",{"id":1277,"title":1278,"titles":1279,"content":528,"level":391},"/guides/best-practices#code-organization","Code Organization",[44],{"id":1281,"title":1282,"titles":1283,"content":1284,"level":449},"/guides/best-practices#use-layers-for-domain-separation","Use Layers for Domain Separation",[44,1278],"Use Nuxt layers to organize your collections by business domain rather than technical function. This approach provides clear boundaries between domains, makes code easier to maintain and test, allows independent deployment of layers, and makes them reusable across projects. For a comprehensive guide on architecture and domain-driven design, see the Architecture documentation.",{"id":1286,"title":1287,"titles":1288,"content":1289,"level":449},"/guides/best-practices#collection-naming-conventions","Collection Naming Conventions",[44,1278],"Use plural names like products, orders, and posts. For multi-word names, use camelCase like blogPosts or orderItems. Be descriptive—choose userProfiles over just profiles. Avoid singular names, abbreviations, or generic names like items or data.",{"id":1291,"title":1292,"titles":1293,"content":1294,"level":449},"/guides/best-practices#field-naming-conventions","Field Naming Conventions",[44,1278],"Use title as the display field: Every collection that needs a human-readable label should use title as the primary display field. This is required for CroutonFormReferenceSelect dropdowns to work correctly. {\n  \"title\": { \"type\": \"string\", \"meta\": { \"required\": true, \"label\": \"Name\" } }\n} Do NOT use name for display fields—use title consistently across all collections. The CroutonFormReferenceSelect component defaults to labelKey=\"title\".",{"id":1296,"title":1297,"titles":1298,"content":1299,"level":449},"/guides/best-practices#file-organization","File Organization",[44,1278],"Keep generated files clean and customizations separate: layers/shop/\n  ├── components/\n  │   └── products/\n  │       ├── _Form.vue             # Generated (customize freely)\n  │       ├── List.vue              # Generated (customize freely)\n  │       └── ProductCard.vue       # Your custom component\n  │\n  ├── composables/\n  │   ├── useProducts.ts            # Generated (customize freely)\n  │   └── useProductHelpers.ts      # Your custom composable\n  │\n  └── utils/\n      └── productFormatters.ts      # Your utilities",{"id":1301,"title":1302,"titles":1303,"content":528,"level":391},"/guides/best-practices#data-operations","Data Operations",[44],{"id":1305,"title":1306,"titles":1307,"content":1308,"level":449},"/guides/best-practices#choose-the-right-mutation-method","Choose the Right Mutation Method",[44,1302],"Mutation Methods: For complete API documentation and examples, see Mutation Composables. Use useCollectionMutation() for: Generated formsRepeated operations on the same collectionMulti-step wizardsBulk operations Use useCroutonMutate() for: Quick toggle buttonsOne-off actionsUtility functionsDifferent collections \u003Cscript setup lang=\"ts\">\n// ✅ Good for quick actions\nconst { mutate } = useCroutonMutate()\n\nconst toggleFeatured = async (product: Product) => {\n  await mutate('update', 'shopProducts', {\n    id: product.id,\n    featured: !product.featured\n  })\n}\n\u003C/script>",{"id":1310,"title":1311,"titles":1312,"content":1313,"level":449},"/guides/best-practices#always-type-your-queries","Always Type Your Queries",[44,1302],"Query Examples: For complete useCollectionQuery patterns, see Querying Data. Use TypeScript generics for type safety: // ❌ Bad - no type safety\nconst { items } = await useCollectionQuery('shopProducts')\n\n// ✅ Good - full type safety\nimport type { ShopProduct } from '~/layers/shop/types/products'\nconst { items } = await useCollectionQuery\u003CShopProduct>('shopProducts') TypeScript with Vue: Learn more about TypeScript generics in Vue in the Vue TypeScript documentation.",{"id":1315,"title":1316,"titles":1317,"content":1318,"level":449},"/guides/best-practices#handle-loading-and-error-states","Handle Loading and Error States",[44,1302],"Always provide feedback to users: \u003Cscript setup lang=\"ts\">\nconst { items, pending, error, refresh } = await useCollectionQuery('shopProducts')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"pending\" class=\"flex justify-center p-8\">\n    \u003CUIcon name=\"i-lucide-refresh-cw\" class=\"animate-spin\" />\n  \u003C/div>\n\n  \u003Cdiv v-else-if=\"error\" class=\"p-4 bg-red-50 text-red-600\">\n    Error loading products.\n    \u003CUButton @click=\"refresh\" size=\"sm\">Retry\u003C/UButton>\n  \u003C/div>\n\n  \u003Cdiv v-else-if=\"items.length === 0\" class=\"p-8 text-center text-gray-500\">\n    No products found.\n  \u003C/div>\n\n  \u003CCroutonCollection v-else :rows=\"items\" :columns=\"columns\" />\n\u003C/template>",{"id":1320,"title":1321,"titles":1322,"content":1323,"level":449},"/guides/best-practices#use-computed-queries-for-reactivity","Use Computed Queries for Reactivity",[44,1302],"Make queries reactive to parameter changes. See Querying Data for complete examples of reactive query patterns.",{"id":1325,"title":1326,"titles":1327,"content":528,"level":391},"/guides/best-practices#forms-and-validation","Forms and Validation",[44],{"id":1329,"title":1330,"titles":1331,"content":1332,"level":449},"/guides/best-practices#keep-validation-in-composables","Keep Validation in Composables",[44,1326],"Centralize validation logic using Zod: // layers/shop/composables/useProducts.ts\nimport { z } from 'zod'\n\nexport function useShopProducts() {\n  const schema = z.object({\n    name: z.string().min(1, 'Name is required'),\n    price: z.number().min(0, 'Price must be positive'),\n    sku: z.string().regex(/^[A-Z]{3}-\\d{4}$/, 'SKU format: ABC-1234')\n  })\n\n  return { schema }\n}",{"id":1334,"title":1335,"titles":1336,"content":1337,"level":449},"/guides/best-practices#validate-before-submit","Validate Before Submit",[44,1326],"Don't rely on form validation alone: \u003Cscript setup lang=\"ts\">\nconst { schema } = useShopProducts()\nconst state = ref({ name: '', price: 0 })\n\nconst handleSubmit = async () => {\n  // ✅ Good - validate before API call\n  const result = schema.safeParse(state.value)\n\n  if (!result.success) {\n    // Show errors\n    return\n  }\n\n  await create(result.data)\n}\n\u003C/script> Zod Validation: Learn more about .safeParse() and validation methods in the Zod documentation.",{"id":1339,"title":1340,"titles":1341,"content":1342,"level":449},"/guides/best-practices#provide-clear-error-messages","Provide Clear Error Messages",[44,1326],"Make validation errors helpful: // ❌ Bad\nz.string().min(1)\n\n// ✅ Good\nz.string().min(1, 'Product name is required')\n\n// ✅ Better\nz.string()\n  .min(1, 'Product name is required')\n  .max(100, 'Product name must be less than 100 characters')",{"id":1344,"title":1345,"titles":1346,"content":528,"level":391},"/guides/best-practices#relations-and-associations","Relations and Associations",[44],{"id":1348,"title":1349,"titles":1350,"content":1351,"level":449},"/guides/best-practices#start-simple-optimize-later","Start Simple, Optimize Later",[44,1345],"Start with foreign keys: // ✅ Start here\nconst product = {\n  id: '123',\n  categoryId: 'cat-456'  // Just store the ID\n}\n\n// Query when needed\nconst category = await db.select()\n  .from(categories)\n  .where(eq(categories.id, product.categoryId)) Add Drizzle relations only if needed: // ✅ Optimize when you have performance issues\nexport const productsRelations = relations(products, ({ one }) => ({\n  category: one(categories, {\n    fields: [products.categoryId],\n    references: [categories.id]\n  })\n})) Drizzle Relations: Learn more about Drizzle ORM relations in the Drizzle documentation.",{"id":1353,"title":1354,"titles":1355,"content":1356,"level":449},"/guides/best-practices#handle-missing-relations","Handle Missing Relations",[44,1345],"Always handle nullable relations: \u003Cscript setup lang=\"ts\">\nconst categoryMap = computed(() =>\n  Object.fromEntries(categories.value.map(c => [c.id, c]))\n)\n\nconst columns = [\n  {\n    key: 'category',\n    label: 'Category',\n    // ✅ Good - handles missing category\n    render: (row) => categoryMap.value[row.categoryId]?.name || 'Uncategorized'\n  }\n]\n\u003C/script>",{"id":1358,"title":1359,"titles":1360,"content":1361,"level":449},"/guides/best-practices#optimize-n1-queries","Optimize N+1 Queries",[44,1345],"Use server-side joins for lists: // ❌ Bad - N+1 query problem\n// 1 query for products + 100 queries for categories\nconst products = await db.select().from(products)\nfor (const product of products) {\n  product.category = await db.select()\n    .from(categories)\n    .where(eq(categories.id, product.categoryId))\n}\n\n// ✅ Good - single query\nconst products = await db.query.products.findMany({\n  with: { category: true }\n})",{"id":1363,"title":1364,"titles":1365,"content":528,"level":391},"/guides/best-practices#performance","Performance",[44],{"id":1367,"title":1368,"titles":1369,"content":1370,"level":449},"/guides/best-practices#implement-pagination-early","Implement Pagination Early",[44,1364],"Don't wait for performance issues. Implement pagination from the start for better performance. Pagination Patterns: For complete pagination examples, see Querying Data.",{"id":1372,"title":1373,"titles":1374,"content":1375,"level":449},"/guides/best-practices#filter-on-the-server","Filter on the Server",[44,1364],"Move filtering to the backend: // server/api/teams/[team]/products/index.get.ts\nexport default defineEventHandler(async (event) => {\n  const query = getQuery(event)\n  const { search, category, inStock } = query\n\n  let dbQuery = db.select().from(products)\n\n  // ✅ Filter in database\n  if (search) {\n    dbQuery = dbQuery.where(like(products.name, `%${search}%`))\n  }\n\n  if (category) {\n    dbQuery = dbQuery.where(eq(products.categoryId, category))\n  }\n\n  if (inStock !== undefined) {\n    dbQuery = dbQuery.where(eq(products.inStock, inStock === 'true'))\n  }\n\n  return dbQuery\n})",{"id":1377,"title":1378,"titles":1379,"content":1380,"level":449},"/guides/best-practices#use-indexes","Use Indexes",[44,1364],"Add database indexes for frequently queried fields: // layers/shop/server/database/schema.ts\nimport { sqliteTable, text, real, index } from 'drizzle-orm/sqlite-core'\n\nexport const shopProducts = sqliteTable('shop_products', {\n  id: text('id').primaryKey(),\n  teamId: text('teamId').notNull(),\n  categoryId: text('categoryId'),\n  name: text('name').notNull(),\n  price: real('price')\n}, (table) => ({\n  // ✅ Add indexes for performance\n  teamIdx: index('products_team_idx').on(table.teamId),\n  categoryIdx: index('products_category_idx').on(table.categoryId),\n  nameIdx: index('products_name_idx').on(table.name)\n}))",{"id":1382,"title":1383,"titles":1384,"content":528,"level":391},"/guides/best-practices#customization","Customization",[44],{"id":1386,"title":1387,"titles":1388,"content":1389,"level":449},"/guides/best-practices#own-the-generated-code","Own the Generated Code",[44,1383],"Don't be afraid to customize: \u003C!-- layers/shop/components/products/_Form.vue -->\n\u003Cscript setup lang=\"ts\">\n// ✅ Keep generated logic\nconst props = defineProps\u003CShopProductsFormProps>()\nconst { create, update } = useCollectionMutation(props.collection)\n\n// ✅ Add your customizations\nconst uploadingImage = ref(false)\nconst imagePreview = ref\u003Cstring | null>(null)\n\nconst handleImageUpload = async (file: File) => {\n  uploadingImage.value = true\n  const url = await uploadToCloudinary(file)\n  state.value.imageUrl = url\n  uploadingImage.value = false\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm @submit=\"handleSubmit\">\n    \u003C!-- Generated fields -->\n    \u003CUFormField label=\"Name\" name=\"name\">\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n\n    \u003C!-- Your custom fields -->\n    \u003CUFormField label=\"Product Image\" name=\"imageUrl\">\n      \u003Cimg v-if=\"state.imageUrl\" :src=\"state.imageUrl\" />\n      \u003CUButton @click=\"triggerUpload\" :loading=\"uploadingImage\">\n        Upload Image\n      \u003C/UButton>\n    \u003C/UFormField>\n\n    \u003CCroutonFormActionButton :action=\"action\" :loading=\"loading\" />\n  \u003C/UForm>\n\u003C/template>",{"id":1391,"title":1392,"titles":1393,"content":1394,"level":449},"/guides/best-practices#document-your-changes","Document Your Changes",[44,1383],"Add comments to explain customizations: \u003Cscript setup lang=\"ts\">\n// GENERATED: Base form setup\nconst props = defineProps\u003CShopProductsFormProps>()\nconst { create, update } = useCollectionMutation(props.collection)\n\n// CUSTOM: Image upload functionality\n// Uses Cloudinary for storage, see docs/image-upload.md\nconst uploadingImage = ref(false)\nconst handleImageUpload = async (file: File) => {\n  // ... upload logic\n}\n\n// CUSTOM: Category management\n// Allows creating new categories inline\nconst showNewCategoryForm = ref(false)\n\u003C/script>",{"id":1396,"title":1397,"titles":1398,"content":1399,"level":449},"/guides/best-practices#regenerate-carefully","Regenerate Carefully",[44,1383],"Before regenerating: Backup customizations: cp layers/shop/components/products/_Form.vue layers/shop/components/products/_Form.vue.backup Regenerate: npx crouton-generate shop products --fields-file schema.json --force Restore customizations:Compare filesMerge custom logicTest thoroughly",{"id":1401,"title":1402,"titles":1403,"content":528,"level":391},"/guides/best-practices#testing","Testing",[44],{"id":1405,"title":1406,"titles":1407,"content":1408,"level":449},"/guides/best-practices#test-crud-operations","Test CRUD Operations",[44,1402],"Ensure all operations work: // tests/products.test.ts\nimport { describe, it, expect } from 'vitest'\n\ndescribe('Product CRUD', () => {\n  it('creates product', async () => {\n    const { create } = useCollectionMutation('shopProducts')\n    const product = await create({ name: 'Test', price: 10 })\n    expect(product.id).toBeDefined()\n  })\n\n  it('updates product', async () => {\n    const { update } = useCollectionMutation('shopProducts')\n    const updated = await update('123', { price: 20 })\n    expect(updated.price).toBe(20)\n  })\n\n  it('deletes product', async () => {\n    const { deleteItems } = useCollectionMutation('shopProducts')\n    await deleteItems(['123'])\n    // Verify deletion\n  })\n})",{"id":1410,"title":1411,"titles":1412,"content":1413,"level":449},"/guides/best-practices#test-validation","Test Validation",[44,1402],"Ensure validation catches errors: describe('Product validation', () => {\n  it('requires name', () => {\n    const { schema } = useShopProducts()\n    const result = schema.safeParse({ price: 10 })\n    expect(result.success).toBe(false)\n  })\n\n  it('validates price is positive', () => {\n    const { schema } = useShopProducts()\n    const result = schema.safeParse({ name: 'Test', price: -5 })\n    expect(result.success).toBe(false)\n  })\n})",{"id":1415,"title":1416,"titles":1417,"content":528,"level":391},"/guides/best-practices#security","Security",[44],{"id":1419,"title":1420,"titles":1421,"content":1422,"level":449},"/guides/best-practices#validate-on-server","Validate on Server",[44,1416],"Never trust client-side validation alone: // server/api/teams/[team]/products/index.post.ts\nimport { z } from 'zod'\n\nconst schema = z.object({\n  name: z.string().min(1).max(200),\n  price: z.number().min(0).max(1000000),\n  teamId: z.string()\n})\n\nexport default defineEventHandler(async (event) => {\n  const body = await readBody(event)\n\n  // ✅ Validate on server\n  const result = schema.safeParse(body)\n\n  if (!result.success) {\n    throw createError({\n      status: 400,\n      statusText: 'Invalid data'\n    })\n  }\n\n  return await db.insert(products).values(result.data)\n})",{"id":1424,"title":1425,"titles":1426,"content":1427,"level":449},"/guides/best-practices#scope-by-team","Scope by Team",[44,1416],"Always filter by team ID: // ✅ Good - team-scoped\nexport default defineEventHandler(async (event) => {\n  const teamId = getRouterParam(event, 'team')\n\n  return await db.select()\n    .from(products)\n    .where(eq(products.teamId, teamId))  // ← Always filter by team\n})",{"id":1429,"title":1430,"titles":1431,"content":1432,"level":449},"/guides/best-practices#dont-expose-secrets","Don't Expose Secrets",[44,1416],"Never commit sensitive data: // ❌ Bad\nconst apiKey = 'sk_live_123abc'\n\n// ✅ Good\nconst apiKey = process.env.API_KEY Add to .gitignore: .env\n.env.local\nsecrets/",{"id":1434,"title":1007,"titles":1435,"content":1436,"level":391},"/guides/best-practices#related-resources",[44],"API Reference - Detailed API docsTroubleshooting - Common issuesMigration Guide - Upgrade guidesWorking with Data - Data operations html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":49,"title":48,"titles":1438,"content":1439,"level":385},[],"How to implement server-side pagination with generated collections in Nuxt Crouton",{"id":1441,"title":48,"titles":1442,"content":1443,"level":385},"/guides/pagination#pagination-guide",[],"This guide explains how to implement pagination in Nuxt Crouton, including how to modify generated collections for server-side pagination.",{"id":1445,"title":1446,"titles":1447,"content":1448,"level":391},"/guides/pagination#understanding-pagination-in-crouton","Understanding Pagination in Crouton",[48],"Nuxt Crouton supports two pagination modes: ModeBest ForHow It WorksClient-side (default)Small datasets (\u003C 500 items)Fetches all data, paginates in browserServer-sideLarge datasets (> 500 items)Fetches one page at a time from API Generated collections use client-side pagination by default. All data is fetched at once, and pagination happens in the browser. This works well for small datasets but doesn't scale.",{"id":1450,"title":1451,"titles":1452,"content":1453,"level":391},"/guides/pagination#when-to-add-server-side-pagination","When to Add Server-Side Pagination",[48],"Add server-side pagination when: Your collection has more than 500 itemsInitial load time is slowYou need to filter by date ranges (e.g., bookings, orders)Memory usage is a concernUsers frequently access only recent data",{"id":1455,"title":1456,"titles":1457,"content":1458,"level":391},"/guides/pagination#adding-server-side-pagination-to-generated-collections","Adding Server-Side Pagination to Generated Collections",[48],"The generator creates working CRUD endpoints, but without pagination support. Here's how to add it:",{"id":1460,"title":1461,"titles":1462,"content":1463,"level":449},"/guides/pagination#step-1-add-paginated-query-function","Step 1: Add Paginated Query Function",[48,1456],"Edit the generated queries.ts file in your layer: // layers/[layer]/collections/[collection]/server/database/queries.ts\n\n// Keep existing imports\nimport { eq, and, desc, sql } from 'drizzle-orm'\n\n// Add this new function alongside existing ones\nexport async function getPaginated[PascalCasePlural](\n  teamId: string,\n  options: {\n    page?: number\n    limit?: number\n    sortBy?: string\n    sortDirection?: 'asc' | 'desc'\n  } = {}\n) {\n  const {\n    page = 1,\n    limit = 20,\n    sortBy = 'createdAt',\n    sortDirection = 'desc'\n  } = options\n\n  const offset = (page - 1) * limit\n  const db = useDB()\n\n  // Run count and data queries in parallel\n  const [items, countResult] = await Promise.all([\n    db.select()\n      .from(tables.[tableName])\n      .where(eq(tables.[tableName].teamId, teamId))\n      .orderBy(\n        sortDirection === 'desc'\n          ? desc(tables.[tableName][sortBy])\n          : asc(tables.[tableName][sortBy])\n      )\n      .limit(limit)\n      .offset(offset),\n\n    db.select({ count: sql\u003Cnumber>`count(*)` })\n      .from(tables.[tableName])\n      .where(eq(tables.[tableName].teamId, teamId))\n  ])\n\n  const totalItems = Number(countResult[0]?.count || 0)\n\n  return {\n    items,\n    pagination: {\n      currentPage: page,\n      pageSize: limit,\n      totalItems,\n      totalPages: Math.ceil(totalItems / limit)\n    }\n  }\n} Example for bookings: // layers/bookings/collections/bookings/server/database/queries.ts\n\nexport async function getPaginatedBookingsBookings(\n  teamId: string,\n  options: { page?: number; limit?: number } = {}\n) {\n  const { page = 1, limit = 20 } = options\n  const offset = (page - 1) * limit\n  const db = useDB()\n\n  const [items, countResult] = await Promise.all([\n    db.select()\n      .from(tables.bookingsBookings)\n      .where(eq(tables.bookingsBookings.teamId, teamId))\n      .orderBy(desc(tables.bookingsBookings.date))\n      .limit(limit)\n      .offset(offset),\n\n    db.select({ count: sql\u003Cnumber>`count(*)` })\n      .from(tables.bookingsBookings)\n      .where(eq(tables.bookingsBookings.teamId, teamId))\n  ])\n\n  return {\n    items,\n    pagination: {\n      currentPage: page,\n      pageSize: limit,\n      totalItems: Number(countResult[0]?.count || 0),\n      totalPages: Math.ceil(Number(countResult[0]?.count || 0) / limit)\n    }\n  }\n}",{"id":1465,"title":1466,"titles":1467,"content":1468,"level":449},"/guides/pagination#step-2-update-the-api-endpoint","Step 2: Update the API Endpoint",[48,1456],"Modify the generated index.get.ts to accept pagination parameters: // layers/[layer]/collections/[collection]/server/api/teams/[id]/[api-path]/index.get.ts\n\n// Add import for new paginated function\nimport {\n  getAll[PascalCasePlural],\n  get[PascalCasePlural]ByIds,\n  getPaginated[PascalCasePlural]  // Add this\n} from '../../../../database/queries'\n\nexport default defineEventHandler(async (event) => {\n  // ... existing team auth code ...\n\n  const query = getQuery(event)\n\n  // Handle ID-based queries (unchanged)\n  if (query.ids) {\n    const ids = String(query.ids).split(',')\n    return await get[PascalCasePlural]ByIds(team.id, ids)\n  }\n\n  // NEW: Handle pagination\n  if (query.page || query.limit || query.paginate === 'true') {\n    const page = Number(query.page) || 1\n    const limit = Number(query.limit) || 20\n    const sortBy = String(query.sortBy || 'createdAt')\n    const sortDirection = query.sortDirection === 'asc' ? 'asc' : 'desc'\n\n    return await getPaginated[PascalCasePlural](team.id, {\n      page,\n      limit,\n      sortBy,\n      sortDirection\n    })\n  }\n\n  // Default: return all (backwards compatible)\n  return await getAll[PascalCasePlural](team.id)\n}) Example for bookings: // layers/bookings/collections/bookings/server/api/teams/[id]/bookings-bookings/index.get.ts\n\nimport {\n  getAllBookingsBookings,\n  getBookingsBookingsByIds,\n  getPaginatedBookingsBookings\n} from '../../../../database/queries'\n\nexport default defineEventHandler(async (event) => {\n  // ... existing team auth code ...\n\n  const query = getQuery(event)\n\n  if (query.ids) {\n    const ids = String(query.ids).split(',')\n    return await getBookingsBookingsByIds(team.id, ids)\n  }\n\n  // Handle pagination\n  if (query.page || query.limit) {\n    return await getPaginatedBookingsBookings(team.id, {\n      page: Number(query.page) || 1,\n      limit: Number(query.limit) || 20\n    })\n  }\n\n  return await getAllBookingsBookings(team.id)\n})",{"id":1470,"title":1471,"titles":1472,"content":1473,"level":449},"/guides/pagination#step-3-use-in-components","Step 3: Use in Components",[48,1456],"Now you can use server-side pagination in your components:",{"id":1475,"title":1476,"titles":1477,"content":1478,"level":748},"/guides/pagination#with-croutontable","With CroutonTable",[48,1456,1471],"\u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst pageSize = ref(20)\n\n// Fetch with pagination params\nconst { data, pending, refresh } = await useFetch(\n  () => `/api/teams/${teamId}/bookings-bookings`,\n  {\n    query: computed(() => ({\n      page: page.value,\n      limit: pageSize.value\n    }))\n  }\n)\n\nconst items = computed(() => data.value?.items || [])\nconst paginationData = computed(() => data.value?.pagination || null)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonTable\n    collection=\"bookingsBookings\"\n    :rows=\"items\"\n    :columns=\"columns\"\n    server-pagination\n    :pagination-data=\"paginationData\"\n    :refresh-fn=\"refresh\"\n  />\n\u003C/template>",{"id":1480,"title":1481,"titles":1482,"content":1483,"level":748},"/guides/pagination#with-usecollectionquery","With useCollectionQuery",[48,1456,1471],"\u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst pageSize = ref(20)\n\nconst { data, pending, refresh } = await useCollectionQuery('bookingsBookings', {\n  query: computed(() => ({\n    page: page.value,\n    limit: pageSize.value\n  }))\n})\n\nconst items = computed(() => data.value?.items || [])\nconst paginationData = computed(() => data.value?.pagination)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonTable\n    collection=\"bookingsBookings\"\n    :rows=\"items\"\n    :columns=\"columns\"\n    server-pagination\n    :pagination-data=\"paginationData\"\n    :refresh-fn=\"refresh\"\n  />\n\u003C/template>",{"id":1485,"title":1486,"titles":1487,"content":1488,"level":748},"/guides/pagination#manual-pagination-controls","Manual Pagination Controls",[48,1456,1471],"\u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst pageSize = ref(20)\n\nconst { data, refresh } = await useFetch('/api/teams/current/bookings-bookings', {\n  query: computed(() => ({ page: page.value, limit: pageSize.value }))\n})\n\nconst items = computed(() => data.value?.items || [])\nconst pagination = computed(() => data.value?.pagination)\n\nasync function goToPage(newPage: number) {\n  page.value = newPage\n  await refresh()\n}\n\nasync function changePageSize(newSize: number) {\n  pageSize.value = newSize\n  page.value = 1  // Reset to first page\n  await refresh()\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003C!-- Your list/table -->\n    \u003Cdiv v-for=\"item in items\" :key=\"item.id\">\n      {{ item.title }}\n    \u003C/div>\n\n    \u003C!-- Pagination controls -->\n    \u003CCroutonTablePagination\n      :page=\"page\"\n      :page-count=\"pageSize\"\n      :total-items=\"pagination?.totalItems || 0\"\n      @update:page=\"goToPage\"\n      @update:page-count=\"changePageSize\"\n    />\n  \u003C/div>\n\u003C/template>",{"id":1490,"title":1491,"titles":1492,"content":1493,"level":391},"/guides/pagination#adding-filtering-with-pagination","Adding Filtering with Pagination",[48],"Often you'll want to combine pagination with filtering. Here's how:",{"id":1495,"title":1496,"titles":1497,"content":1498,"level":449},"/guides/pagination#update-the-query-function","Update the Query Function",[48,1491],"export async function getPaginatedBookingsBookings(\n  teamId: string,\n  options: {\n    page?: number\n    limit?: number\n    // Add filter options\n    startDate?: string\n    endDate?: string\n    locationId?: string\n    status?: string\n  } = {}\n) {\n  const {\n    page = 1,\n    limit = 20,\n    startDate,\n    endDate,\n    locationId,\n    status\n  } = options\n\n  const offset = (page - 1) * limit\n  const db = useDB()\n\n  // Build conditions array\n  const conditions = [eq(tables.bookingsBookings.teamId, teamId)]\n\n  if (startDate) {\n    conditions.push(gte(tables.bookingsBookings.date, new Date(startDate)))\n  }\n  if (endDate) {\n    conditions.push(lte(tables.bookingsBookings.date, new Date(endDate)))\n  }\n  if (locationId) {\n    conditions.push(eq(tables.bookingsBookings.locationId, locationId))\n  }\n  if (status) {\n    conditions.push(eq(tables.bookingsBookings.status, status))\n  }\n\n  const whereClause = and(...conditions)\n\n  const [items, countResult] = await Promise.all([\n    db.select()\n      .from(tables.bookingsBookings)\n      .where(whereClause)\n      .orderBy(desc(tables.bookingsBookings.date))\n      .limit(limit)\n      .offset(offset),\n\n    db.select({ count: sql\u003Cnumber>`count(*)` })\n      .from(tables.bookingsBookings)\n      .where(whereClause)\n  ])\n\n  return {\n    items,\n    pagination: {\n      currentPage: page,\n      pageSize: limit,\n      totalItems: Number(countResult[0]?.count || 0),\n      totalPages: Math.ceil(Number(countResult[0]?.count || 0) / limit)\n    }\n  }\n}",{"id":1500,"title":1501,"titles":1502,"content":1503,"level":449},"/guides/pagination#update-the-api-endpoint","Update the API Endpoint",[48,1491],"export default defineEventHandler(async (event) => {\n  // ... auth code ...\n\n  const query = getQuery(event)\n\n  if (query.page || query.limit) {\n    return await getPaginatedBookingsBookings(team.id, {\n      page: Number(query.page) || 1,\n      limit: Number(query.limit) || 20,\n      startDate: query.startDate as string,\n      endDate: query.endDate as string,\n      locationId: query.locationId as string,\n      status: query.status as string\n    })\n  }\n\n  return await getAllBookingsBookings(team.id)\n})",{"id":1505,"title":1506,"titles":1507,"content":1508,"level":449},"/guides/pagination#use-with-filters-in-component","Use with Filters in Component",[48,1491],"\u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst pageSize = ref(20)\nconst filters = ref({\n  startDate: null,\n  endDate: null,\n  locationId: null,\n  status: null\n})\n\nconst { data, refresh } = await useFetch('/api/teams/current/bookings-bookings', {\n  query: computed(() => ({\n    page: page.value,\n    limit: pageSize.value,\n    ...Object.fromEntries(\n      Object.entries(filters.value).filter(([_, v]) => v != null)\n    )\n  }))\n})\n\n// Reset to page 1 when filters change\nwatch(filters, () => {\n  page.value = 1\n}, { deep: true })\n\u003C/script>",{"id":1510,"title":1511,"titles":1512,"content":1513,"level":391},"/guides/pagination#pagination-response-format","Pagination Response Format",[48],"For consistency, paginated endpoints should return this structure: interface PaginatedResponse\u003CT> {\n  items: T[]\n  pagination: {\n    currentPage: number    // Current page (1-indexed)\n    pageSize: number       // Items per page\n    totalItems: number     // Total count across all pages\n    totalPages: number     // Total number of pages\n  }\n}",{"id":1515,"title":44,"titles":1516,"content":528,"level":391},"/guides/pagination#best-practices",[48],{"id":1518,"title":1519,"titles":1520,"content":1521,"level":449},"/guides/pagination#_1-default-to-reasonable-limits","1. Default to Reasonable Limits",[48,44],"const limit = Math.min(Number(query.limit) || 20, 100)  // Cap at 100",{"id":1523,"title":1524,"titles":1525,"content":1526,"level":449},"/guides/pagination#_2-reset-page-on-filter-changes","2. Reset Page on Filter Changes",[48,44],"watch(filters, () => {\n  page.value = 1  // Always reset to page 1\n}, { deep: true })",{"id":1528,"title":1529,"titles":1530,"content":1531,"level":449},"/guides/pagination#_3-handle-edge-cases","3. Handle Edge Cases",[48,44],"// In query function\nconst totalItems = Number(countResult[0]?.count || 0)\nconst totalPages = Math.max(1, Math.ceil(totalItems / limit))\n\n// Ensure page is valid\nconst validPage = Math.min(Math.max(1, page), totalPages)",{"id":1533,"title":1534,"titles":1535,"content":1536,"level":449},"/guides/pagination#_4-keep-backwards-compatibility","4. Keep Backwards Compatibility",[48,44],"The modified endpoint still returns all items when no pagination params are passed, so existing code continues to work.",{"id":1538,"title":1539,"titles":1540,"content":1541,"level":449},"/guides/pagination#_5-consider-cursor-based-pagination-for-large-datasets","5. Consider Cursor-Based Pagination for Large Datasets",[48,44],"For very large datasets (100k+ items), consider cursor-based pagination instead of offset-based: // Instead of page/offset\n?cursor=abc123&limit=20\n\n// Returns\n{\n  items: [...],\n  nextCursor: \"xyz789\",\n  hasMore: true\n}",{"id":1543,"title":36,"titles":1544,"content":528,"level":391},"/guides/pagination#troubleshooting",[48],{"id":1546,"title":1547,"titles":1548,"content":1549,"level":449},"/guides/pagination#pagination-shows-wrong-total","Pagination shows wrong total",[48,36],"Problem: Total items count is incorrect Solution: Ensure the count query uses the same WHERE conditions as the data query: // Both queries must have identical WHERE clauses\nconst whereClause = and(\n  eq(tables.bookings.teamId, teamId),\n  // ... same filters\n)\n\nconst [items, countResult] = await Promise.all([\n  db.select().from(tables.bookings).where(whereClause).limit(limit).offset(offset),\n  db.select({ count: sql`count(*)` }).from(tables.bookings).where(whereClause)  // Same whereClause!\n])",{"id":1551,"title":1552,"titles":1553,"content":1554,"level":449},"/guides/pagination#page-resets-unexpectedly","Page resets unexpectedly",[48,36],"Problem: Page keeps jumping back to 1 Solution: Check that your query params are reactive but not causing infinite loops: // Use computed for query params\nconst queryParams = computed(() => ({\n  page: page.value,\n  limit: pageSize.value\n}))\n\n// NOT this (causes infinite loops)\nwatch([page, pageSize], refresh)  // Don't do this!",{"id":1556,"title":1557,"titles":1558,"content":1559,"level":449},"/guides/pagination#empty-results-after-filter-change","Empty results after filter change",[48,36],"Problem: Filter changes show empty results Solution: Reset page to 1 when filters change: watch(filters, () => {\n  page.value = 1\n  // The query will auto-refresh due to computed dependency\n}, { deep: true })",{"id":1561,"title":1562,"titles":1563,"content":1564,"level":391},"/guides/pagination#related-topics","Related Topics",[48],"Table Patterns - Complete table documentationQuerying Data - Query composablesAPI Reference - useCollectionQuery API html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":53,"title":52,"titles":1566,"content":1567,"level":385},[],"Complete guide to managing media files with nuxt-crouton-assets The @fyit/crouton-assets package provides a centralized media library system for managing images and files across your Nuxt Crouton application.",{"id":1569,"title":936,"titles":1570,"content":528,"level":391},"/guides/asset-management#overview",[52],{"id":1572,"title":1573,"titles":1574,"content":1575,"level":449},"/guides/asset-management#two-approaches-to-file-uploads","Two Approaches to File Uploads",[52,936],"Nuxt Crouton offers flexibility in how you handle file uploads: 1. Simple Direct Uploads (Base Package) Store URLs directly in your databaseQuick and simple for basic needsUses CroutonImageUpload componentNo metadata tracking 2. Full Asset Management (Assets Package) Centralized media libraryRich metadata (alt text, size, MIME type)Team-based ownershipSearch and browse capabilitiesReuse assets across collections",{"id":1577,"title":13,"titles":1578,"content":528,"level":391},"/guides/asset-management#installation",[52],{"id":1580,"title":426,"titles":1581,"content":1582,"level":449},"/guides/asset-management#prerequisites",[52,13],"Nuxt 4+@fyit/crouton installed@nuxthub/core with blob storage enabled",{"id":1584,"title":1585,"titles":1586,"content":1587,"level":449},"/guides/asset-management#install-the-package","Install the Package",[52,13],"pnpm add @fyit/crouton-assets",{"id":1589,"title":436,"titles":1590,"content":1591,"level":449},"/guides/asset-management#configure-nuxt",[52,13],"Add the assets layer to your nuxt.config.ts: export default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-assets'  // Add this\n  ],\n  hub: {\n    blob: true  // Required for file storage\n  }\n})",{"id":1593,"title":1594,"titles":1595,"content":528,"level":391},"/guides/asset-management#setting-up-the-assets-collection","Setting Up the Assets Collection",[52],{"id":1597,"title":1598,"titles":1599,"content":1600,"level":449},"/guides/asset-management#generate-the-collection","Generate the Collection",[52,1594],"Use the crouton CLI to create the assets collection: crouton-generate core assets \\\n  --fields-file=node_modules/@fyit/crouton-assets/assets-schema.json \\\n  --dialect=sqlite This creates: layers/core/collections/assets/\n├── app/\n│   └── components/\n│       ├── _Form.vue       # CRUD form\n│       └── List.vue        # Asset list view\n├── types.ts                # Type exports\n├── server/\n│   ├── database/\n│   │   └── schema.ts       # Drizzle database schema\n│   └── api/\n│       └── [...].ts        # CRUD endpoints",{"id":1602,"title":1603,"titles":1604,"content":1605,"level":449},"/guides/asset-management#database-schema","Database Schema",[52,1594],"The generated assets collection includes: {\n  id: string              // Unique identifier\n  teamId: string          // Team/organization ownership\n  userId: string          // User who uploaded\n  filename: string        // Original filename\n  pathname: string        // Blob storage path\n  contentType: string     // MIME type (image/jpeg, etc)\n  size: number            // File size in bytes\n  alt: string             // Alt text for accessibility\n  uploadedAt: Date        // Upload timestamp\n  createdAt: Date         // Record created\n  updatedAt: Date         // Record updated\n  updatedBy: string       // Last modifier\n}",{"id":1607,"title":1608,"titles":1609,"content":528,"level":391},"/guides/asset-management#usage","Usage",[52],{"id":1611,"title":1612,"titles":1613,"content":1614,"level":449},"/guides/asset-management#in-schema-definitions","In Schema Definitions",[52,1608],"Reference assets in your collection schemas: {\n  \"imageId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"assets\",\n    \"meta\": {\n      \"component\": \"CroutonAssetsPicker\",\n      \"label\": \"Featured Image\",\n      \"area\": \"main\"\n    }\n  }\n} This automatically generates: Asset picker in the formThumbnail preview in list viewsProper validation Auto-Detection: If your refTarget points to a collection named assets, images, files, or media, the generator will automatically use CroutonAssetsPicker even without specifying the component in meta!",{"id":1616,"title":1617,"titles":1618,"content":1619,"level":449},"/guides/asset-management#asset-picker-component","Asset Picker Component",[52,1608],"Browse and select from your asset library: \u003Ctemplate>\n  \u003CUFormField label=\"Product Image\" name=\"imageId\">\n    \u003CCroutonAssetsPicker v-model=\"state.imageId\" />\n  \u003C/UFormField>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst state = ref({\n  imageId: ''\n})\n\u003C/script> Features: Grid view with thumbnailsReal-time searchUpload new assets inlineAuto-refresh after uploads",{"id":1621,"title":1622,"titles":1623,"content":1624,"level":449},"/guides/asset-management#asset-uploader-component","Asset Uploader Component",[52,1608],"Upload files with metadata: \u003Ctemplate>\n  \u003CUModal v-model=\"showUploader\">\n    \u003Ctemplate #content=\"{ close }\">\n      \u003Cdiv class=\"p-6\">\n        \u003Ch3 class=\"text-lg font-semibold mb-4\">Upload Asset\u003C/h3>\n        \u003CCroutonAssetsUploader @uploaded=\"handleUploaded(close)\" />\n      \u003C/div>\n    \u003C/template>\n  \u003C/UModal>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst showUploader = ref(false)\n\nconst handleUploaded = async (close: () => void, assetId: string) => {\n  console.log('New asset:', assetId)\n  // Refresh your asset list or select the new asset\n  close()\n}\n\u003C/script>",{"id":1626,"title":1627,"titles":1628,"content":1629,"level":449},"/guides/asset-management#programmatic-upload","Programmatic Upload",[52,1608],"Use the composable for custom upload flows: \u003Cscript setup lang=\"ts\">\nconst { uploadAsset, uploading, error } = useAssetUpload()\n\nconst handleDrop = async (event: DragEvent) => {\n  const file = event.dataTransfer?.files[0]\n  if (!file) return\n\n  try {\n    const asset = await uploadAsset(file, {\n      alt: 'Drag-and-drop upload',\n      filename: file.name\n    })\n\n    console.log('Uploaded:', asset.id)\n  } catch (err) {\n    console.error('Upload failed:', error.value)\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv\n    @drop.prevent=\"handleDrop\"\n    @dragover.prevent\n    class=\"border-2 border-dashed p-8 text-center\"\n  >\n    \u003Cp v-if=\"!uploading\">Drop files here to upload\u003C/p>\n    \u003Cp v-else>Uploading...\u003C/p>\n  \u003C/div>\n\u003C/template>",{"id":1631,"title":85,"titles":1632,"content":528,"level":391},"/guides/asset-management#architecture",[52],{"id":1634,"title":1635,"titles":1636,"content":1637,"level":449},"/guides/asset-management#how-it-works","How It Works",[52,85],"Base Package (nuxt-crouton)Provides core upload infrastructure/api/upload-image endpoint/images/[pathname] serving routeBasic upload componentsAssets Package (nuxt-crouton-assets)Provides reusable tools and componentsCroutonAssetsPicker componentCroutonAssetsUploader componentuseAssetUpload() composableReference schema for generationYour Project (Generated Collection)layers/core/collections/assets/CRUD forms and API endpointsDatabase tables and validationTeam-scoped asset management",{"id":1639,"title":1640,"titles":1641,"content":1642,"level":449},"/guides/asset-management#upload-flow","Upload Flow",[52,85],"1. User selects file\n   ↓\n2. File uploaded to NuxtHub blob storage\n   → POST /api/upload-image\n   → Returns { pathname, contentType, size, filename }\n   ↓\n3. Asset record created in database\n   → POST /api/teams/[id]/assets\n   → Stores metadata + pathname\n   ↓\n4. Asset available in library\n   → GET /api/teams/[id]/assets\n   → Returns list with metadata",{"id":1644,"title":1645,"titles":1646,"content":1647,"level":449},"/guides/asset-management#serving-assets","Serving Assets",[52,85],"Browser requests: /images/[pathname]\n                        ↓\n              GET /images/[pathname] route\n                        ↓\n           Fetches from blob storage\n                        ↓\n              Serves file to browser",{"id":1649,"title":1650,"titles":1651,"content":528,"level":391},"/guides/asset-management#common-patterns","Common Patterns",[52],{"id":1653,"title":1654,"titles":1655,"content":1656,"level":449},"/guides/asset-management#product-with-image","Product with Image",[52,1650],"// Schema: shopProducts.json\n{\n  \"name\": {\n    \"type\": \"string\",\n    \"meta\": { \"required\": true }\n  },\n  \"imageId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"assets\",\n    \"meta\": {\n      \"label\": \"Product Image\"\n      // No need to specify component - auto-detected!\n    }\n  }\n} Generated form automatically includes asset picker thanks to auto-detection.",{"id":1658,"title":1659,"titles":1660,"content":1661,"level":449},"/guides/asset-management#multiple-images-gallery","Multiple Images Gallery",[52,1650],"// Schema: blogPosts.json\n{\n  \"title\": {\n    \"type\": \"string\",\n    \"meta\": { \"required\": true }\n  },\n  \"featuredImageId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"assets\",\n    \"meta\": {\n      \"label\": \"Featured Image\"\n      // Auto-detected as asset picker\n    }\n  },\n  \"galleryImageIds\": {\n    \"type\": \"json\",\n    \"meta\": {\n      \"label\": \"Gallery Images\",\n      \"component\": \"MultiAssetPicker\"  // Custom component\n    }\n  }\n} You'll need to create a custom MultiAssetPicker component for multiple selections.",{"id":1663,"title":1664,"titles":1665,"content":1666,"level":449},"/guides/asset-management#avatar-upload","Avatar Upload",[52,1650],"For simple avatar uploads without the asset library: \u003Ctemplate>\n  \u003CCroutonUsersAvatarUpload\n    v-model=\"avatarUrl\"\n    @file-selected=\"handleAvatarUpload\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst avatarUrl = ref('/default-avatar.png')\n\nconst handleAvatarUpload = async (file: File | null) => {\n  if (!file) return\n\n  const formData = new FormData()\n  formData.append('image', file)\n\n  const result = await $fetch('/api/upload-image', {\n    method: 'POST',\n    body: formData\n  })\n  // Returns { pathname, contentType, size, filename }\n\n  avatarUrl.value = `/images/${result.pathname}`\n\n  // Update user profile\n  await $fetch(`/api/users/${user.id}`, {\n    method: 'PATCH',\n    body: { avatar: pathname }\n  })\n}\n\u003C/script>",{"id":1668,"title":1669,"titles":1670,"content":1671,"level":449},"/guides/asset-management#batch-upload","Batch Upload",[52,1650],"Upload multiple files programmatically: \u003Cscript setup lang=\"ts\">\nconst { uploadAssets, uploading } = useAssetUpload()\n\nconst handleBatchUpload = async (files: File[]) => {\n  const assets = await uploadAssets(files, {\n    alt: 'Batch uploaded image'\n  })\n\n  console.log(`Uploaded ${assets.length} files`)\n  return assets.map(a => a.id)\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cinput\n    type=\"file\"\n    multiple\n    @change=\"handleBatchUpload(Array.from($event.target.files))\"\n  />\n  \u003Cdiv v-if=\"uploading\">Uploading {{ files.length }} files...\u003C/div>\n\u003C/template>",{"id":1673,"title":1674,"titles":1675,"content":528,"level":391},"/guides/asset-management#displaying-assets","Displaying Assets",[52],{"id":1677,"title":1678,"titles":1679,"content":1680,"level":449},"/guides/asset-management#in-list-views","In List Views",[52,1674],"The generator automatically creates CardMini components for asset references: \u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"products\"\n    collection=\"shopProducts\"\n  >\n    \u003C!-- Auto-generated for asset references -->\n    \u003Ctemplate #imageId-cell=\"{ row }\">\n      \u003CCardMini\n        v-if=\"row.original.imageId\"\n        :id=\"row.original.imageId\"\n        collection=\"assets\"\n      />\n    \u003C/template>\n  \u003C/CroutonCollection>\n\u003C/template>",{"id":1682,"title":1683,"titles":1684,"content":1685,"level":449},"/guides/asset-management#custom-display","Custom Display",[52,1674],"Fetch and display asset details: \u003Cscript setup lang=\"ts\">\nconst { data: product } = await useFetch('/api/teams/123/shopProducts/456')\n\n// Fetch referenced asset\nconst { data: asset } = await useFetch(\n  `/api/teams/123/assets/${product.value?.imageId}`,\n  { watch: [() => product.value?.imageId] }\n)\n\nconst imageUrl = computed(() =>\n  asset.value?.pathname ? `/images/${asset.value.pathname}` : '/placeholder.png'\n)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cimg\n      :src=\"imageUrl\"\n      :alt=\"asset?.alt || 'Product image'\"\n      class=\"w-full h-64 object-cover rounded-lg\"\n    />\n    \u003Cp class=\"text-sm text-gray-500 mt-2\">\n      {{ asset?.filename }} ({{ formatFileSize(asset?.size) }})\n    \u003C/p>\n  \u003C/div>\n\u003C/template>",{"id":1687,"title":1688,"titles":1689,"content":1690,"level":391},"/guides/asset-management#search-and-browse","Search and Browse",[52],"The asset picker includes built-in search: \u003C!-- In AssetPicker.vue -->\n\u003CUInput\n  v-model=\"searchQuery\"\n  icon=\"i-lucide-search\"\n  placeholder=\"Search assets...\"\n/>\n\n\u003C!-- Filtered results -->\n\u003Cdiv v-for=\"asset in filteredAssets\" :key=\"asset.id\">\n  \u003Cimg :src=\"`/images/${asset.pathname}`\" :alt=\"asset.alt\" />\n\u003C/div> Search matches: FilenameAlt text",{"id":1692,"title":44,"titles":1693,"content":528,"level":391},"/guides/asset-management#best-practices",[52],{"id":1695,"title":1696,"titles":1697,"content":1698,"level":449},"/guides/asset-management#alt-text","Alt Text",[52,44],"Always provide descriptive alt text: await uploadAsset(file, {\n  alt: 'Red Nike sneakers on white background, side view'\n}) Good alt text: Describes the image contentHelps with SEOImproves accessibilityMakes search more effective",{"id":1700,"title":1701,"titles":1702,"content":1703,"level":449},"/guides/asset-management#file-naming","File Naming",[52,44],"Use descriptive filenames: await uploadAsset(file, {\n  filename: 'red-nike-air-max-90-side.jpg',\n  alt: 'Red Nike Air Max 90, side view'\n})",{"id":1705,"title":1706,"titles":1707,"content":1708,"level":449},"/guides/asset-management#organize-by-team","Organize by Team",[52,44],"Assets are automatically scoped to teams: // Assets are team-scoped via teamId\nGET /api/teams/team-123/assets  // Returns only team-123's assets\nGET /api/teams/team-456/assets  // Returns only team-456's assets",{"id":1710,"title":1711,"titles":1712,"content":1713,"level":449},"/guides/asset-management#reuse-assets","Reuse Assets",[52,44],"Reference the same asset multiple times: // Multiple products can share the same asset\n{\n  product1: { imageId: 'asset-123' },\n  product2: { imageId: 'asset-123' },\n  product3: { imageId: 'asset-456' }\n} Benefits: Saves storage spaceConsistent images across productsUpdate once, reflects everywhere",{"id":1715,"title":1716,"titles":1717,"content":1718,"level":391},"/guides/asset-management#custom-asset-collections","Custom Asset Collections",[52],"You can generate multiple asset collections for different purposes: # General assets\ncrouton-generate core assets --fields-file=assets-schema.json\n\n# Product-specific images\ncrouton-generate products productImages --fields-file=product-images-schema.json\n\n# User avatars\ncrouton-generate users avatars --fields-file=avatars-schema.json Then specify the collection when using components: \u003CCroutonAssetsPicker v-model=\"state.imageId\" collection=\"productImages\" />",{"id":1720,"title":36,"titles":1721,"content":528,"level":391},"/guides/asset-management#troubleshooting",[52],{"id":1723,"title":1724,"titles":1725,"content":1726,"level":449},"/guides/asset-management#assets-not-showing","Assets Not Showing",[52,36],"Check NuxtHub blob storage is enabled: // nuxt.config.ts\nexport default defineNuxtConfig({\n  hub: {\n    blob: true\n  }\n}) Verify assets collection is generated: ls layers/core/collections/assets Check team ID in route: // Asset picker needs team in route params\nGET /teams/:team/...",{"id":1728,"title":1729,"titles":1730,"content":1731,"level":449},"/guides/asset-management#upload-fails","Upload Fails",[52,36],"Check file size limitsVerify blob storage permissionsCheck network connectivityReview server logs for errors",{"id":1733,"title":1734,"titles":1735,"content":1736,"level":449},"/guides/asset-management#images-not-displaying","Images Not Displaying",[52,36],"Verify pathname is correct: console.log('Pathname:', asset.pathname)\nconsole.log('Full URL:', `/images/${asset.pathname}`) Check blob storage: # In NuxtHub dashboard, verify file exists Test serving route: curl http://localhost:3000/images/[pathname]",{"id":1738,"title":1739,"titles":1740,"content":1741,"level":391},"/guides/asset-management#migration-from-direct-urls","Migration from Direct URLs",[52],"If you're currently storing URLs directly, you can migrate to the asset system:",{"id":1743,"title":1744,"titles":1745,"content":1746,"level":449},"/guides/asset-management#_1-update-schema","1. Update Schema",[52,1739],"{\n- \"imageUrl\": { \"type\": \"string\" }\n+ \"imageId\": {\n+   \"type\": \"string\",\n+   \"refTarget\": \"assets\"\n+   // Component auto-detected as CroutonAssetsPicker\n+ }\n}",{"id":1748,"title":1749,"titles":1750,"content":1751,"level":449},"/guides/asset-management#_2-migrate-data","2. Migrate Data",[52,1739],"Create a migration script: // scripts/migrate-to-assets.ts\nimport { db } from '~/server/db'\n\nconst products = await db.select().from(shopProducts)\n\nfor (const product of products) {\n  if (!product.imageUrl) continue\n\n  // Extract pathname from URL\n  const pathname = product.imageUrl.replace('/images/', '')\n\n  // Create asset record\n  const asset = await db.insert(assets).values({\n    id: generateId(),\n    teamId: product.teamId,\n    userId: product.userId,\n    filename: pathname.split('/').pop(),\n    pathname,\n    contentType: 'image/jpeg', // Detect from file\n    size: 0, // Could fetch from blob\n    uploadedAt: product.createdAt\n  })\n\n  // Update product reference\n  await db\n    .update(shopProducts)\n    .set({ imageId: asset.id })\n    .where(eq(shopProducts.id, product.id))\n}",{"id":1753,"title":1754,"titles":1755,"content":1756,"level":449},"/guides/asset-management#_3-regenerate-components","3. Regenerate Components",[52,1739],"crouton-generate products shopProducts --overwrite",{"id":1758,"title":1007,"titles":1759,"content":1760,"level":391},"/guides/asset-management#related-resources",[52],"Components Reference - Asset components APIComposables Reference - useAssetUpload APINuxtHub Documentation - Blob storageDrizzle ORM - Database operations html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":57,"title":56,"titles":1762,"content":1763,"level":385},[],"Upcoming features and enhancements being considered for Nuxt Crouton These are features and enhancements being considered for future releases of Nuxt Crouton. They're not promised or scheduled, but represent areas we're exploring based on user feedback and our vision for the project.",{"id":1765,"title":1766,"titles":1767,"content":1768,"level":391},"/guides/future-roadmap#schema-validation","Schema Validation",[56],"Before generating code, Nuxt Crouton could validate your schema file and provide helpful error messages to catch common mistakes early.",{"id":1770,"title":1771,"titles":1772,"content":1773,"level":449},"/guides/future-roadmap#what-it-would-do","What It Would Do",[56,1766],"The generator would check your schema file for common issues like singular collection names, missing required fields, invalid types, or conflicting field names. Instead of generating broken code or failing silently, you'd get clear, actionable error messages before any files are created. npx crouton-generate shop product --fields-file product-schema.json\n\n❌ Schema validation failed:\n\n1. Collection name should be plural: 'product' → 'products'\n2. Field 'price' is missing a type\n3. Field 'description' has invalid type 'longtext' (use 'text' instead)\n4. Field 'id' is reserved and will be auto-generated\n\nRun with --skip-validation to bypass these checks.",{"id":1775,"title":1179,"titles":1776,"content":1777,"level":449},"/guides/future-roadmap#benefits",[56,1766],"You'd catch errors before generating files, learn best practices through helpful error messages, and avoid regenerating code due to schema mistakes. The validation would be smart but not restrictive—you could skip it when you know what you're doing.",{"id":1779,"title":1780,"titles":1781,"content":1782,"level":391},"/guides/future-roadmap#form-area-layouts","Form Area Layouts",[56],"The area metadata in schema files currently exists as infrastructure for the future. This feature would transform it into actual layout options, letting you organize forms into tabs, columns, or sections without writing custom layouts.",{"id":1784,"title":1771,"titles":1785,"content":1786,"level":449},"/guides/future-roadmap#what-it-would-do-1",[56,1780],"When you define areas in your schema, the generated form would automatically organize fields into the layout you specify. You could choose from different layout modes like tabs, columns, or sections. // Schema with areas\n[\n  { \"name\": \"title\", \"type\": \"string\", \"meta\": { \"area\": \"main\" } },\n  { \"name\": \"content\", \"type\": \"text\", \"meta\": { \"area\": \"main\" } },\n  { \"name\": \"status\", \"type\": \"string\", \"meta\": { \"area\": \"sidebar\" } },\n  { \"name\": \"publishedAt\", \"type\": \"date\", \"meta\": { \"area\": \"meta\" } }\n] \u003C!-- Generated form with layout -->\n\u003Ctemplate>\n  \u003CUForm :state=\"state\" :schema=\"schema\" @submit=\"handleSubmit\">\n    \u003C!-- Two-column layout -->\n    \u003Cdiv class=\"grid grid-cols-3 gap-6\">\n      \u003C!-- Main content area (2/3 width) -->\n      \u003Cdiv class=\"col-span-2 space-y-4\">\n        \u003CUFormField label=\"Title\" name=\"title\">\n          \u003CUInput v-model=\"state.title\" />\n        \u003C/UFormField>\n        \u003CUFormField label=\"Content\" name=\"content\">\n          \u003CUTextarea v-model=\"state.content\" />\n        \u003C/UFormField>\n      \u003C/div>\n\n      \u003C!-- Sidebar area (1/3 width) -->\n      \u003Cdiv class=\"space-y-4\">\n        \u003CUFormField label=\"Status\" name=\"status\">\n          \u003CUSelectMenu v-model=\"state.status\" />\n        \u003C/UFormField>\n        \u003CUFormField label=\"Published At\" name=\"publishedAt\">\n          \u003CUInput v-model=\"state.publishedAt\" type=\"date\" />\n        \u003C/UFormField>\n      \u003C/div>\n    \u003C/div>\n  \u003C/UForm>\n\u003C/template> Or with tabs: \u003C!-- Generated form with tabs -->\n\u003Ctemplate>\n  \u003CUForm :state=\"state\" :schema=\"schema\" @submit=\"handleSubmit\">\n    \u003CUTabs :items=\"[\n      { label: 'Content', key: 'main' },\n      { label: 'Settings', key: 'sidebar' },\n      { label: 'SEO', key: 'meta' }\n    ]\">\n      \u003Ctemplate #main>\n        \u003C!-- Main area fields -->\n      \u003C/template>\n      \u003Ctemplate #sidebar>\n        \u003C!-- Sidebar area fields -->\n      \u003C/template>\n      \u003Ctemplate #meta>\n        \u003C!-- Meta area fields -->\n      \u003C/template>\n    \u003C/UTabs>\n  \u003C/UForm>\n\u003C/template>",{"id":1788,"title":1789,"titles":1790,"content":1791,"level":449},"/guides/future-roadmap#configuration","Configuration",[56,1780],"You could configure the layout mode in app.config.ts: export default defineAppConfig({\n  croutonCollections: {\n    blogPosts: {\n      name: 'blogPosts',\n      layer: 'blog',\n      componentName: 'BlogPostsForm',\n      apiPath: 'blog-posts',\n      formLayout: 'columns' // or 'tabs' or 'sections'\n    }\n  }\n})",{"id":1793,"title":1179,"titles":1794,"content":1795,"level":449},"/guides/future-roadmap#benefits-1",[56,1780],"This would differentiate Nuxt Crouton from simpler generators by offering professional form layouts out of the box. You'd get better UX without writing custom layout code, and could still customize the generated layout since it's your code.",{"id":1797,"title":1798,"titles":1799,"content":1800,"level":391},"/guides/future-roadmap#typescript-config-support","TypeScript Config Support",[56],"Currently the multi-collection configuration file uses JavaScript (crouton.config.js). TypeScript support would provide autocomplete, type checking, and better developer experience when configuring multiple collections.",{"id":1802,"title":1771,"titles":1803,"content":1804,"level":449},"/guides/future-roadmap#what-it-would-do-2",[56,1798],"You could use crouton.config.ts instead of the JavaScript version and get full type safety with autocomplete in your editor. // crouton.config.ts\nimport type { CroutonConfig } from '@fyit/crouton-cli'\n\nexport default {\n  collections: [\n    {\n      name: 'products',\n      fieldsFile: './schemas/product-schema.json'\n      // Autocomplete shows available options\n      // Type checking catches invalid configurations\n    },\n    {\n      name: 'categories',\n      fieldsFile: './schemas/category-schema.json'\n    },\n  ],\n  targets: [\n    {\n      layer: 'shop',\n      collections: ['products', 'categories']\n      // Type error if collection name doesn't exist\n    }\n  ],\n  dialect: 'sqlite', // Autocomplete: 'sqlite' | 'postgres' | 'mysql'\n  flags: {\n    force: false,\n    noTranslations: false,\n    noDb: false\n  }\n} satisfies CroutonConfig",{"id":1806,"title":1179,"titles":1807,"content":1808,"level":449},"/guides/future-roadmap#benefits-2",[56,1798],"You'd catch configuration errors before running the generator, get autocomplete for all available options, and have better documentation right in your editor. The TypeScript types would also serve as living documentation for the config format.",{"id":1810,"title":1811,"titles":1812,"content":1813,"level":391},"/guides/future-roadmap#automatic-optimistic-updates","Automatic Optimistic Updates",[56],"Currently optimistic updates require manual implementation. This feature would add an optimistic: true option to useCollectionMutation that handles optimistic updates automatically.",{"id":1815,"title":1771,"titles":1816,"content":1817,"level":449},"/guides/future-roadmap#what-it-would-do-3",[56,1811],"When you enable optimistic updates, mutations would immediately update the UI before the API call completes, then automatically rollback if the call fails. \u003Cscript setup lang=\"ts\">\nconst { items } = await useCollectionQuery('shopProducts')\n\n// With automatic optimistic updates\nconst { update } = useCollectionMutation('shopProducts', {\n  optimistic: true  // Enable automatic optimistic updates\n})\n\nconst toggleFeatured = async (product: Product) => {\n  // UI updates immediately\n  // Automatically rolls back if API fails\n  await update(product.id, {\n    featured: !product.featured\n  })\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-for=\"product in items\" :key=\"product.id\">\n    \u003CUButton\n      :variant=\"product.featured ? 'solid' : 'outline'\"\n      @click=\"toggleFeatured(product)\"\n    >\n      {{ product.featured ? '★' : '☆' }}\n    \u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":1819,"title":1820,"titles":1821,"content":1822,"level":449},"/guides/future-roadmap#how-it-would-work","How It Would Work",[56,1811],"The composable would: Find the item in the current cacheApply the optimistic update to the UISend the API requestOn success: cache refresh happens normallyOn failure: rollback the optimistic change and show error",{"id":1824,"title":1179,"titles":1825,"content":1826,"level":449},"/guides/future-roadmap#benefits-3",[56,1811],"You'd get instant UI feedback without manual optimistic update code, automatic rollback on errors, and consistent behavior across all mutations. This would make apps feel much faster and more responsive with minimal code.",{"id":1828,"title":1829,"titles":1830,"content":1831,"level":391},"/guides/future-roadmap#vote-on-features","Vote on Features",[56],"These features are under consideration, and your feedback helps prioritize them. If any of these would significantly improve your workflow, or if you have other ideas, we'd love to hear from you. How to Provide Feedback: Open an issue on GitHub describing your use caseShare which features would help you most and whySuggest improvements or alternative approachesContribute pull requests if you want to help implement The roadmap evolves based on real-world needs, so your input matters. html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":61,"title":60,"titles":1833,"content":1834,"level":385},[],"Remove generated collections and undo changes with the rollback system Nuxt Crouton includes a comprehensive rollback system to safely remove generated collections. This is useful when you need to: Remove a collection you no longer needClean up after testing or experimentationRegenerate a collection from scratchRemove an entire layer",{"id":1836,"title":1837,"titles":1838,"content":1839,"level":391},"/guides/rollback#single-collection-rollback","Single Collection Rollback",[60],"Remove a specific collection and all its generated files: npx crouton rollback shop products This removes: Components (_Form.vue, List.vue)Composables (useShopProducts.ts)Types (types.ts)API endpoints (if generated)Database schema (if generated)Config entries (nuxt.config.ts)",{"id":1841,"title":1842,"titles":1843,"content":1844,"level":449},"/guides/rollback#preview-before-removing","Preview Before Removing",[60,1837],"Use --dry-run to see what would be removed: npx crouton rollback shop products --dry-run\n\n# Output:\n📋 Preview: Would remove the following:\n\nlayers/shop/collections/products/app/components/\n  ├── _Form.vue\n  └── List.vue\n\nlayers/shop/collections/products/app/composables/\n  └── useShopProducts.ts\n\nlayers/shop/collections/products/\n  └── types.ts\n\nTotal: 4 files\n\nProceed? (y/n)",{"id":1846,"title":1847,"titles":1848,"content":1849,"level":449},"/guides/rollback#keep-files-clean-config-only","Keep Files, Clean Config Only",[60,1837],"Remove config entries but keep generated files: npx crouton rollback shop products --keep-files Useful when you want to keep customized components but remove them from the collection registry.",{"id":1851,"title":1852,"titles":1853,"content":1854,"level":449},"/guides/rollback#skip-confirmation","Skip Confirmation",[60,1837],"For scripts or automation: npx crouton rollback shop products --force",{"id":1856,"title":1857,"titles":1858,"content":1859,"level":391},"/guides/rollback#bulk-rollback","Bulk Rollback",[60],"Remove multiple collections at once.",{"id":1861,"title":1862,"titles":1863,"content":1864,"level":449},"/guides/rollback#remove-entire-layer","Remove Entire Layer",[60,1857],"npx crouton rollback-bulk --layer=shop\n\n# Output:\n⚠️  This will remove ALL collections in the 'shop' layer:\n  - products (5 files)\n  - categories (5 files)\n  - orders (5 files)\n\nTotal: 15 files\n\nContinue? (y/n)",{"id":1866,"title":1867,"titles":1868,"content":1869,"level":449},"/guides/rollback#remove-all-collections-from-config","Remove All Collections from Config",[60,1857],"npx crouton rollback-bulk --config=./crouton.config.js\n\n# Reads the config file and removes all collections defined in it",{"id":1871,"title":1872,"titles":1873,"content":1874,"level":449},"/guides/rollback#bulk-rollback-options","Bulk Rollback Options",[60,1857],"Same options as single rollback: # Preview bulk changes\nnpx crouton rollback-bulk --layer=shop --dry-run\n\n# Keep files, clean config\nnpx crouton rollback-bulk --layer=shop --keep-files\n\n# Skip confirmation\nnpx crouton rollback-bulk --layer=shop --force",{"id":1876,"title":1877,"titles":1878,"content":1879,"level":391},"/guides/rollback#interactive-rollback","Interactive Rollback",[60],"Launch an interactive UI to select collections: npx crouton rollback-interactive Interactive UI: ? Select collections to remove:\n  ◯ shop/products (5 files)\n  ◯ shop/categories (5 files)\n  ◉ shop/orders (5 files)\n  ◯ blog/posts (5 files)\n\nUse arrow keys to navigate, space to select, enter to confirm Options: # Preview mode\nnpx crouton rollback-interactive --dry-run\n\n# Keep files\nnpx crouton rollback-interactive --keep-files",{"id":1881,"title":1882,"titles":1883,"content":1884,"level":391},"/guides/rollback#what-gets-removed","What Gets Removed",[60],"The rollback system removes:",{"id":1886,"title":1887,"titles":1888,"content":1889,"level":449},"/guides/rollback#always-removed","✅ Always Removed",[60,1882],"Generated component filesGenerated composable filesGenerated type filesConfig registry entries (app.config.ts)",{"id":1891,"title":1892,"titles":1893,"content":1894,"level":449},"/guides/rollback#️-conditionally-removed","⚠️ Conditionally Removed",[60,1882],"Database schema files (unless --keep-files)API endpoints (unless --keep-files)Layer directory (if empty after removal)",{"id":1896,"title":1897,"titles":1898,"content":1899,"level":449},"/guides/rollback#never-removed","❌ Never Removed",[60,1882],"Custom modifications you madeNon-generated files in the collection directoryDatabase data (only schema definitions)Git history",{"id":1901,"title":1902,"titles":1903,"content":1904,"level":391},"/guides/rollback#safe-rollback-workflow","Safe Rollback Workflow",[60],"Best practices for safe rollbacks: 1. Always Preview First npx crouton rollback shop products --dry-run 2. Check Git Status git status\n# Verify you have no uncommitted changes 3. Run Rollback npx crouton rollback shop products 4. Verify Changes git status\ngit diff\n# Review what was removed 5. Commit or Revert # If correct:\ngit add .\ngit commit -m \"Remove products collection\"\n\n# If wrong:\ngit restore .",{"id":1906,"title":1907,"titles":1908,"content":528,"level":391},"/guides/rollback#common-scenarios","Common Scenarios",[60],{"id":1910,"title":1911,"titles":1912,"content":1913,"level":449},"/guides/rollback#regenerate-a-collection-from-scratch","Regenerate a Collection from Scratch",[60,1907],"# 1. Remove the old collection\nnpx crouton rollback shop products --force\n\n# 2. Regenerate with updated schema\nnpx crouton generate shop products --fields-file=product-schema.json",{"id":1915,"title":1916,"titles":1917,"content":1918,"level":449},"/guides/rollback#remove-test-collections","Remove Test Collections",[60,1907],"# Remove all test collections at once\nnpx crouton rollback-interactive\n# Select all test collections in the UI",{"id":1920,"title":1921,"titles":1922,"content":1923,"level":449},"/guides/rollback#clean-up-before-deployment","Clean Up Before Deployment",[60,1907],"# Remove unused layer\nnpx crouton rollback-bulk --layer=experiments --force",{"id":1925,"title":1926,"titles":1927,"content":1928,"level":449},"/guides/rollback#preserve-customizations","Preserve Customizations",[60,1907],"# Keep customized files, just remove from registry\nnpx crouton rollback shop products --keep-files",{"id":1930,"title":36,"titles":1931,"content":528,"level":391},"/guides/rollback#troubleshooting",[60],{"id":1933,"title":1934,"titles":1935,"content":1936,"level":449},"/guides/rollback#collection-not-found","\"Collection not found\"",[60,36],"The collection may already be removed or was never generated: # Check what collections exist\nls layers/*/collections/",{"id":1938,"title":1939,"titles":1940,"content":1941,"level":449},"/guides/rollback#permission-denied","\"Permission denied\"",[60,36],"Files may be in use: # Stop your dev server first\n# Then try rollback again",{"id":1943,"title":1944,"titles":1945,"content":1946,"level":449},"/guides/rollback#config-entry-not-found","\"Config entry not found\"",[60,36],"The collection isn't registered in app.config.ts. This is safe to ignore, or you can manually verify: // app.config.ts\nexport default defineAppConfig({\n  croutonCollections: {\n    // Check if collection exists here\n  }\n})",{"id":1948,"title":1949,"titles":1950,"content":1951,"level":449},"/guides/rollback#accidental-removal","Accidental Removal",[60,36],"Restore from Git: git restore layers/shop/collections/products/",{"id":1953,"title":44,"titles":1954,"content":1955,"level":391},"/guides/rollback#best-practices",[60],"✅ DO: Use --dry-run for preview before every rollbackCommit your work before running rollback commandsVerify changes with git status after rollbackUse interactive mode when unsure which collections to removeDocument why you're removing collections (in commit messages) ❌ DON'T: Run bulk rollback without previewUse --force without reviewing what will be removedForget to restart your dev server after rollbackRemove collections that other parts of your app depend onUse rollback as a replacement for proper version control",{"id":1957,"title":1958,"titles":1959,"content":1960,"level":391},"/guides/rollback#related-sections","Related Sections",[60],"Generator Commands - Creating collectionsTroubleshooting - Common issuesBest Practices - Development workflow html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"id":65,"title":64,"titles":1962,"content":1963,"level":385},[],"Step-by-step guide to building custom display components for reference fields This guide walks you through creating custom CardMini components to display related entities in your tables and forms with rich, contextual information.",{"id":1965,"title":1966,"titles":1967,"content":1968,"level":391},"/guides/custom-cardmini#when-to-create-a-custom-cardmini","When to Create a Custom CardMini",[64],"Use custom CardMini components when: User references need avatars and profile infoLocation references should show addresses and mapsProduct references need thumbnails and pricingAny reference where just a title isn't enough context The default CardMini shows only the title field. Custom components let you show whatever makes sense for your data.",{"id":1970,"title":18,"titles":1971,"content":528,"level":391},"/guides/custom-cardmini#quick-start",[64],{"id":1973,"title":1974,"titles":1975,"content":1976,"level":449},"/guides/custom-cardmini#step-1-choose-your-collection","Step 1: Choose Your Collection",[64,18],"Identify which collection needs a custom card. Examples: users - Auth system userslocations - Your internal locations collectionproducts - Your products collection",{"id":1978,"title":1979,"titles":1980,"content":1981,"level":449},"/guides/custom-cardmini#step-2-create-the-component-file","Step 2: Create the Component File",[64,18],"For internal collections (created with generator): collections/locations/app/components/CardMini.vue For auth-related collections (users, teams): app/components/UsersCardMini.vue",{"id":1983,"title":1984,"titles":1985,"content":1986,"level":449},"/guides/custom-cardmini#step-3-use-the-template","Step 3: Use the Template",[64,18],"Start with this template and customize: \u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  item: any\n  pending: boolean\n  error: any\n  id: string\n  collection: string\n  refresh: () => Promise\u003Cvoid>\n}>()\n\nconst { open } = useCrouton()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"group relative\">\n    \u003C!-- Loading State -->\n    \u003CUSkeleton v-if=\"pending\" class=\"h-12 w-full rounded-md\" />\n\n    \u003C!-- Loaded State -->\n    \u003Cdiv v-else-if=\"item\" class=\"border rounded-md p-2 bg-white dark:bg-gray-800\">\n      \u003C!-- Your custom layout here -->\n      \u003Cdiv>{{ item.title }}\u003C/div>\n    \u003C/div>\n\n    \u003C!-- Error State -->\n    \u003Cdiv v-else-if=\"error\" class=\"text-red-500 text-xs p-2\">\n      Error loading item\n    \u003C/div>\n\n    \u003C!-- Action Buttons (keep these!) -->\n    \u003CCroutonItemButtonsMini\n      v-if=\"item\"\n      update\n      @update=\"open('update', collection, [id])\"\n      class=\"absolute -top-1 right-2 transition delay-150 duration-300 ease-in-out group-hover:-translate-y-6 group-hover:scale-110\"\n      buttonClasses=\"pb-4\"\n      containerClasses=\"flex flex-row gap-[2px]\"\n    />\n  \u003C/div>\n\u003C/template>",{"id":1988,"title":1989,"titles":1990,"content":528,"level":391},"/guides/custom-cardmini#complete-examples","Complete Examples",[64],{"id":1992,"title":1993,"titles":1994,"content":1995,"level":449},"/guides/custom-cardmini#example-1-location-card-with-icon-and-badge","Example 1: Location Card with Icon and Badge",[64,1989],"Let's create a custom card for a locations collection: File: collections/locations/app/components/CardMini.vue \u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  item: any\n  pending: boolean\n  error: any\n  id: string\n  collection: string\n  refresh: () => Promise\u003Cvoid>\n}>()\n\nconst { open } = useCrouton()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"group relative\">\n    \u003CUSkeleton v-if=\"pending\" class=\"h-16 w-full rounded-md\" />\n\n    \u003Cdiv v-else-if=\"item\" class=\"border rounded-md p-3 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition\">\n      \u003Cdiv class=\"flex items-start gap-3\">\n        \u003C!-- Icon -->\n        \u003CUIcon name=\"i-lucide-map-pin\" class=\"text-blue-500 mt-1 flex-shrink-0\" />\n\n        \u003C!-- Content -->\n        \u003Cdiv class=\"flex-1 min-w-0\">\n          \u003Cdiv class=\"font-medium text-sm truncate\">{{ item.name }}\u003C/div>\n          \u003Cdiv class=\"text-xs text-gray-500 truncate\">{{ item.address }}\u003C/div>\n\n          \u003C!-- Status Badge -->\n          \u003CUBadge\n            v-if=\"item.active\"\n            color=\"green\"\n            size=\"xs\"\n            class=\"mt-1\"\n          >\n            Active\n          \u003C/UBadge>\n        \u003C/div>\n      \u003C/div>\n    \u003C/div>\n\n    \u003Cdiv v-else-if=\"error\" class=\"text-red-500 text-xs p-2 border border-red-200 rounded-md\">\n      Failed to load location\n    \u003C/div>\n\n    \u003CCroutonItemButtonsMini\n      v-if=\"item\"\n      update\n      @update=\"open('update', collection, [id])\"\n      class=\"absolute -top-1 right-2 transition delay-150 duration-300 ease-in-out group-hover:-translate-y-6 group-hover:scale-110\"\n      buttonClasses=\"pb-4\"\n      containerClasses=\"flex flex-row gap-[2px]\"\n    />\n  \u003C/div>\n\u003C/template> What this shows: 📍 Map pin icon for visual contextLocation name in boldAddress in smaller, muted textGreen \"Active\" badge when applicableHover effect for better UX",{"id":1997,"title":1998,"titles":1999,"content":2000,"level":449},"/guides/custom-cardmini#example-2-user-card-with-avatar","Example 2: User Card with Avatar",[64,1989],"For users from your auth system: File: app/components/UsersCardMini.vue \u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  item: any\n  pending: boolean\n  error: any\n  id: string\n  collection: string\n  refresh: () => Promise\u003Cvoid>\n}>()\n\nconst { open } = useCrouton()\n\n// Generate initials from name\nconst userInitials = computed(() => {\n  if (!props.item?.full_name) return '?'\n  return props.item.full_name\n    .split(' ')\n    .map((n: string) => n[0])\n    .join('')\n    .toUpperCase()\n    .slice(0, 2)  // Max 2 letters\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"group relative\">\n    \u003CUSkeleton v-if=\"pending\" class=\"h-12 w-full rounded-lg\" />\n\n    \u003Cdiv v-else-if=\"item\" class=\"flex items-center gap-3 p-2 border rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition\">\n      \u003C!-- Avatar or Initials -->\n      \u003Cdiv class=\"flex-shrink-0\">\n        \u003Cimg\n          v-if=\"item.avatar_url\"\n          :src=\"item.avatar_url\"\n          :alt=\"item.full_name\"\n          class=\"w-10 h-10 rounded-full object-cover\"\n        />\n        \u003Cdiv\n          v-else\n          class=\"w-10 h-10 rounded-full bg-primary-500 text-white flex items-center justify-center font-medium text-sm\"\n        >\n          {{ userInitials }}\n        \u003C/div>\n      \u003C/div>\n\n      \u003C!-- User Info -->\n      \u003Cdiv class=\"flex-1 min-w-0\">\n        \u003Cdiv class=\"font-medium text-sm truncate\">{{ item.full_name }}\u003C/div>\n        \u003Cdiv class=\"text-xs text-gray-500 truncate\">{{ item.email }}\u003C/div>\n      \u003C/div>\n\n      \u003C!-- Status Badge -->\n      \u003CUBadge\n        v-if=\"item.is_active\"\n        color=\"green\"\n        size=\"xs\"\n        class=\"flex-shrink-0\"\n      >\n        Active\n      \u003C/UBadge>\n    \u003C/div>\n\n    \u003Cdiv v-else-if=\"error\" class=\"text-red-500 text-xs p-2\">\n      User not found\n    \u003C/div>\n\n    \u003CCroutonItemButtonsMini\n      v-if=\"item\"\n      update\n      @update=\"open('update', collection, [id])\"\n      class=\"absolute -top-1 right-2 transition delay-150 duration-300 ease-in-out group-hover:-translate-y-6 group-hover:scale-110\"\n      buttonClasses=\"pb-4\"\n      containerClasses=\"flex flex-row gap-[2px]\"\n    />\n  \u003C/div>\n\u003C/template> What this shows: 👤 Avatar image or colored initials fallbackUser's full nameEmail addressActive status badgeProfessional, clean layout",{"id":2002,"title":2003,"titles":2004,"content":2005,"level":449},"/guides/custom-cardmini#example-3-product-card-with-thumbnail","Example 3: Product Card with Thumbnail",[64,1989],"For products with images and pricing: File: collections/products/app/components/CardMini.vue \u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  item: any\n  pending: boolean\n  error: any\n  id: string\n  collection: string\n  refresh: () => Promise\u003Cvoid>\n}>()\n\nconst { open } = useCrouton()\n\n// Format price for display\nconst formattedPrice = computed(() => {\n  if (!props.item?.price) return '--'\n  return new Intl.NumberFormat('en-US', {\n    style: 'currency',\n    currency: 'USD'\n  }).format(props.item.price)\n})\n\n// Stock badge color based on quantity\nconst stockColor = computed(() => {\n  const stock = props.item?.stock || 0\n  if (stock === 0) return 'red'\n  if (stock \u003C= 10) return 'yellow'\n  return 'green'\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"group relative\">\n    \u003CUSkeleton v-if=\"pending\" class=\"h-20 w-full rounded-lg\" />\n\n    \u003Cdiv v-else-if=\"item\" class=\"flex items-center gap-3 p-2 border rounded-lg bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition\">\n      \u003C!-- Product Thumbnail -->\n      \u003Cdiv class=\"flex-shrink-0\">\n        \u003Cimg\n          v-if=\"item.thumbnail\"\n          :src=\"item.thumbnail\"\n          :alt=\"item.name\"\n          class=\"w-16 h-16 rounded object-cover\"\n        />\n        \u003Cdiv v-else class=\"w-16 h-16 rounded bg-gray-200 dark:bg-gray-700 flex items-center justify-center\">\n          \u003CUIcon name=\"i-lucide-image\" class=\"text-gray-400 text-2xl\" />\n        \u003C/div>\n      \u003C/div>\n\n      \u003C!-- Product Info -->\n      \u003Cdiv class=\"flex-1 min-w-0\">\n        \u003Cdiv class=\"font-medium text-sm truncate\">{{ item.name }}\u003C/div>\n        \u003Cdiv class=\"text-xs text-gray-500 truncate\">SKU: {{ item.sku }}\u003C/div>\n        \u003Cdiv class=\"font-semibold text-sm text-primary-600 dark:text-primary-400 mt-1\">\n          {{ formattedPrice }}\n        \u003C/div>\n      \u003C/div>\n\n      \u003C!-- Stock Badge -->\n      \u003CUBadge\n        :color=\"stockColor\"\n        size=\"xs\"\n        class=\"flex-shrink-0\"\n      >\n        {{ item.stock }} in stock\n      \u003C/UBadge>\n    \u003C/div>\n\n    \u003Cdiv v-else-if=\"error\" class=\"text-red-500 text-xs p-2\">\n      Product not found\n    \u003C/div>\n\n    \u003CCroutonItemButtonsMini\n      v-if=\"item\"\n      update\n      @update=\"open('update', collection, [id])\"\n      class=\"absolute -top-1 right-2 transition delay-150 duration-300 ease-in-out group-hover:-translate-y-6 group-hover:scale-110\"\n      buttonClasses=\"pb-4\"\n      containerClasses=\"flex flex-row gap-[2px]\"\n    />\n  \u003C/div>\n\u003C/template> What this shows: 🖼️ Product thumbnail or placeholderProduct name and SKUFormatted priceDynamic stock badge (color changes based on quantity)Clean, scannable layout",{"id":2007,"title":44,"titles":2008,"content":528,"level":391},"/guides/custom-cardmini#best-practices",[64],{"id":2010,"title":2011,"titles":2012,"content":2013,"level":449},"/guides/custom-cardmini#keep-it-compact","Keep It Compact",[64,44],"CardMini is meant to be compact - it's a preview, not a full detail view. ✅ Good: \u003Cdiv class=\"flex items-center gap-2 p-2\">\n  \u003Cimg class=\"w-10 h-10 rounded\" />\n  \u003Cdiv>\n    \u003Cdiv class=\"text-sm\">{{ item.name }}\u003C/div>\n    \u003Cdiv class=\"text-xs text-gray-500\">{{ item.email }}\u003C/div>\n  \u003C/div>\n\u003C/div> ❌ Too Much: \u003Cdiv class=\"p-6\">\n  \u003Cimg class=\"w-32 h-32\" />\n  \u003Ch2>{{ item.name }}\u003C/h2>\n  \u003Cp>{{ item.bio }}\u003C/p>\n  \u003Cdiv>Created: {{ item.createdAt }}\u003C/div>\n  \u003Cdiv>Updated: {{ item.updatedAt }}\u003C/div>\n  \u003Cdiv>Last Login: {{ item.lastLogin }}\u003C/div>\n\u003C/div>",{"id":2015,"title":2016,"titles":2017,"content":2018,"level":449},"/guides/custom-cardmini#always-handle-all-states","Always Handle All States",[64,44],"Your component receives three states - handle all of them: Loading (pending: true) → Show skeletonSuccess (item: {...}) → Show dataError (error: {...}) → Show error message \u003Ctemplate>\n  \u003Cdiv class=\"group relative\">\n    \u003C!-- 1. Loading -->\n    \u003CUSkeleton v-if=\"pending\" class=\"h-12 w-full\" />\n\n    \u003C!-- 2. Success -->\n    \u003Cdiv v-else-if=\"item\">\n      \u003C!-- Your content -->\n    \u003C/div>\n\n    \u003C!-- 3. Error -->\n    \u003Cdiv v-else-if=\"error\" class=\"text-red-500 text-xs\">\n      Failed to load\n    \u003C/div>\n\n    \u003C!-- Mini buttons (only show when loaded) -->\n    \u003CCroutonItemButtonsMini v-if=\"item\" ... />\n  \u003C/div>\n\u003C/template>",{"id":2020,"title":2021,"titles":2022,"content":2023,"level":449},"/guides/custom-cardmini#use-truncation-for-long-text","Use Truncation for Long Text",[64,44],"Prevent overflow by truncating long text: \u003Cdiv class=\"truncate\">{{ item.veryLongTitle }}\u003C/div> Or use min-w-0 on flex children: \u003Cdiv class=\"flex items-center gap-3\">\n  \u003Cimg class=\"flex-shrink-0\" />\n  \u003Cdiv class=\"flex-1 min-w-0\">  \u003C!-- min-w-0 allows truncation -->\n    \u003Cdiv class=\"truncate\">{{ item.longText }}\u003C/div>\n  \u003C/div>\n\u003C/div>",{"id":2025,"title":2026,"titles":2027,"content":2028,"level":449},"/guides/custom-cardmini#keep-styles-consistent","Keep Styles Consistent",[64,44],"Match Crouton's design system: Use border rounded-md p-2 for card containersUse bg-white dark:bg-gray-800 for backgroundsUse text-sm for primary text, text-xs for secondaryUse text-gray-500 dark:text-gray-400 for muted textAdd hover effects: hover:bg-gray-50 dark:hover:bg-gray-700",{"id":2030,"title":2031,"titles":2032,"content":2033,"level":449},"/guides/custom-cardmini#include-the-action-buttons","Include the Action Buttons",[64,44],"Always include CroutonItemButtonsMini for consistency: \u003CCroutonItemButtonsMini\n  v-if=\"item\"\n  update\n  @update=\"open('update', collection, [id])\"\n  class=\"absolute -top-1 right-2 transition delay-150 duration-300 ease-in-out group-hover:-translate-y-6 group-hover:scale-110\"\n  buttonClasses=\"pb-4\"\n  containerClasses=\"flex flex-row gap-[2px]\"\n/> This provides the edit button that appears on hover.",{"id":2035,"title":1650,"titles":2036,"content":528,"level":391},"/guides/custom-cardmini#common-patterns",[64],{"id":2038,"title":2039,"titles":2040,"content":2041,"level":449},"/guides/custom-cardmini#computing-display-values","Computing Display Values",[64,1650],"Use computed properties for formatted data: \u003Cscript setup lang=\"ts\">\n// Format currency\nconst formattedPrice = computed(() => {\n  return new Intl.NumberFormat('en-US', {\n    style: 'currency',\n    currency: 'USD'\n  }).format(props.item?.price || 0)\n})\n\n// Format date\nconst formattedDate = computed(() => {\n  if (!props.item?.createdAt) return '--'\n  return new Date(props.item.createdAt).toLocaleDateString()\n})\n\n// Conditional badge color\nconst statusColor = computed(() => {\n  return props.item?.active ? 'green' : 'gray'\n})\n\u003C/script>",{"id":2043,"title":2044,"titles":2045,"content":2046,"level":449},"/guides/custom-cardmini#fallback-images","Fallback Images",[64,1650],"Handle missing images gracefully: \u003Cimg\n  v-if=\"item.image\"\n  :src=\"item.image\"\n  class=\"w-10 h-10\"\n/>\n\u003Cdiv v-else class=\"w-10 h-10 bg-gray-200 rounded flex items-center justify-center\">\n  \u003CUIcon name=\"i-lucide-image\" class=\"text-gray-400\" />\n\u003C/div>",{"id":2048,"title":2049,"titles":2050,"content":2051,"level":449},"/guides/custom-cardmini#conditional-badges","Conditional Badges",[64,1650],"Show badges only when relevant: \u003CUBadge v-if=\"item.featured\" color=\"yellow\">Featured\u003C/UBadge>\n\u003CUBadge v-if=\"item.stock === 0\" color=\"red\">Out of Stock\u003C/UBadge>\n\u003CUBadge v-if=\"item.isNew\" color=\"green\">New\u003C/UBadge>",{"id":2053,"title":2054,"titles":2055,"content":528,"level":391},"/guides/custom-cardmini#testing-your-custom-card","Testing Your Custom Card",[64],{"id":2057,"title":2058,"titles":2059,"content":2060,"level":449},"/guides/custom-cardmini#visual-testing","Visual Testing",[64,2054],"Start dev server:pnpm dev\nNavigate to a table/form with your reference fieldCheck all states:Loading skeleton appearsData displays correctlyHover shows action buttonsDark mode looks good",{"id":2062,"title":2063,"titles":2064,"content":2065,"level":449},"/guides/custom-cardmini#test-edge-cases","Test Edge Cases",[64,2054],"Long names - Do they truncate?Missing data - Does fallback work?No image - Does placeholder show?Mobile view - Does it work on small screens?",{"id":2067,"title":36,"titles":2068,"content":528,"level":391},"/guides/custom-cardmini#troubleshooting",[64],{"id":2070,"title":2071,"titles":2072,"content":2073,"level":449},"/guides/custom-cardmini#card-not-showing-up","Card not showing up",[64,36],"Problem: Still seeing default card (just title). Solutions: Check file name matches format: {Collection}CardMini.vueCheck file location is correctRestart dev server to refresh auto-imports:\npnpm dev",{"id":2075,"title":2076,"titles":2077,"content":2078,"level":449},"/guides/custom-cardmini#typescript-errors","TypeScript errors",[64,36],"Problem: Props are undefined or type errors. Solution: Ensure all required props are defined: \u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  item: any\n  pending: boolean\n  error: any\n  id: string\n  collection: string\n  refresh: () => Promise\u003Cvoid>\n}>()\n\u003C/script>",{"id":2080,"title":2081,"titles":2082,"content":2083,"level":449},"/guides/custom-cardmini#layout-issues","Layout issues",[64,36],"Problem: Card is too large or overflowing. Solutions: Add truncate to text elementsUse flex-shrink-0 on images/iconsUse min-w-0 on flex childrenKeep padding small (p-2 or p-3)",{"id":2085,"title":2086,"titles":2087,"content":2088,"level":449},"/guides/custom-cardmini#data-not-loading","Data not loading",[64,36],"Problem: item is always null. Solution: The parent CardMini handles data fetching automatically. You don't need to fetch anything. Just use the item prop.",{"id":2090,"title":418,"titles":2091,"content":2092,"level":391},"/guides/custom-cardmini#next-steps",[64],"Read CardMini Components for full referenceCheck useCollectionItem API to understand data fetchingExplore Custom Components for form customizationSee Table Configuration for table display options html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":69,"title":68,"titles":2094,"content":2095,"level":385},[],"Deploy nuxt-crouton applications to production",{"id":2097,"title":2098,"titles":2099,"content":2100,"level":385},"/guides/deployment#deployment-guide","Deployment Guide",[],"This guide covers deploying nuxt-crouton applications to production environments.",{"id":2102,"title":2103,"titles":2104,"content":2105,"level":391},"/guides/deployment#deployment-targets","Deployment Targets",[2098],"nuxt-crouton apps are built on Nuxt and can deploy to any platform that supports Nuxt: PlatformDatabaseRecommended ForCloudflare PagesD1 (SQLite)Production apps (recommended)VercelTurso/PlanetScaleVercel-native workflowsNetlifyTurso/PlanetScaleNetlify-native workflowsSelf-hostedAny SQLite/PostgreSQLFull control Recommended: Cloudflare Pages with D1 provides the best experience for nuxt-crouton apps with edge computing, low latency, and integrated SQLite.",{"id":2107,"title":2108,"titles":2109,"content":528,"level":391},"/guides/deployment#cloudflare-pages-deployment","Cloudflare Pages Deployment",[2098],{"id":2111,"title":426,"titles":2112,"content":2113,"level":449},"/guides/deployment#prerequisites",[2098,2108],"Cloudflare Account with Pages enabledWrangler CLI installed:\nnpm install -g wrangler\nAuthentication:\nwrangler login",{"id":2115,"title":2116,"titles":2117,"content":528,"level":449},"/guides/deployment#step-1-create-cloud-resources","Step 1: Create Cloud Resources",[2098,2108],{"id":2119,"title":2120,"titles":2121,"content":2122,"level":748},"/guides/deployment#create-d1-database","Create D1 Database",[2098,2108,2116],"# Create the database\nwrangler d1 create my-app-db\n\n# Output will include database_id - save this!\n# ✅ Successfully created DB 'my-app-db'\n# database_id = \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"",{"id":2124,"title":2125,"titles":2126,"content":2127,"level":748},"/guides/deployment#create-kv-namespace","Create KV Namespace",[2098,2108,2116],"# Create KV for sessions/cache\nwrangler kv:namespace create KV\n\n# Output will include the id - save this!\n# ✅ Successfully created KV namespace \"KV\"\n# id = \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"",{"id":2129,"title":2130,"titles":2131,"content":2132,"level":449},"/guides/deployment#step-2-configure-wranglertoml","Step 2: Configure wrangler.toml",[2098,2108],"Create or update wrangler.toml in your app directory: name = \"my-app\"\ncompatibility_date = \"2024-09-02\"\ncompatibility_flags = [\"nodejs_compat\"]\npages_build_output_dir = \"dist\"\n\n[[d1_databases]]\nbinding = \"DB\"\ndatabase_name = \"my-app-db\"\ndatabase_id = \"your-database-id-here\"\n\n[[kv_namespaces]]\nbinding = \"KV\"\nid = \"your-kv-id-here\"",{"id":2134,"title":2135,"titles":2136,"content":2137,"level":449},"/guides/deployment#step-3-configure-nuxt","Step 3: Configure Nuxt",[2098,2108],"Ensure your nuxt.config.ts is configured for Cloudflare: export default defineNuxtConfig({\n  // ... your extends\n\n  // CRITICAL: Use db: 'sqlite', NOT database: true\n  hub: {\n    db: 'sqlite',\n    kv: true\n  },\n\n  // Disable incompatible features\n  croutonAuth: {\n    passkeys: false  // Not supported on Cloudflare Workers\n  }\n}) Common Mistake: Using hub: { database: true } will cause build failures. Always use hub: { db: 'sqlite' }.",{"id":2139,"title":2140,"titles":2141,"content":528,"level":449},"/guides/deployment#step-4-set-environment-variables","Step 4: Set Environment Variables",[2098,2108],{"id":2143,"title":2144,"titles":2145,"content":2146,"level":748},"/guides/deployment#required-variables","Required Variables",[2098,2108,2140],"VariableDescriptionExampleBETTER_AUTH_SECRETSession encryption (32+ chars)Generate with openssl rand -base64 32BETTER_AUTH_URLProduction URLhttps://my-app.pages.dev",{"id":2148,"title":2149,"titles":2150,"content":2151,"level":748},"/guides/deployment#setting-via-cli","Setting via CLI",[2098,2108,2140],"# Generate a secure secret\nopenssl rand -base64 32\n\n# Add to Cloudflare\nnpx wrangler pages secret put BETTER_AUTH_SECRET\n# Paste the generated secret when prompted\n\nnpx wrangler pages secret put BETTER_AUTH_URL\n# Enter your production URL",{"id":2153,"title":2154,"titles":2155,"content":2156,"level":748},"/guides/deployment#setting-via-dashboard","Setting via Dashboard",[2098,2108,2140],"Go to dash.cloudflare.comNavigate to Workers & Pages → your-appGo to Settings → Environment VariablesAdd variables for Production (and Preview if needed)Click Save",{"id":2158,"title":2159,"titles":2160,"content":2161,"level":449},"/guides/deployment#step-5-run-database-migrations","Step 5: Run Database Migrations",[2098,2108],"# Generate migrations (if not already done)\npnpm run db:generate\n\n# Apply to production D1\npnpm run db:migrate:prod\n# or: npx wrangler d1 migrations apply my-app-db --remote Important: Always run migrations before deploying code that depends on schema changes.",{"id":2163,"title":2164,"titles":2165,"content":2166,"level":449},"/guides/deployment#step-6-deploy","Step 6: Deploy",[2098,2108],"# Build and deploy to production\npnpm run cf:deploy\n# or: nuxt build && npx wrangler pages deploy dist For preview deployments: pnpm run cf:preview\n# or: nuxt build && npx wrangler pages deploy dist --branch preview",{"id":2168,"title":2169,"titles":2170,"content":528,"level":391},"/guides/deployment#oauth-configuration","OAuth Configuration",[2098],{"id":2172,"title":2173,"titles":2174,"content":2175,"level":449},"/guides/deployment#google-oauth","Google OAuth",[2098,2169],"Create credentials at Google Cloud ConsoleAdd authorized redirect URI: https://your-domain.com/api/auth/callback/googleSet environment variables:\nnpx wrangler pages secret put GOOGLE_CLIENT_ID\nnpx wrangler pages secret put GOOGLE_CLIENT_SECRET",{"id":2177,"title":2178,"titles":2179,"content":2180,"level":449},"/guides/deployment#github-oauth","GitHub OAuth",[2098,2169],"Create OAuth app at GitHub SettingsSet callback URL: https://your-domain.com/api/auth/callback/githubSet environment variables:\nnpx wrangler pages secret put GITHUB_CLIENT_ID\nnpx wrangler pages secret put GITHUB_CLIENT_SECRET",{"id":2182,"title":2183,"titles":2184,"content":2185,"level":391},"/guides/deployment#cloudflare-worker-limitations","Cloudflare Worker Limitations",[2098],"Some features are disabled or limited on Cloudflare Workers: FeatureStatusReasonWorkaroundPasskeys/WebAuthnDisabledtsyringe incompatibleUse email/password or OAuthOG Image GenerationOptionalReduces bundle (~4MB)Use static OG imagesLong-running tasksLimited30s CPU limitUse Cloudflare Queues Configuration for disabled features: // nuxt.config.ts\nexport default defineNuxtConfig({\n  croutonAuth: {\n    passkeys: false\n  }\n})",{"id":2187,"title":2188,"titles":2189,"content":528,"level":391},"/guides/deployment#deployment-checklists","Deployment Checklists",[2098],{"id":2191,"title":2192,"titles":2193,"content":2194,"level":449},"/guides/deployment#first-time-deployment","First-Time Deployment",[2098,2188],"Create Cloudflare account and wrangler login Create D1 database: wrangler d1 create {app}-db Create KV namespace: wrangler kv:namespace create KV Update wrangler.toml with database_id and KV id Set BETTER_AUTH_SECRET (32+ character secret) Set BETTER_AUTH_URL (your production URL) Configure OAuth providers (if using) Run pnpm run db:migrate:prod Deploy: pnpm run cf:deploy Test registration and login Verify OAuth flows (if configured)",{"id":2196,"title":2197,"titles":2198,"content":2199,"level":449},"/guides/deployment#subsequent-deployments","Subsequent Deployments",[2098,2188],"If schema changed: pnpm run db:generate If schema changed: pnpm run db:migrate:prod Deploy: pnpm run cf:deploy Verify app functionality",{"id":2201,"title":36,"titles":2202,"content":528,"level":391},"/guides/deployment#troubleshooting",[2098],{"id":2204,"title":2205,"titles":2206,"content":2207,"level":449},"/guides/deployment#better_auth_secret-is-required","BETTER_AUTH_SECRET is required",[2098,36],"Cause: Environment variable not set in Cloudflare. Fix: openssl rand -base64 32  # Generate secret\nnpx wrangler pages secret put BETTER_AUTH_SECRET\n# Paste the generated secret Then redeploy.",{"id":2209,"title":2210,"titles":2211,"content":2212,"level":449},"/guides/deployment#cannot-resolve-entry-module-nuxthubdbschemaentryts","Cannot resolve entry module .nuxt/hub/db/schema.entry.ts",[2098,36],"Cause: Using hub: { database: true } instead of hub: { db: 'sqlite' }. Fix: Update nuxt.config.ts: hub: {\n  db: 'sqlite',  // NOT database: true\n  kv: true\n}",{"id":2214,"title":2215,"titles":2216,"content":2217,"level":449},"/guides/deployment#database-migrations-fail","Database migrations fail",[2098,36],"Cause: Database not created or wrong database_id. Fix: Verify database exists: wrangler d1 listCheck wrangler.toml has correct database_idRun: wrangler d1 migrations apply {app}-db --remote",{"id":2219,"title":2220,"titles":2221,"content":2222,"level":449},"/guides/deployment#oauth-callback-fails","OAuth callback fails",[2098,36],"Cause: Callback URL mismatch or missing BETTER_AUTH_URL. Fix: Ensure BETTER_AUTH_URL is set to your production URLVerify OAuth provider callback URLs match exactly:\nGoogle: https://your-domain.com/api/auth/callback/googleGitHub: https://your-domain.com/api/auth/callback/github",{"id":2224,"title":2225,"titles":2226,"content":2227,"level":449},"/guides/deployment#build-errors-with-passkeys","Build errors with passkeys",[2098,36],"Cause: Passkey dependencies are incompatible with Workers. Fix: Disable passkeys in config: croutonAuth: {\n  passkeys: false\n}",{"id":2229,"title":2230,"titles":2231,"content":2232,"level":391},"/guides/deployment#useful-commands","Useful Commands",[2098],"# Wrangler Management\nwrangler login                # Authenticate\nwrangler d1 list              # List databases\nwrangler kv:namespace list    # List KV namespaces\nwrangler pages secret list    # List secrets\nwrangler pages secret put X   # Add secret X\nwrangler tail                 # View live logs\n\n# Database Operations\npnpm run db:generate          # Generate migrations\npnpm run db:migrate           # Apply locally\npnpm run db:migrate:prod      # Apply to production\n\n# Deployment\npnpm run cf:deploy            # Deploy production\npnpm run cf:preview           # Deploy preview",{"id":2234,"title":2235,"titles":2236,"content":528,"level":391},"/guides/deployment#alternative-platforms","Alternative Platforms",[2098],{"id":2238,"title":2239,"titles":2240,"content":2241,"level":449},"/guides/deployment#vercel","Vercel",[2098,2235],"Configure for Vercel in nuxt.config.ts:export default defineNuxtConfig({\n  nitro: {\n    preset: 'vercel'\n  }\n})\nUse Turso or PlanetScale for databaseSet environment variables in Vercel dashboardDeploy via Vercel CLI or Git integration",{"id":2243,"title":2244,"titles":2245,"content":2246,"level":449},"/guides/deployment#self-hosted","Self-Hosted",[2098,2235],"Build the application:nuxt build\nRun with Node.js:node .output/server/index.mjs\nUse SQLite file or configure PostgreSQLSet environment variables on your serverUse PM2, systemd, or Docker for process management html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":30,"title":29,"titles":2248,"content":2249,"level":385},[],"Practical guides for common tasks and best practices with Nuxt Crouton",{"id":2251,"title":29,"titles":2252,"content":2253,"level":385},"/guides#guides",[],"Step-by-step guides for common tasks and best practices.",{"id":2255,"title":2256,"titles":2257,"content":2258,"level":391},"/guides#contents","Contents",[29],"Common issues and how to resolve them.Upgrading between versions and migrating from other solutions.Recommended patterns for maintainable Nuxt Crouton applications.Implementing pagination in tables and lists.Managing file uploads, images, and other assets.Reverting generated code and handling regeneration.Upcoming features and planned improvements.Creating custom mini card components for relation displays.",{"id":82,"title":81,"titles":2260,"content":2261,"level":385},[],"Understanding collections and layers in Nuxt Crouton",{"id":2263,"title":2264,"titles":2265,"content":2266,"level":391},"/fundamentals/collections#collections","Collections",[81],"A collection is a group of related data, similar to a database table. For example, you might have collections for products, posts, users, or orders. By convention, use plural names like products rather than product.",{"id":2268,"title":2269,"titles":2270,"content":2271,"level":391},"/fundamentals/collections#layers","Layers",[81],"A layer represents a domain or module in your application. You might have a shop layer for e-commerce features, a blog layer for content management, an admin layer for user administration, or a marketing layer for campaigns and newsletters. Layers provide separation of concerns, can be reused across projects, and support independent deployment.",{"id":2273,"title":2274,"titles":2275,"content":2276,"level":391},"/fundamentals/collections#domain-driven-design-with-layers","Domain-Driven Design with Layers",[81],"You can organize your collections by domain, grouping related functionality together. For instance, your shop layer might contain products, orders, and inventory collections, while your blog layer holds posts, authors, and comments. Each layer is self-contained and can be deployed independently. layers/\n  ├── shop/        # E-commerce domain\n  │   └── collections/\n  │       ├── products/\n  │       │   └── app/\n  │       │       ├── components/\n  │       │       └── composables/\n  │       ├── orders/\n  │       │   └── app/\n  │       │       ├── components/\n  │       │       └── composables/\n  │       └── inventory/\n  │           └── app/\n  │               ├── components/\n  │               └── composables/\n  │\n  ├── blog/        # Content domain\n  │   └── collections/\n  │       ├── posts/\n  │       │   └── app/\n  │       │       ├── components/\n  │       │       └── composables/\n  │       ├── authors/\n  │       │   └── app/\n  │       │       ├── components/\n  │       │       └── composables/\n  │       └── comments/\n  │           └── app/\n  │               ├── components/\n  │               └── composables/\n  │\n  └── admin/       # Admin domain\n      └── collections/\n          ├── users/\n          │   └── app/\n          │       ├── components/\n          │       └── composables/\n          ├── roles/\n          │   └── app/\n          │       ├── components/\n          │       └── composables/\n          └── permissions/\n              └── app/\n                  ├── components/\n                  └── composables/",{"id":2278,"title":2279,"titles":2280,"content":2281,"level":391},"/fundamentals/collections#the-two-layer-architecture","The Two-Layer Architecture",[81],"Nuxt Crouton separates generated code from the core library. Your generated code (forms, lists, tables) lives in your project and can be customized freely. This code uses the stable core library (composables, utilities, modal management, caching) which gets updated via npm. The key insight is that your generated code can diverge from the original templates while the core library remains consistent. ┌─────────────────────────────────────┐\n│  Generated Code (Yours)             │\n│  - Forms, Lists, Tables             │\n│  - You customize freely             │\n│  - Lives in YOUR project            │\n└─────────────────────────────────────┘\n            ↓ uses\n┌─────────────────────────────────────┐\n│  Core Library (Stable)              │\n│  - Composables, utilities           │\n│  - Modal management, caching        │\n│  - Updates via npm                  │\n└─────────────────────────────────────┘",{"id":2283,"title":1562,"titles":2284,"content":2285,"level":391},"/fundamentals/collections#related-topics",[81],"Generated vs Core CodeWorking with CollectionsGenerator Commands",{"id":86,"title":85,"titles":2287,"content":2288,"level":385},[],"Understanding Nuxt Crouton's two-layer architecture and domain-driven design approach Nuxt Crouton uses a two-layer architecture that separates generated code from the stable core library. This approach, combined with Nuxt's layer system, allows you to organize your code by business domain while maintaining flexibility and control.",{"id":2290,"title":2279,"titles":2291,"content":2292,"level":391},"/fundamentals/architecture#the-two-layer-architecture",[85],"The core principle behind Nuxt Crouton is the separation between your customizable generated code and the stable core library. This architecture gives you the freedom to modify generated files while benefiting from updates to the underlying framework. ┌─────────────────────────────────────┐\n│  Generated Code (Yours)             │\n│  - Forms, Lists, Tables             │\n│  - You customize freely             │\n│  - Lives in YOUR project            │\n└─────────────────────────────────────┘\n            ↓ uses\n┌─────────────────────────────────────┐\n│  Core Library (Stable)              │\n│  - Composables, utilities           │\n│  - Modal management, caching        │\n│  - Updates via npm                  │\n└─────────────────────────────────────┘",{"id":2294,"title":2295,"titles":2296,"content":2297,"level":449},"/fundamentals/architecture#generated-code-layer","Generated Code Layer",[85,2279],"The generated code layer includes all the CRUD interfaces you create using the Crouton generator: Forms - Create and edit forms with validationLists - Collection list views with filteringTables - Data table components with sorting and actionsComposables - Data fetching and mutation logicTypeScript Types - Fully typed interfaces for your collections This code lives in your project and is yours to customize however you need. You can modify components, add new features, change styling, or completely refactor the generated code—it's all under your control.",{"id":2299,"title":2300,"titles":2301,"content":2302,"level":449},"/fundamentals/architecture#core-library-layer","Core Library Layer",[85,2279],"The core library provides the foundational utilities and composables that your generated code uses: Data Operations - useCollectionQuery(), useCollectionMutation(), useCroutonMutate()Container Management - useCrouton() with container type parameter (modal, slideover, dialog, inline)Notifications - useNotify() for user feedbackCaching - Automatic cache invalidation built on Nuxt's useFetchUtilities - Helper functions for common tasks The core library stays consistent and receives updates via npm. When you update the nuxt-crouton package, you get bug fixes and new features in the composables without affecting your customized code.",{"id":2304,"title":2305,"titles":2306,"content":2307,"level":449},"/fundamentals/architecture#why-this-separation-works","Why This Separation Works",[85,2279],"This architecture solves a common problem in code generation tools: how to provide updates without breaking customizations. With Nuxt Crouton: You own the generated code - No magic, no hidden abstractions. The generated files are just Vue components and TypeScript code you can read, understand, and modify.Core stays stable - The composables and utilities follow semantic versioning. Breaking changes are rare and clearly documented.Customizations persist - Your modifications to generated code won't be overwritten by updates to the core library.You can regenerate - If you want fresh templates, you can regenerate collections. Just back up your customizations first.",{"id":2309,"title":2310,"titles":2311,"content":2312,"level":391},"/fundamentals/architecture#domain-driven-design-with-nuxt-layers","Domain-Driven Design with Nuxt Layers",[85],"Nuxt layers allow you to organize your collections by business domain rather than technical function. This creates clear boundaries, improves maintainability, and enables domain-specific deployments.",{"id":2314,"title":2315,"titles":2316,"content":2317,"level":449},"/fundamentals/architecture#why-use-layers","Why Use Layers?",[85,2310],"Clear Boundaries - Each domain is isolated with its own components, composables, and API routes. Changes in one domain don't affect others. Easier Maintenance - Related code stays together. When working on e-commerce features, everything you need is in the shop layer. Independent Deployment - Deploy layers separately or together. This is particularly useful for larger applications or multi-tenant systems. Reusability - Share layers across projects. Build a blog layer once, use it in multiple applications.",{"id":2319,"title":2320,"titles":2321,"content":2322,"level":449},"/fundamentals/architecture#layer-structure","Layer Structure",[85,2310],"A typical Nuxt Crouton project using layers looks like this: layers/\n  ├── shop/              # E-commerce domain\n  │   └── collections/\n  │       ├── products/\n  │       │   ├── app/\n  │       │   │   ├── components/\n  │       │   │   │   ├── _Form.vue\n  │       │   │   │   └── List.vue\n  │       │   │   └── composables/\n  │       │   │       └── useShopProducts.ts\n  │       │   ├── server/\n  │       │   │   ├── api/teams/[id]/shop-products/\n  │       │   │   └── database/\n  │       │   │       └── schema.ts\n  │       │   └── types.ts\n  │       ├── orders/\n  │       └── inventory/\n  │\n  ├── blog/              # Content domain\n  │   └── collections/\n  │       ├── posts/\n  │       ├── authors/\n  │       └── comments/\n  │\n  └── admin/             # Admin domain\n      └── collections/\n          ├── users/\n          └── roles/",{"id":2324,"title":2325,"titles":2326,"content":2327,"level":449},"/fundamentals/architecture#when-to-use-layers","When to Use Layers",[85,2310],"Use layers when you have: Multiple business domains (shop, blog, admin)Large applications with clear domain boundariesReusable functionality to share across projectsTeams working on different domains Don't use layers for: Simple applications with only a few collectionsTightly coupled features that share lots of codePrototypes or early-stage projects Start simple—you can always add layers later as your application grows.",{"id":2329,"title":2330,"titles":2331,"content":2332,"level":449},"/fundamentals/architecture#generating-into-layers","Generating into Layers",[85,2310],"To generate collections into a specific layer, pass the layer as the first positional argument: # Generate into the shop layer\nnpx crouton-generate shop products --fields-file schemas/products.json\n\n# Generate into the blog layer\nnpx crouton-generate blog posts --fields-file schemas/posts.json\n\n# Generate into the admin layer\nnpx crouton-generate admin users --fields-file schemas/users.json The layer is a required positional argument (not a flag). If you omit both layer and collection, Crouton looks for a crouton.config.js file instead.",{"id":2334,"title":2335,"titles":2336,"content":2337,"level":391},"/fundamentals/architecture#layer-configuration","Layer Configuration",[85],"Each layer can have its own nuxt.config.ts file for layer-specific configuration: // layers/shop/nuxt.config.ts\nexport default defineNuxtConfig({\n  // Layer-specific configuration\n  components: {\n    dirs: [\n      { path: '~/components', prefix: 'Shop' }\n    ]\n  }\n}) This allows you to: Set up layer-specific component prefixesConfigure layer-specific API routesAdd layer-specific modules or pluginsOverride settings for specific domains",{"id":2339,"title":44,"titles":2340,"content":528,"level":391},"/fundamentals/architecture#best-practices",[85],{"id":2342,"title":2343,"titles":2344,"content":2345,"level":449},"/fundamentals/architecture#organize-by-domain-not-technical-function","Organize by Domain, Not Technical Function",[85,44],"Good - Organized by business domain: layers/\n  ├── shop/           # All e-commerce features\n  │   └── collections/\n  │       ├── products/\n  │       ├── orders/\n  │       └── inventory/\n  └── blog/           # All content features\n      └── collections/\n          ├── posts/\n          ├── authors/\n          └── comments/ Bad - Organized by technical function: components/\n  ├── forms/         # All forms together\n  ├── lists/         # All lists together\n  └── tables/        # All tables together",{"id":2347,"title":2348,"titles":2349,"content":2350,"level":449},"/fundamentals/architecture#keep-related-code-together","Keep Related Code Together",[85,44],"Everything related to a domain should live in that domain's layer: ComponentsComposablesAPI routesTypeScript typesUtilitiesTests This makes it easy to find code, understand dependencies, and make changes without affecting other domains.",{"id":2352,"title":2353,"titles":2354,"content":2355,"level":449},"/fundamentals/architecture#use-clear-naming-conventions","Use Clear Naming Conventions",[85,44],"Layer names: shop, blog, admin (lowercase, singular or plural based on domain)Collection names: products, orders, posts (plural)Component names: _Form.vue, List.vue (generated per collection)Composable names: useShopProducts, useShopOrders (camelCase, layer prefix + collection)",{"id":2357,"title":2358,"titles":2359,"content":2360,"level":449},"/fundamentals/architecture#document-layer-dependencies","Document Layer Dependencies",[85,44],"If one layer depends on another, document it clearly: // layers/shop/README.md\n# Shop Layer\n\nE-commerce functionality for products, orders, and inventory.\n\n## Dependencies\n- Requires `admin` layer for user management\n- Uses shared types from `core` layer",{"id":2362,"title":1007,"titles":2363,"content":2364,"level":391},"/fundamentals/architecture#related-resources",[85],"Best Practices - Recommended patterns and practicesGeneration - Learn about code generationNuxt Layers Documentation - Official Nuxt layers guide html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"id":90,"title":89,"titles":2366,"content":2367,"level":385},[],"Working with forms and modals in Nuxt Crouton",{"id":2369,"title":2370,"titles":2371,"content":2372,"level":391},"/fundamentals/forms-modals#opening-forms","Opening Forms",[89],"\u003Cscript setup lang=\"ts\">\nconst { open } = useCrouton()\n\n// Create new item\nconst handleCreate = () => {\n  open('create', 'shopProducts')\n}\n\n// Edit existing item\nconst handleEdit = (productId: string) => {\n  open('update', 'shopProducts', [productId])\n}\n\n// Delete items\nconst handleDelete = (productIds: string[]) => {\n  open('delete', 'shopProducts', productIds)\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUButton @click=\"handleCreate\">New Product\u003C/UButton>\n  \u003CUButton @click=\"handleEdit('product-123')\">Edit\u003C/UButton>\n  \u003CUButton @click=\"handleDelete(['id1', 'id2'])\" color=\"red\">Delete\u003C/UButton>\n\u003C/template>",{"id":2374,"title":2375,"titles":2376,"content":2377,"level":391},"/fundamentals/forms-modals#container-types","Container Types",[89],"Nuxt Crouton supports four container types for forms: // Slideover (default)\nopen('create', 'shopProducts', [], 'slideover')\n\n// Modal\nopen('create', 'shopProducts', [], 'modal')\n\n// Dialog\nopen('delete', 'shopProducts', ['id1'], 'dialog')\n\n// Inline\nopen('update', 'shopProducts', ['id1'], 'inline')",{"id":2379,"title":2380,"titles":2381,"content":2382,"level":449},"/fundamentals/forms-modals#slideover","Slideover",[89,2375],"Default container typeSlides in from the rightGood for forms with many fields",{"id":2384,"title":2385,"titles":2386,"content":2387,"level":449},"/fundamentals/forms-modals#modal","Modal",[89,2375],"Center-screen overlayGood for focused actionsBetter for mobile",{"id":2389,"title":2390,"titles":2391,"content":2392,"level":449},"/fundamentals/forms-modals#dialog","Dialog",[89,2375],"Compact confirmationGood for delete confirmationsSimple yes/no actions",{"id":2394,"title":2395,"titles":2396,"content":2397,"level":449},"/fundamentals/forms-modals#inline","Inline",[89,2375],"Renders form inline within the pageNo overlay or panelGood for embedded editing experiences",{"id":2399,"title":2400,"titles":2401,"content":2402,"level":391},"/fundamentals/forms-modals#nested-forms","Nested Forms",[89],"Nuxt Crouton supports nesting forms up to 5 levels deep: \u003Cscript setup lang=\"ts\">\n// Open product form\nopen('create', 'shopProducts')\n\n// From inside product form, open category form\nopen('create', 'shopCategories')  // Opens on top of product form\n\n// Supports up to 5 levels deep\n\u003C/script> This is useful when: Adding related items from within a formCreating lookup values on the flyQuick-adding categories, tags, etc.",{"id":2404,"title":2405,"titles":2406,"content":2407,"level":391},"/fundamentals/forms-modals#programmatic-control","Programmatic Control",[89],"\u003Cscript setup lang=\"ts\">\nconst { open, close, closeAll, showCrouton } = useCrouton()\n\n// Check if any form is open\nif (showCrouton.value) {\n  console.log('Form is open')\n}\n\n// Close current form\nclose()\n\n// Close all forms\ncloseAll()\n\u003C/script>",{"id":2409,"title":2410,"titles":2411,"content":2412,"level":391},"/fundamentals/forms-modals#form-props","Form Props",[89],"Generated forms accept these props: interface Props {\n  action: 'create' | 'update' | 'delete' | 'view'\n  activeItem?: any\n  items?: string[]\n  loading: LoadingState // 'notLoading' | 'create_send' | 'update_send' | 'delete_send' | 'view_send' | 'create_open' | 'update_open' | 'delete_open' | 'view_open'\n  collection: string\n}",{"id":2414,"title":2415,"titles":2416,"content":2417,"level":449},"/fundamentals/forms-modals#action","action",[89,2410],"The operation being performed: create - Creating a new itemupdate - Editing an existing itemdelete - Deleting one or more itemsview - Viewing an item (read-only)",{"id":2419,"title":2420,"titles":2421,"content":2422,"level":449},"/fundamentals/forms-modals#activeitem","activeItem",[89,2410],"The item being edited (for update action)",{"id":2424,"title":2425,"titles":2426,"content":2427,"level":449},"/fundamentals/forms-modals#items","items",[89,2410],"Array of item IDs (for delete action)",{"id":2429,"title":2430,"titles":2431,"content":2432,"level":449},"/fundamentals/forms-modals#loading","loading",[89,2410],"Loading state identifier",{"id":2434,"title":2435,"titles":2436,"content":2437,"level":449},"/fundamentals/forms-modals#collection","collection",[89,2410],"The collection name (e.g., 'shopProducts')",{"id":2439,"title":2440,"titles":2441,"content":2442,"level":391},"/fundamentals/forms-modals#form-customization","Form Customization",[89],"\u003C!-- layers/shop/components/products/_Form.vue -->\n\u003Cscript setup lang=\"ts\">\n// Keep generated props\nconst props = defineProps\u003CShopProductsFormProps>()\n\n// Add custom state\nconst uploadingImage = ref(false)\nconst imagePreview = ref\u003Cstring | null>(null)\n\n// Add custom methods\nconst handleImageUpload = async (file: File) => {\n  uploadingImage.value = true\n  const url = await uploadToCloudinary(file)\n  state.value.imageUrl = url\n  imagePreview.value = url\n  uploadingImage.value = false\n}\n\n// Keep generated mutation logic\nconst { create, update } = useCollectionMutation(props.collection)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm @submit=\"handleSubmit\">\n    \u003C!-- Generated fields -->\n    \u003CUFormField label=\"Name\" name=\"name\">\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n\n    \u003C!-- Your custom field -->\n    \u003CUFormField label=\"Product Image\" name=\"imageUrl\">\n      \u003Cimg v-if=\"imagePreview\" :src=\"imagePreview\" class=\"w-32 h-32 object-cover\" />\n      \u003CUButton @click=\"triggerFileInput\" :loading=\"uploadingImage\">\n        Upload Image\n      \u003C/UButton>\n    \u003C/UFormField>\n\n    \u003C!-- Keep generated button -->\n    \u003CCroutonFormActionButton :action=\"action\" :loading=\"loading\" />\n  \u003C/UForm>\n\u003C/template>",{"id":2444,"title":2445,"titles":2446,"content":2447,"level":391},"/fundamentals/forms-modals#multi-step-forms","Multi-Step Forms",[89],"\u003Cscript setup lang=\"ts\">\nconst currentStep = ref(1)\nconst totalSteps = 3\n\nconst nextStep = () => {\n  if (currentStep.value \u003C totalSteps) {\n    currentStep.value++\n  }\n}\n\nconst prevStep = () => {\n  if (currentStep.value > 1) {\n    currentStep.value--\n  }\n}\n\nconst handleSubmit = async () => {\n  if (currentStep.value \u003C totalSteps) {\n    nextStep()\n  } else {\n    // Final submit\n    await create(state.value)\n    close()\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm @submit=\"handleSubmit\">\n    \u003C!-- Step indicator -->\n    \u003Cdiv class=\"flex justify-between mb-4\">\n      \u003Cdiv v-for=\"step in totalSteps\" :key=\"step\"\n           :class=\"{ 'font-bold': step === currentStep }\">\n        Step {{ step }}\n      \u003C/div>\n    \u003C/div>\n\n    \u003C!-- Step 1: Basic info -->\n    \u003Cdiv v-if=\"currentStep === 1\">\n      \u003CUFormField label=\"Name\" name=\"name\">\n        \u003CUInput v-model=\"state.name\" />\n      \u003C/UFormField>\n    \u003C/div>\n\n    \u003C!-- Step 2: Details -->\n    \u003Cdiv v-if=\"currentStep === 2\">\n      \u003CUFormField label=\"Description\" name=\"description\">\n        \u003CUTextarea v-model=\"state.description\" />\n      \u003C/UFormField>\n    \u003C/div>\n\n    \u003C!-- Step 3: Pricing -->\n    \u003Cdiv v-if=\"currentStep === 3\">\n      \u003CUFormField label=\"Price\" name=\"price\">\n        \u003CUInput v-model.number=\"state.price\" type=\"number\" />\n      \u003C/UFormField>\n    \u003C/div>\n\n    \u003C!-- Navigation -->\n    \u003Cdiv class=\"flex justify-between\">\n      \u003CUButton v-if=\"currentStep > 1\" @click=\"prevStep\" variant=\"ghost\">\n        Back\n      \u003C/UButton>\n      \u003CUButton type=\"submit\">\n        {{ currentStep \u003C totalSteps ? 'Next' : 'Submit' }}\n      \u003C/UButton>\n    \u003C/div>\n  \u003C/UForm>\n\u003C/template>",{"id":2449,"title":2450,"titles":2451,"content":2452,"level":391},"/fundamentals/forms-modals#conditional-fields","Conditional Fields",[89],"\u003Ctemplate>\n  \u003CUForm>\n    \u003CUFormField label=\"Product Type\" name=\"type\">\n      \u003CUSelectMenu\n        v-model=\"state.type\"\n        :options=\"['physical', 'digital']\"\n      />\n    \u003C/UFormField>\n\n    \u003C!-- Show only for physical products -->\n    \u003CUFormField v-if=\"state.type === 'physical'\" label=\"Weight\" name=\"weight\">\n      \u003CUInput v-model.number=\"state.weight\" type=\"number\" />\n    \u003C/UFormField>\n\n    \u003C!-- Show only for digital products -->\n    \u003CUFormField v-if=\"state.type === 'digital'\" label=\"Download URL\" name=\"downloadUrl\">\n      \u003CUInput v-model=\"state.downloadUrl\" />\n    \u003C/UFormField>\n  \u003C/UForm>\n\u003C/template>",{"id":2454,"title":1562,"titles":2455,"content":2456,"level":391},"/fundamentals/forms-modals#related-topics",[89],"Data OperationsCustomizing FormsValidation html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":94,"title":93,"titles":2458,"content":2459,"level":385},[],"Creating, updating, and deleting data in Nuxt Crouton Nuxt Crouton provides two ways to mutate data:",{"id":2461,"title":2462,"titles":2463,"content":2464,"level":391},"/fundamentals/data-operations#quick-way-usecroutonmutate","Quick Way: useCroutonMutate()",[93],"Use when: One-off actions, utilities, prototyping \u003Cscript setup lang=\"ts\">\nconst { mutate } = useCroutonMutate()\n\n// Create\nawait mutate('create', 'shopProducts', {\n  name: 'New Product',\n  price: 29.99\n})\n\n// Update\nawait mutate('update', 'shopProducts', {\n  id: 'product-123',\n  name: 'Updated Name'\n})\n\n// Delete\nawait mutate('delete', 'shopProducts', ['id1', 'id2'])\n\u003C/script>",{"id":2466,"title":2467,"titles":2468,"content":2469,"level":391},"/fundamentals/data-operations#optimized-way-usecollectionmutation","Optimized Way: useCollectionMutation()",[93],"Use when: Forms, repeated operations Complete Examples: For full useCollectionMutation usage patterns and API details, see Mutation Composables.",{"id":2471,"title":1157,"titles":2472,"content":2473,"level":391},"/fundamentals/data-operations#when-to-use-which",[93],"For a detailed comparison and decision matrix, see the Mutation Composables API Reference.",{"id":2475,"title":2476,"titles":2477,"content":528,"level":391},"/fundamentals/data-operations#create-operations","Create Operations",[93],{"id":2479,"title":2480,"titles":2481,"content":2482,"level":449},"/fundamentals/data-operations#basic-create","Basic Create",[93,2476],"\u003Cscript setup lang=\"ts\">\nconst { create } = useCollectionMutation('shopProducts')\n\nconst handleCreate = async () => {\n  const newProduct = await create({\n    name: 'Widget',\n    price: 19.99,\n    inStock: true\n  })\n\n  console.log('Created:', newProduct.id)\n}\n\u003C/script>",{"id":2484,"title":2485,"titles":2486,"content":2487,"level":449},"/fundamentals/data-operations#create-with-relations","Create with Relations",[93,2476],"\u003Cscript setup lang=\"ts\">\nconst { create } = useCollectionMutation('shopProducts')\n\n// Create product with category relation\nawait create({\n  name: 'Widget',\n  price: 19.99,\n  categoryId: 'cat-123'  // Foreign key\n})",{"id":2489,"title":2490,"titles":2491,"content":528,"level":391},"/fundamentals/data-operations#update-operations","Update Operations",[93],{"id":2493,"title":2494,"titles":2495,"content":2496,"level":449},"/fundamentals/data-operations#basic-update","Basic Update",[93,2490],"\u003Cscript setup lang=\"ts\">\nconst { update } = useCollectionMutation('shopProducts')\n\nconst handleUpdate = async (productId: string) => {\n  await update(productId, {\n    name: 'Updated Widget',\n    price: 24.99\n  })\n  // updatedBy and updatedAt are automatically set\n}\n\u003C/script> Automatic Audit Tracking: When you update a record, the system automatically sets:updatedBy to the current user's IDupdatedAt to the current timestampYou don't need to manually include these fields in your update data. The createdBy field (creator) remains unchanged.",{"id":2498,"title":2499,"titles":2500,"content":2501,"level":449},"/fundamentals/data-operations#partial-update","Partial Update",[93,2490],"\u003Cscript setup lang=\"ts\">\n// Only update specific fields\nawait update('product-123', {\n  price: 29.99  // Only price changes\n})",{"id":2503,"title":2504,"titles":2505,"content":528,"level":391},"/fundamentals/data-operations#delete-operations","Delete Operations",[93],{"id":2507,"title":2508,"titles":2509,"content":2510,"level":449},"/fundamentals/data-operations#single-delete","Single Delete",[93,2504],"\u003Cscript setup lang=\"ts\">\nconst { deleteItems } = useCollectionMutation('shopProducts')\n\nconst handleDelete = async (productId: string) => {\n  await deleteItems([productId])\n}\n\u003C/script>",{"id":2512,"title":2513,"titles":2514,"content":2515,"level":449},"/fundamentals/data-operations#bulk-delete","Bulk Delete",[93,2504],"\u003Cscript setup lang=\"ts\">\nconst { deleteItems } = useCollectionMutation('shopProducts')\n\nconst handleBulkDelete = async (productIds: string[]) => {\n  await deleteItems(productIds)\n}\n\u003C/script>",{"id":2517,"title":268,"titles":2518,"content":2519,"level":391},"/fundamentals/data-operations#bulk-operations",[93],"For bulk update, delete, and other batch operations on multiple records, see the dedicated Bulk Operations guide. Quick tip: Use a loop with useCroutonMutate for simple bulk operations, or implement optimistic updates for better UX.",{"id":2521,"title":2522,"titles":2523,"content":2524,"level":391},"/fundamentals/data-operations#error-handling","Error Handling",[93],"\u003Cscript setup lang=\"ts\">\nconst { create } = useCollectionMutation('shopProducts')\n\nconst handleCreate = async () => {\n  try {\n    await create({\n      name: 'Widget',\n      price: 19.99\n    })\n    // Success - cache automatically refreshes\n  } catch (error) {\n    console.error('Failed to create product:', error)\n    // Show error toast\n  }\n}\n\u003C/script>",{"id":2526,"title":2527,"titles":2528,"content":2529,"level":391},"/fundamentals/data-operations#optimistic-updates","Optimistic Updates",[93],"For implementing optimistic updates with rollback on error, see the dedicated Optimistic Updates guide. Quick tip: Optimistic updates improve perceived performance by updating the UI before the server responds.",{"id":2531,"title":2532,"titles":2533,"content":2534,"level":391},"/fundamentals/data-operations#cache-invalidation","Cache Invalidation",[93],"Mutations automatically invalidate and refresh all related queries: // After mutation, all matching caches refresh automatically\nawait create({ name: 'New Product' })\n// → Triggers refetch for all shopProducts queries\n\n// These all get refreshed:\n// collection:shopProducts:{}\n// collection:shopProducts:{\"page\":1}\n// collection:shopProducts:{\"page\":2}\n// collection:shopProducts:{\"locale\":\"en\"}",{"id":2536,"title":2537,"titles":2538,"content":2539,"level":391},"/fundamentals/data-operations#see-also","See Also",[93],"Related Documentation:Mutation Composables API Reference - Complete API signatures and advanced patternsBulk Operations - Batch operations on multiple recordsOptimistic Updates - Improve UX with instant feedbackMigration Guide - Migrating from v1 mutation APIs",{"id":2541,"title":1562,"titles":2542,"content":2543,"level":391},"/fundamentals/data-operations#related-topics",[93],"Querying DataCachingForms & Modals html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"id":98,"title":97,"titles":2545,"content":2546,"level":385},[],"How caching works in Nuxt Crouton Query Examples: For complete useCollectionQuery patterns (basic, filtering, pagination, sorting, relations), see Querying Data. Nuxt Crouton uses Nuxt's built-in useFetch caching system for optimal performance. Each unique combination of collection and query parameters gets its own cache entry, and mutations automatically invalidate all related caches. // Each combination of collection + query params = separate cache\ncollection:shopProducts:{}                      // All products\ncollection:shopProducts:{\"page\":1}              // Page 1\ncollection:shopProducts:{\"page\":2}              // Page 2\ncollection:shopProducts:{\"locale\":\"en\"}         // English products\ncollection:shopProducts:{\"page\":1,\"locale\":\"fr\"} // Page 1, French",{"id":2548,"title":1635,"titles":2549,"content":2550,"level":391},"/fundamentals/caching#how-it-works",[97],"When you query data, Nuxt creates a cache entry based on the collection name and query parameters. For example, collection:shopProducts:{} for all products or collection:shopProducts:{\"page\":1} for page 1. After any mutation (create, update, or delete), all matching caches refresh automatically.",{"id":2552,"title":2553,"titles":2554,"content":2555,"level":391},"/fundamentals/caching#cache-keys","Cache Keys",[97],"Cache keys are generated based on: Collection nameQuery parameters (as JSON string) // Key format: collection:{name}:{jsonQuery}\n\n// No query params\n'collection:shopProducts:{}'\n\n// With page\n'collection:shopProducts:{\"page\":1}'\n\n// With multiple params\n'collection:shopProducts:{\"page\":1,\"locale\":\"en\",\"search\":\"widget\"}'",{"id":2557,"title":2558,"titles":2559,"content":2560,"level":391},"/fundamentals/caching#manual-cache-refresh","Manual Cache Refresh",[97],"You can manually refresh cached data using the refresh() method returned by useCollectionQuery. This is useful when you need to force a data reload without waiting for automatic invalidation.",{"id":2562,"title":2563,"titles":2564,"content":2565,"level":391},"/fundamentals/caching#global-cache-invalidation","Global Cache Invalidation",[97],"To invalidate all caches for a collection: // Refresh all shopProducts queries\nawait refreshNuxtData((key) => key.startsWith('collection:shopProducts:'))",{"id":2567,"title":2568,"titles":2569,"content":2570,"level":391},"/fundamentals/caching#cache-behavior-with-mutations","Cache Behavior with Mutations",[97],"When you perform a mutation using useCollectionMutation, all cache entries for that collection are automatically invalidated and refreshed. This includes all query variations (different pages, filters, search terms, etc.) to ensure data consistency across your application.",{"id":2572,"title":2573,"titles":2574,"content":2575,"level":391},"/fundamentals/caching#reactive-queries","Reactive Queries",[97],"Queries automatically re-fetch when their parameters change. When reactive query parameters update, a new cache entry is created for the new parameter combination while the old cache entry remains until page refresh.",{"id":2577,"title":2578,"titles":2579,"content":2580,"level":391},"/fundamentals/caching#cache-performance","Cache Performance",[97],"The caching system provides fast subsequent loads because cached data is served instantly. It reduces server load by avoiding duplicate requests. Mutations automatically update all related caches to keep everything synchronized. Keep in mind that each unique query creates a separate cache entry, so more query variations mean more cache entries. Old cache entries remain until page refresh.",{"id":2582,"title":44,"titles":2583,"content":2584,"level":449},"/fundamentals/caching#best-practices",[97,2578],"Use computed query params for reactive queries that should create separate cache entries per parameter combination. Avoid dynamic cache keys that change on every render (like Date.now() as a query parameter). Instead, use the manual refresh() method when you need fresh data. Leverage automatic invalidation - there's no need to manually refresh after mutations since useCollectionMutation does it automatically.",{"id":2586,"title":2587,"titles":2588,"content":2589,"level":391},"/fundamentals/caching#cache-debugging","Cache Debugging",[97],"To debug caching issues, check the cache key format in your browser console. Cache keys follow the pattern collection:{collectionName}:{jsonQuery}. For example, a query with pagination would have a key like collection:shopProducts:{\"page\":1}.",{"id":2591,"title":2592,"titles":2593,"content":2594,"level":391},"/fundamentals/caching#disabling-cache","Disabling Cache",[97],"If you need to disable caching for specific queries, use Nuxt's useFetch directly instead of useCollectionQuery and set key: null to disable caching.",{"id":2596,"title":1562,"titles":2597,"content":2598,"level":391},"/fundamentals/caching#related-topics",[97],"Querying DataData OperationsPerformance Optimization html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}",{"id":102,"title":101,"titles":2600,"content":2601,"level":385},[],"Understanding the Nuxt Crouton ecosystem and package structure Nuxt Crouton is a modular ecosystem consisting of a main module and multiple feature packages. The architecture provides flexibility through config-based feature enabling, keeping bundle sizes small while allowing you to use only what you need. Single-install experience: Installing @fyit/crouton automatically includes the core, auth, admin, and i18n features. Optional features are enabled via configuration.",{"id":2603,"title":2604,"titles":2605,"content":2606,"level":391},"/fundamentals/packages#architecture-overview","Architecture Overview",[101],"@fyit/crouton (main module)\n│\n├── Always includes:\n│   └── @fyit/crouton-core (base CRUD layer)\n│\n├── Core add-ons (enabled by default):\n│   ├── @fyit/crouton-auth (authentication)\n│   ├── @fyit/crouton-admin (admin dashboard)\n│   └── @fyit/crouton-i18n (translations)\n│\n├── Optional add-ons (disabled by default):\n│   ├── @fyit/crouton-editor (TipTap)\n│   ├── @fyit/crouton-flow (Vue Flow)\n│   ├── @fyit/crouton-assets (media library)\n│   ├── @fyit/crouton-maps (Mapbox)\n│   ├── @fyit/crouton-ai (LLM integration)\n│   ├── @fyit/crouton-email (Vue Email)\n│   ├── @fyit/crouton-events (audit trail)\n│   ├── @fyit/crouton-collab (real-time)\n│   ├── @fyit/crouton-pages (CMS)\n│   ├── @fyit/crouton-devtools (dev tools)\n│   └── @fyit/crouton-themes (UI themes)\n│\n└── Mini-apps (experimental):\n    ├── @fyit/crouton-bookings\n    └── @fyit/crouton-sales",{"id":2608,"title":2609,"titles":2610,"content":528,"level":391},"/fundamentals/packages#main-module","Main Module",[101],{"id":2612,"title":2613,"titles":2614,"content":2615,"level":449},"/fundamentals/packages#fyitcrouton","@fyit/crouton",[101,2609],"Purpose: Unified Nuxt module that aggregates all Crouton packages with opt-in features\nInstall: pnpm add @fyit/crouton This is the main entry point for the Crouton ecosystem. It automatically includes core features and allows enabling optional add-ons via configuration. Configuration: // nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['@fyit/crouton'],\n\n  crouton: {\n    // Global settings\n    apiPrefix: '/api',\n    defaultPageSize: 20,\n\n    // Core add-ons (enabled by default, can disable)\n    auth: true,\n    admin: true,\n    i18n: true,\n\n    // Optional add-ons (disabled by default)\n    editor: false,\n    flow: false,\n    assets: false,\n    maps: false,\n    ai: false,\n    email: false,\n    events: false,\n    collab: false,\n    pages: false,\n    devtools: undefined,  // Auto-detect in dev mode\n    themes: false,\n\n    // Mini-apps (experimental, disabled by default)\n    bookings: false,\n    sales: false\n  }\n}) Minimal setup (core features only): export default defineNuxtConfig({\n  modules: ['@fyit/crouton']\n  // Gets: core, auth, admin, i18n automatically\n})",{"id":2617,"title":2618,"titles":2619,"content":2620,"level":391},"/fundamentals/packages#core-packages-auto-included","Core Packages (Auto-Included)",[101],"These packages are automatically included when you install @fyit/crouton. No separate installation needed.",{"id":2622,"title":2623,"titles":2624,"content":2625,"level":449},"/fundamentals/packages#fyitcrouton-core","@fyit/crouton-core",[101,2618],"Purpose: Base CRUD layer with collections, composables, and components\nStatus: Always included Contains: Composables (useCollectionQuery, useCollectionMutation, useCrouton, useCroutonApps)Base components (CroutonCollection, CroutonFormActionButton, CroutonForm)Container management (slideover, modal, dialog, inline)Cache invalidation systemApp auto-discovery systemTypeScript types",{"id":2627,"title":2628,"titles":2629,"content":2630,"level":449},"/fundamentals/packages#fyitcrouton-auth","@fyit/crouton-auth",[101,2618],"Purpose: Authentication layer with teams, passkeys, and 2FA\nStatus: Enabled by default Contains: Better Auth integrationTeam/organization management (multi-tenant, single-tenant, personal modes)Passkey/WebAuthn supportTwo-factor authentication (TOTP)OAuth providers (Google, GitHub, etc.)Auth composables (useAuth, useSession, useTeam)Pre-built auth components Routes provided: /auth/* (login, register, forgot-password, etc.) Disable if needed: crouton: {\n  auth: false  // Use your own auth system\n}",{"id":2632,"title":2633,"titles":2634,"content":2635,"level":449},"/fundamentals/packages#fyitcrouton-admin","@fyit/crouton-admin",[101,2618],"Purpose: Super admin dashboard for system-wide management\nStatus: Enabled by default Contains: Super admin dashboard with statsUser management (list, create, ban, unban, delete)Team oversight (view all teams/members)User impersonation (debug as any user)Super admin middleware Routes provided: /super-admin/*",{"id":2637,"title":2638,"titles":2639,"content":2640,"level":449},"/fundamentals/packages#fyitcrouton-i18n","@fyit/crouton-i18n",[101,2618],"Purpose: Translation and multi-language support\nStatus: Enabled by default Contains: CroutonI18nInput componentLanguageSwitcher componentDevModeToggle componentuseEntityTranslations composableuseT composable for translationsDatabase-backed team overrides Provides: common.* - Generic UI stringsforms.* - Form field labelserrors.* - Error messagestime.* - Time formatting stringsnavigation.* - Navigation labels",{"id":2642,"title":536,"titles":2643,"content":2644,"level":391},"/fundamentals/packages#optional-add-ons",[101],"Enable these features in your crouton config as needed.",{"id":2646,"title":2647,"titles":2648,"content":2649,"level":449},"/fundamentals/packages#fyitcrouton-editor","@fyit/crouton-editor",[101,536],"Purpose: Rich text editing with Tiptap\nEnable: crouton: { editor: true } Contains: EditorSimple component (WYSIWYG editor)EditorToolbar componentTiptap integrationPre-configured extensions When to use: Blog posts, descriptions, content management",{"id":2651,"title":2652,"titles":2653,"content":2654,"level":449},"/fundamentals/packages#fyitcrouton-ai","@fyit/crouton-ai",[101,536],"Purpose: AI chat and completion integration\nEnable: crouton: { ai: true } Contains: useChat() composable (streaming chat with conversation history)useCompletion() composable (single-turn text completion)useAIProvider() composable (provider/model configuration)CroutonAiChatbox, CroutonAiMessage, CroutonAiInput componentsServer utilities for OpenAI and AnthropicChat conversations persistence schema Environment Variables: NUXT_OPENAI_API_KEY=sk-...\nNUXT_ANTHROPIC_API_KEY=sk-ant-...",{"id":2656,"title":2657,"titles":2658,"content":2659,"level":449},"/fundamentals/packages#fyitcrouton-assets","@fyit/crouton-assets",[101,536],"Purpose: Centralized asset management with NuxtHub blob storage\nEnable: crouton: { assets: true } Contains: CroutonAssetsPicker component (visual asset browser)CroutonAssetsUploader component (file upload with metadata)useAssetUpload() composableAssets collection schemaNuxtHub blob storage integration Requires: hub: { blob: true } in nuxt.config",{"id":2661,"title":2662,"titles":2663,"content":2664,"level":449},"/fundamentals/packages#fyitcrouton-events","@fyit/crouton-events",[101,536],"Purpose: Event management with audit trail\nEnable: crouton: { events: true } Contains: Lifecycle hooks (beforeCreate, afterUpdate, etc.)Custom event handlersEvent bus integrationWebhooks supportAudit trail tracking",{"id":2666,"title":2667,"titles":2668,"content":2669,"level":449},"/fundamentals/packages#fyitcrouton-maps","@fyit/crouton-maps",[101,536],"Purpose: Map integration with location fields\nEnable: crouton: { maps: true } Contains: Map display componentsLocation input fields with geocodingMarker and pin supportAddress autocomplete",{"id":2671,"title":2672,"titles":2673,"content":2674,"level":449},"/fundamentals/packages#fyitcrouton-flow","@fyit/crouton-flow",[101,536],"Purpose: Visual flow builder with drag-and-drop\nEnable: crouton: { flow: true } Contains: Visual flow editor componentConfigurable node typesFlow execution engineFlow collection schemaVue Flow integration",{"id":2676,"title":2677,"titles":2678,"content":2679,"level":449},"/fundamentals/packages#fyitcrouton-email","@fyit/crouton-email",[101,536],"Purpose: Email infrastructure with Vue Email templates and Resend\nEnable: crouton: { email: true } Contains: useEmailService() composableConvenience senders (sendVerificationEmail, sendMagicLink, etc.)Vue Email templatesClient flow components (EmailVerificationFlow, MagicLinkSent)Resend integration Environment Variables: RESEND_API_KEY=re_xxx Integration: Works with @fyit/crouton-auth for auth emails",{"id":2681,"title":2682,"titles":2683,"content":2684,"level":449},"/fundamentals/packages#fyitcrouton-collab","@fyit/crouton-collab",[101,536],"Purpose: Real-time collaboration with Yjs CRDTs\nEnable: crouton: { collab: true } Contains: Cloudflare Durable Objects for Yjs syncuseCollabConnection, useCollabSync, useCollabPresence composablesuseCollabEditor for TipTap integrationCollabStatus, CollabPresence, CollabCursors componentsPresence tracking and cursor sharing Room types: page (TipTap), flow (graphs), document (text), generic Note: Sync mode requires Cloudflare Durable Objects",{"id":2686,"title":2687,"titles":2688,"content":2689,"level":449},"/fundamentals/packages#fyitcrouton-pages","@fyit/crouton-pages",[101,536],"Purpose: CMS-like page management system\nEnable: crouton: { pages: true } Contains: Page types from app packagesTree/sortable layout for page orderingPublic page rendering at /[team]/[slug]Block-based page editorCustom domain support with automatic team resolutionusePageTypes, useDomainContext, useNavigation composables URL Structure: Public pages: /[team]/[slug]Admin: /admin/[team]/pages",{"id":2691,"title":2692,"titles":2693,"content":2694,"level":449},"/fundamentals/packages#fyitcrouton-devtools","@fyit/crouton-devtools",[101,536],"Purpose: Development tools and debugging utilities\nEnable: crouton: { devtools: true } (auto-detected in dev mode) Contains: Debug panel for inspecting collectionsAPI explorer for testing endpointsSchema viewer for collection schemasDevelopment helpers Note: Auto-enabled in development mode",{"id":2696,"title":2697,"titles":2698,"content":2699,"level":449},"/fundamentals/packages#fyitcrouton-themes","@fyit/crouton-themes",[101,536],"Purpose: Swappable UI themes for Nuxt UI applications\nEnable: crouton: { themes: true } or extend directly Available Themes: ThemeDescriptionExtend PathKOHardware-inspired (Teenage Engineering KO II)@fyit/crouton-themes/ko Usage (theme-specific extend): export default defineNuxtConfig({\n  extends: ['@fyit/crouton-themes/ko']\n}) Contains: Design tokens (CSS custom properties)Nuxt UI component variant overrides (variant=\"ko\")Theme-specific components (KoLed, KoKnob, KoPanel)",{"id":2701,"title":2702,"titles":2703,"content":2704,"level":391},"/fundamentals/packages#experimental-mini-apps","Experimental Mini-Apps",[101],"These are experimental domain-specific applications built on Crouton. APIs may change.",{"id":2706,"title":2707,"titles":2708,"content":2709,"level":449},"/fundamentals/packages#fyitcrouton-bookings","@fyit/crouton-bookings",[101,2702],"Purpose: Booking system for slots and inventory\nEnable: crouton: { bookings: true }Status: Experimental ⚠️ Contains: Slot-based bookings (courts, rooms, appointments)Inventory-based reservations (equipment, rentals)Customer booking wizarduseBookingAvailability, useBookingCart composablesEmail template management (opt-in) Requires: @fyit/crouton-auth, optionally @fyit/crouton-email",{"id":2711,"title":2712,"titles":2713,"content":2714,"level":449},"/fundamentals/packages#fyitcrouton-sales","@fyit/crouton-sales",[101,2702],"Purpose: Event-based Point of Sale (POS) system\nEnable: crouton: { sales: true }Status: Experimental ⚠️ Contains: Products, categories, ordersCustomer-facing order interfaceHelper authentication (PIN-based)Optional thermal receipt printingusePosOrder, useHelperAuth composables Requires: @fyit/crouton-auth",{"id":2716,"title":2717,"titles":2718,"content":2719,"level":391},"/fundamentals/packages#development-tools-not-runtime","Development Tools (Not Runtime)",[101],"These packages are used during development and are not included in your production bundle.",{"id":2721,"title":2722,"titles":2723,"content":2724,"level":449},"/fundamentals/packages#fyitcrouton-cli","@fyit/crouton-cli",[101,2717],"Purpose: CLI tool for code generation\nInstall: pnpm add -D @fyit/crouton-cli Commands: crouton generate \u003Clayer> \u003Ccollection>\ncrouton rollback \u003Clayer> \u003Ccollection>\ncrouton rollback-bulk --layer=\u003Cname>\ncrouton rollback-interactive\ncrouton init\ncrouton install Key insight: Dev-time dependency only. Generated code doesn't depend on it.",{"id":2726,"title":2727,"titles":2728,"content":2729,"level":449},"/fundamentals/packages#fyitcrouton-mcp","@fyit/crouton-mcp",[101,2717],"Purpose: MCP server for AI-assisted collection generation\nInstall: Dev tool, configured in .claude/settings.json Contains: Schema design toolsValidation toolsCLI wrapper for generationCollection/layer scanning Tools: design_schema, validate_schema, generate_collection, list_collections, dry_run, rollback",{"id":2731,"title":2732,"titles":2733,"content":2734,"level":449},"/fundamentals/packages#fyitcrouton-designer","@fyit/crouton-designer",[101,2717],"Purpose: Visual schema design tool\nInstall: Dev tool Contains: Visual schema builder UIField type selectorSchema export to JSON",{"id":2736,"title":2737,"titles":2738,"content":528,"level":391},"/fundamentals/packages#installation-patterns","Installation Patterns",[101],{"id":2740,"title":2741,"titles":2742,"content":2743,"level":449},"/fundamentals/packages#standard-setup-recommended","Standard Setup (Recommended)",[101,2737],"# Install main module\npnpm add @fyit/crouton\n\n# Install CLI for code generation (dev dependency)\npnpm add -D @fyit/crouton-cli // nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['@fyit/crouton']\n  // Gets: core, auth, admin, i18n automatically\n})",{"id":2745,"title":2746,"titles":2747,"content":2748,"level":449},"/fundamentals/packages#enable-optional-features","Enable Optional Features",[101,2737],"// nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['@fyit/crouton'],\n\n  crouton: {\n    // Enable features you need\n    editor: true,\n    assets: true,\n    events: true,\n    pages: true\n  }\n})",{"id":2750,"title":2751,"titles":2752,"content":2753,"level":449},"/fundamentals/packages#full-featured-app","Full-Featured App",[101,2737],"// nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['@fyit/crouton'],\n\n  crouton: {\n    // All optional features\n    editor: true,\n    flow: true,\n    assets: true,\n    maps: true,\n    ai: true,\n    email: true,\n    events: true,\n    collab: true,\n    pages: true\n  }\n})",{"id":2755,"title":2756,"titles":2757,"content":2758,"level":449},"/fundamentals/packages#booking-application","Booking Application",[101,2737],"// nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['@fyit/crouton'],\n\n  crouton: {\n    bookings: true,\n    email: true,\n    pages: true\n  }\n})",{"id":2760,"title":2761,"titles":2762,"content":2763,"level":391},"/fundamentals/packages#three-tier-route-architecture","Three-Tier Route Architecture",[101],"TierRoute PatternPurposeAccessUser/dashboard/[team]/*User-facing featuresAny team memberAdmin/admin/[team]/*Team managementTeam admins/ownersSuper Admin/super-admin/*System managementApp owner only",{"id":2765,"title":2766,"titles":2767,"content":2768,"level":391},"/fundamentals/packages#field-types-and-required-packages","Field Types and Required Packages",[101],"When defining collection schemas, certain field types require specific features to be enabled. Field TypeRequired FeatureConfigeditorEditorcrouton: { editor: true }i18n / translationi18ncrouton: { i18n: true } (default)assetAssetscrouton: { assets: true }map / locationMapscrouton: { maps: true }flowFlowcrouton: { flow: true }",{"id":2770,"title":2771,"titles":2772,"content":528,"level":391},"/fundamentals/packages#why-this-architecture","Why This Architecture?",[101],{"id":2774,"title":1179,"titles":2775,"content":2776,"level":449},"/fundamentals/packages#benefits",[101,2771],"1. Small Bundle Sizes Only include what you needFeatures are tree-shaken when disabledNo unused code in production 2. Independent Updates Core can be updated without breaking add-onsAdd-ons can evolve independentlyGenerator improvements don't require core updates 3. Clear Separation Generated code vs runtime codeCore features vs optional add-onsDevelopment tools vs production dependencies 4. Flexible Adoption Start simple, add complexity as neededDisable features you're not usingMix and match capabilities",{"id":2778,"title":2779,"titles":2780,"content":2781,"level":449},"/fundamentals/packages#design-decisions","Design Decisions",[101,2771],"Q: Why is the generator a separate package?\nA: It's a dev-time tool. Your production app doesn't need the generator, templates, or CLI code. Q: Why use module-based config instead of extends?\nA: Cleaner config, better tree-shaking, and automatic feature discovery. Q: Can I use the core without the module?\nA: Yes! You can extend individual packages directly if you prefer the layer pattern. Q: Do add-ons depend on each other?\nA: Mostly no. Each add-on depends on core. Some have optional integrations (e.g., email + auth).",{"id":2783,"title":36,"titles":2784,"content":528,"level":391},"/fundamentals/packages#troubleshooting",[101],{"id":2786,"title":2787,"titles":2788,"content":2789,"level":449},"/fundamentals/packages#module-not-found-fyitcrouton","\"Module not found: @fyit/crouton\"",[101,36],"The core package isn't installed: pnpm add @fyit/crouton",{"id":2791,"title":2792,"titles":2793,"content":2794,"level":449},"/fundamentals/packages#crouton-generate-command-not-found","\"crouton-generate command not found\"",[101,36],"The generator isn't installed: pnpm add -D @fyit/crouton-cli",{"id":2796,"title":2797,"titles":2798,"content":2799,"level":449},"/fundamentals/packages#feature-component-not-found","Feature component not found",[101,36],"Make sure the feature is enabled: crouton: {\n  editor: true  // Enable the feature\n}",{"id":2801,"title":2802,"titles":2803,"content":2804,"level":449},"/fundamentals/packages#typescript-errors-after-enabling-feature","TypeScript errors after enabling feature",[101,36],"Run Nuxt prepare to regenerate types: npx nuxt prepare\nnpx nuxt typecheck",{"id":2806,"title":1958,"titles":2807,"content":2808,"level":391},"/fundamentals/packages#related-sections",[101],"Installation - Setup guideGenerator Commands - CLI usageTeam-Based Auth - Authentication with teamsAdmin Dashboard - User managementTranslations - i18n featureAI Integration - AI feature html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"id":106,"title":105,"titles":2810,"content":2811,"level":385},[],"Understanding the difference between generated code and core library",{"id":2813,"title":2814,"titles":2815,"content":2816,"level":391},"/fundamentals/generated-code#the-golden-rule","The Golden Rule",[105],"The most important thing to understand is that generated code is your code—edit it freely. Once files are generated into your project, you own them and can customize them however you need. Just remember not to regenerate unless you're willing to lose your changes.",{"id":2818,"title":2819,"titles":2820,"content":2821,"level":391},"/fundamentals/generated-code#comparison","Comparison",[105],"Generated CodeCore LibraryLives in YOUR projectLives in node_modulesYou customize freelyYou don't editCan divergeStays consistentRegenerate to updatenpm update to updateForms, lists, typesComposables, utilities",{"id":2823,"title":2824,"titles":2825,"content":2826,"level":391},"/fundamentals/generated-code#generated-code-location","Generated Code Location",[105],"layers/[layer]/\n  └── collections/\n      └── [collection]/\n          ├── app/\n          │   ├── components/\n          │   │   ├── List.vue                    # Generated\n          │   │   └── _Form.vue                   # Generated\n          │   └── composables/\n          │       └── use[Layer][Collection].ts    # Generated\n          │\n          ├── server/\n          │   ├── api/teams/[id]/\n          │   │   └── [layer]-[collection]/\n          │   │       ├── index.get.ts             # GET all / by IDs\n          │   │       ├── index.post.ts            # CREATE\n          │   │       ├── [collectionId].patch.ts  # UPDATE\n          │   │       └── [collectionId].delete.ts # DELETE\n          │   └── database/\n          │       ├── schema.ts                    # Drizzle schema\n          │       └── queries.ts                   # Database query functions\n          │\n          └── types.ts                             # Generated",{"id":2828,"title":2829,"titles":2830,"content":2831,"level":391},"/fundamentals/generated-code#what-generated-apis-provide","What Generated APIs Provide",[105],"The generated API endpoints provide a working CRUD implementation: EndpointFunctionalityGET index.get.tsFetch all items, or by IDs with ?ids=POST index.post.tsCreate new itemPATCH [collectionId].patch.tsUpdate item by IDDELETE [collectionId].delete.tsDelete item by ID",{"id":2833,"title":2834,"titles":2835,"content":2836,"level":449},"/fundamentals/generated-code#common-customizations","Common Customizations",[105,2829],"The generated APIs are starting points. Common modifications include: Pagination - Add page and limit query params (Pagination Guide)Filtering - Add date ranges, status filters, searchSorting - Add sortBy and sortDirection paramsValidation - Add Zod schemas for stricter input validationPermissions - Add role-based access checks",{"id":2838,"title":2839,"titles":2840,"content":2841,"level":391},"/fundamentals/generated-code#core-library-components","Core Library Components",[105],"The core library provides: useCrouton() - Modal managementuseCollectionQuery() - Data fetchinguseCollectionMutation() - Data mutationsCroutonCollection - Collection componentCroutonFormActionButton - Submit buttonToast notificationsCache invalidation",{"id":2843,"title":404,"titles":2844,"content":2845,"level":391},"/fundamentals/generated-code#the-rails-scaffold-approach",[105],"Nuxt Crouton works like Rails scaffolding: # Rails\nrails generate scaffold Post title:string body:text\n\n# Nuxt Crouton\nnpx crouton-generate blog posts --fields-file post-schema.json Both: Generate starting codeYou own it immediatelyCustomize as neededCore framework stays stable",{"id":2847,"title":2848,"titles":2849,"content":2850,"level":391},"/fundamentals/generated-code#when-to-regenerate","When to Regenerate",[105],"Regenerate when you're starting a new collection, want to update to the latest template, need to add a new field structure, or haven't made customizations yet. Don't regenerate if you've customized the generated files, added custom logic to forms, or modified validation—unless you use --force and accept losing your changes.",{"id":2852,"title":44,"titles":2853,"content":2854,"level":391},"/fundamentals/generated-code#best-practices",[105],"Customize the generated code freely since it's yours. Use version control to track your changes and add comments to document your customizations. When you update the core library, test your customizations to make sure everything still works. And always keep backups before regenerating with --force.",{"id":2856,"title":1562,"titles":2857,"content":2858,"level":391},"/fundamentals/generated-code#related-topics",[105],"Collections & LayersCustomizing Generated CodeGenerator Commands html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":74,"title":78,"titles":2860,"content":2861,"level":385},[],"This section covers the core concepts and architecture of Nuxt Crouton. Understanding these fundamentals is essential for building applications with the framework.",{"id":2863,"title":78,"titles":2864,"content":2861,"level":385},"/fundamentals#fundamentals-overview",[],{"id":2866,"title":2867,"titles":2868,"content":2869,"level":391},"/fundamentals#what-youll-learn","What You'll Learn",[78],"The Fundamentals section provides a deep understanding of how Nuxt Crouton works under the hood: Core Architecture: How Nuxt Crouton uses domain-driven design with Nuxt LayersCollections: The building blocks of your data modelForms & Modals: How CRUD operations work out of the boxData Operations: Create, read, update, and delete patternsQuerying Data: How to filter, sort, and paginate collectionsCaching: Performance optimization through intelligent cachingGenerated Code: Understanding what gets generated vs. what you customizePackages: Core dependencies and their roles",{"id":2871,"title":2872,"titles":2873,"content":528,"level":391},"/fundamentals#section-contents","Section Contents",[78],{"id":2875,"title":2876,"titles":2877,"content":2878,"level":449},"/fundamentals#_1-collections","1. Collections",[78,2872],"File: 1.collections.md Learn about collections - the fundamental building blocks of Nuxt Crouton. Collections represent your data models and define: Schema structure and field typesDatabase table mappingsGenerated components and composablesCRUD operations",{"id":2880,"title":2881,"titles":2882,"content":2883,"level":449},"/fundamentals#_2-architecture","2. Architecture",[78,2872],"File: 2.architecture.md Understand the two-layer architecture pattern that powers Nuxt Crouton: Generated Layer: Auto-generated code from your schemaCore Layer: Framework code you never touchDomain-driven design principlesHow layers interact and extend each other",{"id":2885,"title":2886,"titles":2887,"content":2888,"level":449},"/fundamentals#_3-forms-modals","3. Forms & Modals",[78,2872],"File: 3.forms-modals.md Explore the built-in form and modal system: Auto-generated forms from collection schemasModal patterns for create/edit operationsForm validation and error handlingCustomizing form behavior",{"id":2890,"title":2891,"titles":2892,"content":2893,"level":449},"/fundamentals#_4-data-operations","4. Data Operations",[78,2872],"File: 4.data-operations.md Master the core CRUD operations: Creating new recordsReading and fetching dataUpdating existing recordsDeleting recordsBatch operations",{"id":2895,"title":2896,"titles":2897,"content":2898,"level":449},"/fundamentals#_5-querying-data","5. Querying Data",[78,2872],"File: querying.md Learn how to query your collections effectively: Filtering by field valuesSorting resultsPagination patternsSearch functionalityComplex queries with Drizzle ORM",{"id":2900,"title":2901,"titles":2902,"content":2903,"level":449},"/fundamentals#_6-caching","6. Caching",[78,2872],"File: 6.caching.md Understand Nuxt Crouton's caching strategy: Client-side cache managementCache invalidation patternsPerformance optimization tipsWhen to use caching vs. real-time data",{"id":2905,"title":2906,"titles":2907,"content":2908,"level":449},"/fundamentals#_7-packages","7. Packages",[78,2872],"File: 7.packages.md Explore the core dependencies: Nuxt framework integrationNuxt UI componentsDrizzle ORM for database operationsAdditional utilities and helpers",{"id":2910,"title":2911,"titles":2912,"content":2913,"level":449},"/fundamentals#_8-generated-code","8. Generated Code",[78,2872],"File: generated-code.md Understand what gets auto-generated: Generated files structureHow to customize generated codeWhen to use overrides vs. custom componentsRegeneration workflow",{"id":2915,"title":2916,"titles":2917,"content":2918,"level":391},"/fundamentals#where-to-go-next","Where to Go Next",[78],"After mastering the fundamentals: Generation → Learn how to generate collections from schemasPatterns → Explore common patterns for forms, tables, and relationsCustomization → Customize components, fields, and layoutsFeatures → Discover advanced features like i18n, rich text, and maps",{"id":2920,"title":426,"titles":2921,"content":2922,"level":391},"/fundamentals#prerequisites",[78],"Before diving into the fundamentals, make sure you've: Completed the Getting Started guideInstalled Nuxt Crouton in your projectUnderstand basic Nuxt and Vue concepts",{"id":2924,"title":2925,"titles":2926,"content":2927,"level":391},"/fundamentals#external-resources","External Resources",[78],"For general Nuxt concepts not specific to Nuxt Crouton: Nuxt DocumentationNuxt Layers GuideVue Composition APIDrizzle ORM",{"id":110,"title":109,"titles":2929,"content":2930,"level":385},[],"Fetching and displaying data in Nuxt Crouton The useCollectionQuery composable is the primary way to fetch data in Nuxt Crouton. This guide covers the most common querying patterns. API Reference: For complete API details, see useCollectionQuery composable.",{"id":2932,"title":2933,"titles":2934,"content":2935,"level":391},"/fundamentals/querying#core-patterns","Core Patterns",[109],"These five patterns cover 95% of real-world use cases.",{"id":2937,"title":2938,"titles":2939,"content":2940,"level":449},"/fundamentals/querying#_1-basic-query","1. Basic Query",[109,2933],"Fetch all items from a collection with error and loading state handling. \u003Cscript setup lang=\"ts\">\n// Returns typed items array plus loading/error states\nconst { items, pending, error, refresh } = await useCollectionQuery('shopProducts')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"pending\">Loading...\u003C/div>\n  \u003Cdiv v-else-if=\"error\">Error: {{ error }}\u003C/div>\n  \u003Cdiv v-else>\n    \u003Cdiv v-for=\"product in items\" :key=\"product.id\">\n      {{ product.name }} - ${{ product.price }}\n    \u003C/div>\n  \u003C/div>\n\u003C/template> Return Values: items (typed array), pending (loading state), error (error object), refresh (manual refetch function), data (raw response). See Return Values below.",{"id":2942,"title":2943,"titles":2944,"content":2945,"level":449},"/fundamentals/querying#_2-with-filters-and-search","2. With Filters and Search",[109,2933],"Reactive filtering with search and category selection - the most common real-world pattern. \u003Cscript setup lang=\"ts\">\nconst searchQuery = ref('')\nconst category = ref\u003Cstring | null>(null)\n\n// Query automatically re-runs when reactive params change\nconst { items, pending } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({\n    search: searchQuery.value,\n    category: category.value\n  }))\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003CUInput v-model=\"searchQuery\" placeholder=\"Search products...\" />\n    \u003CUSelectMenu v-model=\"category\" :options=\"categories\" />\n    \u003CCroutonCollection :rows=\"items\" :loading=\"pending\" />\n  \u003C/div>\n\u003C/template> Auto-Refetch: Queries wrapped in computed() automatically re-fetch when reactive values change. No manual watch() needed! Learn more about Vue reactivity fundamentals.",{"id":2947,"title":2948,"titles":2949,"content":2950,"level":449},"/fundamentals/querying#_3-with-pagination","3. With Pagination",[109,2933],"Page-based queries for handling large datasets efficiently. \u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst limit = ref(10)\n\nconst { items, pending } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({\n    page: page.value,\n    limit: limit.value\n  }))\n})\n\nconst nextPage = () => page.value++\nconst prevPage = () => page.value--\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonCollection :rows=\"items\" :loading=\"pending\" />\n\n    \u003Cdiv class=\"flex gap-2\">\n      \u003CUButton @click=\"prevPage\" :disabled=\"page === 1\">\n        Previous\n      \u003C/UButton>\n      \u003Cspan>Page {{ page }}\u003C/span>\n      \u003CUButton @click=\"nextPage\">\n        Next\n      \u003C/UButton>\n    \u003C/div>\n  \u003C/div>\n\u003C/template> API Setup Required: Generated API endpoints use client-side pagination by default (all data fetched, paginated in browser). For server-side pagination with large datasets, you need to modify the generated files. See the complete Pagination Guide for step-by-step instructions.",{"id":2952,"title":2953,"titles":2954,"content":2955,"level":449},"/fundamentals/querying#_4-with-sorting","4. With Sorting",[109,2933],"Sort controls with ascending/descending order. \u003Cscript setup lang=\"ts\">\nconst sortBy = ref('name')\nconst sortOrder = ref\u003C'asc' | 'desc'>('asc')\n\nconst { items } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({\n    sortBy: sortBy.value,\n    sortOrder: sortOrder.value\n  }))\n})\n\nconst toggleSort = () => {\n  sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"flex gap-2 items-center\">\n    \u003CUSelectMenu\n      v-model=\"sortBy\"\n      :options=\"['name', 'price', 'createdAt']\"\n      placeholder=\"Sort by...\"\n    />\n    \u003CUButton @click=\"toggleSort\" icon=\"i-lucide-arrow-up-down\">\n      {{ sortOrder === 'asc' ? '↑ Ascending' : '↓ Descending' }}\n    \u003C/UButton>\n  \u003C/div>\n\n  \u003CCroutonCollection :rows=\"items\" />\n\u003C/template>",{"id":2957,"title":2958,"titles":2959,"content":2960,"level":449},"/fundamentals/querying#_5-with-relations-fetch-separately","5. With Relations (Fetch Separately)",[109,2933],"Display related data by fetching collections separately and mapping them client-side. \u003Cscript setup lang=\"ts\">\n// Fetch both collections\nconst { items: products } = await useCollectionQuery('shopProducts')\nconst { items: categories } = await useCollectionQuery('shopCategories')\n\n// Create lookup map for quick access\nconst categoryMap = computed(() =>\n  Object.fromEntries(categories.value.map(c => [c.id, c]))\n)\n\n// Define columns with relation lookup\nconst columns = [\n  { key: 'name', label: 'Product' },\n  { key: 'price', label: 'Price' },\n  {\n    key: 'category',\n    label: 'Category',\n    // Look up category name from related collection\n    render: (row) => categoryMap.value[row.categoryId]?.name || 'N/A'\n  }\n]\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonTable :rows=\"products\" :columns=\"columns\" />\n\u003C/template> Relations Pattern: Nuxt Crouton fetches collections separately (not joins). This approach is simpler, more cacheable, and works well with edge caching. For complex joins, use Drizzle queries.",{"id":2962,"title":2963,"titles":2964,"content":2965,"level":391},"/fundamentals/querying#additional-patterns","Additional Patterns",[109],"These patterns handle specific use cases beyond the core five.",{"id":2967,"title":2968,"titles":2969,"content":2970,"level":449},"/fundamentals/querying#client-side-filtering","Client-Side Filtering",[109,2963],"For simple filtering without server round-trips (useful for small datasets): \u003Cscript setup lang=\"ts\">\nconst { items, pending } = await useCollectionQuery('shopProducts')\n\nconst searchQuery = ref('')\nconst filteredProducts = computed(() =>\n  items.value.filter(p =>\n    p.name.toLowerCase().includes(searchQuery.value.toLowerCase())\n  )\n)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUInput v-model=\"searchQuery\" placeholder=\"Search...\" />\n  \u003CCroutonCollection :rows=\"filteredProducts\" :loading=\"pending\" />\n\u003C/template> Computed Properties: Learn more about Vue's computed() for derived state in the Vue documentation.",{"id":2972,"title":2973,"titles":2974,"content":2975,"level":449},"/fundamentals/querying#loading-states","Loading States",[109,2963],"Comprehensive loading, error, and empty state handling: \u003Cscript setup lang=\"ts\">\nconst { items, pending, error } = await useCollectionQuery('shopProducts')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cdiv v-if=\"pending\" class=\"flex justify-center p-8\">\n      \u003CUSkeleton class=\"h-32 w-full\" />\n    \u003C/div>\n    \u003Cdiv v-else-if=\"error\" class=\"text-red-500\">\n      Failed to load: {{ error }}\n    \u003C/div>\n    \u003Cdiv v-else-if=\"items.length === 0\" class=\"text-center p-8\">\n      No products found\n    \u003C/div>\n    \u003CCroutonCollection v-else :rows=\"items\" />\n  \u003C/div>\n\u003C/template>",{"id":2977,"title":2978,"titles":2979,"content":2980,"level":449},"/fundamentals/querying#manual-refetch","Manual Refetch",[109,2963],"Trigger a manual data refresh: \u003Cscript setup lang=\"ts\">\nconst { items, refresh } = await useCollectionQuery('shopProducts')\n\nconst handleRefresh = async () => {\n  await refresh()\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUButton @click=\"handleRefresh\">Refresh Data\u003C/UButton>\n\u003C/template>",{"id":2982,"title":2983,"titles":2984,"content":2985,"level":449},"/fundamentals/querying#dependent-queries","Dependent Queries",[109,2963],"Fetch data based on another query's results: \u003Cscript setup lang=\"ts\">\nconst { items: categories } = await useCollectionQuery('shopCategories')\nconst selectedCategory = ref\u003Cstring | null>(null)\n\n// Auto-refetches when selectedCategory changes\nconst { items: subcategories } = await useCollectionQuery('shopSubcategories', {\n  query: computed(() => ({\n    categoryId: selectedCategory.value\n  }))\n})\n\u003C/script>",{"id":2987,"title":2988,"titles":2989,"content":2990,"level":449},"/fundamentals/querying#with-translations-i18n","With Translations (i18n)",[109,2963],"Integrate with @nuxtjs/i18n for locale-aware queries: \u003Cscript setup lang=\"ts\">\nconst { locale } = useI18n()\n\nconst { items } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({\n    locale: locale.value\n  }))\n})\n// Auto-refetches when locale changes!\n\u003C/script> i18n Integration: Learn more in the Internationalization feature guide.",{"id":2992,"title":2993,"titles":2994,"content":2995,"level":449},"/fundamentals/querying#disable-auto-watch","Disable Auto-Watch",[109,2963],"By default, queries auto-watch computed params. To disable: \u003Cscript setup lang=\"ts\">\nconst page = ref(1)\n\nconst { items, refresh } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({ page: page.value })),\n  watch: false  // Disable auto-watch\n})\n\n// Manually trigger refresh when needed\nwatch(page, () => refresh())\n\u003C/script> Vue Watchers: Learn more about Vue's watch() API in the Vue documentation.",{"id":2997,"title":2998,"titles":2999,"content":3000,"level":391},"/fundamentals/querying#return-values","Return Values",[109],"useCollectionQuery returns: {\n  items: ComputedRef\u003CT[]>    // Typed array of collection items\n  data: Ref\u003Cany>              // Raw response data\n  pending: Ref\u003Cboolean>       // Loading state\n  error: Ref\u003Cany>            // Error object if request failed\n  refresh: () => Promise\u003Cvoid> // Manual refresh function\n}",{"id":3002,"title":1562,"titles":3003,"content":3004,"level":391},"/fundamentals/querying#related-topics",[109],"Pagination Guide - Adding server-side pagination to generated collectionsData OperationsCachingWorking with Relations html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":122,"title":121,"titles":3006,"content":3007,"level":385},[],"Define your collection structure with schema files Schema files define the structure of your collections, specifying fields, types, validation rules, and metadata. A schema is a JSON object where keys are field names and values are field configurations: {\n  \"title\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"required\": true\n    }\n  },\n  \"description\": {\n    \"type\": \"text\"\n  },\n  \"price\": {\n    \"type\": \"number\",\n    \"meta\": {\n      \"default\": 0\n    }\n  },\n  \"inStock\": {\n    \"type\": \"boolean\",\n    \"meta\": {\n      \"default\": true\n    }\n  },\n  \"publishedAt\": {\n    \"type\": \"date\"\n  }\n}",{"id":3009,"title":3010,"titles":3011,"content":3012,"level":391},"/generation/schema-format#auto-generated-fields","Auto-Generated Fields",[121],"The generator automatically adds certain fields to every collection based on your configuration. You should NEVER define these fields in your schema JSON files - doing so will cause duplicate key errors.",{"id":3014,"title":3015,"titles":3016,"content":3017,"level":449},"/generation/schema-format#always-auto-generated","Always Auto-Generated",[121,3010],"These fields are always added to every collection, regardless of configuration: id - Primary key (UUID for PostgreSQL, nanoid for SQLite)",{"id":3019,"title":3020,"titles":3021,"content":3022,"level":449},"/generation/schema-format#team-authentication-fields","Team Authentication Fields",[121,3010],"All generated collections are team-scoped by default. The generator automatically adds: teamId - Team/organization reference for multi-tenancy (required, text field)owner - User who created the record (required, text field) These fields enable team-based authentication and automatically scope all data to the current team: All API endpoints use @fyit/crouton-auth/server for authenticationAll database queries are automatically scoped to the user's teamTeam membership is validated on every request See Team-Based Authentication for complete details.",{"id":3024,"title":3025,"titles":3026,"content":528,"level":449},"/generation/schema-format#metadata-fields","Metadata Fields",[121,3010],{"id":3028,"title":3029,"titles":3030,"content":3031,"level":748},"/generation/schema-format#when-usemetadata-true-default","When useMetadata: true (default)",[121,3010,3025],"The generator automatically adds: createdAt - Timestamp when record was created (auto-populated on insert)updatedAt - Timestamp when record was last modified (auto-updated on change)createdBy - User ID of who created the record (auto-populated on insert)updatedBy - User ID of who last modified the record (auto-updated on change) When useMetadata: false: These fields are NOT added, giving you full control over timestamp and audit tracking. Warning: Duplicate Field ErrorsIf you manually define any auto-generated fields in your schema JSON files, you will get duplicate key errors during build:[esbuild] (schema.ts:8:2) Duplicate key \"owner\" in object literal\n[esbuild] (schema.ts:9:2) Duplicate key \"teamId\" in object literal\nSolution: Remove these fields from your schema JSON files. The generator adds them automatically based on your configuration flags.Common mistake:{\n  \"owner\": {            // ❌ Don't define this!\n    \"type\": \"string\",\n    \"refTarget\": \"users\"\n  },\n  \"teamId\": {           // ❌ Don't define this!\n    \"type\": \"string\"\n  },\n  \"title\": {            // ✅ Only define your custom fields\n    \"type\": \"string\"\n  }\n}\nSee Troubleshooting for more solutions to common errors. Automatic Audit Trail: When useMetadata: true (default), the system automatically tracks:owner - Who created the record (set once on creation)createdBy / updatedBy - Who created/last modified the recordcreatedAt / updatedAt - When the record was created/modifiedThis provides a complete audit trail without any manual tracking. On create operations, owner, createdBy, and updatedBy are all set to the creating user. On update operations, updatedBy automatically changes to whoever made the modification.",{"id":3033,"title":3034,"titles":3035,"content":3036,"level":391},"/generation/schema-format#supported-field-types","Supported Field Types",[121],"Nuxt Crouton supports the following field types: string - Short text (single line input)text - Long content (textarea)number - Integersdecimal - Precise decimal values (e.g., money)boolean - True/false values (checkbox)date - Date/timestamp valuesjson - JSON objectsimage - Image asset reference (stores asset ID, renders CroutonAssetsPicker with crop)file - File asset reference (stores asset ID, renders CroutonAssetsPicker without crop)repeater - Arrays of structured data (see Repeater Fields)array - Arrays of strings (stored as JSON text)",{"id":3038,"title":3039,"titles":3040,"content":3041,"level":391},"/generation/schema-format#reference-fields","Reference Fields",[121],"Reference fields create relationships between collections using the refTarget property. The generator automatically creates UI components for selecting and displaying related items.",{"id":3043,"title":3044,"titles":3045,"content":3046,"level":449},"/generation/schema-format#basic-reference-field","Basic Reference Field",[121,3039],"{\n  \"authorId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"authors\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"Author\"\n    }\n  }\n} Note: Reference fields use type: \"string\" because they store the referenced item's ID (UUID or nanoid string).",{"id":3048,"title":3049,"titles":3050,"content":3051,"level":449},"/generation/schema-format#what-gets-generated","What Gets Generated",[121,3039],"When you define a reference field, the generator creates: Form component - A ReferenceSelect dropdown with:Searchable list of all items from the referenced collection\"+\" button to create new related itemsAuto-selection of newly created itemsList component - A CardMini cell showing:The referenced item's titleQuick-edit button on hover",{"id":3053,"title":3054,"titles":3055,"content":3056,"level":449},"/generation/schema-format#example-schema-with-references","Example Schema with References",[121,3039],"{\n  \"title\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"Article Title\"\n    }\n  },\n  \"authorId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"authors\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"Author\"\n    }\n  },\n  \"categoryId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"categories\",\n    \"meta\": {\n      \"label\": \"Category\"\n    }\n  }\n}",{"id":3058,"title":3059,"titles":3060,"content":3061,"level":449},"/generation/schema-format#referencing-external-collections","Referencing External Collections",[121,3039],"By default, refTarget values are prefixed with the current layer name. For example, in the shop layer, \"refTarget\": \"categories\" becomes shopCategories. To reference a collection outside your layer or an existing table (like a shared users table), prefix the collection name with a colon (:): {\n  \"authorId\": {\n    \"type\": \"string\",\n    \"refTarget\": \":users\",\n    \"meta\": {\n      \"label\": \"Author\"\n    }\n  }\n} This generates collection=\"users\" instead of collection=\"shopUsers\", allowing you to reference global collections or existing database tables. Use cases: Referencing a shared users table across multiple layersLinking to system-wide collections like teams or organizationsConnecting to external collections not managed by this layer",{"id":3063,"title":3064,"titles":3065,"content":3066,"level":449},"/generation/schema-format#user-experience","User Experience",[121,3039],"When working with reference fields: Creating items: Select from dropdown OR click \"+\" to create newViewing lists: See referenced item titles with quick-edit buttonsEditing items: Change relationships via searchable dropdown For more details, see Working with Relations.",{"id":3068,"title":3069,"titles":3070,"content":3071,"level":391},"/generation/schema-format#select-dropdown-fields","Select Dropdown Fields",[121],"Select dropdown fields provide a fixed set of options for the user to choose from. Use these when you have a predefined list of values like status, type, or category.",{"id":3073,"title":3074,"titles":3075,"content":3076,"level":449},"/generation/schema-format#static-options-inline","Static Options (Inline)",[121,3069],"For fields with a known set of options, use meta.options with displayAs: \"optionsSelect\": {\n  \"status\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"label\": \"Status\",\n      \"options\": [\"draft\", \"published\", \"archived\"],\n      \"displayAs\": \"optionsSelect\",\n      \"required\": true\n    }\n  },\n  \"triggerType\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"label\": \"Trigger Type\",\n      \"options\": [\"booking_created\", \"reminder_before\", \"booking_cancelled\"],\n      \"displayAs\": \"optionsSelect\"\n    }\n  }\n} The generator automatically: Creates a \u003CUSelect> component in the formFormats option labels from snake_case to human-readable (e.g., \"booking_created\" → \"Booking Created\")Generates the options array in the script section Label Formatting: Option values are automatically converted to labels:\"draft\" → \"Draft\"\"in_progress\" → \"In Progress\"\"follow_up_after\" → \"Follow Up After\"",{"id":3078,"title":3079,"titles":3080,"content":3081,"level":449},"/generation/schema-format#database-driven-options","Database-Driven Options",[121,3069],"For options stored in another collection (admin-configurable), use optionsCollection and optionsField: {\n  \"category\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"label\": \"Category\",\n      \"displayAs\": \"optionsSelect\",\n      \"optionsCollection\": \"settings\",\n      \"optionsField\": \"categories\",\n      \"creatable\": true\n    }\n  }\n} This generates a CroutonFormOptionsSelect component that: Fetches options dynamically from the databaseAllows creating new options on-the-fly (when creatable: true)Auto-refreshes when options change",{"id":3083,"title":3084,"titles":3085,"content":3086,"level":449},"/generation/schema-format#when-to-use-each","When to Use Each",[121,3069],"PatternUse WhenStatic Options (meta.options)Fixed values that rarely change (status, type)Database Options (optionsCollection)Admin-configurable values that can be added/editedReference Field (refTarget)Options are full records in another collection",{"id":3088,"title":3089,"titles":3090,"content":3091,"level":391},"/generation/schema-format#repeater-fields","Repeater Fields",[121],"Repeater fields allow you to store and manage arrays of structured data without creating separate database tables. They're perfect for time slots, contact information, price tiers, or any scenario where you need multiple items of the same type.",{"id":3093,"title":3094,"titles":3095,"content":3096,"level":449},"/generation/schema-format#basic-repeater-field","Basic Repeater Field",[121,3089],"{\n  \"slots\": {\n    \"type\": \"repeater\",\n    \"meta\": {\n      \"label\": \"Available Time Slots\",\n      \"repeaterComponent\": \"Slot\",\n      \"addLabel\": \"Add Time Slot\",\n      \"sortable\": true,\n      \"area\": \"main\"\n    }\n  }\n}",{"id":3098,"title":3099,"titles":3100,"content":3101,"level":449},"/generation/schema-format#repeater-meta-properties","Repeater Meta Properties",[121,3089],"The meta object for repeater fields supports these properties: repeaterComponent (required) - Name of the component that renders each itemaddLabel (optional) - Button text for adding items (default: \"Add Item\")sortable (optional) - Enable drag-to-reorder functionality (default: true)label (optional) - Form field labelarea (optional) - Form area placement",{"id":3103,"title":3049,"titles":3104,"content":3105,"level":449},"/generation/schema-format#what-gets-generated-1",[121,3089],"When you define a repeater field, the generator automatically creates: Database column - JSON/JSONB column to store the arrayForm component - CroutonRepeater with add/remove/reorder functionalityZod validation - z.array(z.any()).optional() for type safetyDefault value - Empty array []Placeholder item component - A working Vue component with TODO comments for customization NEW: Auto-generated Placeholder ComponentsThe generator now automatically creates a working placeholder component for each repeater field. This placeholder includes:Proper Vue component structure (props, emits, v-model)Default values and TypeScript interfacesTODO comments guiding you to customizeDebug section showing raw dataVisual styling indicating it's a placeholderYou can immediately test your repeater functionality, then customize the component with your specific fields.",{"id":3107,"title":3108,"titles":3109,"content":3110,"level":449},"/generation/schema-format#customizing-item-components","Customizing Item Components",[121,3089],"The auto-generated placeholder component is fully functional but basic. You'll want to customize it to match your data structure. Location convention: layers/[layer]/collections/[collection]/app/components/[ComponentName].vue Example: Slot.vue \u003Cscript setup lang=\"ts\">\nimport { nanoid } from 'nanoid'\n\ninterface TimeSlot {\n  id: string\n  label: string\n  startTime: string\n  endTime: string\n}\n\nconst props = defineProps\u003C{\n  modelValue: TimeSlot\n}>()\n\nconst emit = defineEmits\u003C{\n  'update:modelValue': [value: TimeSlot]\n}>()\n\n// Initialize with defaults if empty\nconst localValue = computed({\n  get: () => props.modelValue || {\n    id: nanoid(),\n    label: '',\n    startTime: '09:00',\n    endTime: '17:00'\n  },\n  set: (val) => emit('update:modelValue', val)\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"grid grid-cols-4 gap-4\">\n    \u003CUFormField label=\"ID\" name=\"id\">\n      \u003CUInput\n        v-model=\"localValue.id\"\n        disabled\n        class=\"bg-gray-50\"\n      />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Label\" name=\"label\">\n      \u003CUInput v-model=\"localValue.label\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Start Time\" name=\"startTime\">\n      \u003CUInput\n        v-model=\"localValue.startTime\"\n        type=\"time\"\n      />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"End Time\" name=\"endTime\">\n      \u003CUInput\n        v-model=\"localValue.endTime\"\n        type=\"time\"\n      />\n    \u003C/UFormField>\n  \u003C/div>\n\u003C/template>",{"id":3112,"title":3113,"titles":3114,"content":3115,"level":449},"/generation/schema-format#component-requirements","Component Requirements",[121,3089],"Your item component must: Accept a modelValue prop with the item dataEmit update:modelValue when data changesProvide default values in the computed getterUse two-way binding with v-model on form fields",{"id":3117,"title":3064,"titles":3118,"content":3119,"level":449},"/generation/schema-format#user-experience-1",[121,3089],"The repeater field provides: Add items - Click the button to create new items with auto-generated IDsRemove items - Click × to delete an item (no confirmation)Reorder items - Drag the handle (⋮⋮) to reorder (if sortable: true)Empty state - Shows helpful message when no items exist",{"id":3121,"title":3122,"titles":3123,"content":3124,"level":449},"/generation/schema-format#common-use-cases","Common Use Cases",[121,3089],"Time Slots (Bookings) {\n  \"slots\": {\n    \"type\": \"repeater\",\n    \"meta\": {\n      \"label\": \"Available Time Slots\",\n      \"repeaterComponent\": \"TimeSlot\",\n      \"addLabel\": \"Add Slot\"\n    }\n  }\n} Contact Persons {\n  \"contacts\": {\n    \"type\": \"repeater\",\n    \"meta\": {\n      \"label\": \"Contact Persons\",\n      \"repeaterComponent\": \"ContactPerson\",\n      \"addLabel\": \"Add Contact\"\n    }\n  }\n} Price Tiers {\n  \"priceTiers\": {\n    \"type\": \"repeater\",\n    \"meta\": {\n      \"label\": \"Pricing Tiers\",\n      \"repeaterComponent\": \"PriceTier\",\n      \"addLabel\": \"Add Tier\",\n      \"sortable\": false\n    }\n  }\n} Social Media Links {\n  \"socialLinks\": {\n    \"type\": \"repeater\",\n    \"meta\": {\n      \"label\": \"Social Media\",\n      \"repeaterComponent\": \"SocialLink\",\n      \"addLabel\": \"Add Link\"\n    }\n  }\n}",{"id":3126,"title":3127,"titles":3128,"content":3129,"level":449},"/generation/schema-format#data-storage","Data Storage",[121,3089],"Repeater data is stored as JSON in the database: {\n  \"slots\": [\n    {\n      \"id\": \"V1StGXR8_Z5jdHi6B-myT\",\n      \"label\": \"Morning\",\n      \"startTime\": \"09:00\",\n      \"endTime\": \"12:00\"\n    },\n    {\n      \"id\": \"k3Rl9mPw_Q7xYvNc4-abZ\",\n      \"label\": \"Afternoon\",\n      \"startTime\": \"13:00\",\n      \"endTime\": \"17:00\"\n    }\n  ]\n} Auto-generated IDs: Each item gets a unique ID using nanoid(), providing collision-resistant identifiers that remain stable through reordering and editing. These IDs are more reliable than timestamps for rapid user interactions. When to use repeater vs relations: Use repeater fields when items are tightly coupled to their parent and don't need to be queried independently. Use relation fields when items need their own table, complex relationships, or querying/filtering capabilities.",{"id":3131,"title":3132,"titles":3133,"content":3134,"level":391},"/generation/schema-format#dependent-fields","Dependent Fields",[121],"Dependent fields create dynamic form inputs that load data from a referenced collection and display it conditionally. This is perfect for scenarios like selecting time slots from a location, choosing sizes from a product, or picking rooms from a building.",{"id":3136,"title":3137,"titles":3138,"content":3139,"level":449},"/generation/schema-format#basic-dependent-field","Basic Dependent Field",[121,3132],"{\n  \"location\": {\n    \"type\": \"string\",\n    \"refTarget\": \"locations\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"Location\",\n      \"area\": \"main\"\n    }\n  },\n  \"slot\": {\n    \"type\": \"number\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"Time Slot\",\n      \"area\": \"main\",\n      \"dependsOn\": \"location\",\n      \"dependsOnField\": \"slots\",\n      \"dependsOnCollection\": \"locations\",\n      \"displayAs\": \"slotButtonGroup\"\n    }\n  }\n}",{"id":3141,"title":3142,"titles":3143,"content":3144,"level":449},"/generation/schema-format#dependent-field-meta-properties","Dependent Field Meta Properties",[121,3132],"The meta object for dependent fields supports these properties: dependsOn (required) - Name of the field to watch for changesdependsOnField (required) - Field name in the referenced record to loaddependsOnCollection (required) - Collection to fetch data fromdisplayAs (required) - Display mode for the field (currently supports: slotButtonGroup)",{"id":3146,"title":3049,"titles":3147,"content":3148,"level":449},"/generation/schema-format#what-gets-generated-2",[121,3132],"When you define a dependent field, the generator automatically creates: Data Fetching - useFetch with watch on the dependent fieldReactive Updates - Automatic refetch when dependency changesReset Logic - Clears field value when dependency changesConditional UI - Shows different states:\n\"Please select dependency first\" when no selectionDynamic options when data is loaded\"No items configured\" when empty",{"id":3150,"title":3151,"titles":3152,"content":528,"level":449},"/generation/schema-format#display-modes","Display Modes",[121,3132],{"id":3154,"title":3155,"titles":3156,"content":3157,"level":748},"/generation/schema-format#slot-button-group-displayas-slotbuttongroup","Slot Button Group (displayAs: \"slotButtonGroup\")",[121,3132,3151],"Displays options as a visual button group, perfect for time slots or preset choices: \u003CUButtonGroup class=\"w-full flex-wrap\">\n  \u003CUButton\n    v-for=\"option in locationData.slots\"\n    :key=\"option.id\"\n    :variant=\"state.slot === option.id ? 'solid' : 'outline'\"\n    @click=\"state.slot = option.id\"\n    class=\"flex-1 min-w-[200px]\"\n  >\n    \u003Cdiv class=\"flex flex-col items-center py-2\">\n      \u003Cspan class=\"font-medium\">{{ option.label }}\u003C/span>\n      \u003Cspan class=\"text-xs opacity-75\">{{ option.startTime }} - {{ option.endTime }}\u003C/span>\n    \u003C/div>\n  \u003C/UButton>\n\u003C/UButtonGroup>",{"id":3159,"title":3160,"titles":3161,"content":3162,"level":449},"/generation/schema-format#complete-example-booking-system","Complete Example: Booking System",[121,3132],"Location Schema (location-schema.json): {\n  \"title\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"Location Name\"\n    }\n  },\n  \"slots\": {\n    \"type\": \"repeater\",\n    \"meta\": {\n      \"label\": \"Available Time Slots\",\n      \"repeaterComponent\": \"Slot\",\n      \"addLabel\": \"Add Time Slot\"\n    }\n  }\n} Booking Schema (booking-schema.json): {\n  \"location\": {\n    \"type\": \"string\",\n    \"refTarget\": \"locations\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"Location\"\n    }\n  },\n  \"date\": {\n    \"type\": \"date\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"Booking Date\"\n    }\n  },\n  \"slot\": {\n    \"type\": \"number\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"Time Slot\",\n      \"dependsOn\": \"location\",\n      \"dependsOnField\": \"slots\",\n      \"dependsOnCollection\": \"locations\",\n      \"displayAs\": \"slotButtonGroup\"\n    }\n  }\n} Generated Form Behavior: User selects a location from dropdownForm automatically fetches location dataAvailable time slots appear as visual buttonsUser clicks desired slotIf location changes, slot resets to empty",{"id":3164,"title":3165,"titles":3166,"content":3167,"level":449},"/generation/schema-format#generated-code-example","Generated Code Example",[121,3132],"The generator creates this form code automatically: \u003Cscript setup lang=\"ts\">\n// ... existing setup code ...\n\nconst state = ref\u003CBookingFormData>(initialValues)\n\n// Auto-generated dependent field logic\nconst route = useRoute()\nconst teamId = computed(() => route.params.teamId)\n\n// Fetch location data when location is selected\nconst { data: locationData } = await useFetch(() =>\n  state.value.location\n    ? `/api/teams/${teamId}/bookingsLocations/${state.value.location}`\n    : null\n, {\n  watch: [() => state.value.location],\n  immediate: false\n})\n\n// Reset slot when location changes\nwatch(() => state.value.location, () => {\n  state.value.slot = 0\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUFormField label=\"Time Slot\" name=\"slot\">\n    \u003Cdiv v-if=\"!state.location\" class=\"text-gray-400 text-sm\">\n      Please select location first\n    \u003C/div>\n    \u003CUButtonGroup v-else-if=\"locationData && locationData.slots && locationData.slots.length > 0\" class=\"w-full flex-wrap\">\n      \u003CUButton\n        v-for=\"option in locationData.slots\"\n        :key=\"option.id\"\n        :variant=\"state.slot === option.id ? 'solid' : 'outline'\"\n        @click=\"state.slot = option.id\"\n        class=\"flex-1 min-w-[200px]\"\n      >\n        \u003Cdiv class=\"flex flex-col items-center py-2\">\n          \u003Cspan class=\"font-medium\">{{ option.label }}\u003C/span>\n          \u003Cspan class=\"text-xs opacity-75\">{{ option.startTime }} - {{ option.endTime }}\u003C/span>\n        \u003C/div>\n      \u003C/UButton>\n    \u003C/UButtonGroup>\n    \u003Cdiv v-else class=\"text-gray-400 text-sm\">\n      No slots configured for this location\n    \u003C/div>\n  \u003C/UFormField>\n\u003C/template>",{"id":3169,"title":3170,"titles":3171,"content":3172,"level":449},"/generation/schema-format#use-cases","Use Cases",[121,3132],"Time Slot Selection {\n  \"slot\": {\n    \"type\": \"number\",\n    \"meta\": {\n      \"dependsOn\": \"location\",\n      \"dependsOnField\": \"slots\",\n      \"dependsOnCollection\": \"locations\",\n      \"displayAs\": \"slotButtonGroup\"\n    }\n  }\n} Product Sizes {\n  \"size\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"dependsOn\": \"product\",\n      \"dependsOnField\": \"availableSizes\",\n      \"dependsOnCollection\": \"products\",\n      \"displayAs\": \"slotButtonGroup\"\n    }\n  }\n} Building Rooms {\n  \"room\": {\n    \"type\": \"number\",\n    \"meta\": {\n      \"dependsOn\": \"building\",\n      \"dependsOnField\": \"rooms\",\n      \"dependsOnCollection\": \"buildings\",\n      \"displayAs\": \"slotButtonGroup\"\n    }\n  }\n} Regeneration-Safe: Unlike manual form customizations, dependent field logic is defined in your schema and regenerates automatically. Your dynamic form behavior persists through schema changes and regeneration. Data Structure: The referenced field (dependsOnField) should contain an array of objects with at least an id property. For slotButtonGroup display, objects should also have label, startTime, and endTime properties. When to use dependent fields vs conditional fields: Use dependent fields when you need to fetch and display data from a related collection. Use conditional fields (manual v-if) when you only need to show/hide fields based on local state without data fetching.",{"id":3174,"title":3175,"titles":3176,"content":3177,"level":391},"/generation/schema-format#field-metadata","Field Metadata",[121],"Fields can include a meta object with additional configuration: {\n  \"title\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"Article Title\",\n      \"maxLength\": 200,\n      \"component\": \"CustomInput\",\n      \"area\": \"main\"\n    }\n  }\n}",{"id":3179,"title":3180,"titles":3181,"content":3182,"level":449},"/generation/schema-format#metadata-properties","Metadata Properties",[121,3175],"You can add metadata properties like required for validation, label for human-readable form labels, maxLength for string length limits, component to specify a custom input component, readOnly to display reference fields as non-editable cards (useful for audit fields), area to logically group fields for form layout, group to create tabbed groups when multiple main groups exist, nullable to generate .nullish() instead of .optional() in the Zod schema, and translatable to mark fields for i18n support.",{"id":3184,"title":3185,"titles":3186,"content":3187,"level":449},"/generation/schema-format#translatable-fields","Translatable Fields",[121,3175],"Mark fields that should support multiple languages with translatable: true: {\n  \"title\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"required\": true,\n      \"translatable\": true\n    }\n  },\n  \"description\": {\n    \"type\": \"text\",\n    \"meta\": {\n      \"translatable\": true\n    }\n  },\n  \"sku\": {\n    \"type\": \"string\"\n  }\n} When a field has translatable: true: The generated form includes a CroutonI18nInput component for multi-language editingTranslations are stored in a translations JSON field on the entityThe @fyit/crouton-i18n package is required Field-level translatable is the recommended approach. You can also configure translatable fields at the config level using translations.collections in your crouton.config.js, but field-level is more explicit and self-documenting.",{"id":3189,"title":3190,"titles":3191,"content":3192,"level":391},"/generation/schema-format#form-areas","Form Areas",[121],"The area property lets you organize fields into logical sections. You might use main for primary content, sidebar for secondary metadata like status and categories, meta for SEO and publishing info, or advanced for advanced options. Currently all fields render in a single list regardless of area, but this property sets up the infrastructure for future layout features where fields can be organized into columns, tabs, or sections. {\n  \"title\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"area\": \"main\"  // Primary content area\n    }\n  },\n  \"status\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"area\": \"sidebar\"  // Sidebar metadata\n    }\n  },\n  \"publishedAt\": {\n    \"type\": \"date\",\n    \"meta\": {\n      \"area\": \"meta\"  // SEO/publishing info\n    }\n  }\n}",{"id":3194,"title":3195,"titles":3196,"content":3197,"level":391},"/generation/schema-format#complete-example","Complete Example",[121],"Here's a complete product schema with various field types and metadata: {\n  \"name\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"Product Name\",\n      \"maxLength\": 200,\n      \"area\": \"main\"\n    }\n  },\n  \"description\": {\n    \"type\": \"text\",\n    \"meta\": {\n      \"label\": \"Description\",\n      \"area\": \"main\"\n    }\n  },\n  \"price\": {\n    \"type\": \"decimal\",\n    \"meta\": {\n      \"required\": true,\n      \"default\": 0,\n      \"label\": \"Price\",\n      \"area\": \"sidebar\"\n    }\n  },\n  \"priceTiers\": {\n    \"type\": \"repeater\",\n    \"meta\": {\n      \"label\": \"Volume Pricing Tiers\",\n      \"repeaterComponent\": \"PriceTier\",\n      \"addLabel\": \"Add Tier\",\n      \"area\": \"main\"\n    }\n  },\n  \"inStock\": {\n    \"type\": \"boolean\",\n    \"meta\": {\n      \"default\": true,\n      \"label\": \"In Stock\",\n      \"area\": \"sidebar\"\n    }\n  },\n  \"sku\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"SKU\",\n      \"maxLength\": 50,\n      \"area\": \"meta\"\n    }\n  },\n  \"publishedAt\": {\n    \"type\": \"date\",\n    \"meta\": {\n      \"label\": \"Publish Date\",\n      \"area\": \"meta\"\n    }\n  }\n}",{"id":3199,"title":44,"titles":3200,"content":3201,"level":391},"/generation/schema-format#best-practices",[121],"Use descriptive names like publishedAt instead of abbreviations like pub. Set sensible defaults for fields when appropriate. Mark fields as required when they're necessary for your data model. Choose the right types—use decimal for money, text for long descriptions, and string for short labels. Remember that properties like required, default, and translatable go inside the meta object. // Good practices\n{\n  \"publishedAt\": {\n    \"type\": \"date\",\n    \"meta\": { \"label\": \"Published At\" }\n  },\n  \"status\": {\n    \"type\": \"string\",\n    \"meta\": { \"default\": \"draft\" }\n  },\n  \"email\": {\n    \"type\": \"string\",\n    \"meta\": { \"required\": true }\n  },\n  \"price\": {\n    \"type\": \"decimal\",\n    \"meta\": { \"label\": \"Price\" }\n  },\n  \"description\": {\n    \"type\": \"text\",\n    \"meta\": { \"label\": \"Description\" }\n  }\n}",{"id":3203,"title":418,"titles":3204,"content":3205,"level":391},"/generation/schema-format#next-steps",[121],"Learn about Generator Commands to create collectionsExplore Multi-Collection Configuration for complex projectsSee Customizing Generated Code to modify the generated forms html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":126,"title":125,"titles":3207,"content":3208,"level":385},[],"Manage multiple collections with a configuration file For larger projects with multiple collections, use a configuration file to generate and manage them all at once.",{"id":3210,"title":3211,"titles":3212,"content":3213,"level":391},"/generation/multi-collection#choosing-a-config-format","Choosing a Config Format",[125],"There are two configuration formats available. Choose based on your needs: Use CaseFormatKeyAll collections share the same schemaSimple formatschemaPathEach collection has its own schemaEnhanced formatcollections[]Multiple layers with targetsEnhanced formatcollections[] + targets[] Important: When using schemaPath, it must point to a file, not a directory. If you have multiple schema files in a directory, use the enhanced format with collections[] instead.",{"id":3215,"title":3216,"titles":3217,"content":3218,"level":449},"/generation/multi-collection#simple-format-single-schema","Simple Format (Single Schema)",[125,3211],"For quick prototyping when all collections use the same field structure: // crouton.config.js\nexport default {\n  schemaPath: './my-schema.json',  // Must be a FILE path\n  dialect: 'sqlite',\n  targets: [\n    { layer: 'shop', collections: ['products'] }\n  ]\n}",{"id":3220,"title":3221,"titles":3222,"content":3223,"level":449},"/generation/multi-collection#enhanced-format-multiple-schemas","Enhanced Format (Multiple Schemas)",[125,3211],"For production projects where each collection has its own schema: // crouton.config.js\nexport default {\n  collections: [\n    { name: 'products', fieldsFile: './schemas/products.json' },\n    { name: 'categories', fieldsFile: './schemas/categories.json' },\n  ],\n  dialect: 'sqlite',\n  targets: [\n    { layer: 'shop', collections: ['products', 'categories'] }\n  ]\n} Recommended: Use the enhanced format for most projects. It's more flexible and makes it clear which schema belongs to which collection.",{"id":3225,"title":3226,"titles":3227,"content":3228,"level":391},"/generation/multi-collection#config-file-structure","Config File Structure",[125],"// crouton.config.js\nexport default {\n  // Define all collections\n  collections: [\n    { name: 'products', fieldsFile: './schemas/product.json' },\n    { name: 'categories', fieldsFile: './schemas/category.json', hierarchy: true },\n    { name: 'orders', fieldsFile: './schemas/order.json' },\n    { name: 'posts', fieldsFile: './schemas/post.json' },\n    { name: 'slides', fieldsFile: './schemas/slide.json', sortable: true },\n    { name: 'bookings', fieldsFile: './schemas/booking.json', collab: true },\n    { name: 'users', fieldsFile: './schemas/user.json' },\n    { name: 'roles', fieldsFile: './schemas/role.json' },\n  ],\n\n  // Organize into layers\n  targets: [\n    {\n      layer: 'shop',\n      collections: ['products', 'categories', 'orders']\n    },\n    {\n      layer: 'blog',\n      collections: ['posts', 'slides']\n    },\n    {\n      layer: 'admin',\n      collections: ['users', 'roles']\n    }\n  ],\n\n  // Database\n  dialect: 'sqlite',  // or 'pg'\n\n  // Translations (i18n) - define translatable fields per collection\n  translations: {\n    collections: {\n      products: ['name', 'description'],\n      posts: ['title', 'content']\n    }\n  },\n\n  // Flags\n  flags: {\n    force: false,           // Overwrite existing files?\n    noTranslations: false,  // Skip translations?\n    noDb: false,           // Skip database generation?\n    dryRun: false,         // Preview only?\n    autoRelations: true,   // Generate relation stubs?\n    useMetadata: true      // Add createdAt/updatedAt?\n  }\n}",{"id":3230,"title":3231,"titles":3232,"content":3233,"level":391},"/generation/multi-collection#generate-from-config","Generate from Config",[125],"# Generate all collections\nnpx crouton-generate config ./crouton.config.js\n\n# With flags\nnpx crouton-generate config ./crouton.config.js --force --dry-run",{"id":3235,"title":3236,"titles":3237,"content":528,"level":391},"/generation/multi-collection#configuration-options","Configuration Options",[125],{"id":3239,"title":3240,"titles":3241,"content":3242,"level":449},"/generation/multi-collection#collections-array","Collections Array",[125,3236],"Define all your collections in one place: collections: [\n  { name: 'products', fieldsFile: './schemas/product.json' },\n  { name: 'categories', fieldsFile: './schemas/category.json' },\n] Each collection object requires: name - Collection name (plural)fieldsFile - Path to schema JSON file Optional collection properties: hierarchy - Enable tree structure (adds parentId, path, depth, order fields)sortable - Enable drag-to-reorder (adds order field and reorder endpoint)collab - Enable presence indicators (shows who's editing items in realtime)",{"id":3244,"title":3245,"titles":3246,"content":3247,"level":449},"/generation/multi-collection#targets-array","Targets Array",[125,3236],"Organize collections into layers: targets: [\n  {\n    layer: 'shop',\n    collections: ['products', 'categories', 'orders']\n  },\n  {\n    layer: 'blog',\n    collections: ['posts', 'authors', 'comments']\n  }\n] This creates a clean domain-driven structure: layers/\n  ├── shop/\n  │   └── collections/\n  │       ├── products/\n  │       │   └── app/\n  │       │       ├── components/\n  │       │       └── composables/\n  │       ├── categories/\n  │       │   └── app/\n  │       │       ├── components/\n  │       │       └── composables/\n  │       └── orders/\n  │           └── app/\n  │               ├── components/\n  │               └── composables/\n  └── blog/\n      └── collections/\n          ├── posts/\n          │   └── app/\n          │       ├── components/\n          │       └── composables/\n          ├── authors/\n          │   └── app/\n          │       ├── components/\n          │       └── composables/\n          └── comments/\n              └── app/\n                  ├── components/\n                  └── composables/",{"id":3249,"title":3250,"titles":3251,"content":3252,"level":449},"/generation/multi-collection#database-dialect","Database Dialect",[125,3236],"Specify your database type: dialect: 'sqlite'  // or 'pg' This affects the generated migrations and database schema.",{"id":3254,"title":3255,"titles":3256,"content":3257,"level":449},"/generation/multi-collection#translations-configuration","Translations Configuration",[125,3236],"Define which fields should be translatable per collection: translations: {\n  collections: {\n    // Products: translate name and description\n    products: ['name', 'description'],\n\n    // Posts: translate title and content\n    posts: ['title', 'content', 'excerpt'],\n\n    // Pages: full content translation\n    pages: ['title', 'content', 'metaTitle', 'metaDescription']\n  }\n} When translations are configured: Listed fields get CroutonI18nInput in forms for per-language editingData is stored as: { translations: { en: {...}, nl: {...} } }The i18n layer is automatically added to nuxt.config.tsLocale files are created in layers/[layer]/i18n/locales/ Note: If you don't need translations, set flags.noTranslations: true to skip all i18n code generation.",{"id":3259,"title":3260,"titles":3261,"content":3262,"level":449},"/generation/multi-collection#flags","Flags",[125,3236],"Control the generation behavior: flags: {\n  force: false,           // Overwrite existing files?\n  noTranslations: false,  // Skip translations?\n  noDb: false,           // Skip database generation?\n  dryRun: false,         // Preview only?\n  autoRelations: true,   // Generate relation stubs?\n  useMetadata: true      // Add createdAt/updatedAt?\n}",{"id":3264,"title":3265,"titles":3266,"content":3267,"level":391},"/generation/multi-collection#project-templates","Project Templates",[125],"Create reusable templates for common project types:",{"id":3269,"title":3270,"titles":3271,"content":3272,"level":449},"/generation/multi-collection#saas-starter-template","SaaS Starter Template",[125,3265],"// templates/saas-starter.config.js\nexport default {\n  collections: [\n    { name: 'users', fieldsFile: './schemas/user.json' },\n    { name: 'teams', fieldsFile: './schemas/team.json' },\n    { name: 'subscriptions', fieldsFile: './schemas/subscription.json' },\n    { name: 'billing', fieldsFile: './schemas/billing.json' },\n  ],\n  targets: [\n    {\n      layer: 'admin',\n      collections: ['users', 'teams', 'subscriptions', 'billing']\n    }\n  ],\n  flags: {\n    useMetadata: true\n  }\n}",{"id":3274,"title":3275,"titles":3276,"content":3277,"level":449},"/generation/multi-collection#e-commerce-template","E-commerce Template",[125,3265],"// templates/ecommerce.config.js\nexport default {\n  collections: [\n    { name: 'products', fieldsFile: './schemas/product.json' },\n    { name: 'categories', fieldsFile: './schemas/category.json' },\n    { name: 'orders', fieldsFile: './schemas/order.json' },\n    { name: 'customers', fieldsFile: './schemas/customer.json' },\n    { name: 'inventory', fieldsFile: './schemas/inventory.json' },\n  ],\n  targets: [\n    {\n      layer: 'shop',\n      collections: ['products', 'categories', 'inventory']\n    },\n    {\n      layer: 'orders',\n      collections: ['orders', 'customers']\n    }\n  ],\n  dialect: 'pg',\n  flags: {\n    useMetadata: true,\n    autoRelations: true\n  }\n}",{"id":3279,"title":3280,"titles":3281,"content":3282,"level":449},"/generation/multi-collection#blog-platform-template","Blog Platform Template",[125,3265],"// templates/blog.config.js\nexport default {\n  collections: [\n    { name: 'posts', fieldsFile: './schemas/post.json' },\n    { name: 'authors', fieldsFile: './schemas/author.json' },\n    { name: 'categories', fieldsFile: './schemas/category.json' },\n    { name: 'tags', fieldsFile: './schemas/tag.json' },\n    { name: 'comments', fieldsFile: './schemas/comment.json' },\n  ],\n  targets: [\n    {\n      layer: 'blog',\n      collections: ['posts', 'authors', 'categories', 'tags', 'comments']\n    }\n  ],\n  dialect: 'sqlite',\n  flags: {\n    noTranslations: false,  // Enable translations for content\n    useMetadata: true\n  }\n}",{"id":3284,"title":3285,"titles":3286,"content":3287,"level":391},"/generation/multi-collection#using-templates","Using Templates",[125],"Copy a template to your project and generate: # Copy template\ncp templates/saas-starter.config.js ./crouton.config.js\n\n# Customize as needed\nnano crouton.config.js\n\n# Generate collections\nnpx crouton-generate config ./crouton.config.js",{"id":3289,"title":44,"titles":3290,"content":528,"level":391},"/generation/multi-collection#best-practices",[125],{"id":3292,"title":3293,"titles":3294,"content":3295,"level":449},"/generation/multi-collection#organize-by-domain","Organize by Domain",[125,44],"Group related collections into layers: targets: [\n  { layer: 'shop', collections: ['products', 'categories'] },\n  { layer: 'blog', collections: ['posts', 'authors'] },\n  { layer: 'admin', collections: ['users', 'roles'] }\n]",{"id":3297,"title":3298,"titles":3299,"content":3300,"level":449},"/generation/multi-collection#use-dry-run-first","Use Dry Run First",[125,44],"Preview changes before generating: npx crouton-generate config ./crouton.config.js --dry-run",{"id":3302,"title":3303,"titles":3304,"content":3305,"level":449},"/generation/multi-collection#version-control-your-config","Version Control Your Config",[125,44],"Commit your config file and schemas: git add crouton.config.js schemas/\ngit commit -m \"Add collection configuration\"",{"id":3307,"title":3308,"titles":3309,"content":3310,"level":449},"/generation/multi-collection#keep-schemas-separate","Keep Schemas Separate",[125,44],"Store schemas in a dedicated directory: project/\n  ├── crouton.config.js\n  └── schemas/\n      ├── product.json\n      ├── category.json\n      └── order.json",{"id":3312,"title":3313,"titles":3314,"content":3315,"level":391},"/generation/multi-collection#regenerating-collections","Regenerating Collections",[125],"When you need to update generated code: # Regenerate with force flag\nnpx crouton-generate config ./crouton.config.js --force Using --force will overwrite existing files. Make sure to commit your changes first or back up any customizations.",{"id":3317,"title":418,"titles":3318,"content":3319,"level":391},"/generation/multi-collection#next-steps",[125],"Learn about Generator Commands for single collection generationExplore Schema Format to define your collectionsSee Working with Collections to understand the generated code html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"id":130,"title":129,"titles":3321,"content":3322,"level":385},[],"Complete reference for all crouton-generate CLI commands and options The @fyit/crouton-cli package provides a powerful CLI tool for generating complete CRUD collections. This reference covers all available commands, options, and workflows.",{"id":3324,"title":13,"titles":3325,"content":528,"level":391},"/generation/cli-reference#installation",[129],{"id":3327,"title":3328,"titles":3329,"content":3330,"level":449},"/generation/cli-reference#global-installation-recommended","Global Installation (Recommended)",[129,13],"npm install -g @fyit/crouton-cli After global installation, both crouton and crouton-generate commands are available everywhere: crouton --help\n# or\ncrouton-generate --help Tip: The crouton command is an alias for crouton-generate. Both work identically.",{"id":3332,"title":3333,"titles":3334,"content":3335,"level":449},"/generation/cli-reference#project-level-with-npx","Project-Level with npx",[129,13],"# Use directly without installation\nnpx @fyit/crouton-cli \u003Ccommand>\n\n# Or add to project devDependencies\npnpm add -D @fyit/crouton-cli\nnpx crouton-generate \u003Ccommand>",{"id":3337,"title":3338,"titles":3339,"content":3340,"level":391},"/generation/cli-reference#command-overview","Command Overview",[129],"The CLI provides the following commands: CommandPurposeUsagegenerateGenerate collectionsSingle collection or from config fileinitScaffold a full appEnd-to-end: scaffold, generate, doctor, summaryconfigUse config fileAlternative syntax for config-based generationaddAdd modules/featuresAdd Crouton modules like auth, editor, eventsrollbackRemove collectionsSingle, bulk, or interactive removaldoctorValidate appChecks deps, wrangler, stubs, schema wiringscaffold-appCreate app skeletonGenerates boilerplate files for a new appseed-translationsSeed i18n dataSeed translations from JSON locale files to DBdb-pullPull remote DBPull remote D1 database into local devdeploy-setupSetup deploymentInteractive Cloudflare Pages deployment setupdeploy-checkCheck deploy readinessValidate wrangler config, CI workflow, bindings",{"id":3342,"title":3343,"titles":3344,"content":528,"level":391},"/generation/cli-reference#commands","Commands",[129],{"id":3346,"title":3347,"titles":3348,"content":3349,"level":449},"/generation/cli-reference#generate","generate",[129,3343],"Generate a new CRUD collection with all necessary files.",{"id":3351,"title":3352,"titles":3353,"content":3354,"level":748},"/generation/cli-reference#basic-syntax","Basic Syntax",[129,3343,3347],"crouton-generate \u003Clayer> \u003Ccollection> [options]",{"id":3356,"title":3357,"titles":3358,"content":3359,"level":748},"/generation/cli-reference#arguments","Arguments",[129,3343,3347],"\u003Clayer> - Target layer name (e.g., shop, admin, blog)\u003Ccollection> - Collection name in plural form (e.g., products, users, posts) Naming convention: Collection names are lowercased for folder paths. For example, productOptions becomes productoptions/ in the file system. Use kebab-case (e.g., product-options) for multi-word collection names.",{"id":3361,"title":3362,"titles":3363,"content":3364,"level":748},"/generation/cli-reference#options","Options",[129,3343,3347],"OptionTypeDefaultDescription-f, --fields-file \u003Cpath>string-Path to JSON schema file (required)-d, --dialect \u003Ctype>pg|sqlitesqliteDatabase dialect--auto-relationsbooleanfalseAdd relation stubs in comments--dry-runbooleanfalsePreview without creating files--no-translationsbooleanfalseSkip translation field generation--forcebooleanfalseOverwrite existing files--no-dbbooleanfalseSkip database table creation--hierarchybooleanfalseEnable hierarchy support (parentId, path, depth, order)--seedbooleanfalseGenerate seed data file with drizzle-seed--count \u003Cnumber>number25Number of seed records to generate--no-auto-mergebooleanfalseSkip automatic merging of generated files with existing ones-c, --config \u003Cpath>string-Use config file instead",{"id":3366,"title":3367,"titles":3368,"content":3369,"level":748},"/generation/cli-reference#examples","Examples",[129,3343,3347],"Basic generation: crouton-generate shop products --fields-file=./schemas/product.json With options: crouton-generate admin users \\\n  --fields-file=./schemas/user.json \\\n  --dialect=pg \\\n  --force Preview changes (dry run): crouton-generate blog posts \\\n  --fields-file=./schemas/post.json \\\n  --dry-run Using config file: crouton-generate --config ./crouton.config.js With hierarchy support: crouton-generate content pages \\\n  --fields-file=./schemas/page.json \\\n  --hierarchy",{"id":3371,"title":3049,"titles":3372,"content":3373,"level":748},"/generation/cli-reference#what-gets-generated",[129,3343,3347],"Each generate command creates: layers/[layer]/collections/[collection]/\n├── app/\n│   ├── components/\n│   │   ├── _Form.vue                  # CRUD form with validation\n│   │   ├── List.vue                   # Data table/list\n│   │   └── RepeaterItem.vue           # For repeater fields (if needed)\n│   └── composables/\n│       └── use[Layer][Collection].ts  # Zod schema, columns, defaults (e.g., useShopProducts.ts)\n├── server/\n│   ├── api/teams/[id]/[collection]/\n│   │   ├── index.get.ts               # GET all/by IDs\n│   │   ├── index.post.ts              # CREATE\n│   │   ├── [id].patch.ts              # UPDATE\n│   │   └── [id].delete.ts             # DELETE\n│   └── database/\n│       ├── queries.ts                 # Query functions\n│       ├── schema.ts                  # Drizzle schema\n│       └── seed.ts                    # Seed data (with --seed flag)\n├── types.ts                            # TypeScript interfaces\n└── nuxt.config.ts                     # Layer configuration",{"id":3375,"title":3376,"titles":3377,"content":3378,"level":748},"/generation/cli-reference#hierarchy-support","Hierarchy Support",[129,3343,3347],"When using the --hierarchy flag (or hierarchy: true in config), additional files and fields are generated for tree/nested data structures: Additional database fields: parentId - Reference to parent record (nullable)path - Materialized path for efficient tree queriesdepth - Nesting level (0 for root items)order - Sort order within siblings Additional API endpoints: [id]/move.patch.ts - Move item to new parentreorder.patch.ts - Reorder sibling items Additional queries: getTreeData() - Fetch hierarchical dataupdatePosition() - Update item positionreorderSiblings() - Reorder items at same level Zod schema includes: parentId: z.string().nullable().optional() Default values include: parentId: null Tip: Hierarchy is ideal for nested categories, organizational structures, page trees, or any parent-child relationships.",{"id":3380,"title":3381,"titles":3382,"content":3383,"level":748},"/generation/cli-reference#seed-data-generation","Seed Data Generation",[129,3343,3347],"Generate realistic test data alongside your collections using drizzle-seed + Faker. CLI usage: # Generate with seed data (25 records by default)\ncrouton-generate shop products --fields-file=products.json --seed\n\n# Generate with custom record count\ncrouton-generate shop products --fields-file=products.json --seed --count=100 Config file usage: // crouton.config.js\nexport default {\n  collections: [\n    { name: 'products', fieldsFile: './schemas/products.json', seed: true },           // 25 records\n    { name: 'categories', fieldsFile: './schemas/categories.json', seed: { count: 50 } } // custom count\n  ],\n  seed: {\n    defaultCount: 25,          // default for all collections\n    defaultTeamId: 'seed-team' // team ID for seeded data\n  },\n  // ... other config\n} Running seeds: After generation, execute the seed file: # Run directly\nnpx tsx ./layers/shop/collections/products/server/database/seed.ts\n\n# Or import in your code\nimport { seedShopProducts } from './layers/shop/collections/products/server/database/seed'\n\nawait seedShopProducts({\n  count: 100,\n  teamId: 'my-team',\n  reset: true  // optionally clear existing data first\n}) Field-to-generator mapping: The seed generator auto-detects field names and generates appropriate data: Field PatternGenerated Dataemailf.email() - realistic email addressesname, fullNamef.fullName() - person namestitlef.loremIpsum({ sentencesCount: 1 })description, contentf.loremIpsum({ sentencesCount: 3 })price, amountf.number({ minValue: 1, maxValue: 1000 })Foreign keysPlaceholder values with dependency comments Tip: Seed data is great for development, demos, and testing. The generated seed file can be customized after generation.",{"id":3385,"title":3386,"titles":3387,"content":3388,"level":449},"/generation/cli-reference#init","init",[129,3343],"Scaffold a full Crouton app end-to-end: creates the app skeleton, generates collections from config, runs doctor validation, and prints a summary with next steps.",{"id":3390,"title":3391,"titles":3392,"content":3393,"level":748},"/generation/cli-reference#syntax","Syntax",[129,3343,3386],"crouton-generate init \u003Cname> [options]",{"id":3395,"title":3357,"titles":3396,"content":3397,"level":748},"/generation/cli-reference#arguments-1",[129,3343,3386],"\u003Cname> - App name (required)",{"id":3399,"title":3362,"titles":3400,"content":3401,"level":748},"/generation/cli-reference#options-1",[129,3343,3386],"OptionTypeDefaultDescription--features \u003Clist>string-Comma-separated feature names (e.g., bookings,pages,editor)--theme \u003Cname>string-Theme to wire into extends (e.g., ko)-d, --dialect \u003Ctype>stringsqliteDatabase dialect (sqlite or pg)--no-cfbooleanfalseSkip Cloudflare-specific config (wrangler.toml, CF stubs)--dry-runbooleanfalsePreview what will be generated without writing files",{"id":3403,"title":3404,"titles":3405,"content":3406,"level":748},"/generation/cli-reference#example","Example",[129,3343,3386],"# Create app with default settings\ncrouton-generate init my-app\n\n# With features and theme\ncrouton-generate init my-app --features bookings,pages,editor --theme ko\n\n# Preview without writing\ncrouton-generate init my-app --dry-run",{"id":3408,"title":3409,"titles":3410,"content":3411,"level":748},"/generation/cli-reference#what-gets-created","What Gets Created",[129,3343,3386],"The init command runs a full pipeline: scaffold-app -- Creates the app skeleton (nuxt.config, package.json, schemas/, etc.)generate -- Generates collections from crouton.config.js (if collections are defined)doctor -- Validates everything is wired correctlySummary -- Prints next steps (dev server, deploy) Tip: After running init, you can customize the generated crouton.config.js and schemas, then re-run crouton-generate config to regenerate collections.",{"id":3413,"title":3414,"titles":3415,"content":3416,"level":449},"/generation/cli-reference#add","add",[129,3343],"Add pre-built features to your existing Nuxt Crouton project.",{"id":3418,"title":3391,"titles":3419,"content":3420,"level":748},"/generation/cli-reference#syntax-1",[129,3343,3414],"crouton-generate add \u003Cfeature> [options]",{"id":3422,"title":3357,"titles":3423,"content":3424,"level":748},"/generation/cli-reference#arguments-2",[129,3343,3414],"\u003Cfeature> - Feature to add (available: auth, i18n, admin, bookings, editor, assets, events, flow, email, maps, ai, devtools)",{"id":3426,"title":3362,"titles":3427,"content":3428,"level":748},"/generation/cli-reference#options-2",[129,3343,3414],"OptionTypeDefaultDescription--dry-runbooleanfalsePreview what will be generated--forcebooleanfalseOverwrite existing files",{"id":3430,"title":3431,"titles":3432,"content":528,"level":748},"/generation/cli-reference#available-features","Available Features",[129,3343,3414],{"id":3434,"title":3435,"titles":3436,"content":3437,"level":3438},"/generation/cli-reference#events","events",[129,3343,3414,3431],"Add the crouton-events layer for audit trail tracking. This creates a complete event tracking system that automatically logs all collection mutations (create, update, delete). # Add events layer\ncrouton-generate add events\n\n# Preview what will be created\ncrouton-generate add events --dry-run\n\n# Overwrite existing files\ncrouton-generate add events --force",5,{"id":3440,"title":3049,"titles":3441,"content":3442,"level":748},"/generation/cli-reference#what-gets-generated-1",[129,3343,3414],"The add events command creates: layers/crouton-events/\n├── nuxt.config.ts                      # Layer configuration\n├── types.ts                            # TypeScript interfaces\n└── server/\n    ├── database/\n    │   ├── schema.ts                   # Drizzle schema for events table\n    │   └── queries.ts                  # Query functions (getAll, create, etc.)\n    └── api/teams/[id]/crouton-collection-events/\n        ├── index.get.ts                # GET all events\n        ├── index.post.ts               # CREATE event\n        ├── [eventId].patch.ts          # UPDATE event\n        └── [eventId].delete.ts         # DELETE event",{"id":3444,"title":3445,"titles":3446,"content":3447,"level":748},"/generation/cli-reference#auto-updated-files","Auto-Updated Files",[129,3343,3414],"The command also updates: nuxt.config.ts - Adds './layers/crouton-events' to extends arrayserver/database/schema/index.ts - Adds schema export",{"id":3449,"title":3450,"titles":3451,"content":3452,"level":748},"/generation/cli-reference#next-steps-after-adding-events","Next Steps After Adding Events",[129,3343,3414],"# 1. Run database migration\npnpm drizzle-kit generate\npnpm drizzle-kit migrate\n\n# 2. Install the events tracking package (for auto-tracking)\npnpm add @fyit/crouton-events\n\n# 3. Add to nuxt.config.ts extends\n# '@fyit/crouton-events' Note: The add events command creates the server-side storage layer. To enable automatic event tracking for all collection mutations, also install @fyit/crouton-events package. See Events Package for full documentation.",{"id":3454,"title":3455,"titles":3456,"content":3457,"level":449},"/generation/cli-reference#config","config",[129,3343],"Alternative syntax for generating collections using a configuration file.",{"id":3459,"title":3391,"titles":3460,"content":3461,"level":748},"/generation/cli-reference#syntax-2",[129,3343,3455],"crouton-generate config [configPath] [options]",{"id":3463,"title":3357,"titles":3464,"content":3465,"level":748},"/generation/cli-reference#arguments-3",[129,3343,3455],"[configPath] - Path to config file (auto-detected if not specified) Auto-detection: If no config path is provided, the CLI automatically searches for config files in this order:crouton.config.tscrouton.config.jscrouton.config.mjscrouton.config.cjs",{"id":3467,"title":3362,"titles":3468,"content":3469,"level":748},"/generation/cli-reference#options-3",[129,3343,3455],"OptionTypeDescription--only \u003Cname>stringGenerate only a specific collection from the config",{"id":3471,"title":3367,"titles":3472,"content":3473,"level":748},"/generation/cli-reference#examples-1",[129,3343,3455],"# Auto-detect config file (searches for crouton.config.js/.mjs/.cjs/.ts)\ncrouton-generate config\n\n# Use specific config file\ncrouton-generate config ./configs/production.config.mjs\n\n# Generate only a single collection from config\ncrouton-generate config --only products\n\n# Combine config path with --only flag\ncrouton-generate config ./crouton.config.js --only pages\n\n# Alternative: Use --config flag with generate command\ncrouton-generate --config ./crouton.config.js",{"id":3475,"title":3476,"titles":3477,"content":3478,"level":748},"/generation/cli-reference#config-file-format","Config File Format",[129,3343,3455],"See Configuration File section below for complete details.",{"id":3480,"title":3481,"titles":3482,"content":3483,"level":449},"/generation/cli-reference#rollback","rollback",[129,3343],"Remove generated collections with various strategies.",{"id":3485,"title":1837,"titles":3486,"content":3487,"level":748},"/generation/cli-reference#single-collection-rollback",[129,3343,3481],"Remove a specific collection: crouton-generate rollback \u003Clayer> \u003Ccollection> [options] Arguments: \u003Clayer> - Layer name containing the collection\u003Ccollection> - Collection name to remove Options: OptionDescription--dry-runPreview what will be removed--keep-filesOnly clean configs, keep generated files--forceForce removal without warnings Example: # Preview removal\ncrouton-generate rollback shop products --dry-run\n\n# Remove with confirmation\ncrouton-generate rollback shop products\n\n# Force remove without prompts\ncrouton-generate rollback shop products --force",{"id":3489,"title":1857,"titles":3490,"content":3491,"level":748},"/generation/cli-reference#bulk-rollback",[129,3343,3481],"Remove entire layers or multiple collections: crouton-generate rollback-bulk [options] Options: OptionDescription--layer \u003Cname>Remove entire layer with all collections--config \u003Cpath>Remove collections defined in config file--dry-runPreview what will be removed--keep-filesOnly clean configs, keep files--forceForce removal without warnings Examples: # Remove entire layer\ncrouton-generate rollback-bulk --layer=shop\n\n# Preview layer removal\ncrouton-generate rollback-bulk --layer=shop --dry-run\n\n# Remove collections from config file\ncrouton-generate rollback-bulk --config=./crouton.config.js\n\n# Force bulk removal\ncrouton-generate rollback-bulk --layer=shop --force",{"id":3493,"title":1877,"titles":3494,"content":3495,"level":748},"/generation/cli-reference#interactive-rollback",[129,3343,3481],"Use an interactive UI to select what to remove: crouton-generate rollback-interactive [options] Options: OptionDescription--dry-runPreview what will be removed--keep-filesOnly clean configs, keep files Interactive Flow: $ crouton-generate rollback-interactive\n\n═══════════════════════════════════════════════════════════\n  INTERACTIVE ROLLBACK\n═══════════════════════════════════════════════════════════\n\nFound 3 layers:\n\n  • shop (5 collections)\n  • blog (3 collections)\n  • admin (2 collections)\n\n? What would you like to rollback?\n  ❯ Entire layer (all collections)\n    Specific collections\n    Cancel\n\n? Select layer:\n  ❯ shop (5 collections)\n    blog (3 collections)\n    admin (2 collections)\n\n? Are you sure you want to remove layer \"shop\"? (Y/n) Warning: Rollback operations are destructive. Always use --dry-run first to preview changes. The rollback command removes:Generated files in layers/[layer]/collections/[collection]/Schema exports from server/database/schema/index.tsCollection registrations from app.config.tsLayer references from root nuxt.config.ts (for bulk operations)",{"id":3497,"title":3498,"titles":3499,"content":3500,"level":449},"/generation/cli-reference#install","install",[129,3343],"Check and install required Nuxt modules (mainly for development). crouton-generate install This command checks for and installs: @fyit/crouton (core package)Required Drizzle ORM packagesOther dependencies",{"id":3502,"title":3503,"titles":3504,"content":3505,"level":391},"/generation/cli-reference#configuration-file","Configuration File",[129],"The configuration file provides a declarative way to define all generation settings.",{"id":3507,"title":3508,"titles":3509,"content":3510,"level":449},"/generation/cli-reference#basic-structure","Basic Structure",[129,3503],"// crouton.config.js\nexport default {\n  // Path to JSON schema files directory\n  schemaPath: './schemas/',\n\n  // Database dialect\n  dialect: 'sqlite',  // or 'pg'\n\n  // Target layers and collections\n  targets: [\n    {\n      layer: 'shop',\n      collections: [\n        // Simple string for basic collections\n        'products',\n        // Or object with per-collection options\n        {\n          name: 'orders',\n          fieldsFile: './schemas/order.json',\n          seed: true,\n          hierarchy: false\n        }\n      ]\n    }\n  ],\n\n  // Seed configuration (optional)\n  seed: {\n    defaultCount: 25,\n    defaultTeamId: 'seed-team'\n  },\n\n  // Generation flags\n  flags: {}\n}",{"id":3512,"title":3236,"titles":3513,"content":528,"level":449},"/generation/cli-reference#configuration-options",[129,3503],{"id":3515,"title":3516,"titles":3517,"content":3518,"level":748},"/generation/cli-reference#schemapath","schemaPath",[129,3503,3236],"Type: string Path to your JSON schema file(s). Can be: Single file: './product-schema.json'Directory: './schemas/' (uses convention [collection]-schema.json) export default {\n  schemaPath: './schemas/',  // Looks for products-schema.json, etc.\n  targets: [\n    {\n      layer: 'shop',\n      collections: ['products', 'categories']  // Uses products-schema.json, categories-schema.json\n    }\n  ]\n}",{"id":3520,"title":3521,"titles":3522,"content":3523,"level":748},"/generation/cli-reference#dialect","dialect",[129,3503,3236],"Type: 'pg' | 'sqlite'Default: 'sqlite' Database dialect for generated schemas and queries. export default {\n  dialect: 'pg',  // PostgreSQL\n  // Or\n  dialect: 'sqlite'  // SQLite (default)\n}",{"id":3525,"title":3526,"titles":3527,"content":3528,"level":748},"/generation/cli-reference#targets","targets",[129,3503,3236],"Type: Array\u003C{ layer: string, collections: (string | CollectionConfig)[] }> Defines which collections to generate in which layers. Collections can be: String: Simple collection name (uses schema from schemaPath/[name]-schema.json)Object: Collection with per-collection options export default {\n  targets: [\n    {\n      layer: 'shop',\n      collections: [\n        'products',           // Simple: uses schemas/products-schema.json\n        'categories',         // Simple: uses schemas/categories-schema.json\n        {                     // With options\n          name: 'orders',\n          fieldsFile: './schemas/order.json',\n          seed: { count: 50 },\n          hierarchy: true\n        }\n      ]\n    },\n    {\n      layer: 'blog',\n      collections: [\n        { name: 'posts', seed: true },\n        { name: 'authors', seed: { count: 10 } }\n      ]\n    }\n  ]\n} Per-collection options: OptionTypeDescriptionnamestringCollection name (required)fieldsFilestringPath to schema file (overrides schemaPath)seedboolean | { count: number }Generate seed datahierarchybooleanEnable tree/hierarchy support",{"id":3530,"title":3531,"titles":3532,"content":3533,"level":748},"/generation/cli-reference#flags","flags",[129,3503,3236],"Type: object Generation behavior flags. Team-Scoped by Default: All generated collections are team-scoped. The generator automatically adds teamId and owner fields and uses @fyit/crouton-auth/server for authentication. Do NOT define these fields in your schemas.",{"id":3535,"title":3536,"titles":3537,"content":3538,"level":3438},"/generation/cli-reference#usemetadata","useMetadata",[129,3503,3236,3531],"Type: booleanDefault: true Automatically add timestamp and audit fields: createdAt - Timestamp when record was createdupdatedAt - Timestamp when record was last modifiedcreatedBy - User ID who created the recordupdatedBy - User ID who last modified the record export default {\n  flags: {\n    useMetadata: true  // Add timestamps (default)\n  }\n} When useMetadata: true, do NOT define createdAt, updatedAt, or updatedBy in your schemas.",{"id":3540,"title":3541,"titles":3542,"content":3543,"level":3438},"/generation/cli-reference#autorelations","autoRelations",[129,3503,3236,3531],"Type: booleanDefault: false Add relation helper comments to generated schemas. export default {\n  flags: {\n    autoRelations: true\n  }\n}",{"id":3545,"title":3546,"titles":3547,"content":3548,"level":3438},"/generation/cli-reference#notranslations","noTranslations",[129,3503,3236,3531],"Type: booleanDefault: false Skip translation field generation. export default {\n  flags: {\n    noTranslations: true  // Don't generate translation fields\n  }\n}",{"id":3550,"title":3551,"titles":3552,"content":3553,"level":3438},"/generation/cli-reference#force","force",[129,3503,3236,3531],"Type: booleanDefault: false Overwrite existing files without prompting. export default {\n  flags: {\n    force: true  // Overwrite files\n  }\n}",{"id":3555,"title":3556,"titles":3557,"content":3558,"level":3438},"/generation/cli-reference#nodb","noDb",[129,3503,3236,3531],"Type: booleanDefault: false Skip database table creation (generate code only). export default {\n  flags: {\n    noDb: true  // Skip database operations\n  }\n}",{"id":3560,"title":3561,"titles":3562,"content":3563,"level":3438},"/generation/cli-reference#dryrun","dryRun",[129,3503,3236,3531],"Type: booleanDefault: false Preview what will be generated without creating files. export default {\n  flags: {\n    dryRun: true  // Preview only\n  }\n}",{"id":3565,"title":3566,"titles":3567,"content":3568,"level":449},"/generation/cli-reference#complete-configuration-example","Complete Configuration Example",[129,3503],"// crouton.config.js\nexport default {\n  // Schema location\n  schemaPath: './schemas/',\n\n  // Database\n  dialect: 'sqlite',\n\n  // Target layers and collections\n  targets: [\n    {\n      layer: 'shop',\n      collections: [\n        { name: 'products', seed: true },\n        { name: 'categories', seed: { count: 10 } },\n        { name: 'orders', seed: { count: 100 } }\n      ]\n    },\n    {\n      layer: 'content',\n      collections: [\n        { name: 'pages', hierarchy: true },\n        { name: 'posts', seed: true },\n        'authors'  // Simple string also works\n      ]\n    }\n  ],\n\n  // Seed configuration\n  seed: {\n    defaultCount: 25,\n    defaultTeamId: 'seed-team'\n  },\n\n  // Generation flags\n  flags: {\n    // Timestamps\n    useMetadata: true,\n\n    // Relations\n    autoRelations: false,\n\n    // Translation fields\n    noTranslations: false,\n\n    // Database\n    noDb: false,\n\n    // Behavior\n    force: false,\n    dryRun: false\n  }\n}",{"id":3570,"title":3571,"titles":3572,"content":528,"level":391},"/generation/cli-reference#common-workflows","Common Workflows",[129],{"id":3574,"title":3575,"titles":3576,"content":3577,"level":449},"/generation/cli-reference#create-a-new-project-from-scratch","Create a New Project from Scratch",[129,3571],"# 1. Scaffold a full app (runs scaffold-app → generate → doctor → summary)\ncrouton-generate init my-app\n\n# 2. Customize the generated schemas and crouton.config.js\n\n# 3. Regenerate collections if needed\ncrouton-generate config\n\n# 4. Start dev server\ncd my-app\npnpm dev",{"id":3579,"title":3580,"titles":3581,"content":3582,"level":449},"/generation/cli-reference#generate-multiple-collections","Generate Multiple Collections",[129,3571],"# 1. Create config file\ncat > crouton.config.js \u003C\u003C 'CONF'\nexport default {\n  schemaPath: './schemas/',\n  dialect: 'sqlite',\n  targets: [\n    {\n      layer: 'shop',\n      collections: ['products', 'categories', 'orders']\n    }\n  ],\n  flags: {\n    useMetadata: true,\n    force: false\n  }\n}\nCONF\n\n# 2. Create schema files\nmkdir -p schemas\n# (Create products-schema.json, categories-schema.json, orders-schema.json)\n\n# 3. Generate all at once\ncrouton-generate config\n\n# 4. Export schemas\necho \"export * from '~/layers/shop/collections/products/server/database/schema'\" >> server/database/schema/index.ts\necho \"export * from '~/layers/shop/collections/categories/server/database/schema'\" >> server/database/schema/index.ts\necho \"export * from '~/layers/shop/collections/orders/server/database/schema'\" >> server/database/schema/index.ts",{"id":3584,"title":3585,"titles":3586,"content":3587,"level":449},"/generation/cli-reference#preview-changes-before-generating","Preview Changes Before Generating",[129,3571],"# Use --dry-run to see what would be generated\ncrouton-generate shop products \\\n  --fields-file=schemas/product.json \\\n  --dry-run\n\n# Output shows:\n# - Files that would be created\n# - Directories that would be created\n# - Schemas that would be registered",{"id":3589,"title":3590,"titles":3591,"content":3592,"level":449},"/generation/cli-reference#safely-remove-a-collection","Safely Remove a Collection",[129,3571],"# 1. Preview removal\ncrouton-generate rollback shop products --dry-run\n\n# 2. Review what will be removed\n\n# 3. Execute removal\ncrouton-generate rollback shop products\n\n# 4. Remove schema export manually from server/database/schema/index.ts",{"id":3594,"title":3595,"titles":3596,"content":3597,"level":449},"/generation/cli-reference#clean-up-an-entire-layer","Clean Up an Entire Layer",[129,3571],"# Use interactive mode for safety\ncrouton-generate rollback-interactive\n\n# Select \"Entire layer\"\n# Select the layer to remove\n# Confirm removal",{"id":3599,"title":3600,"titles":3601,"content":3602,"level":391},"/generation/cli-reference#integration-with-extraction-notes","Integration with Extraction Notes",[129],"The generator incorporates fixes and improvements documented in /docs/extraction-notes.md:",{"id":3604,"title":3605,"titles":3606,"content":3607,"level":449},"/generation/cli-reference#authorization-middleware","Authorization Middleware",[129,3600],"Generated API endpoints include team-based authorization by default: // Generated: server/api/teams/[id]/products/index.get.ts\nimport { resolveTeamAndCheckMembership } from '@fyit/crouton-auth/server/utils/team'\n\nexport default defineEventHandler(async (event) => {\n  const { team, user } = await resolveTeamAndCheckMembership(event)\n  // team is automatically resolved and verified\n  // All queries automatically scoped to team.id\n  const products = await getProducts({ teamId: team.id })\n  return products\n}) Note: The auth package is automatically included when using @fyit/crouton. You don't need to install it separately.",{"id":3609,"title":3610,"titles":3611,"content":3612,"level":449},"/generation/cli-reference#date-handling","Date Handling",[129,3600],"All date fields use proper UTC storage and timezone handling: {\n  \"publishedAt\": {\n    \"type\": \"date\"\n  }\n} Generated code includes: UTC storage in databaseLocal timezone display in UIProper date parsing and validationISO 8601 format for portability",{"id":3614,"title":3615,"titles":3616,"content":3617,"level":449},"/generation/cli-reference#connector-integration","Connector Integration",[129,3600],"External references (:users, :teams) trigger automatic connector setup with proper type safety and composables.",{"id":3619,"title":36,"titles":3620,"content":528,"level":391},"/generation/cli-reference#troubleshooting",[129],{"id":3622,"title":3623,"titles":3624,"content":3625,"level":449},"/generation/cli-reference#command-not-found","Command Not Found",[129,36],"Problem: crouton-generate: command not found Solutions: # Option 1: Install globally\nnpm install -g @fyit/crouton-cli\n\n# Option 2: Use npx\nnpx @fyit/crouton-cli \u003Ccommand>\n\n# Option 3: Add to project and use npx\npnpm add -D @fyit/crouton-cli\nnpx crouton-generate \u003Ccommand>",{"id":3627,"title":3628,"titles":3629,"content":3630,"level":449},"/generation/cli-reference#duplicate-key-errors","Duplicate Key Errors",[129,36],"Problem: Build fails with duplicate key errors (e.g., userId, teamId) Cause: You defined auto-generated fields in your schema Solution: Remove auto-generated fields from schema: Remove id (always auto-generated)Remove teamId, owner (always auto-generated for team-scoped collections)Remove createdAt, updatedAt, updatedBy (when useMetadata: true)",{"id":3632,"title":3633,"titles":3634,"content":3635,"level":449},"/generation/cli-reference#config-file-not-found","Config File Not Found",[129,36],"Problem: Error: Config file not found: ./crouton.config.js Solutions: # Use correct path\ncrouton-generate config ./path/to/config.js\n\n# Or use --config flag\ncrouton-generate --config ./path/to/config.js\n\n# Or ensure default location exists\nls crouton.config.js",{"id":3637,"title":3638,"titles":3639,"content":3640,"level":449},"/generation/cli-reference#schema-file-not-found","Schema File Not Found",[129,36],"Problem: Error: Schema file not found Solutions: # Use absolute path\ncrouton-generate shop products --fields-file=/full/path/to/schema.json\n\n# Or use relative path from project root\ncrouton-generate shop products --fields-file=./schemas/product.json\n\n# Verify file exists\nls ./schemas/product.json",{"id":3642,"title":3643,"titles":3644,"content":3645,"level":449},"/generation/cli-reference#typescript-errors-after-generation","TypeScript Errors After Generation",[129,36],"Problem: Type errors in generated code Solutions: # 1. Ensure Nuxt Crouton is installed\npnpm add @fyit/crouton\n\n# 2. Run Nuxt prepare to regenerate types\nnpx nuxt prepare\n\n# 3. Run typecheck\nnpx nuxt typecheck\n\n# 4. Restart TypeScript server in your editor",{"id":3647,"title":3648,"titles":3649,"content":3650,"level":449},"/generation/cli-reference#rollback-doesnt-remove-everything","Rollback Doesn't Remove Everything",[129,36],"Problem: Some files remain after rollback Explanation: Rollback only removes generated files, not manual modifications or schema exports. Manual cleanup needed: Remove schema exports from server/database/schema/index.tsRemove any manual modifications to generated filesRemove database migrations if created",{"id":3652,"title":3653,"titles":3654,"content":3655,"level":449},"/generation/cli-reference#permission-errors","Permission Errors",[129,36],"Problem: EACCES: permission denied Solutions: # Check file permissions\nls -la layers/\n\n# Fix permissions\nchmod -R u+w layers/\n\n# Or run with sudo (not recommended)\nsudo crouton-generate ...",{"id":3657,"title":44,"titles":3658,"content":528,"level":391},"/generation/cli-reference#best-practices",[129],{"id":3660,"title":3661,"titles":3662,"content":3663,"level":449},"/generation/cli-reference#schema-design","Schema Design",[129,44],"Start simple - Add fields incrementallyUse metadata - Enable useMetadata: true for audit trailsReference external collections - Use :prefix for external refsValidate in schema - Use meta.required, meta.maxLength, etc.",{"id":3665,"title":1789,"titles":3666,"content":3667,"level":449},"/generation/cli-reference#configuration",[129,44],"Use config files for multi-collection projectsVersion control your crouton.config.jsDocument connectors in config commentsUse dryRun before actual generation",{"id":3669,"title":3670,"titles":3671,"content":3672,"level":449},"/generation/cli-reference#development-workflow","Development Workflow",[129,44],"Generate collectionExport schema in server/database/schema/index.tsRun typecheck (npx nuxt typecheck)Test in dev modeCommit generated files (they're your code now!)",{"id":3674,"title":3675,"titles":3676,"content":3677,"level":449},"/generation/cli-reference#rollback-strategy","Rollback Strategy",[129,44],"Always --dry-run firstUse interactive mode for bulk operationsBackup before major rollbacksClean manually if needed",{"id":3679,"title":3680,"titles":3681,"content":3682,"level":391},"/generation/cli-reference#related-documentation","Related Documentation",[129],"Generator Commands - Overview of generation workflowsSchema Format - Complete schema referenceMulti-Collection - Config-based generationTeam-Based Auth - Multi-tenancy setupTroubleshooting - Common issues and solutions",{"id":3684,"title":3685,"titles":3686,"content":3687,"level":391},"/generation/cli-reference#version-history","Version History",[129],"v0.1.0 (Current) - Package @fyit/crouton-cliPackage renamed from nuxt-crouton-collection-generatorBoth crouton and crouton-generate commands availableEnhanced MCP server integrationFull pipeline: init, add, doctor, scaffold-app, seed-translations, db-pull, deploy-setup, deploy-checkEnhanced rollback commands (single, bulk, interactive)Improved connector detection and setupBetter error messages and validationDate handling improvementsAuthorization middleware generation",{"id":3689,"title":3690,"titles":3691,"content":3692,"level":391},"/generation/cli-reference#support","Support",[129],"Issues: GitHub IssuesPackage: @fyit/crouton-cliRepository: nuxt-crouton html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":134,"title":133,"titles":3694,"content":3695,"level":385},[],"Learn how to use Nuxt Crouton generator commands to create collections Nuxt Crouton provides CLI commands to generate collections quickly. You can generate single collections or use a configuration file to generate multiple collections at once. # Basic syntax\nnpx crouton-generate \u003Clayer> \u003Ccollection> --fields-file \u003Cschema-file>\n\n# Example\nnpx crouton-generate shop products --fields-file ./schemas/product-schema.json",{"id":3697,"title":3698,"titles":3699,"content":3700,"level":391},"/generation/cli-commands#single-collection","Single Collection",[133],"Use the basic command to generate a single collection. The --fields-file option (required) specifies the path to your schema JSON file. Additional options include --force to overwrite files, --no-db to skip database generation, and --dry-run to preview changes. npx crouton-generate shop products --fields-file ./schemas/product-schema.json The generator creates several files in your project: layers/[layer]/\n  └── collections/\n      └── [collection]/\n          ├── app/\n          │   ├── components/\n          │   │   ├── List.vue           # Table/list view\n          │   │   └── _Form.vue          # Create/edit form\n          │   └── composables/\n          │       └── use[Layer][Collection].ts # Validation, columns, defaults (e.g., useShopProducts.ts)\n          ├── server/\n          │   ├── api/                   # CRUD endpoints\n          │   ├── database/              # Drizzle schema\n          │   └── utils/                 # Server-side helpers\n          └── types.ts                   # TypeScript types",{"id":3702,"title":125,"titles":3703,"content":3704,"level":391},"/generation/cli-commands#multi-collection-configuration",[133],"For larger projects with multiple collections, create a configuration file and generate everything at once: # Generate from config\nnpx crouton-generate config ./crouton.config.js\n\n# With options\nnpx crouton-generate config ./crouton.config.js --force --dry-run",{"id":3706,"title":3476,"titles":3707,"content":3708,"level":449},"/generation/cli-commands#config-file-format",[133,125],"// crouton.config.js\nexport default {\n  collections: [\n    { name: 'products', fieldsFile: './schemas/product-schema.json' },\n    { name: 'categories', fieldsFile: './schemas/category-schema.json' },\n  ],\n  targets: [\n    {\n      layer: 'shop',\n      collections: ['products', 'categories']\n    }\n  ],\n  dialect: 'sqlite',\n  flags: {\n    useMetadata: true,       // Add createdAt/updatedAt timestamps\n    force: false,\n    noTranslations: false,\n    noDb: false\n  }\n}",{"id":3710,"title":3711,"titles":3712,"content":3713,"level":449},"/generation/cli-commands#configuration-flags","Configuration Flags",[133,125],"Team-Scoped by Default: All generated collections include team-based authentication. The generator automatically adds teamId and owner fields and uses @fyit/crouton-auth/server for authentication. Important: Do NOT define teamId or owner in your schema JSON files. The generator adds them automatically, and manual definitions will cause duplicate key errors. See Team-Based Authentication for usage examples.",{"id":3715,"title":3716,"titles":3717,"content":3718,"level":748},"/generation/cli-commands#usemetadata-boolean-default-true","useMetadata (boolean, default: true)",[133,125,3711],"Automatically adds timestamp fields to track record creation and updates. When set to true: Database schema changes: Automatically adds createdAt timestamp field (auto-populated on record creation)Automatically adds updatedAt timestamp field (auto-updated on record modification)Automatically adds createdBy user ID field (auto-populated on record creation)Automatically adds updatedBy user ID field (auto-updated on record modification) Database behavior: createdAt is set automatically when a record is createdupdatedAt is set automatically whenever a record is modifiedcreatedBy is set to the creating user's ID on record creationupdatedBy is set to the modifying user's ID on every updateTimestamp fields use the database's native timestamp type When to disable (false): You want to implement custom timestamp trackingYou're integrating with an existing database schemaYou need different timestamp field names or behavior Important: Do NOT define createdAt, updatedAt, createdBy, or updatedBy in your schema JSON files when this flag is enabled. The generator adds them automatically, and manual definitions will cause duplicate key errors.",{"id":3720,"title":3721,"titles":3722,"content":3723,"level":449},"/generation/cli-commands#example-generate-multiple-collections","Example: Generate Multiple Collections",[133,125],"# Create config file\ncat > crouton.config.js \u003C\u003C 'EOF'\nexport default {\n  collections: [\n    { name: 'products', fieldsFile: './schemas/product-schema.json' },\n    { name: 'categories', fieldsFile: './schemas/category-schema.json' },\n    { name: 'orders', fieldsFile: './schemas/order-schema.json' },\n  ],\n  targets: [\n    {\n      layer: 'shop',\n      collections: ['products', 'categories', 'orders']\n    }\n  ],\n  dialect: 'sqlite'\n}\nEOF\n\n# Generate all collections at once\nnpx crouton-generate config ./crouton.config.js",{"id":3725,"title":3726,"titles":3727,"content":528,"level":391},"/generation/cli-commands#helper-commands","Helper Commands",[133],{"id":3729,"title":3730,"titles":3731,"content":3732,"level":449},"/generation/cli-commands#initialize-a-new-app","Initialize a New App",[133,3726],"Scaffold a full Crouton app end-to-end. The init command runs a complete pipeline: it creates the app skeleton (scaffold-app), generates collections from config, runs doctor validation, and prints a summary with next steps. crouton-generate init \u003Cname>\n\n# Example: Create a new app called \"my-shop\"\ncrouton-generate init my-shop With features and theme: crouton-generate init my-shop --features bookings,pages,editor --theme ko Preview without writing files: crouton-generate init my-shop --dry-run After initialization, you can customize the generated crouton.config.js and schemas, then re-run crouton-generate config to regenerate collections.",{"id":3734,"title":3735,"titles":3736,"content":3737,"level":449},"/generation/cli-commands#install-required-modules","Install Required Modules",[133,3726],"Install Nuxt Crouton and dependencies: crouton-generate install This installs: @fyit/croutonRequired peer dependenciesUpdates nuxt.config.ts with extends Manual installation: pnpm add @fyit/crouton Then update nuxt.config.ts: export default defineNuxtConfig({\n  extends: ['@fyit/crouton']\n}) Additional Commands: The CLI also provides add, doctor, scaffold-app, seed-translations, db-pull, deploy-setup, and deploy-check commands. See the CLI Reference for complete documentation of all available commands.",{"id":3739,"title":3740,"titles":3741,"content":3742,"level":391},"/generation/cli-commands#rollback-commands","Rollback Commands",[133],"See Rollback & Undo Guide for complete documentation on removing collections. Quick reference: # Remove single collection\ncrouton rollback \u003Clayer> \u003Ccollection>\n\n# Remove entire layer\ncrouton rollback-bulk --layer=\u003Cname>\n\n# Interactive removal\ncrouton rollback-interactive",{"id":3744,"title":3745,"titles":3746,"content":528,"level":391},"/generation/cli-commands#complete-cli-flags-reference","Complete CLI Flags Reference",[133],{"id":3748,"title":3749,"titles":3750,"content":3751,"level":449},"/generation/cli-commands#generation-flags","Generation Flags",[133,3745],"FlagTypeDefaultDescription--fields-filestringrequiredPath to JSON schema file--dialectstringsqliteDatabase dialect: sqlite or pg--configstring-Use config file instead of CLI args--dry-runbooleanfalsePreview mode - Show what would be generated without creating files--forcebooleanfalseForce generation even if files exist (overwrites)--no-translationsbooleanfalseSkip translation field generation--no-dbbooleanfalseSkip database schema generation--auto-relationsbooleanfalseAdd relation stub comments in generated code",{"id":3753,"title":3754,"titles":3755,"content":3756,"level":449},"/generation/cli-commands#preview-mode-dry-run","Preview Mode (--dry-run)",[133,3745],"See exactly what will be generated before creating any files: crouton-generate shop products --fields-file=product-schema.json --dry-run\n\n# Output:\n📋 Preview: Would generate the following files:\n\nlayers/shop/collections/products/\n  ├── app/\n  │   ├── components/\n  │   │   ├── List.vue (new)\n  │   │   └── _Form.vue (new)\n  │   └── composables/\n  │       └── useShopProducts.ts (new)\n  └── types.ts (new)\n\nTotal: 5 files (5 new)\n\nWould also update:\n  - app.config.ts (add products collection)\n\nProceed? (y/n) Use when: First time generating a collectionUnsure about file placementChecking if files will be overwrittenTesting a new schema structure",{"id":3758,"title":3759,"titles":3760,"content":3761,"level":449},"/generation/cli-commands#force-mode-force","Force Mode (--force)",[133,3745],"Overwrite existing files without prompting: crouton-generate shop products --fields-file=product-schema.json --force ⚠️ Warning: This will overwrite any customizations you made to generated files. Safe workflow: # 1. Check what would be overwritten\ncrouton-generate shop products --fields-file=product-schema.json --dry-run\n\n# 2. Commit your changes\ngit add .\ngit commit -m \"Save customizations before regenerate\"\n\n# 3. Force regenerate\ncrouton-generate shop products --fields-file=product-schema.json --force",{"id":3763,"title":3764,"titles":3765,"content":3766,"level":449},"/generation/cli-commands#skip-translations-no-translations","Skip Translations (--no-translations)",[133,3745],"Generate without i18n support: crouton-generate shop products --fields-file=product-schema.json --no-translations Useful when: Building a single-language appAdding translations laterFaster generation for testing",{"id":3768,"title":3769,"titles":3770,"content":3771,"level":449},"/generation/cli-commands#skip-database-no-db","Skip Database (--no-db)",[133,3745],"Generate UI components only, no database schema: crouton-generate shop products --fields-file=product-schema.json --no-db Useful when: Using an existing databaseOnly need frontend componentsDatabase is managed separately",{"id":3773,"title":3774,"titles":3775,"content":3776,"level":449},"/generation/cli-commands#auto-relations-auto-relations","Auto Relations (--auto-relations)",[133,3745],"Add commented relation stubs in generated code: crouton-generate shop products --fields-file=product-schema.json --auto-relations Generates comments like: // TODO: Add relation\n// export const productsRelations = relations(products, ({ one }) => ({\n//   category: one(categories, {\n//     fields: [products.categoryId],\n//     references: [categories.id]\n//   })\n// })) Useful when: Planning to add Drizzle relations laterWant reminders about relation opportunitiesLearning relation patterns",{"id":3778,"title":3779,"titles":3780,"content":3781,"level":449},"/generation/cli-commands#config-file-options","Config File Options",[133,3745],"When using --config or config command, flags are set in the config file: // crouton.config.js\nexport default {\n  dialect: 'sqlite',\n  flags: {\n    useMetadata: true,        // Metadata fields (createdAt/updatedAt/createdBy/updatedBy)\n    force: false,\n    noTranslations: false,\n    noDb: false,\n    autoRelations: true,\n    dryRun: false\n  }\n} CLI flags override config file settings: # Config has force: false, but CLI overrides to true\ncrouton-generate config ./crouton.config.js --force",{"id":3783,"title":3784,"titles":3785,"content":3786,"level":449},"/generation/cli-commands#common-flag-combinations","Common Flag Combinations",[133,3745],"Safe First Generation: crouton-generate shop products --fields-file=schema.json --dry-run\n# Review output, then run without --dry-run Standard SaaS Application: # All collections are team-scoped by default\ncrouton-generate config ./crouton.config.js Quick Testing (No DB): crouton-generate shop products --fields-file=schema.json --no-db --no-translations Full Featured Generation: crouton-generate shop products --fields-file=schema.json --auto-relations Force Regenerate: crouton-generate shop products --fields-file=schema.json --force",{"id":3788,"title":418,"titles":3789,"content":3790,"level":391},"/generation/cli-commands#next-steps",[133],"Learn about the Schema Format for defining your collectionsExplore Multi-Collection Configuration for complex projectsSee Working with Collections to understand the generated codeRead the Rollback Guide to learn how to remove collections html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":114,"title":118,"titles":3792,"content":3793,"level":385},[],"This section explains how Nuxt Crouton's code generation system works. The CLI tools transform your collection schemas into fully functional CRUD interfaces with minimal configuration.",{"id":3795,"title":118,"titles":3796,"content":3793,"level":385},"/generation#generation-overview",[],{"id":3798,"title":2867,"titles":3799,"content":3800,"level":391},"/generation#what-youll-learn",[118],"The Generation section covers everything about the code generation workflow: CLI Commands: How to use the crouton-generate CLISchema Format: How to define collection schemas in JSONMulti-Collection: Working with multiple collections at onceCLI Reference: Complete reference of all CLI options and flags",{"id":3802,"title":2872,"titles":3803,"content":528,"level":391},"/generation#section-contents",[118],{"id":3805,"title":3806,"titles":3807,"content":3808,"level":449},"/generation#_1-cli-commands","1. CLI Commands",[118,2872],"File: cli-commands.md Master the Nuxt Crouton CLI: npx crouton-generate - Generate collections from schemasCommon workflows and best practices",{"id":3810,"title":3811,"titles":3812,"content":3813,"level":449},"/generation#_2-schema-format","2. Schema Format",[118,2872],"File: 2.schema-format.md Learn the YAML schema format for defining collections: Collection metadata (name, description, icon)Field definitions and typesField validation rulesRelationships between collectionsAdvanced schema options Field Types: Text fields (short text, long text, rich text)Number fields (integer, decimal)Date/time fieldsBoolean/checkbox fieldsSelect/enum fieldsReference fields (relationships)File/asset fields Schema Example: name: products\ndescription: Product catalog\nicon: i-lucide-shopping-bag\nfields:\n  - name: title\n    type: text\n    required: true\n  - name: price\n    type: number\n    validation:\n      min: 0\n  - name: category\n    type: select\n    options:\n      - electronics\n      - clothing\n      - books",{"id":3815,"title":3816,"titles":3817,"content":3818,"level":449},"/generation#_3-multi-collection","3. Multi-Collection",[118,2872],"File: 3.multi-collection.md Work with multiple collections efficiently: Organizing schemas in collections/ directoryGenerating all collections at onceManaging relationships between collectionsBest practices for complex data models",{"id":3820,"title":3821,"titles":3822,"content":3823,"level":449},"/generation#_4-cli-reference-old","4. CLI Reference (Old)",[118,2872],"File: 4.cli-reference.md Legacy CLI reference (may be outdated - refer to 5.cli-reference.md for latest).",{"id":3825,"title":3826,"titles":3827,"content":3828,"level":449},"/generation#_5-cli-reference-current","5. CLI Reference (Current)",[118,2872],"File: 5.cli-reference.md Complete reference of all CLI commands and options: Command syntax and usageAll available flags and optionsEnvironment variablesConfiguration filesExit codes and error handling",{"id":3830,"title":3831,"titles":3832,"content":3833,"level":391},"/generation#code-generation-workflow","Code Generation Workflow",[118],"The typical workflow for generating collections: Define Schema: Create a JSON file in schemas/[name]-schema.jsonRun Generator: Execute npx crouton-generate \u003Clayer> \u003Ccollection> --fields-file schemas/[name]-schema.jsonReview Output: Check generated files in layers/[name]/Customize: Override components or add custom logicRegenerate: Re-run generator if schema changes",{"id":3835,"title":3049,"titles":3836,"content":3837,"level":391},"/generation#what-gets-generated",[118],"When you run npx crouton-generate, Nuxt Crouton creates: Components:CroutonForm.vue - Auto-generated form componentCroutonTable.vue - Table/list view componentCroutonModal.vue - Modal for create/edit operationsField components for each field typeComposables:useCollectionForm() - Form state managementuseCollectionTable() - Table data and paginationuseCollection() - CRUD operationsuseCollectionQuery() - Advanced queryingServer API:GET /api/[collection] - List endpointGET /api/[collection]/[id] - Get single itemPOST /api/[collection] - Create endpointPUT /api/[collection]/[id] - Update endpointDELETE /api/[collection]/[id] - Delete endpointDatabase:Drizzle schema definitionsMigration filesDatabase typesPages (optional):List page with table viewDetail page for single items",{"id":3839,"title":3840,"titles":3841,"content":3842,"level":449},"/generation#extending-api-endpoints","Extending API Endpoints",[118,3049],"Generated list endpoints (GET /api/[collection]) support ?ids=xxx by default for fetching specific items by ID. To add custom filtering (e.g., filtering by a parent entity like eventId), you need to extend both the query function and API handler. 1. Add a query function in server/database/queries.ts: export async function getProductsByEventId(teamId: string, eventId: string) {\n  const db = useDB()\n  return await db\n    .select({ ...tables.products })\n    .from(tables.products)\n    .where(\n      and(\n        eq(tables.products.teamId, teamId),\n        eq(tables.products.eventId, eventId)\n      )\n    )\n} 2. Handle the query param in index.get.ts: import { getAllProducts, getProductsByIds, getProductsByEventId } from '...'\n\nexport default defineEventHandler(async (event) => {\n  const { team } = await resolveTeamAndCheckMembership(event)\n  const query = getQuery(event)\n\n  if (query.ids) {\n    return await getProductsByIds(team.id, String(query.ids).split(','))\n  }\n\n  // Custom filter: eventId\n  if (query.eventId) {\n    return await getProductsByEventId(team.id, String(query.eventId))\n  }\n\n  return await getAllProducts(team.id)\n}) 3. Pass the filter from the client using useCollectionQuery: const eventId = computed(() => event.value?.id)\n\nconst { items } = await useCollectionQuery('products', {\n  query: computed(() => ({ eventId: eventId.value }))\n}) The query object is automatically serialized to URL parameters (e.g., ?eventId=abc123) and the API endpoint reads them via getQuery(event).",{"id":3844,"title":3845,"titles":3846,"content":3847,"level":391},"/generation#generation-best-practices","Generation Best Practices",[118],"Start Simple: Begin with basic schemas, add complexity laterUse Conventions: Follow naming conventions (collections plural, fields singular)Version Control: Commit schemas and generated code separatelyDon't Edit Generated Files: Use overrides and custom components insteadRegenerate Safely: Generated code can be regenerated without losing customizations",{"id":3849,"title":2916,"titles":3850,"content":3851,"level":391},"/generation#where-to-go-next",[118],"After understanding generation: Patterns → Learn common patterns for forms, tables, and relationsCustomization → Customize generated components and fieldsFundamentals → Understand the architecture behind the generated code",{"id":3853,"title":426,"titles":3854,"content":3855,"level":391},"/generation#prerequisites",[118],"Before working with generation: Installed Nuxt Crouton via Getting StartedBasic understanding of YAML syntaxFamiliarity with Vue components (for customization)",{"id":3857,"title":2925,"titles":3858,"content":3859,"level":391},"/generation#external-resources",[118],"For related concepts: YAML Syntax GuideDrizzle Schema ReferenceNuxt Layers - Understanding the layer architecture html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":146,"title":145,"titles":3861,"content":3862,"level":385},[],"Learn how to connect data between collections using foreign keys and Drizzle relations Relations connect data between collections, like linking products to categories or posts to authors. Crouton stores relations as foreign key IDs only - it does not embed full related objects. Most apps can start simple by storing these IDs and querying manually when needed: // Store the relationship\nconst product = {\n  id: '123',\n  name: 'Widget',\n  categoryId: 'cat-456'  // ← Just store the ID\n}\n\n// Query when needed\nconst category = await db.select()\n  .from(categories)\n  .where(eq(categories.id, product.categoryId)) This approach works well when you: Have simple CRUD appsOnly need relations occasionallyPrefer explicit queriesAre learning and prototyping It's the recommended starting point for most applications.",{"id":3864,"title":3865,"titles":3866,"content":3867,"level":391},"/patterns/relations#reference-fields-with-reftarget","Reference Fields with refTarget",[145],"The generator automatically creates UI components for reference fields when you use the refTarget property in your schema: {\n  \"authorId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"authors\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"Author\"\n    }\n  }\n}",{"id":3869,"title":3870,"titles":3871,"content":3872,"level":449},"/patterns/relations#cross-layer-references","Cross-Layer References",[145,3865],"By default, references are scoped to the current layer. To reference collections outside your layer (like a shared users table), prefix with a colon: {\n  \"updatedBy\": {\n    \"type\": \"string\",\n    \"refTarget\": \":users\",\n    \"meta\": {\n      \"label\": \"Updated By\"\n    }\n  }\n} This generates collection=\"users\" instead of collection=\"shopUsers\", allowing references to global or external collections.",{"id":3874,"title":3875,"titles":3876,"content":3877,"level":449},"/patterns/relations#automatic-form-component","Automatic Form Component",[145,3865],"The generator creates a CroutonFormReferenceSelect component that provides: Searchable dropdown - Shows all items from the referenced collectionCreate button - Quick creation of new related itemsAuto-selection - Newly created items are automatically selected \u003C!-- Generated automatically in _Form.vue -->\n\u003CUFormField label=\"Author\" name=\"authorId\">\n  \u003CCroutonFormReferenceSelect\n    v-model=\"state.authorId\"\n    collection=\"authors\"\n    label=\"Author\"\n  />\n\u003C/UFormField>",{"id":3879,"title":3880,"titles":3881,"content":3882,"level":449},"/patterns/relations#automatic-list-component","Automatic List Component",[145,3865],"In table views, reference fields display as CroutonItemCardMini components showing the referenced item's title with a quick-edit button: \u003C!-- Generated automatically in List.vue -->\n\u003Ctemplate #authorId-cell=\"{ row }\">\n  \u003CCroutonItemCardMini\n    v-if=\"row.original.authorId\"\n    :id=\"row.original.authorId\"\n    collection=\"authors\"\n  />\n\u003C/template>",{"id":3884,"title":3885,"titles":3886,"content":3887,"level":449},"/patterns/relations#user-experience-flow","User Experience Flow",[145,3865],"User opens \"Create Post\" formSees \"Author\" field with searchable dropdownCan select existing author OR click \"+\" to create newNew author modal opensAfter saving, new author is automatically selectedUser continues with post creation This provides a seamless experience for managing related data without manual coding.",{"id":3889,"title":3890,"titles":3891,"content":3892,"level":391},"/patterns/relations#advanced-drizzle-relations","Advanced: Drizzle Relations",[145],"For apps with lots of related data queries, Drizzle relations let you fetch related data in one query, avoiding N+1 query problems: // Define relation (one-time setup in schema.ts)\nimport { relations } from 'drizzle-orm'\n\nexport const productsRelations = relations(products, ({ one }) => ({\n  category: one(categories, {\n    fields: [products.categoryId],\n    references: [categories.id]\n  })\n}))\n\n// Query with automatic join\nconst product = await db.query.products.findFirst({\n  where: eq(products.id, '123'),\n  with: { category: true }  // ← Drizzle handles the join\n})\n\n// Now product.category is populated in ONE query\nconsole.log(product.category.name) When to use this: ✅ Fetching lists with related data (100 products + their categories = 1 query, not 101)✅ Nested data (Product → Category → ParentCategory)✅ Complex filtering (\"Get products WHERE category.name = 'Electronics'\")✅ Performance critical queries",{"id":3894,"title":44,"titles":3895,"content":3896,"level":391},"/patterns/relations#best-practices",[145],"✅ DO: Start simple - Store foreign keys, query manually when neededAdd relations later - Only if you have performance problems or N+1 queriesUse useCollectionQuery to fetch related collections for dropdownsAdd database indexes on foreign key columns for performanceDocument your relation patterns in code comments ❌ DON'T: Over-engineer with Drizzle relations unless you need themForget to handle null/missing relations (category?.name || 'N/A')Mix manual joins and Drizzle relations in the same query (pick one approach)Skip validation on foreign keys (ensure referenced item exists)",{"id":3898,"title":1562,"titles":3899,"content":3900,"level":391},"/patterns/relations#related-topics",[145],"Form Patterns: Relation DropdownsTable Patterns: Display Related DataManual Drizzle Setup html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":150,"title":149,"titles":3902,"content":3903,"level":385},[],"Working with forms in Nuxt Crouton - custom fields, validation, relation dropdowns, and architecture",{"id":3905,"title":149,"titles":3906,"content":3907,"level":385},"/patterns/forms#form-patterns",[],"Learn how to work with Nuxt Crouton's form system - from adding custom fields and validation to understanding the underlying architecture. Complete CroutonForm API Reference: For comprehensive form component documentation including all props, slots, and events, see Form Components API Reference. For general form concepts and Nuxt UI form components, see the Nuxt UI Forms documentation.",{"id":3909,"title":3910,"titles":3911,"content":3912,"level":391},"/patterns/forms#adding-custom-fields","Adding Custom Fields",[149],"Generated forms provide a foundation - you extend them by adding custom fields as needed.",{"id":3914,"title":3915,"titles":3916,"content":3917,"level":449},"/patterns/forms#basic-custom-field","Basic Custom Field",[149,3910],"Add a category dropdown to a product form: \u003C!-- layers/shop/components/products/_Form.vue -->\n\u003Cscript setup lang=\"ts\">\n// Keep generated form setup\nconst props = defineProps\u003CShopProductsFormProps>()\nconst { create, update } = useCollectionMutation('shopProducts')\n\n// Add: Fetch related collection for dropdown\nconst { items: categories } = await useCollectionQuery('shopCategories')  // See /fundamentals/querying for query patterns\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm :state=\"state\" :schema=\"schema\" @submit=\"handleSubmit\">\n    \u003C!-- Generated fields -->\n    \u003CUFormField label=\"Name\" name=\"name\">\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Price\" name=\"price\">\n      \u003CUInput v-model.number=\"state.price\" type=\"number\" />\n    \u003C/UFormField>\n\n    \u003C!-- Custom field: Category dropdown -->\n    \u003CUFormField label=\"Category\" name=\"categoryId\">\n      \u003CUSelectMenu\n        v-model=\"state.categoryId\"\n        :options=\"categories\"\n        option-attribute=\"name\"\n        value-attribute=\"id\"\n        placeholder=\"Select category\"\n      />\n    \u003C/UFormField>\n\n    \u003CCroutonFormActionButton :action=\"action\" :loading=\"loading\" />\n  \u003C/UForm>\n\u003C/template>",{"id":3919,"title":3920,"titles":3921,"content":3922,"level":449},"/patterns/forms#searchable-dropdown","Searchable Dropdown",[149,3910],"For large datasets, add search functionality: \u003Cscript setup lang=\"ts\">\nconst { items: categories } = await useCollectionQuery('shopCategories')\nconst searchQuery = ref('')\n\nconst filteredCategories = computed(() =>\n  categories.value.filter(c =>\n    c.name.toLowerCase().includes(searchQuery.value.toLowerCase())\n  )\n)  // For advanced filtering, see /fundamentals/querying\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUFormField label=\"Category\" name=\"categoryId\">\n    \u003CUInput v-model=\"searchQuery\" placeholder=\"Search categories...\" class=\"mb-2\" />\n    \u003CUSelectMenu\n      v-model=\"state.categoryId\"\n      :options=\"filteredCategories\"\n      option-attribute=\"name\"\n      value-attribute=\"id\"\n    />\n  \u003C/UFormField>\n\u003C/template>",{"id":3924,"title":3925,"titles":3926,"content":3927,"level":449},"/patterns/forms#handle-loading-states","Handle Loading States",[149,3910],"Show loading indicators while fetching data: \u003Cscript setup lang=\"ts\">\nconst { items: categories, pending: loadingCategories } = await useCollectionQuery('shopCategories')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUFormField label=\"Category\" name=\"categoryId\">\n    \u003CUSelectMenu\n      v-model=\"state.categoryId\"\n      :options=\"categories\"\n      :loading=\"loadingCategories\"\n    />\n  \u003C/UFormField>\n\u003C/template>",{"id":3929,"title":3930,"titles":3931,"content":3932,"level":391},"/patterns/forms#validation-patterns","Validation Patterns",[149],"Nuxt Crouton uses Zod for schema validation.",{"id":3934,"title":3935,"titles":3936,"content":3937,"level":449},"/patterns/forms#basic-validation-rules","Basic Validation Rules",[149,3930],"// layers/shop/composables/useProducts.ts\nimport { z } from 'zod'\n\nexport function useShopProducts() {\n  const schema = z.object({\n    // Generated fields\n    name: z.string().min(1, 'Name is required'),\n    price: z.number().min(0, 'Price must be positive'),\n\n    // Custom validation rules\n    sku: z.string()\n      .regex(/^[A-Z]{3}-\\d{4}$/, 'SKU must be format: ABC-1234'),\n\n    email: z.string()\n      .email('Must be a valid email address'),\n\n    url: z.string()\n      .url('Must be a valid URL')\n      .optional(),\n\n    quantity: z.number()\n      .int('Must be a whole number')\n      .min(0)\n      .max(9999, 'Quantity cannot exceed 9999')\n  })\n\n  // Rest of composable...\n} Zod Validation: Learn more about Zod's validation methods in the Zod documentation.",{"id":3939,"title":3940,"titles":3941,"content":3942,"level":449},"/patterns/forms#cross-field-validation","Cross-Field Validation",[149,3930],"Validate relationships between multiple fields: const schema = z.object({\n  price: z.number().min(0, 'Price must be positive'),\n  discountPrice: z.number().optional()\n}).refine((data) => {\n  // Ensure discount is less than regular price\n  if (data.discountPrice && data.discountPrice >= data.price) {\n    return false\n  }\n  return true\n}, {\n  message: 'Discount price must be less than regular price',\n  path: ['discountPrice']  // Show error on discountPrice field\n}) Zod Refine: Learn more about cross-field validation with .refine() in the Zod documentation.",{"id":3944,"title":3945,"titles":3946,"content":3947,"level":449},"/patterns/forms#async-validation","Async Validation",[149,3930],"Validate against your database or external APIs: const schema = z.object({\n  name: z.string().min(1),\n\n  // Check if SKU already exists\n  sku: z.string().refine(async (sku) => {\n    const exists = await $fetch(`/api/products/check-sku?sku=${sku}`)\n    return !exists\n  }, 'SKU already exists'),\n\n  // Validate against external API\n  domain: z.string().refine(async (domain) => {\n    const isValid = await $fetch(`/api/verify-domain?domain=${domain}`)\n    return isValid\n  }, 'Domain is not accessible')\n}) Async validation can slow down your forms. Use it sparingly and consider debouncing user input.",{"id":3949,"title":3950,"titles":3951,"content":3952,"level":449},"/patterns/forms#conditional-validation","Conditional Validation",[149,3930],"Apply different rules based on field values: const schema = z.object({\n  type: z.enum(['physical', 'digital']),\n  weight: z.number().optional(),\n  downloadUrl: z.string().optional()\n}).refine((data) => {\n  // Physical products must have weight\n  if (data.type === 'physical' && !data.weight) {\n    return false\n  }\n  return true\n}, {\n  message: 'Weight is required for physical products',\n  path: ['weight']\n}).refine((data) => {\n  // Digital products must have download URL\n  if (data.type === 'digital' && !data.downloadUrl) {\n    return false\n  }\n  return true\n}, {\n  message: 'Download URL is required for digital products',\n  path: ['downloadUrl']\n}) Conditional Validation: For more patterns with .refine() and conditional logic, see the Zod documentation.",{"id":3954,"title":3955,"titles":3956,"content":3957,"level":391},"/patterns/forms#form-system-architecture","Form System Architecture",[149],"Understanding how Nuxt Crouton's form system works helps you customize it effectively.",{"id":3959,"title":3960,"titles":3961,"content":3962,"level":449},"/patterns/forms#component-hierarchy","Component Hierarchy",[149,3955],"User Action (Click \"Create\")\n  ↓\nuseCrouton().open('create', 'users', [])\n  ↓\nForm.vue (Global Container)\n  ├─ Renders: Modal | Slideover | Dialog\n  ├─ Manages: State via useCrouton()\n  └─ Delegates to: FormDynamicLoader\n      ↓\nFormDynamicLoader.vue\n  ├─ Resolves: collection → component mapping\n  └─ Loads: [Collection]Form.vue\n      ↓\nUsersForm.vue (Generated)\n  ├─ Uses: FormLayout for structure\n  ├─ Uses: UForm for validation\n  ├─ Renders: Field components\n  └─ Submits: via useCollectionMutation()\n      ↓\nData Layer (useCollectionMutation)\n  ├─ create() / update() / deleteItems()\n  ├─ Optimistic updates\n  ├─ Cache invalidation\n  └─ Toast notifications",{"id":3964,"title":3965,"titles":3966,"content":3967,"level":449},"/patterns/forms#state-management","State Management",[149,3955],"Global State (useCrouton): const { open, close, closeAll, croutonStates } = useCrouton()\n\n// Open form\nopen('create', 'users', [])                    // Create in slideover\nopen('update', 'users', ['user-123'], 'modal') // Edit in modal\nopen('delete', 'users', ['id1', 'id2'])        // Delete confirmation\nopen('view', 'users', ['user-123'])            // View-only\n\n// Close forms\nclose()      // Close current\ncloseAll()   // Close all forms Local Form State (per component): \u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  action: 'create' | 'update' | 'delete'\n  loading: string\n  activeItem?: any\n}>()\n\n// Local reactive state\nconst state = ref({\n  id: props.activeItem?.id || null,\n  name: props.activeItem?.name || '',\n  email: props.activeItem?.email || '',\n})\n\n// Validation schema\nconst schema = z.object({\n  name: z.string().min(1),\n  email: z.string().email()\n})\n\n// Form submission\nconst handleSubmit = async () => {\n  const { create, update } = useCollectionMutation('users')\n\n  if (props.action === 'create') {\n    await create(state.value)\n  } else if (props.action === 'update') {\n    await update(state.value.id, state.value)\n  }\n\n  close()\n}\n\u003C/script>",{"id":3969,"title":2375,"titles":3970,"content":3971,"level":449},"/patterns/forms#container-types",[149,3955],"Modal - Standard forms, simple edits: Centered on screenBackdrop overlaySingle instance recommended Slideover - Complex forms, nested workflows: Side panel from rightSupports up to 5 levels of nestingExpandable to fullscreenBreadcrumb navigation Dialog - Simple confirmations: Minimal UITypically for destructive actions",{"id":3973,"title":3974,"titles":3975,"content":3976,"level":449},"/patterns/forms#specialized-components","Specialized Components",[149,3955],"FormReferenceSelect - Select related entities: \u003CCroutonFormReferenceSelect\n  v-model=\"state.categoryId\"\n  collection=\"categories\"\n  :multiple=\"false\"\n/> Features: Single/multi-select, searchable, inline creation, auto-selection FormRepeater - Manage arrays of structured data: \u003CCroutonFormRepeater\n  v-model=\"state.contacts\"\n  :component=\"ContactItem\"\n/> Features: Add/remove, drag-to-reorder, dynamic components FormDependentFieldLoader - Conditional fields: \u003CCroutonFormDependentFieldLoader\n  :depends-on=\"['category']\"\n  :values=\"{ category: state.category }\"\n  component-path=\"CategorySpecificFields\"\n/>",{"id":3978,"title":3571,"titles":3979,"content":528,"level":391},"/patterns/forms#common-workflows",[149],{"id":3981,"title":3982,"titles":3983,"content":3984,"level":449},"/patterns/forms#create-workflow","Create Workflow",[149,3571],"User clicks \"Create\" buttonopen('create', 'users', [])Form.vue renders slideoverFormDynamicLoader loads UsersForm.vueUser fills form fieldshandleSubmit() calls create(data)API call succeedsCache invalidatedForm closes automaticallyToast notification shown",{"id":3986,"title":3987,"titles":3988,"content":3989,"level":449},"/patterns/forms#update-workflow","Update Workflow",[149,3571],"User clicks edit buttonopen('update', 'users', ['user-123'])Form pre-populated with activeItem dataUser modifies fieldshandleSubmit() calls update(id, data)Optimistic update in cacheForm closesTable row updatesToast notification shown",{"id":3991,"title":3992,"titles":3993,"content":3994,"level":449},"/patterns/forms#nested-creation-workflow","Nested Creation Workflow",[149,3571],"Example: Creating a product and adding a new category inline open('create', 'products', []) → Product form (Level 1)User fills product fieldsUser clicks \"+ Create new\" in category dropdownopen('create', 'categories', []) → Category form (Level 2, nested)User creates categoryCategory form closes, new category auto-selectedUser completes product formProduct created with new category",{"id":3996,"title":3997,"titles":3998,"content":528,"level":391},"/patterns/forms#advanced-patterns","Advanced Patterns",[149],{"id":4000,"title":3132,"titles":4001,"content":4002,"level":449},"/patterns/forms#dependent-fields",[149,3997],"Show different fields based on selected category: \u003Ctemplate>\n  \u003C!-- Category selector -->\n  \u003CUFormField label=\"Category\" name=\"category\">\n    \u003CCroutonFormReferenceSelect\n      v-model=\"state.category\"\n      collection=\"categories\"\n    />\n  \u003C/UFormField>\n\n  \u003C!-- Dependent field loader -->\n  \u003CCroutonFormDependentFieldLoader\n    :depends-on=\"['category']\"\n    :values=\"{ category: state.category }\"\n    component-path=\"CategorySpecificFields\"\n  />\n\u003C/template>",{"id":4004,"title":2445,"titles":4005,"content":4006,"level":449},"/patterns/forms#multi-step-forms",[149,3997],"Use tabs with validation per step: \u003Cscript setup lang=\"ts\">\nconst activeSection = ref('step1')\n\nconst navigationItems = [\n  { label: 'Basic Info', value: 'step1' },\n  { label: 'Details', value: 'step2' },\n  { label: 'Review', value: 'step3' }\n]\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonFormLayout\n    :tabs=\"true\"\n    :navigation-items=\"navigationItems\"\n    v-model=\"activeSection\"\n  >\n    \u003C!-- Step content with v-show -->\n  \u003C/CroutonFormLayout>\n\u003C/template>",{"id":4008,"title":4009,"titles":4010,"content":4011,"level":449},"/patterns/forms#validation-error-tracking","Validation Error Tracking",[149,3997],"Track errors per tab: // Field → Tab mapping\nconst fieldToGroup: Record\u003Cstring, string> = {\n  'name': 'general',\n  'email': 'general',\n  'metaTitle': 'seo',\n  'metaDescription': 'seo'\n}\n\nconst validationErrors = ref\u003Cany[]>([])\n\nconst handleValidationError = (event: any) => {\n  if (event?.errors) {\n    validationErrors.value = event.errors\n  }\n}\n\n// Count errors per tab\nconst tabErrorCounts = computed(() => {\n  const counts: Record\u003Cstring, number> = {}\n  validationErrors.value.forEach(error => {\n    const tabName = fieldToGroup[error.name] || 'general'\n    counts[tabName] = (counts[tabName] || 0) + 1\n  })\n  return counts\n})",{"id":4013,"title":44,"titles":4014,"content":528,"level":391},"/patterns/forms#best-practices",[149],{"id":4016,"title":4017,"titles":4018,"content":4019,"level":449},"/patterns/forms#keep-generated-code-intact","Keep Generated Code Intact",[149,44],"When adding custom fields: Don't remove generated fields unless you're sureAdd custom fields after generated ones for clarityKeep the generated submit handler and buttonComment your customizations for future reference \u003Ctemplate>\n  \u003CUForm>\n    \u003C!-- Generated fields -->\n    \u003CUFormField label=\"Name\" name=\"name\">\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n\n    \u003C!-- Custom fields - Added for category support -->\n    \u003CUFormField label=\"Category\" name=\"categoryId\">\n      \u003CUSelectMenu v-model=\"state.categoryId\" :options=\"categories\" />\n    \u003C/UFormField>\n\n    \u003C!-- Keep generated button -->\n    \u003CCroutonFormActionButton :action=\"action\" :loading=\"loading\" />\n  \u003C/UForm>\n\u003C/template>",{"id":4021,"title":4022,"titles":4023,"content":4024,"level":449},"/patterns/forms#update-typescript-types","Update TypeScript Types",[149,44],"Add custom fields to your type definitions: // layers/shop/types/products.ts\nexport interface ShopProduct {\n  // Generated fields\n  id: string\n  name: string\n  price: number\n\n  // Custom fields\n  categoryId?: string\n  sku?: string\n  tags?: string[]\n}",{"id":4026,"title":4027,"titles":4028,"content":4029,"level":449},"/patterns/forms#performance-considerations","Performance Considerations",[149,44],"For large forms (50+ fields): Use tabs to organize into sectionsLazy load heavy componentsDebounce auto-save operationsOptimize repeaters with virtual scrolling For nested forms: Limit nesting to 2-3 levels for better UXUse breadcrumbs clearlyConsider alternative UX (modal for simple edits)Add \"Back\" buttons with context Cache management: Forms auto-invalidate cache on successNo manual refresh neededList refreshes automatically",{"id":4031,"title":36,"titles":4032,"content":528,"level":391},"/patterns/forms#troubleshooting",[149],{"id":4034,"title":4035,"titles":4036,"content":4037,"level":449},"/patterns/forms#form-doesnt-open","Form doesn't open",[149,36],"Symptoms: Clicking create/edit does nothing Solutions: Check collection name matches exactlyRun generator: npx crouton-generate config crouton.config.jsCheck component exists: components/[Collection]Form.vueCheck browser console for errors",{"id":4039,"title":4040,"titles":4041,"content":4042,"level":449},"/patterns/forms#form-submits-but-nothing-happens","Form submits but nothing happens",[149,36],"Symptoms: No error, no success toast, form doesn't close Solutions: Check API returns 200/201 statusCheck mutation composable setupCheck close() is called after successCheck cache invalidation in devtools",{"id":4044,"title":4045,"titles":4046,"content":4047,"level":449},"/patterns/forms#validation-errors-not-showing","Validation errors not showing",[149,36],"Symptoms: Form submits with invalid data Solutions: Check schema is definedCheck @error handler attachedCheck field names match schema keysCheck UForm wraps all fields",{"id":4049,"title":4050,"titles":4051,"content":4052,"level":391},"/patterns/forms#summary","Summary",[149],"Nuxt Crouton's form system provides: ✅ Automatic form generation from schemas✅ Multiple container types (modal/slideover/dialog/inline)✅ Dynamic component loading per collection✅ Nested form support (up to 5 levels)✅ Validation tracking with visual indicators✅ Specialized components (references, repeaters, dependent fields)✅ Optimistic updates with cache invalidation✅ Responsive layouts with tabs and sidebar✅ Error handling with user-friendly messages For most use cases, the generated forms \"just work\". For advanced scenarios, every piece is customizable while maintaining the core architecture.",{"id":4054,"title":1562,"titles":4055,"content":4056,"level":391},"/patterns/forms#related-topics",[149],"Working with RelationsTable PatternsCustom Components html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":154,"title":153,"titles":4058,"content":4059,"level":385},[],"Working with tables in Nuxt Crouton - composition, configuration, pagination, and related data",{"id":4061,"title":153,"titles":4062,"content":4063,"level":385},"/patterns/tables#table-patterns",[],"Query Examples: For complete useCollectionQuery patterns (basic, filtering, pagination, sorting, relations), see Querying Data. Learn how to work with Nuxt Crouton's table system - from basic composition to advanced configurations and displaying related data. For general table concepts and Nuxt UI table components, see the Nuxt UI Table documentation. Complete CroutonTable API Reference: For comprehensive table component documentation including all props, slots, and events, see Table Components API Reference.",{"id":4065,"title":4066,"titles":4067,"content":4068,"level":391},"/patterns/tables#table-component-architecture","Table Component Architecture",[153],"Nuxt Crouton provides specialized components that work together to create feature-rich data tables: ComponentPurposeKey FeaturesTableHeaderNavigation bar with title and create buttonCollection formatting, responsive labels, modal integrationTableSearchDebounced search inputv-model support, configurable debounce, search iconTableActionsBulk operations (delete, column visibility)Row selection state, delete confirmation, column togglesTablePaginationPage navigation and size controlsPage range display, loading states, i18n support",{"id":4070,"title":4071,"titles":4072,"content":4073,"level":449},"/patterns/tables#component-layout","Component Layout",[153,4066],"\u003CUDashboardPanel>\n  \u003C!-- 1. Header Section -->\n  \u003Ctemplate #header>\n    \u003CTableHeader :collection=\"collection\" :create-button=\"true\" />\n  \u003C/template>\n\n  \u003C!-- 2. Body Section -->\n  \u003Ctemplate #body>\n    \u003C!-- 2a. Controls Row -->\n    \u003Cdiv class=\"flex justify-between\">\n      \u003CTableSearch v-model=\"search\" />\n      \u003CTableActions\n        :selected-rows=\"selectedRows\"\n        :collection=\"collection\"\n        :table=\"tableRef\"\n        @delete=\"handleDelete\"\n      />\n    \u003C/div>\n\n    \u003C!-- 2b. Data Table -->\n    \u003CUTable\n      v-model:row-selection=\"selectedRows\"\n      ref=\"tableRef\"\n      :data=\"rows\"\n      :columns=\"columns\"\n      :loading=\"loading\"\n    />\n\n    \u003C!-- 2c. Footer Controls -->\n    \u003CTablePagination\n      :page=\"page\"\n      :page-count=\"pageCount\"\n      :total-items=\"totalItems\"\n      :loading=\"loading\"\n      @update:page=\"page = $event\"\n      @update:page-count=\"handlePageCountChange\"\n    />\n  \u003C/template>\n\u003C/UDashboardPanel>",{"id":4075,"title":4076,"titles":4077,"content":528,"level":391},"/patterns/tables#basic-table-setup","Basic Table Setup",[153],{"id":4079,"title":4080,"titles":4081,"content":528,"level":449},"/patterns/tables#progressive-examples","Progressive Examples",[153,4076],{"id":4083,"title":4084,"titles":4085,"content":4086,"level":748},"/patterns/tables#basic-table","Basic Table",[153,4076,4080],"\u003Cscript setup lang=\"ts\">\nconst columns = [\n  { accessorKey: 'name', header: 'Name' },\n  { accessorKey: 'email', header: 'Email' }\n]\n\nconst { data } = await useCollectionQuery('users')\nconst rows = computed(() => data.value?.items || [])\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUTable :data=\"rows\" :columns=\"columns\" />\n\u003C/template>",{"id":4088,"title":4089,"titles":4090,"content":4091,"level":748},"/patterns/tables#with-search","With Search",[153,4076,4080],"\u003Cscript setup lang=\"ts\">\nconst search = ref('')\n\nconst { data } = await useCollectionQuery('users', {\n  query: computed(() => ({ search: search.value }))\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CTableSearch v-model=\"search\" placeholder=\"Search users...\" />\n  \u003CUTable :data=\"rows\" :columns=\"columns\" />\n\u003C/template>",{"id":4093,"title":4094,"titles":4095,"content":4096,"level":748},"/patterns/tables#with-pagination","With Pagination",[153,4076,4080],"\u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst pageCount = ref(10)\n\nconst { data, refresh } = await useCollectionQuery('users', {\n  query: computed(() => ({\n    page: page.value,\n    pageSize: pageCount.value\n  }))\n})\n\nconst rows = computed(() => data.value?.items || [])\nconst totalItems = computed(() => data.value?.pagination?.totalItems || 0)\n\nasync function handlePageChange(newPage: number) {\n  page.value = newPage\n  await refresh()\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUTable :data=\"rows\" :columns=\"columns\" />\n  \u003CTablePagination\n    :page=\"page\"\n    :page-count=\"pageCount\"\n    :total-items=\"totalItems\"\n    @update:page=\"handlePageChange\"\n  />\n\u003C/template>",{"id":4098,"title":4099,"titles":4100,"content":4101,"level":748},"/patterns/tables#complete-with-actions","Complete with Actions",[153,4076,4080],"\u003Cscript setup lang=\"ts\">\nconst collection = 'users'\nconst selectedRows = ref([])\nconst tableRef = ref()\n\nasync function handleDelete(ids: string[]) {\n  selectedRows.value = []\n  await refresh()\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUDashboardPanel>\n    \u003Ctemplate #header>\n      \u003CTableHeader :collection=\"collection\" :create-button=\"true\" />\n    \u003C/template>\n\n    \u003Ctemplate #body>\n      \u003CTableActions\n        :selected-rows=\"selectedRows\"\n        :collection=\"collection\"\n        :table=\"tableRef\"\n        @delete=\"handleDelete\"\n      />\n\n      \u003CUTable\n        v-model:row-selection=\"selectedRows\"\n        ref=\"tableRef\"\n        :data=\"rows\"\n        :columns=\"columns\"\n      />\n    \u003C/template>\n  \u003C/UDashboardPanel>\n\u003C/template>",{"id":4103,"title":4104,"titles":4105,"content":4106,"level":391},"/patterns/tables#displaying-related-data","Displaying Related Data",[153],"There are two main approaches to displaying related data in tables, each with different trade-offs.",{"id":4108,"title":4109,"titles":4110,"content":4111,"level":449},"/patterns/tables#option-1-fetch-separately-simple","Option 1: Fetch Separately (Simple)",[153,4104],"Best for: Small datasets, simple apps, prototyping Fetch both collections separately and map them in the component: \u003Cscript setup lang=\"ts\">\nconst { items: products } = await useCollectionQuery('shopProducts')\nconst { items: categories } = await useCollectionQuery('shopCategories')\n\n// Map categories by ID for quick lookup\nconst categoryMap = computed(() =>\n  Object.fromEntries(categories.value.map(c => [c.id, c]))\n)\n\nconst columns = [\n  { accessorKey: 'name', header: 'Product' },\n  { accessorKey: 'price', header: 'Price' },\n  {\n    accessorKey: 'category',\n    header: 'Category',\n    // Look up category name\n    cell: ({ row }) => categoryMap.value[row.original.categoryId]?.name || 'N/A'\n  }\n]\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection :rows=\"products\" :columns=\"columns\" />\n\u003C/template> Object Mapping: Learn more about object mapping with computed() in the Vue documentation. Advantages: Simple to implementUses existing collection queriesWorks well for small datasetsEasy to understand and debug Disadvantages: Two separate queriesAll categories loaded even if not usedNot ideal for large datasets",{"id":4113,"title":4114,"titles":4115,"content":4116,"level":449},"/patterns/tables#option-2-server-side-join-efficient","Option 2: Server-Side Join (Efficient)",[153,4104],"Best for: Large datasets, performance-critical apps, complex filtering Create a custom API endpoint that joins the data on the server: // server/api/teams/[team]/shop-products-with-category.get.ts\nimport { db } from '~/server/database'\nimport { shopProducts, shopCategories } from '~/layers/shop/server/database/schema'\n\nexport default defineEventHandler(async (event) => {\n  const teamId = getRouterParam(event, 'team')\n\n  // Drizzle relations query (if you set up relations)\n  const products = await db.query.shopProducts.findMany({\n    where: eq(shopProducts.teamId, teamId),\n    with: { category: true }  // Join automatically\n  })\n\n  return products\n}) Use in your component: \u003Cscript setup lang=\"ts\">\n// Custom endpoint with joined data\nconst { data: products } = await useFetch('/api/teams/current/shop-products-with-category')\n\nconst columns = [\n  { accessorKey: 'name', header: 'Product' },\n  { accessorKey: 'price', header: 'Price' },\n  {\n    accessorKey: 'category.name',  // Access nested data\n    header: 'Category'\n  }\n]\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection :rows=\"products\" :columns=\"columns\" />\n\u003C/template> Advantages: Single query (efficient)Scales to large datasetsCan filter by related fieldsBetter performance Disadvantages: More setup requiredNeeds Drizzle relations configuredCustom API endpoint to maintain",{"id":4118,"title":4119,"titles":4120,"content":4121,"level":449},"/patterns/tables#when-to-use-each-approach","When to Use Each Approach",[153,4104],"Use Option 1 (Fetch Separately) When: You have less than 100 itemsRelations are optional/occasionalYou're prototyping or learningSimplicity is more important than performance Use Option 2 (Server-Side Join) When: You have hundreds or thousands of itemsYou need to filter by related fieldsPerformance is criticalYou're fetching related data frequently Rule of thumb: Start with Option 1, migrate to Option 2 when you encounter performance issues.",{"id":4123,"title":4124,"titles":4125,"content":4126,"level":391},"/patterns/tables#pagination-strategies","Pagination Strategies",[153],"Complete Pagination Guide: For step-by-step instructions on adding server-side pagination to generated collections, see the Pagination Guide.",{"id":4128,"title":4129,"titles":4130,"content":4131,"level":449},"/patterns/tables#client-side-pagination-default","Client-Side Pagination (Default)",[153,4124],"Best for small datasets (\u003C 1000 items). All data loads at once, pagination happens in the browser: \u003Cscript setup lang=\"ts\">\nconst { items, pending } = await useCollectionQuery('shopProducts')\nconst { columns } = useShopProducts()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"items\"\n    :columns=\"columns\"\n    layout=\"table\"\n  />\n  \u003C!-- Pagination handled automatically in browser -->\n\u003C/template> Pros: Instant pagination, no API calls, offline-capable\nCons: Slow initial load for large datasets, high memory usage",{"id":4133,"title":4134,"titles":4135,"content":4136,"level":449},"/patterns/tables#server-side-pagination","Server-Side Pagination",[153,4124],"Best for large datasets (> 1000 items). Only loads one page at a time: \u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst pageSize = ref(25)\n\nconst { items, pending, refresh } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({\n    page: page.value,\n    pageSize: pageSize.value\n  }))\n})\n\nconst { columns } = useShopProducts()\n\n// Pagination data from server\nconst paginationData = computed(() => ({\n  currentPage: page.value,\n  pageSize: pageSize.value,\n  totalItems: 10000, // From your API\n  totalPages: 400\n}))\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"items\"\n    :columns=\"columns\"\n    layout=\"table\"\n    server-pagination\n    :pagination-data=\"paginationData\"\n    :refresh-fn=\"refresh\"\n  />\n\u003C/template> Pros: Fast initial load, low memory, scalable\nCons: Network latency on page changes",{"id":4138,"title":4139,"titles":4140,"content":4141,"level":449},"/patterns/tables#api-implementation-for-server-pagination","API Implementation for Server Pagination",[153,4124],"// server/api/teams/[team]/shop-products/index.get.ts\nexport default defineEventHandler(async (event) => {\n  const query = getQuery(event)\n  const page = Number(query.page) || 1\n  const pageSize = Number(query.pageSize) || 25\n  const offset = (page - 1) * pageSize\n\n  const [items, totalCount] = await Promise.all([\n    db.select()\n      .from(products)\n      .limit(pageSize)\n      .offset(offset),\n    db.select({ count: count() })\n      .from(products)\n      .then(r => r[0].count)\n  ])\n\n  return {\n    items,\n    pagination: {\n      currentPage: page,\n      pageSize,\n      totalItems: totalCount,\n      totalPages: Math.ceil(totalCount / pageSize)\n    }\n  }\n})",{"id":4143,"title":4144,"titles":4145,"content":528,"level":391},"/patterns/tables#advanced-configuration","Advanced Configuration",[153],{"id":4147,"title":4148,"titles":4149,"content":4150,"level":449},"/patterns/tables#column-visibility-toggle","Column Visibility Toggle",[153,4144],"Users can show/hide columns dynamically: \u003Cscript setup lang=\"ts\">\nconst columnVisibility = ref({\n  id: false,          // Hide ID column by default\n  sku: true,\n  price: true,\n  category: true\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection\n    v-model:column-visibility=\"columnVisibility\"\n    :rows=\"items\"\n    :columns=\"columns\"\n  />\n\u003C/template> The table toolbar automatically includes a column visibility menu.",{"id":4152,"title":4153,"titles":4154,"content":4155,"level":449},"/patterns/tables#hiding-default-columns","Hiding Default Columns",[153,4144],"Tables include default columns: createdAt, updatedAt, updatedBy, select, presence, and actions. Hide them selectively: \u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"items\"\n    :columns=\"columns\"\n    :hide-default-columns=\"{\n      createdAt: true,   // Hide creation date\n      updatedAt: true,   // Hide update date\n      updatedBy: false,  // Show updated by user (default: shown)\n      actions: false     // Show actions (edit/delete buttons)\n    }\"\n  />\n\u003C/template>",{"id":4157,"title":4158,"titles":4159,"content":4160,"level":449},"/patterns/tables#row-selection-bulk-operations","Row Selection & Bulk Operations",[153,4144],"Enable bulk operations with row selection: \u003Cscript setup lang=\"ts\">\nconst selectedRows = ref([])\nconst { deleteItems } = useCollectionMutation('shopProducts')\n\nconst handleBulkDelete = async () => {\n  const ids = selectedRows.value.map(row => row.id)\n  await deleteItems(ids)\n  selectedRows.value = []\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonCollection\n      v-model:selected=\"selectedRows\"\n      :rows=\"items\"\n      :columns=\"columns\"\n      selectable\n    />\n\n    \u003CUButton\n      v-if=\"selectedRows.length > 0\"\n      @click=\"handleBulkDelete\"\n      color=\"red\"\n    >\n      Delete {{ selectedRows.length }} items\n    \u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":4162,"title":4163,"titles":4164,"content":4165,"level":449},"/patterns/tables#sorting","Sorting",[153,4144],"Client-Side Sorting: \u003Cscript setup lang=\"ts\">\nconst columns = [\n  { accessorKey: 'name', header: 'Name', sortable: true },\n  { accessorKey: 'price', header: 'Price', sortable: true },\n  { accessorKey: 'category', header: 'Category', sortable: false }\n]\n\u003C/script> Server-Side Sorting: \u003Cscript setup lang=\"ts\">\nconst sortBy = ref('createdAt')\nconst sortDirection = ref\u003C'asc' | 'desc'>('desc')\n\nconst { items } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({\n    sortBy: sortBy.value,\n    sortDirection: sortDirection.value\n  }))\n})\n\nconst paginationData = computed(() => ({\n  sortBy: sortBy.value,\n  sortDirection: sortDirection.value\n}))\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"items\"\n    :columns=\"columns\"\n    server-pagination\n    :pagination-data=\"paginationData\"\n  />\n\u003C/template>",{"id":4167,"title":4168,"titles":4169,"content":4170,"level":449},"/patterns/tables#drag-and-drop-row-reordering","Drag-and-Drop Row Reordering",[153,4144],"Enable users to reorder table rows by dragging them. This feature uses SortableJS under the hood. New in v1.7: Drag-and-drop reordering is now available for table layouts.",{"id":4172,"title":4173,"titles":4174,"content":4175,"level":748},"/patterns/tables#basic-usage","Basic Usage",[153,4144,4168],"Enable drag-and-drop with the sortable prop: \u003Ctemplate>\n  \u003CCroutonCollection\n    layout=\"table\"\n    collection=\"tasks\"\n    :rows=\"items\"\n    sortable\n  />\n\u003C/template> This adds a drag handle column and allows users to reorder rows by dragging.",{"id":4177,"title":4178,"titles":4179,"content":4180,"level":748},"/patterns/tables#sortable-options","Sortable Options",[153,4144,4168],"Pass an object to customize the behavior: \u003Ctemplate>\n  \u003CCroutonCollection\n    layout=\"table\"\n    collection=\"tasks\"\n    :rows=\"items\"\n    :sortable=\"{\n      handle: true,      // Show drag handle column (default: true)\n      animation: 150,    // Animation duration in ms (default: 150)\n      disabled: false    // Temporarily disable dragging\n    }\"\n  />\n\u003C/template> OptionTypeDefaultDescriptionhandlebooleantrueShow drag handle column with grip iconanimationnumber150SortableJS animation duration in millisecondsdisabledbooleanfalseTemporarily disable drag-and-drop",{"id":4182,"title":4183,"titles":4184,"content":4185,"level":748},"/patterns/tables#requirements","Requirements",[153,4144,4168],"1. Add an order field to your schema: // server/database/schema.ts\nexport const tasks = sqliteTable('tasks', {\n  id: text('id').primaryKey().$default(() => nanoid()),\n  title: text('title').notNull(),\n  order: integer('order').notNull().$default(() => 0),\n  // ... other fields\n}) 2. Create a reorder API endpoint: // server/api/teams/[id]/tasks/reorder.patch.ts\nimport { eq, and } from 'drizzle-orm'\nimport { tasks } from '~/server/database/schema'\nimport { resolveTeamAndCheckMembership } from '@fyit/crouton-auth/server'\n\nexport default defineEventHandler(async (event) => {\n  const { team, user } = await resolveTeamAndCheckMembership(event)\n  const body = await readBody\u003C{ updates: Array\u003C{ id: string; order: number }> }>(event)\n\n  if (!body.updates || !Array.isArray(body.updates)) {\n    throw createError({ status: 400, statusText: 'Invalid updates array' })\n  }\n\n  const db = useDB()\n\n  await Promise.all(\n    body.updates.map(({ id, order }) =>\n      db.update(tasks)\n        .set({ order, updatedBy: user.id })\n        .where(and(eq(tasks.id, id), eq(tasks.teamId, team.id)))\n    )\n  )\n\n  return { success: true }\n}) 3. Sort by order in your queries: // In your query function\nimport { asc, desc } from 'drizzle-orm'\n\nconst items = await db.select()\n  .from(tasks)\n  .where(eq(tasks.teamId, teamId))\n  .orderBy(asc(tasks.order), desc(tasks.createdAt))",{"id":4187,"title":1635,"titles":4188,"content":4189,"level":748},"/patterns/tables#how-it-works",[153,4144,4168],"When sortable is enabled, a drag handle column is added to the tableUsers drag rows using the grip iconOn drop, useTreeMutation().reorderSiblings() is called automaticallyThe API endpoint updates all affected order valuesThe table refreshes to reflect the new order",{"id":4191,"title":4192,"titles":4193,"content":4194,"level":748},"/patterns/tables#example-task-list-with-reordering","Example: Task List with Reordering",[153,4144,4168],"\u003Cscript setup lang=\"ts\">\nconst { items, pending } = await useCollectionQuery('tasks', {\n  query: { projectId: props.projectId }\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003Cdiv class=\"flex justify-between items-center\">\n      \u003Ch2>Tasks\u003C/h2>\n      \u003CUButton icon=\"i-lucide-plus\" @click=\"openCreate\">Add Task\u003C/UButton>\n    \u003C/div>\n\n    \u003CCroutonCollection\n      v-if=\"items?.length\"\n      layout=\"table\"\n      collection=\"tasks\"\n      :rows=\"items\"\n      sortable\n      :hide-default-columns=\"{ createdAt: true, updatedAt: true }\"\n    />\n\n    \u003Cp v-else class=\"text-muted text-center py-8\">\n      No tasks yet. Create one to get started.\n    \u003C/p>\n  \u003C/div>\n\u003C/template> Tip: Drag-and-drop works best with smaller datasets. For large lists (100+ items), consider using server-side pagination and only allowing reordering within the current page.",{"id":4196,"title":3997,"titles":4197,"content":528,"level":391},"/patterns/tables#advanced-patterns",[153],{"id":4199,"title":4200,"titles":4201,"content":4202,"level":449},"/patterns/tables#server-side-filtering-with-ui","Server-Side Filtering with UI",[153,3997],"Combine search and filters with server-side processing: For a complete working example demonstrating server-side filtering with a collapsible filter panel, see this interactive demo: View Full Interactive Demo →Fork the demo to explore advanced filtering patterns. The complete example includes:Collapsible filter paneluseCollectionQuery with reactive filtersSearch + advanced filters combinedFilter reset functionalityTable components (Header, Search, Actions, Pagination)Server-side processing Focused Example: Reactive Server-Side Filters This snippet shows the key pattern for combining search and advanced filters with server-side processing: \u003Cscript setup lang=\"ts\">\nconst showFilters = ref(false)\nconst filters = ref({\n  status: null,\n  role: null,\n  dateRange: null\n})\n\nconst { data, pending: loading, refresh } = await useCollectionQuery('users', {\n  query: computed(() => ({\n    page: page.value,\n    pageSize: pageCount.value,\n    search: search.value,\n    ...filters.value\n  }))\n})\n\nasync function applyFilters() {\n  page.value = 1 // Reset to first page\n  await refresh()\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUDashboardPanel>\n    \u003Ctemplate #header>\n      \u003CTableHeader :collection=\"collection\" :create-button=\"true\">\n        \u003Ctemplate #extraButtons>\n          \u003CUButton icon=\"i-lucide-filter\" @click=\"showFilters = true\">\n            Filters\n          \u003C/UButton>\n        \u003C/template>\n      \u003C/TableHeader>\n    \u003C/template>\n    \u003C!-- See interactive demo for complete filter panel and table -->\n  \u003C/UDashboardPanel>\n\u003C/template>",{"id":4204,"title":4205,"titles":4206,"content":4207,"level":449},"/patterns/tables#custom-bulk-actions","Custom Bulk Actions",[153,3997],"Extend TableActions with custom bulk operations: \u003Ctemplate>\n  \u003Cdiv class=\"flex items-center gap-2\">\n    \u003CTableActions\n      :selected-rows=\"selectedRows\"\n      :collection=\"collection\"\n      :table=\"tableRef\"\n      @delete=\"handleDelete\"\n    />\n\n    \u003Ctemplate v-if=\"selectedRows.length > 0\">\n      \u003CUButton color=\"green\" variant=\"soft\" icon=\"i-lucide-check-circle\" @click=\"bulkApprove\">\n        Approve Selected\n      \u003C/UButton>\n\n      \u003CUButton color=\"gray\" variant=\"soft\" icon=\"i-lucide-download\" @click=\"bulkExport\">\n        Export CSV\n      \u003C/UButton>\n    \u003C/template>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedRows = ref([])\n\nasync function bulkApprove() {\n  const ids = selectedRows.value.map(row => row.id)\n  await $fetch('/api/users/bulk-approve', { method: 'POST', body: { ids } })\n  await refresh()\n  selectedRows.value = []\n}\n\nasync function bulkExport() {\n  const ids = selectedRows.value.map(row => row.id)\n  const csv = await $fetch('/api/users/export', { query: { ids: ids.join(',') } })\n  const blob = new Blob([csv], { type: 'text/csv' })\n  const url = URL.createObjectURL(blob)\n  const a = document.createElement('a')\n  a.href = url\n  a.download = 'users.csv'\n  a.click()\n  selectedRows.value = []\n}\n\u003C/script>",{"id":4209,"title":4210,"titles":4211,"content":4212,"level":449},"/patterns/tables#persistent-state-url-query-params","Persistent State (URL Query Params)",[153,3997],"Persist table state in URL for shareable links: \u003Cscript setup lang=\"ts\">\nconst route = useRoute()\nconst router = useRouter()\n\n// Initialize from URL\nconst search = ref(route.query.search as string || '')\nconst page = ref(Number(route.query.page) || 1)\nconst pageCount = ref(Number(route.query.limit) || 10)\n\n// Watch for changes and update URL\nwatch([search, page, pageCount], () => {\n  router.push({\n    query: {\n      ...route.query,\n      search: search.value || undefined,\n      page: page.value > 1 ? String(page.value) : undefined,\n      limit: pageCount.value !== 10 ? String(pageCount.value) : undefined\n    }\n  })\n})\n\n// When user shares URL, table state is preserved\n\u003C/script> URL State Sync: Learn more about watch() and Vue Router query parameters in the Vue documentation and Vue Router docs.",{"id":4214,"title":4215,"titles":4216,"content":4217,"level":391},"/patterns/tables#using-croutontable-all-in-one","Using CroutonTable (All-in-One)",[153],"CroutonTable automatically composes all table components for you: \u003Ctemplate>\n  \u003C!-- All-in-one approach -->\n  \u003CCroutonTable\n    :collection=\"collection\"\n    :rows=\"rows\"\n    :columns=\"columns\"\n    :create=\"true\"              \u003C!-- TableHeader with create button -->\n    searchable                   \u003C!-- TableSearch included -->\n    selection                    \u003C!-- TableActions included -->\n    :server-pagination=\"true\"    \u003C!-- TablePagination included -->\n    :pagination-data=\"paginationData\"\n  />\n\u003C/template> When to use CroutonTable vs Manual Composition: Use CroutonTable WhenUse Manual Composition WhenStandard CRUD tablesCustom layouts neededRapid prototypingAdvanced filtering UIFollowing conventionsCustom bulk actionsSimple data displayComplex state management",{"id":4219,"title":44,"titles":4220,"content":528,"level":391},"/patterns/tables#best-practices",[153],{"id":4222,"title":3965,"titles":4223,"content":4224,"level":449},"/patterns/tables#state-management",[153,44],"✅ DO: Use computed properties for derived state const rows = computed(() => data.value?.items || [])\nconst totalItems = computed(() => data.value?.pagination?.totalItems || 0) ❌ DON'T: Duplicate state // Bad - duplicates source of truth\nconst rows = ref([])\nwatch(data, (newData) => {\n  rows.value = newData.items // Unnecessary duplication\n})",{"id":4226,"title":4227,"titles":4228,"content":4229,"level":449},"/patterns/tables#pagination-reset","Pagination Reset",[153,44],"✅ DO: Reset to page 1 when filters change async function handlePageCountChange(newCount: number) {\n  pageCount.value = newCount\n  page.value = 1 // Always reset to first page\n  await refresh()\n}\n\nwatch(search, () => {\n  page.value = 1 // Reset when search changes\n}) ❌ DON'T: Stay on current page after filter change // Bad - might show empty results if page 5 doesn't exist with new filter\nwatch(search, refresh) // Stays on current page",{"id":4231,"title":4232,"titles":4233,"content":4234,"level":449},"/patterns/tables#search-optimization","Search Optimization",[153,44],"✅ DO: Use debounce for search \u003CTableSearch\n  v-model=\"search\"\n  :debounce-ms=\"300\"  \u003C!-- Prevents excessive API calls -->\n/> ❌ DON'T: Search on every keystroke // Bad - triggers API on every keystroke\nwatch(search, async (value) => {\n  await $fetch('/api/search', { query: { q: value } })\n})",{"id":4236,"title":2973,"titles":4237,"content":4238,"level":449},"/patterns/tables#loading-states",[153,44],"✅ DO: Show loading states during operations const loading = ref(false)\n\nasync function handleDelete(ids: string[]) {\n  loading.value = true\n  try {\n    await $fetch('/api/delete', { body: { ids } })\n  } finally {\n    loading.value = false\n  }\n}",{"id":4240,"title":4241,"titles":4242,"content":4243,"level":449},"/patterns/tables#general-guidelines","General Guidelines",[153,44],"✅ DO: Use server pagination for datasets > 1000 itemsImplement search on the server for better performanceShow loading states during data fetchesEnable sorting on relevant columns onlyHide unnecessary default columns ❌ DON'T: Mix client and server pagination logicForget to handle loading statesMake every column sortable (UX anti-pattern)Skip error handling on refreshLoad all data with client pagination if you have 10,000+ items",{"id":4245,"title":4246,"titles":4247,"content":528,"level":391},"/patterns/tables#performance-tips","Performance Tips",[153],{"id":4249,"title":4250,"titles":4251,"content":4252,"level":449},"/patterns/tables#virtualize-large-tables","Virtualize Large Tables",[153,4246],"For tables with 1000+ rows, use virtualization: \u003Ctemplate>\n  \u003CUTable\n    :data=\"rows\"\n    :columns=\"columns\"\n    virtual\n    :virtual-row-height=\"48\"\n  />\n\u003C/template>",{"id":4254,"title":4255,"titles":4256,"content":4257,"level":449},"/patterns/tables#optimize-search-debounce","Optimize Search Debounce",[153,4246],"Adjust debounce based on operation cost: \u003C!-- Light operations: 300ms -->\n\u003CTableSearch :debounce-ms=\"300\" />\n\n\u003C!-- Heavy API calls: 500-1000ms -->\n\u003CTableSearch :debounce-ms=\"800\" />",{"id":4259,"title":4260,"titles":4261,"content":4262,"level":449},"/patterns/tables#use-server-side-pagination","Use Server-Side Pagination",[153,4246],"For large datasets, always use server-side pagination: const { data } = await useCollectionQuery('products', {\n  query: computed(() => ({\n    page: page.value,\n    pageSize: pageCount.value\n  }))\n})\n// Only fetches current page, not all items",{"id":4264,"title":36,"titles":4265,"content":528,"level":391},"/patterns/tables#troubleshooting",[153],{"id":4267,"title":4268,"titles":4269,"content":4270,"level":449},"/patterns/tables#search-not-working","Search not working",[153,36],"Problem: Search input changes but table doesn't update Solution: Ensure search is reactive (ref())Pass as computed to useCollectionQueryReset page to 1 when search changes const search = ref('')\n\nwatch(search, () => {\n  page.value = 1 // Important!\n})\n\nconst { data } = await useCollectionQuery('users', {\n  query: computed(() => ({ search: search.value }))\n})",{"id":4272,"title":4273,"titles":4274,"content":4275,"level":449},"/patterns/tables#pagination-shows-wrong-range","Pagination shows wrong range",[153,36],"Problem: \"Showing 1-10 of 0 results\" even though items exist Solution: Ensure totalItems reflects the actual total count: const totalItems = computed(() => {\n  return data.value?.pagination?.totalItems || 0\n  // NOT: data.value?.items?.length (this is just current page)\n})",{"id":4277,"title":4278,"titles":4279,"content":4280,"level":449},"/patterns/tables#delete-button-always-disabled","Delete button always disabled",[153,36],"Problem: Delete button is grayed out even when rows are selected Solution: Verify selectedRows is a non-empty array: \u003CUTable\n  v-model:row-selection=\"selectedRows\"\n  \u003C!-- ... -->\n/>\n\n\u003CTableActions\n  :selected-rows=\"selectedRows\"  \u003C!-- Must be the same ref -->\n/>",{"id":4282,"title":1562,"titles":4283,"content":4284,"level":391},"/patterns/tables#related-topics",[153],"Pagination Guide - Adding server-side paginationWorking with RelationsForm PatternsCustom ColumnsResponsive Layouts html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":158,"title":157,"titles":4286,"content":4287,"level":385},[],"Card-based list view with automatic field detection and custom card components Query Examples: For complete useCollectionQuery patterns (basic, filtering, pagination, sorting, relations), see Querying Data. The list layout renders collection data as a vertical, card-based list. Each row is rendered by a card component — either the built-in CroutonDefaultCard or a custom component you provide via the card prop.",{"id":4289,"title":1635,"titles":4290,"content":4291,"level":391},"/patterns/list-layouts#how-it-works",[157],"When you set layout=\"list\" on CroutonCollection, each row is rendered as: \u003Ccomponent :is=\"customCardComponent || CroutonDefaultCard\" :item=\"row\" layout=\"list\" :collection=\"collection\" /> The card component receives the row data as the item prop. CroutonDefaultCard uses useDisplayConfig() to automatically map your data fields to title, subtitle, image, and badge slots.",{"id":4293,"title":4294,"titles":4295,"content":4296,"level":391},"/patterns/list-layouts#automatic-field-detection","Automatic Field Detection",[157],"useDisplayConfig(collectionName) resolves display roles from your collection config. When no explicit display config exists, it infers fields by name and type: RoleInferred from (priority order)Fallbacktitletitle, name, labelFirst string fieldsubtitlesubtitle, description, summary(skips title field)imageFirst field of type image or asset—badgestatus, state, categoryFirst field with displayAs: 'badge'descriptiondescription, summary, excerpt(skips fields already used) You can also set these explicitly in your collection config: // app.config.ts\ncroutonCollections: {\n  products: {\n    display: { title: 'name', subtitle: 'brand', image: 'photo', badge: 'status' }\n  }\n}",{"id":4298,"title":4173,"titles":4299,"content":4300,"level":391},"/patterns/list-layouts#basic-usage",[157],"With standard field names, zero configuration is needed: \u003Cscript setup lang=\"ts\">\nconst { items } = await useCollectionQuery('users')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"items\"\n    layout=\"list\"\n    collection=\"users\"\n  />\n\u003C/template> CroutonDefaultCard automatically displays the name as title, email as subtitle, and avatar/image as a thumbnail.",{"id":4302,"title":4303,"titles":4304,"content":4305,"level":391},"/patterns/list-layouts#responsive-layouts","Responsive Layouts",[157],"Combine list and table layouts using responsive breakpoints: \u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"items\"\n    :layout=\"{\n      base: 'list',\n      lg: 'table'\n    }\"\n    collection=\"users\"\n  />\n\u003C/template> On mobile, users see a card-style list. On desktop (lg+), it switches to a full table. All Tailwind breakpoints are supported: sm, md, lg, xl, 2xl.",{"id":4307,"title":4308,"titles":4309,"content":4310,"level":391},"/patterns/list-layouts#custom-card-components","Custom Card Components",[157],"For full control over how each row renders, create a custom card component and pass it via the card prop.",{"id":4312,"title":4313,"titles":4314,"content":4315,"level":449},"/patterns/list-layouts#how-the-card-prop-works","How the card Prop Works",[157,4308],"The card prop specifies a variant suffix. CroutonCollection resolves it by combining the PascalCase collection name with the variant: collection=\"users\" + card=\"Card\" resolves UsersCardcollection=\"shopProducts\" + card=\"ListItem\" resolves ShopProductsListItem If no card prop is set, the default convention is {Collection}Card (e.g., UsersCard). If that component does not exist, CroutonDefaultCard is used.",{"id":4317,"title":4318,"titles":4319,"content":4320,"level":449},"/patterns/list-layouts#example-user-card","Example: User Card",[157,4308],"\u003C!-- components/UsersCard.vue -->\n\u003Cscript setup lang=\"ts\">\ninterface Props {\n  item: any\n  layout: 'list' | 'grid'\n  collection: string\n  stateless?: boolean\n}\n\nconst props = defineProps\u003CProps>()\n\nconst crouton = useCrouton()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"flex items-center gap-3 w-full\">\n    \u003Cimg\n      v-if=\"item.avatar\"\n      :src=\"item.avatar\"\n      :alt=\"item.name\"\n      class=\"size-10 rounded-full object-cover\"\n    >\n    \u003Cdiv class=\"flex-1 min-w-0\">\n      \u003Cp class=\"font-medium truncate\">{{ item.name }}\u003C/p>\n      \u003Cp class=\"text-sm text-muted truncate\">{{ item.email }}\u003C/p>\n    \u003C/div>\n    \u003CUBadge :color=\"item.role === 'admin' ? 'primary' : 'neutral'\" size=\"xs\">\n      {{ item.role }}\n    \u003C/UBadge>\n    \u003CUDropdownMenu\n      v-if=\"!stateless\"\n      :items=\"[\n        { label: 'Edit', icon: 'i-lucide-edit', click: () => crouton.open('update', collection, [item.id]) },\n        { label: 'Delete', icon: 'i-lucide-trash', color: 'red', click: () => crouton.open('delete', collection, [item.id]) }\n      ]\"\n    >\n      \u003CUButton icon=\"i-lucide-more-vertical\" variant=\"ghost\" size=\"sm\" />\n    \u003C/UDropdownMenu>\n  \u003C/div>\n\u003C/template> Then use it: \u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"users\"\n    layout=\"list\"\n    collection=\"users\"\n  />\n\u003C/template> Since the component is named UsersCard, it is auto-resolved for the users collection. No card prop needed.",{"id":4322,"title":4323,"titles":4324,"content":4325,"level":449},"/patterns/list-layouts#example-product-card","Example: Product Card",[157,4308],"\u003C!-- components/ShopProductsCard.vue -->\n\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{ item: any; layout: 'list' | 'grid'; collection: string; stateless?: boolean }>()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"flex items-center gap-3 w-full\">\n    \u003Cdiv v-if=\"item.image\" class=\"shrink-0 size-12 rounded overflow-hidden bg-gray-100 dark:bg-gray-800\">\n      \u003Cimg :src=\"item.image\" :alt=\"item.name\" class=\"size-full object-cover\">\n    \u003C/div>\n    \u003Cdiv class=\"flex-1 min-w-0\">\n      \u003Cp class=\"font-medium truncate\">{{ item.name }}\u003C/p>\n      \u003Cp class=\"text-sm text-muted truncate\">{{ item.description }}\u003C/p>\n    \u003C/div>\n    \u003Cspan class=\"font-semibold tabular-nums\">${{ item.price?.toFixed(2) }}\u003C/span>\n    \u003CUBadge :color=\"item.inStock ? 'success' : 'error'\" size=\"xs\">\n      {{ item.inStock ? 'In Stock' : 'Out of Stock' }}\n    \u003C/UBadge>\n  \u003C/div>\n\u003C/template>",{"id":4327,"title":4328,"titles":4329,"content":4330,"level":449},"/patterns/list-layouts#example-contact-card-with-actions","Example: Contact Card with Actions",[157,4308],"\u003C!-- components/ContactsCard.vue -->\n\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{ item: any; layout: 'list' | 'grid'; collection: string; stateless?: boolean }>()\n\nconst callContact = (phone: string) => window.location.href = `tel:${phone}`\nconst emailContact = (email: string) => window.location.href = `mailto:${email}`\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"flex items-center gap-3 w-full\">\n    \u003Cimg\n      v-if=\"item.profileImage\"\n      :src=\"item.profileImage\"\n      :alt=\"item.name\"\n      class=\"size-10 rounded-full object-cover\"\n    >\n    \u003Cdiv class=\"flex-1 min-w-0\">\n      \u003Cp class=\"font-medium truncate\">{{ item.name }}\u003C/p>\n      \u003Cp class=\"text-sm text-muted truncate\">{{ item.email }}\u003C/p>\n    \u003C/div>\n    \u003Cdiv class=\"flex items-center gap-1\">\n      \u003CUButton\n        v-if=\"item.phone\"\n        icon=\"i-lucide-phone\"\n        variant=\"ghost\"\n        size=\"sm\"\n        @click=\"callContact(item.phone)\"\n      />\n      \u003CUButton\n        icon=\"i-lucide-mail\"\n        variant=\"ghost\"\n        size=\"sm\"\n        @click=\"emailContact(item.email)\"\n      />\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":4332,"title":44,"titles":4333,"content":4334,"level":391},"/patterns/list-layouts#best-practices",[157],"Field naming -- Use standard names (name, title, email, description, status, image) for automatic detection. Unusual names like usr_nm will not be auto-detected. Custom cards -- Keep list cards horizontally compact. Use truncate on text, icon-only buttons for actions, and dropdown menus when you have more than two actions. Responsive layouts -- Always test on actual mobile widths. The { base: 'list', lg: 'table' } pattern covers most use cases. Touch targets -- Ensure action buttons are at least 44x44px on mobile.",{"id":4336,"title":36,"titles":4337,"content":4338,"level":391},"/patterns/list-layouts#troubleshooting",[157],"Items show IDs instead of titles: Your data fields are not matching the auto-detection heuristics. Either rename fields to standard names, or set explicit display config in your collection. Images not showing: CroutonDefaultCard looks for an image field mapped via useDisplayConfig(). Ensure your field is typed as image or asset in the collection schema, or set display.image explicitly. Custom card not picked up: Verify the component name matches the pattern {PascalCollection}Card (e.g., ShopProductsCard for collection shopProducts). The component must be auto-importable (in components/ directory).",{"id":4340,"title":4341,"titles":4342,"content":4343,"level":391},"/patterns/list-layouts#related","Related",[157],"Responsive Layouts - Layout breakpoint presetsTable Configuration - Table layout patternsCustom Components - Building custom components html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":162,"title":161,"titles":4345,"content":4346,"level":385},[],"Learn how to set up Drizzle relations for advanced querying and performance optimization If you want Drizzle relations for performance, set them up manually. This is optional but recommended for apps with complex relational queries.",{"id":4348,"title":4349,"titles":4350,"content":4351,"level":391},"/patterns/drizzle#step-1-add-foreign-key-to-schema","Step 1: Add Foreign Key to Schema",[161],"Add the foreign key field to your JSON schema: // schemas/product-schema.json\n{\n  \"name\": { \"type\": \"string\" },\n  \"price\": { \"type\": \"number\" },\n  \"categoryId\": { \"type\": \"string\" }\n}",{"id":4353,"title":4354,"titles":4355,"content":4356,"level":391},"/patterns/drizzle#step-2-define-relations-in-schema-files","Step 2: Define Relations in Schema Files",[161],"Add relation definitions to your Drizzle schema: // layers/shop/server/database/schema.ts\nimport { sqliteTable, text, real } from 'drizzle-orm/sqlite-core'\nimport { relations } from 'drizzle-orm'\n\nexport const shopProducts = sqliteTable('shop_products', {\n  id: text('id').primaryKey(),\n  teamId: text('teamId').notNull(),\n  categoryId: text('categoryId'),  // Foreign key\n  name: text('name').notNull(),\n  price: real('price')\n})\n\nexport const shopCategories = sqliteTable('shop_categories', {\n  id: text('id').primaryKey(),\n  teamId: text('teamId').notNull(),\n  name: text('name').notNull()\n})\n\n// Define relations\nexport const shopProductsRelations = relations(shopProducts, ({ one }) => ({\n  category: one(shopCategories, {\n    fields: [shopProducts.categoryId],\n    references: [shopCategories.id]\n  })\n}))\n\nexport const shopCategoriesRelations = relations(shopCategories, ({ many }) => ({\n  products: many(shopProducts)\n}))",{"id":4358,"title":4359,"titles":4360,"content":4361,"level":391},"/patterns/drizzle#step-3-create-query-helpers-optional","Step 3: Create Query Helpers (Optional)",[161],"Create reusable query functions: // layers/shop/server/database/queries.ts\nexport async function getShopProductsWithCategories(teamId: string) {\n  const db = useDB()\n\n  return await db.query.shopProducts.findMany({\n    where: eq(shopProducts.teamId, teamId),\n    with: { category: true },\n    orderBy: desc(shopProducts.createdAt)\n  })\n}\n\nexport async function getShopProductWithCategory(productId: string, teamId: string) {\n  const db = useDB()\n\n  return await db.query.shopProducts.findFirst({\n    where: and(\n      eq(shopProducts.id, productId),\n      eq(shopProducts.teamId, teamId)\n    ),\n    with: { category: true }\n  })\n}",{"id":4363,"title":4364,"titles":4365,"content":4366,"level":391},"/patterns/drizzle#step-4-use-in-api-routes","Step 4: Use in API Routes",[161],"Use your query helpers in API endpoints: // server/api/teams/[team]/shop-products/index.get.ts\nimport { getShopProductsWithCategories } from '~/layers/shop/server/database/queries'\n\nexport default defineEventHandler(async (event) => {\n  const teamId = getRouterParam(event, 'team')\n  return await getShopProductsWithCategories(teamId)\n})",{"id":4368,"title":4369,"titles":4370,"content":4371,"level":391},"/patterns/drizzle#step-5-add-typescript-types","Step 5: Add TypeScript Types",[161],"Define types for your data structures: // layers/shop/types/products.ts\nimport type { shopProducts, shopCategories } from '../server/database/schema'\n\nexport type ShopProduct = typeof shopProducts.$inferSelect\nexport type ShopCategory = typeof shopCategories.$inferSelect\n\n// With relations\nexport interface ShopProductWithCategory extends ShopProduct {\n  category?: ShopCategory | null\n}",{"id":4373,"title":4374,"titles":4375,"content":528,"level":391},"/patterns/drizzle#common-relation-patterns","Common Relation Patterns",[161],{"id":4377,"title":4378,"titles":4379,"content":4380,"level":449},"/patterns/drizzle#belongsto-many-to-one","belongsTo (many-to-one)",[161,4374],"Use case: Many products belong to one category // Schema\nexport const shopProducts = sqliteTable('shop_products', {\n  id: text('id').primaryKey(),\n  categoryId: text('categoryId')  // Foreign key\n})\n\n// Drizzle relation\nexport const shopProductsRelations = relations(shopProducts, ({ one }) => ({\n  category: one(shopCategories, {\n    fields: [shopProducts.categoryId],\n    references: [shopCategories.id]\n  })\n}))\n\n// Query\nconst product = await db.query.shopProducts.findFirst({\n  where: eq(shopProducts.id, '123'),\n  with: { category: true }\n})\nconsole.log(product.category.name)",{"id":4382,"title":4383,"titles":4384,"content":4385,"level":449},"/patterns/drizzle#hasmany-one-to-many","hasMany (one-to-many)",[161,4374],"Use case: One category has many products // Drizzle relation\nexport const shopCategoriesRelations = relations(shopCategories, ({ many }) => ({\n  products: many(shopProducts)\n}))\n\n// Query\nconst category = await db.query.shopCategories.findFirst({\n  where: eq(shopCategories.id, 'cat-123'),\n  with: { products: true }\n})\nconsole.log(category.products.length)  // All products in this category",{"id":4387,"title":4388,"titles":4389,"content":4390,"level":449},"/patterns/drizzle#hasone-one-to-one","hasOne (one-to-one)",[161,4374],"Use case: One user has one profile // Schema\nexport const userProfiles = sqliteTable('user_profiles', {\n  id: text('id').primaryKey(),\n  userId: text('userId').notNull().unique()  // One-to-one\n})\n\n// Drizzle relation\nexport const usersRelations = relations(users, ({ one }) => ({\n  profile: one(userProfiles, {\n    fields: [users.id],\n    references: [userProfiles.userId]\n  })\n}))",{"id":4392,"title":4393,"titles":4394,"content":4395,"level":449},"/patterns/drizzle#manytomany-advanced","manyToMany (advanced)",[161,4374],"Use case: Products can have many tags, tags can belong to many products // Junction table\nexport const productTags = sqliteTable('product_tags', {\n  productId: text('productId').notNull(),\n  tagId: text('tagId').notNull()\n})\n\n// Relations\nexport const shopProductsRelations = relations(shopProducts, ({ many }) => ({\n  productTags: many(productTags)\n}))\n\nexport const productTagsRelations = relations(productTags, ({ one }) => ({\n  product: one(shopProducts, {\n    fields: [productTags.productId],\n    references: [shopProducts.id]\n  }),\n  tag: one(tags, {\n    fields: [productTags.tagId],\n    references: [tags.id]\n  })\n}))\n\nexport const tagsRelations = relations(tags, ({ many }) => ({\n  productTags: many(productTags)\n}))\n\n// Query (requires nested relations)\nconst product = await db.query.shopProducts.findFirst({\n  with: {\n    productTags: {\n      with: {\n        tag: true\n      }\n    }\n  }\n})",{"id":4397,"title":4398,"titles":4399,"content":528,"level":391},"/patterns/drizzle#when-to-query-relations","When to Query Relations",[161],{"id":4401,"title":4402,"titles":4403,"content":4404,"level":449},"/patterns/drizzle#in-the-component-option-1","In the Component (Option 1)",[161,4398],"Best for: ✅ Simple queries✅ Data already cached✅ Quick prototypes Query Examples: For complete useCollectionQuery patterns, see Querying Data. \u003Cscript setup lang=\"ts\">\nconst { items: products } = await useCollectionQuery('shopProducts')\nconst { items: categories } = await useCollectionQuery('shopCategories')\n// Map in component\n\u003C/script>",{"id":4406,"title":4407,"titles":4408,"content":4409,"level":449},"/patterns/drizzle#in-the-api-route-option-2","In the API Route (Option 2)",[161,4398],"Best for: ✅ Complex joins✅ Performance critical✅ Large datasets✅ Filtering by related fields // server/api/teams/[team]/products-full.get.ts\nexport default defineEventHandler(async (event) => {\n  // Join on server, return combined data\n  return await db.query.products.findMany({\n    with: { category: true }\n  })\n}) Rule of thumb: Start with Option 1, move to Option 2 when you see performance issues.",{"id":4411,"title":1562,"titles":4412,"content":4413,"level":391},"/patterns/drizzle#related-topics",[161],"Working with RelationsForm Patterns: Relation DropdownsTable Patterns: Display Related Data html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":138,"title":142,"titles":4415,"content":4416,"level":385},[],"This section explores common patterns and best practices for working with data in Nuxt Crouton. Learn how to build forms, tables, relationships, and integrate with Drizzle ORM.",{"id":4418,"title":142,"titles":4419,"content":4416,"level":385},"/patterns#patterns-overview",[],{"id":4421,"title":2867,"titles":4422,"content":4423,"level":391},"/patterns#what-youll-learn",[142],"The Patterns section provides practical guidance for common use cases: Relations: How to define and work with relationships between collectionsForms: Patterns for building and customizing formsTables: Patterns for tables, columns, and data displayDrizzle Integration: Working directly with Drizzle ORMList Layouts: Alternative layouts for displaying collection data",{"id":4425,"title":2872,"titles":4426,"content":528,"level":391},"/patterns#section-contents",[142],{"id":4428,"title":4429,"titles":4430,"content":4431,"level":449},"/patterns#_1-relations","1. Relations",[142,2872],"File: 1.relations.md Master relationships between collections: One-to-many relationshipsMany-to-many relationshipsReference fields (ref-target)Querying related dataDisplaying related records in forms and tables Example: # posts.yml\nfields:\n  - name: author\n    type: reference\n    ref-target: users\n  - name: categories\n    type: reference\n    ref-target: categories\n    multiple: true",{"id":4433,"title":4434,"titles":4435,"content":4436,"level":449},"/patterns#_2-forms","2. Forms",[142,2872],"File: 2.forms.md Form patterns and customization: Auto-generated forms from schemasCustom field componentsForm validation patternsConditional fieldsMulti-step formsForm state managementError handling Key Composables: useCollectionForm() - Form state and CRUD operationsuseFormValidation() - Custom validation logic",{"id":4438,"title":4439,"titles":4440,"content":4441,"level":449},"/patterns#_3-tables","3. Tables",[142,2872],"File: 3.tables.md Table patterns and composition: Auto-generated table columnsCustom column componentsSorting and filteringPagination patternsRow actions (edit, delete, view)Bulk operationsTable configuration Key Composables: useCollectionTable() - Table data and paginationuseCollectionQuery() - Advanced filtering and search",{"id":4443,"title":4444,"titles":4445,"content":4446,"level":449},"/patterns#_4-drizzle-integration","4. Drizzle Integration",[142,2872],"File: drizzle.md Working directly with Drizzle ORM: Understanding generated Drizzle schemasWriting custom queriesMigrations and schema changesAdvanced database operationsRaw SQL when needed Example: import { db } from '~/server/database/db'\nimport { posts } from '~/server/database/schema'\nimport { eq } from 'drizzle-orm'\n\n// Custom query\nconst publishedPosts = await db.select()\n  .from(posts)\n  .where(eq(posts.published, true))\n  .orderBy(posts.createdAt)",{"id":4448,"title":4449,"titles":4450,"content":4451,"level":449},"/patterns#_5-list-layouts","5. List Layouts",[142,2872],"File: 5.list-layouts.md Alternative layouts for displaying collections: Card layouts (grid, masonry)List layouts (compact, detailed)Custom list componentsResponsive layout patternsMini card components",{"id":4453,"title":1650,"titles":4454,"content":528,"level":391},"/patterns#common-patterns",[142],{"id":4456,"title":4457,"titles":4458,"content":4459,"level":449},"/patterns#creating-with-relations","Creating with Relations",[142,1650],"const { create } = useCollection('posts')\n\nawait create({\n  title: 'My Post',\n  author: userId, // Reference to users collection\n  categories: [categoryId1, categoryId2] // Multiple references\n})",{"id":4461,"title":4462,"titles":4463,"content":4464,"level":449},"/patterns#querying-with-relations","Querying with Relations",[142,1650],"const { data } = await useCollectionQuery('posts', {\n  include: ['author', 'categories'], // Include related data\n  filters: {\n    'author.name': 'John Doe'\n  }\n})",{"id":4466,"title":4467,"titles":4468,"content":4469,"level":449},"/patterns#custom-form-fields","Custom Form Fields",[142,1650],"\u003Cscript setup lang=\"ts\">\nconst { formData, save } = useCollectionForm('products')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonForm v-model=\"formData\" @save=\"save\">\n    \u003C!-- Override price field with custom component -->\n    \u003Ctemplate #field-price>\n      \u003CCustomPriceField v-model=\"formData.price\" />\n    \u003C/template>\n  \u003C/CroutonForm>\n\u003C/template>",{"id":4471,"title":4472,"titles":4473,"content":4474,"level":449},"/patterns#custom-table-columns","Custom Table Columns",[142,1650],"\u003Cscript setup lang=\"ts\">\nconst { data, pagination } = useCollectionTable('products')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonTable :data=\"data\" :pagination=\"pagination\">\n    \u003C!-- Override price column -->\n    \u003Ctemplate #column-price=\"{ row }\">\n      \u003Cspan class=\"font-bold text-green-600\">\n        ${{ row.price.toFixed(2) }}\n      \u003C/span>\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template>",{"id":4476,"title":44,"titles":4477,"content":4478,"level":391},"/patterns#best-practices",[142],"Use Generated Components First: Start with auto-generated forms and tables, customize only when neededLeverage Composables: Use useCollectionForm() and useCollectionTable() for state managementKeep Schemas Simple: Complex logic belongs in custom components, not schemasUse Slots for Customization: Override specific fields/columns via slots instead of rebuilding entire componentsFollow Drizzle Patterns: Use Drizzle's query builder for type-safe database operations",{"id":4480,"title":2916,"titles":4481,"content":4482,"level":391},"/patterns#where-to-go-next",[142],"After mastering patterns: Customization → Learn how to create custom fields and componentsFeatures → Explore advanced features like i18n, rich text, and file uploadsAPI Reference → Deep dive into composables and components",{"id":4484,"title":426,"titles":4485,"content":4486,"level":391},"/patterns#prerequisites",[142],"Before diving into patterns: Completed Getting StartedUnderstand Fundamentals (especially collections and architecture)Familiarity with Generation workflow",{"id":4488,"title":2925,"titles":4489,"content":4490,"level":391},"/patterns#external-resources",[142],"For related technologies: Nuxt UI Components - Base component libraryDrizzle ORM - Database toolkitVue Slots - Component customizationVueUse - Utility composables html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}",{"id":166,"title":165,"titles":4492,"content":4493,"level":385},[],"Learn how to customize and extend the code generated by Nuxt Crouton Nuxt Crouton follows a generate-then-customize approach. Once code is generated, it becomes yours to modify and extend as needed. This is the fundamental principle: generated code is your code, so edit it freely. After generation, the files live in your project and you own them completely. Customize them however you need. Just remember: regenerating will lose your changes. Unlike runtime admin panels or frameworks that lock you into their patterns, Nuxt Crouton gives you a starting point that you can modify without restriction.",{"id":4495,"title":4496,"titles":4497,"content":4498,"level":391},"/customization#understanding-the-ownership-model","Understanding the Ownership Model",[165],"When you run the generator, it creates files in your project structure: layers/[layer]/\n  └── collections/\n      └── [collection]/\n          ├── app/\n          │   ├── components/\n          │   │   ├── List.vue           # Yours to customize\n          │   │   └── _Form.vue          # Yours to customize\n          │   └── composables/\n          │       └── use[Layer][Collection].ts # Yours to customize (e.g., useShopProducts.ts)\n          └── types.ts                   # Yours to customize These files are independent and not linked to the Nuxt Crouton core library. They simply use the core library's utilities. You can modify them without breaking updates to the core library, regenerate them if needed (though you'll lose customizations), copy patterns between collections, and version control your customizations.",{"id":4500,"title":4501,"titles":4502,"content":4503,"level":391},"/customization#when-to-customize-vs-regenerate","When to Customize vs Regenerate",[165],"Customize when: Adding new fields to formsChanging validation logicAdding custom UI componentsImplementing business logicStyling components Regenerate when: You want to start over with a clean slateYou need to add/remove base fieldsYou're updating to a new Nuxt Crouton version and want new patternsYou made a mistake and want to reset Regenerating will overwrite your customizations. Always use version control and commit your work before regenerating.",{"id":4505,"title":418,"titles":4506,"content":4507,"level":391},"/customization#next-steps",[165],"Now that you understand the ownership model, learn how to make common customizations: Add Custom Components - Extend forms with rich editors, file uploads, and moreAdd Custom Columns - Customize table display and computed columns",{"id":172,"title":171,"titles":4509,"content":4510,"level":385},[],"Integrate advanced UI components like image uploads, rich text editors, and multi-step forms Extend your forms with advanced UI components for richer user experiences.",{"id":4512,"title":4513,"titles":4514,"content":4515,"level":391},"/customization/custom-components#add-image-upload","Add Image Upload",[171],"Add file upload capabilities to your forms with preview and custom storage integration. \u003Cscript setup lang=\"ts\">\nconst uploadingImage = ref(false)\n\nconst handleImageUpload = async (file: File) => {\n  uploadingImage.value = true\n\n  // Upload to your storage (Cloudinary, S3, etc.)\n  const formData = new FormData()\n  formData.append('file', file)\n\n  const { url } = await $fetch('/api/upload', {\n    method: 'POST',\n    body: formData\n  })\n\n  state.value.imageUrl = url\n  uploadingImage.value = false\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUFormField label=\"Product Image\" name=\"imageUrl\">\n    \u003Cimg v-if=\"state.imageUrl\" :src=\"state.imageUrl\" class=\"w-32 h-32\" />\n\n    \u003Cinput\n      ref=\"fileInput\"\n      type=\"file\"\n      accept=\"image/*\"\n      class=\"hidden\"\n      @change=\"handleImageUpload($event.target.files[0])\"\n    />\n\n    \u003CUButton @click=\"$refs.fileInput.click()\" :loading=\"uploadingImage\">\n      Upload Image\n    \u003C/UButton>\n  \u003C/UFormField>\n\u003C/template>",{"id":4517,"title":4518,"titles":4519,"content":4520,"level":449},"/customization/custom-components#with-image-preview","With Image Preview",[171,4513],"Add a preview before upload: \u003Cscript setup lang=\"ts\">\nconst imagePreview = ref\u003Cstring | null>(null)\nconst uploadingImage = ref(false)\n\nconst handleImageSelect = (event: Event) => {\n  const file = (event.target as HTMLInputElement).files?.[0]\n  if (!file) return\n\n  // Show preview\n  const reader = new FileReader()\n  reader.onload = (e) => {\n    imagePreview.value = e.target?.result as string\n  }\n  reader.readAsDataURL(file)\n\n  // Upload\n  uploadImage(file)\n}\n\nconst uploadImage = async (file: File) => {\n  uploadingImage.value = true\n  const formData = new FormData()\n  formData.append('file', file)\n\n  const { url } = await $fetch('/api/upload', {\n    method: 'POST',\n    body: formData\n  })\n\n  state.value.imageUrl = url\n  uploadingImage.value = false\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUFormField label=\"Product Image\" name=\"imageUrl\">\n    \u003Cdiv v-if=\"imagePreview\" class=\"mb-4\">\n      \u003Cimg :src=\"imagePreview\" class=\"w-full max-w-md rounded-lg\" />\n    \u003C/div>\n\n    \u003Cinput\n      ref=\"fileInput\"\n      type=\"file\"\n      accept=\"image/*\"\n      class=\"hidden\"\n      @change=\"handleImageSelect\"\n    />\n\n    \u003CUButton @click=\"$refs.fileInput.click()\" :loading=\"uploadingImage\">\n      {{ imagePreview ? 'Change Image' : 'Upload Image' }}\n    \u003C/UButton>\n  \u003C/UFormField>\n\u003C/template>",{"id":4522,"title":4523,"titles":4524,"content":4525,"level":449},"/customization/custom-components#multiple-images","Multiple Images",[171,4513],"Handle multiple image uploads: \u003Cscript setup lang=\"ts\">\nconst images = ref\u003Cstring[]>([])\nconst uploadingImages = ref(false)\n\nconst handleMultipleImages = async (event: Event) => {\n  const files = Array.from((event.target as HTMLInputElement).files || [])\n  uploadingImages.value = true\n\n  for (const file of files) {\n    const formData = new FormData()\n    formData.append('file', file)\n\n    const { url } = await $fetch('/api/upload', {\n      method: 'POST',\n      body: formData\n    })\n\n    images.value.push(url)\n  }\n\n  state.value.images = images.value\n  uploadingImages.value = false\n}\n\nconst removeImage = (index: number) => {\n  images.value.splice(index, 1)\n  state.value.images = images.value\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUFormField label=\"Product Images\" name=\"images\">\n    \u003Cdiv v-if=\"images.length\" class=\"grid grid-cols-3 gap-4 mb-4\">\n      \u003Cdiv v-for=\"(image, index) in images\" :key=\"index\" class=\"relative\">\n        \u003Cimg :src=\"image\" class=\"w-full h-32 object-cover rounded\" />\n        \u003CUButton\n          size=\"xs\"\n          color=\"red\"\n          class=\"absolute top-1 right-1\"\n          @click=\"removeImage(index)\"\n        >\n          Remove\n        \u003C/UButton>\n      \u003C/div>\n    \u003C/div>\n\n    \u003Cinput\n      ref=\"fileInput\"\n      type=\"file\"\n      accept=\"image/*\"\n      multiple\n      class=\"hidden\"\n      @change=\"handleMultipleImages\"\n    />\n\n    \u003CUButton @click=\"$refs.fileInput.click()\" :loading=\"uploadingImages\">\n      Add Images\n    \u003C/UButton>\n  \u003C/UFormField>\n\u003C/template>",{"id":4527,"title":4528,"titles":4529,"content":4530,"level":391},"/customization/custom-components#add-rich-text-editor","Add Rich Text Editor",[171],"Nuxt Crouton provides an optional rich text editor layer powered by TipTap, perfect for blog posts, content management, and text-heavy forms.",{"id":4532,"title":4533,"titles":4534,"content":4535,"level":449},"/customization/custom-components#quick-setup","Quick Setup",[171,4528],"1. Install the editor package: pnpm add @fyit/crouton-editor @nuxt/icon 2. Configure Nuxt: export default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-editor'  // Add this layer\n  ]\n}) 3. Use in Forms: \u003Cscript setup lang=\"ts\">\nconst state = ref({\n  title: '',\n  content: '\u003Cp>\u003C/p>'\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUFormField label=\"Content\" name=\"content\">\n    \u003CCroutonEditorSimple v-model=\"state.content\" />\n  \u003C/UFormField>\n\u003C/template>",{"id":4537,"title":4538,"titles":4539,"content":4540,"level":449},"/customization/custom-components#generator-integration","Generator Integration",[171,4528],"Automatically use the editor for specific fields by marking them in your schema: {\n  \"title\": {\n    \"type\": \"string\",\n    \"meta\": {\n      \"required\": true,\n      \"label\": \"Post Title\"\n    }\n  },\n  \"content\": {\n    \"type\": \"text\",\n    \"meta\": {\n      \"component\": \"EditorSimple\",\n      \"label\": \"Post Content\"\n    }\n  }\n} When you generate this collection, the content field will automatically use EditorSimple.",{"id":4542,"title":4543,"titles":4544,"content":4545,"level":449},"/customization/custom-components#features-included","Features Included",[171,4528],"The editor includes: ✅ Text Formatting: Bold, italic, strikethrough✅ Headings: H1, H2, H3✅ Lists: Bullet points and numbered lists✅ Code Blocks: Inline code and code blocks✅ Blockquotes: Quote formatting✅ Text Colors: Custom text coloring✅ Floating Toolbar: Appears on text selection✅ Dark Mode: Automatic dark mode support✅ Keyboard Shortcuts: Standard shortcuts (Cmd+B for bold, etc.)",{"id":4547,"title":4548,"titles":4549,"content":4550,"level":449},"/customization/custom-components#database-storage","Database Storage",[171,4528],"The editor outputs HTML. Store it in a TEXT field: // Drizzle schema\nexport const blogPosts = sqliteTable('blog_posts', {\n  id: text('id').primaryKey(),\n  title: text('title').notNull(),\n  content: text('content').notNull(),  // HTML from editor\n  createdAt: integer('createdAt', { mode: 'timestamp' })\n})",{"id":4552,"title":4553,"titles":4554,"content":4555,"level":449},"/customization/custom-components#display-rendered-content","Display Rendered Content",[171,4528],"Query Examples: For complete useCollectionQuery patterns, see Querying Data. Render the HTML safely on your pages: \u003Cscript setup lang=\"ts\">\nconst { items: posts } = await useCollectionQuery('blogPosts')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-for=\"post in posts\" :key=\"post.id\">\n    \u003Ch1>{{ post.title }}\u003C/h1>\n    \u003C!-- Render HTML (ensure it's sanitized on the backend!) -->\n    \u003Cdiv class=\"prose\" v-html=\"post.content\" />\n  \u003C/div>\n\u003C/template> Security: Always sanitize HTML on the backend before saving to prevent XSS attacks. Use a library like sanitize-html.",{"id":4557,"title":44,"titles":4558,"content":4559,"level":449},"/customization/custom-components#best-practices",[171,4528],"✅ DO: Sanitize HTML on the backend before storingUse the prose class (Tailwind Typography) for consistent renderingMark editor fields in your schema for automatic generationStore editor content in a TEXT database field ❌ DON'T: Render unsanitized HTML (XSS risk)Store editor content in a VARCHAR (may truncate)Forget to add @nuxt/icon dependency",{"id":4561,"title":4562,"titles":4563,"content":4564,"level":391},"/customization/custom-components#add-multi-step-form","Add Multi-Step Form",[171],"Break complex forms into multiple steps for better UX. \u003Cscript setup lang=\"ts\">\nconst currentStep = ref(1)\nconst totalSteps = 3\n\nconst nextStep = () => {\n  if (currentStep.value \u003C totalSteps) {\n    currentStep.value++\n  }\n}\n\nconst prevStep = () => {\n  if (currentStep.value > 1) {\n    currentStep.value--\n  }\n}\n\nconst handleSubmit = async () => {\n  if (currentStep.value \u003C totalSteps) {\n    nextStep()\n  } else {\n    // Final submit\n    await create(state.value)\n    close()\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm @submit=\"handleSubmit\">\n    \u003C!-- Step indicator -->\n    \u003Cdiv class=\"flex justify-between mb-4\">\n      \u003Cdiv v-for=\"step in totalSteps\" :key=\"step\"\n           :class=\"{ 'font-bold': step === currentStep }\">\n        Step {{ step }}\n      \u003C/div>\n    \u003C/div>\n\n    \u003C!-- Step 1: Basic info -->\n    \u003Cdiv v-if=\"currentStep === 1\">\n      \u003CUFormField label=\"Name\" name=\"name\">\n        \u003CUInput v-model=\"state.name\" />\n      \u003C/UFormField>\n    \u003C/div>\n\n    \u003C!-- Step 2: Details -->\n    \u003Cdiv v-if=\"currentStep === 2\">\n      \u003CUFormField label=\"Description\" name=\"description\">\n        \u003CUTextarea v-model=\"state.description\" />\n      \u003C/UFormField>\n    \u003C/div>\n\n    \u003C!-- Step 3: Pricing -->\n    \u003Cdiv v-if=\"currentStep === 3\">\n      \u003CUFormField label=\"Price\" name=\"price\">\n        \u003CUInput v-model.number=\"state.price\" type=\"number\" />\n      \u003C/UFormField>\n    \u003C/div>\n\n    \u003C!-- Navigation -->\n    \u003Cdiv class=\"flex justify-between\">\n      \u003CUButton v-if=\"currentStep > 1\" @click=\"prevStep\" variant=\"ghost\">\n        Back\n      \u003C/UButton>\n      \u003CUButton type=\"submit\">\n        {{ currentStep \u003C totalSteps ? 'Next' : 'Submit' }}\n      \u003C/UButton>\n    \u003C/div>\n  \u003C/UForm>\n\u003C/template>",{"id":4566,"title":4567,"titles":4568,"content":4569,"level":449},"/customization/custom-components#with-progress-bar","With Progress Bar",[171,4562],"Add visual progress indication: \u003Ctemplate>\n  \u003CUForm @submit=\"handleSubmit\">\n    \u003C!-- Progress bar -->\n    \u003Cdiv class=\"mb-6\">\n      \u003Cdiv class=\"flex justify-between mb-2\">\n        \u003Cspan class=\"text-sm font-medium\">Progress\u003C/span>\n        \u003Cspan class=\"text-sm text-gray-500\">{{ currentStep }}/{{ totalSteps }}\u003C/span>\n      \u003C/div>\n      \u003Cdiv class=\"w-full bg-gray-200 rounded-full h-2\">\n        \u003Cdiv\n          class=\"bg-primary-500 h-2 rounded-full transition-all\"\n          :style=\"{ width: `${(currentStep / totalSteps) * 100}%` }\"\n        />\n      \u003C/div>\n    \u003C/div>\n\n    \u003C!-- Step content -->\n    \u003Cdiv v-if=\"currentStep === 1\">\n      \u003C!-- Step 1 fields -->\n    \u003C/div>\n\n    \u003C!-- Navigation -->\n    \u003Cdiv class=\"flex justify-between mt-6\">\n      \u003CUButton v-if=\"currentStep > 1\" @click=\"prevStep\" variant=\"ghost\">\n        Back\n      \u003C/UButton>\n      \u003CUButton type=\"submit\" :loading=\"loading\">\n        {{ currentStep \u003C totalSteps ? 'Next' : 'Submit' }}\n      \u003C/UButton>\n    \u003C/div>\n  \u003C/UForm>\n\u003C/template>",{"id":4571,"title":4572,"titles":4573,"content":4574,"level":449},"/customization/custom-components#with-validation-per-step","With Validation Per Step",[171,4562],"Validate each step before proceeding: \u003Cscript setup lang=\"ts\">\nimport { z } from 'zod'\n\nconst currentStep = ref(1)\n\n// Define validation for each step\nconst step1Schema = z.object({\n  name: z.string().min(1, 'Name is required'),\n  sku: z.string().min(1, 'SKU is required')\n})\n\nconst step2Schema = z.object({\n  description: z.string().min(10, 'Description must be at least 10 characters')\n})\n\nconst step3Schema = z.object({\n  price: z.number().min(0, 'Price must be positive')\n})\n\nconst validateCurrentStep = async () => {\n  try {\n    if (currentStep.value === 1) {\n      await step1Schema.parseAsync(state.value)\n    } else if (currentStep.value === 2) {\n      await step2Schema.parseAsync(state.value)\n    } else if (currentStep.value === 3) {\n      await step3Schema.parseAsync(state.value)\n    }\n    return true\n  } catch (error) {\n    // Show validation errors\n    console.error(error)\n    return false\n  }\n}\n\nconst handleSubmit = async () => {\n  const isValid = await validateCurrentStep()\n  if (!isValid) return\n\n  if (currentStep.value \u003C totalSteps) {\n    nextStep()\n  } else {\n    await create(state.value)\n    close()\n  }\n}\n\u003C/script>",{"id":4576,"title":44,"titles":4577,"content":528,"level":391},"/customization/custom-components#best-practices-1",[171],{"id":4579,"title":4580,"titles":4581,"content":4582,"level":449},"/customization/custom-components#component-organization","Component Organization",[171,44],"Keep custom components reusable: layers/shop/components/\n  ├── products/\n  │   ├── Form.vue\n  │   └── List.vue\n  └── shared/\n      ├── ImageUpload.vue      # Reusable component\n      ├── RichTextEditor.vue   # Reusable component\n      └── MultiStepForm.vue    # Reusable component",{"id":4584,"title":2522,"titles":4585,"content":4586,"level":449},"/customization/custom-components#error-handling",[171,44],"Always handle errors in custom components: \u003Cscript setup lang=\"ts\">\nconst uploadImage = async (file: File) => {\n  uploadingImage.value = true\n\n  try {\n    const formData = new FormData()\n    formData.append('file', file)\n\n    const { url } = await $fetch('/api/upload', {\n      method: 'POST',\n      body: formData\n    })\n\n    state.value.imageUrl = url\n  } catch (error) {\n    console.error('Upload failed:', error)\n    // Show error toast\n    toast.add({\n      title: 'Upload Failed',\n      description: 'Failed to upload image. Please try again.',\n      color: 'red'\n    })\n  } finally {\n    uploadingImage.value = false\n  }\n}\n\u003C/script>",{"id":4588,"title":4027,"titles":4589,"content":4590,"level":449},"/customization/custom-components#performance-considerations",[171,44],"Debounce rich text editor updatesLazy load heavy componentsOptimize image uploads (compress, resize)Cache editor instances when possible",{"id":4592,"title":1562,"titles":4593,"content":4594,"level":391},"/customization/custom-components#related-topics",[171],"Forms & Modals - Form lifecycle and state managementCustom Columns - Display custom data in tables html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":176,"title":175,"titles":4596,"content":4597,"level":385},[],"Customize table columns with computed values, custom rendering, and custom components Customize how data is displayed in your tables by adding computed columns, custom renderers, and custom components.",{"id":4599,"title":4600,"titles":4601,"content":4602,"level":391},"/customization/custom-columns#add-custom-columns","Add Custom Columns",[175],"Columns are defined in your composable and control how data appears in table views.",{"id":4604,"title":4605,"titles":4606,"content":4607,"level":449},"/customization/custom-columns#basic-column-definition","Basic Column Definition",[175,4600],"TanStack Table format: Crouton uses TanStack Table under the hood. Column definitions use accessorKey and header instead of key and label. The examples below use the current TanStack Table API format. // layers/shop/composables/useProducts.ts\nexport function useShopProducts() {\n  const columns = [\n    { accessorKey: 'name', header: 'Name' },\n    { accessorKey: 'price', header: 'Price' },\n    { accessorKey: 'inStock', header: 'In Stock' }\n  ]\n\n  return {\n    columns,\n    // ...other exports\n  }\n}",{"id":4609,"title":4610,"titles":4611,"content":4612,"level":391},"/customization/custom-columns#computed-columns","Computed Columns",[175],"Add virtual columns that don't exist in your data but are computed on the fly.",{"id":4614,"title":4615,"titles":4616,"content":4617,"level":449},"/customization/custom-columns#using-cell-function","Using Cell Function",[175,4610],"const columns = [\n  { accessorKey: 'name', header: 'Name' },\n  { accessorKey: 'price', header: 'Price' },\n\n  // Add computed column\n  {\n    accessorKey: 'status',\n    header: 'Status',\n    cell: ({ row }) => row.original.inStock ? 'Available' : 'Out of Stock'\n  }\n]",{"id":4619,"title":4620,"titles":4621,"content":4622,"level":449},"/customization/custom-columns#computing-values","Computing Values",[175,4610],"const columns = [\n  { accessorKey: 'name', header: 'Product' },\n  { accessorKey: 'price', header: 'Price' },\n  { accessorKey: 'cost', header: 'Cost' },\n\n  // Calculate profit\n  {\n    accessorKey: 'profit',\n    header: 'Profit',\n    cell: ({ row }) => row.original.price - row.original.cost\n  },\n\n  // Calculate margin percentage\n  {\n    accessorKey: 'margin',\n    header: 'Margin %',\n    cell: ({ row }) => {\n      const margin = ((row.original.price - row.original.cost) / row.original.price) * 100\n      return `${margin.toFixed(2)}%`\n    }\n  }\n]",{"id":4624,"title":4625,"titles":4626,"content":4627,"level":449},"/customization/custom-columns#formatting-values","Formatting Values",[175,4610],"const columns = [\n  {\n    accessorKey: 'price',\n    header: 'Price',\n    cell: ({ row }) => {\n      // Format as currency\n      return new Intl.NumberFormat('en-US', {\n        style: 'currency',\n        currency: 'USD'\n      }).format(row.original.price)\n    }\n  },\n\n  {\n    accessorKey: 'createdAt',\n    header: 'Created',\n    cell: ({ row }) => {\n      // Format date\n      return new Intl.DateTimeFormat('en-US', {\n        dateStyle: 'medium',\n        timeStyle: 'short'\n      }).format(new Date(row.original.createdAt))\n    }\n  },\n\n  {\n    accessorKey: 'quantity',\n    header: 'Quantity',\n    cell: ({ row }) => {\n      // Add thousands separator\n      return row.original.quantity.toLocaleString()\n    }\n  }\n]",{"id":4629,"title":171,"titles":4630,"content":4631,"level":391},"/customization/custom-columns#custom-components",[175],"Use custom Vue components for complex column rendering.",{"id":4633,"title":4634,"titles":4635,"content":4636,"level":449},"/customization/custom-columns#basic-component-column","Basic Component Column",[175,171],"const columns = [\n  { accessorKey: 'name', header: 'Product' },\n  { accessorKey: 'price', header: 'Price' },\n\n  // Add custom component column using cell function\n  {\n    id: 'actions',\n    header: '',\n    cell: ({ row }) => h(resolveComponent('ProductActions'), { row: row.original })\n  }\n] Then create the component: \u003C!-- layers/shop/components/products/ProductActions.vue -->\n\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{\n  row: any\n}>()\n\nconst { open } = useCrouton()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"flex gap-2\">\n    \u003CUButton size=\"xs\" @click=\"open('update', 'shopProducts', [row.id])\">\n      Edit\n    \u003C/UButton>\n    \u003CUButton size=\"xs\" color=\"red\" @click=\"open('delete', 'shopProducts', [row.id])\">\n      Delete\n    \u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":4638,"title":4639,"titles":4640,"content":4641,"level":449},"/customization/custom-columns#badge-component","Badge Component",[175,171],"Display status badges: const columns = [\n  { accessorKey: 'name', header: 'Product' },\n  {\n    accessorKey: 'status',\n    header: 'Status',\n    cell: ({ row }) => h(resolveComponent('ProductStatus'), { row: row.original })\n  }\n] \u003C!-- layers/shop/components/products/ProductStatus.vue -->\n\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  row: any\n}>()\n\nconst statusColor = computed(() => {\n  switch (props.row.status) {\n    case 'active': return 'green'\n    case 'draft': return 'yellow'\n    case 'archived': return 'gray'\n    default: return 'gray'\n  }\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUBadge :color=\"statusColor\">\n    {{ row.status }}\n  \u003C/UBadge>\n\u003C/template>",{"id":4643,"title":4644,"titles":4645,"content":4646,"level":391},"/customization/custom-columns#dynamic-columns","Dynamic Columns",[175],"Use computed columns for reactive behavior.",{"id":4648,"title":4649,"titles":4650,"content":4651,"level":449},"/customization/custom-columns#based-on-user-permissions","Based on User Permissions",[175,4644],"export function useShopProducts() {\n  const { hasPermission } = useAuth()\n\n  const columns = computed(() => [\n    { accessorKey: 'name', header: 'Name' },\n    { accessorKey: 'price', header: 'Price' },\n\n    // Show cost only to admins\n    ...(hasPermission('view_cost') ? [{\n      accessorKey: 'cost',\n      header: 'Cost'\n    }] : []),\n\n    // Show profit only to managers\n    ...(hasPermission('view_profit') ? [{\n      accessorKey: 'profit',\n      header: 'Profit',\n      cell: ({ row }) => row.original.price - row.original.cost\n    }] : [])\n  ])\n\n  return { columns }\n}",{"id":4653,"title":4654,"titles":4655,"content":4656,"level":449},"/customization/custom-columns#based-on-screen-size","Based on Screen Size",[175,4644],"export function useShopProducts() {\n  const { width } = useWindowSize()\n  const isMobile = computed(() => width.value \u003C 768)\n\n  const columns = computed(() => [\n    { accessorKey: 'name', header: 'Name' },\n\n    // Hide on mobile\n    ...(!isMobile.value ? [\n      { accessorKey: 'description', header: 'Description' },\n      { accessorKey: 'category', header: 'Category' }\n    ] : []),\n\n    { accessorKey: 'price', header: 'Price' },\n    {\n      id: 'actions',\n      header: '',\n      cell: ({ row }) => h(resolveComponent('ProductActions'), { row: row.original })\n    }\n  ])\n\n  return { columns }\n}",{"id":4658,"title":4659,"titles":4660,"content":4661,"level":391},"/customization/custom-columns#default-columns-with-special-rendering","Default Columns with Special Rendering",[175],"Some default columns use custom rendering automatically:",{"id":4663,"title":4664,"titles":4665,"content":4666,"level":449},"/customization/custom-columns#updatedby-column","updatedBy Column",[175,4659],"The updatedBy column automatically displays user information using a CardMini component: \u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"items\"\n    :columns=\"columns\"\n    collection=\"shopProducts\"\n    layout=\"list\"\n  />\n  \u003C!-- updatedBy column automatically shows user card -->\n\u003C/template> The updatedBy field: Is automatically added to all collections when useMetadata: true in the generatorLinks to the users collectionDisplays with a CardMini component showing user detailsCan be hidden with :hide-default-columns=\"{ updatedBy: true }\" Don't add updatedBy to your schema JSON files - it's metadata, not a business field. The generator adds it automatically.",{"id":4668,"title":4104,"titles":4669,"content":4670,"level":391},"/customization/custom-columns#displaying-related-data",[175],"Show data from related collections. Query Examples: For complete useCollectionQuery patterns, see Querying Data.",{"id":4672,"title":4673,"titles":4674,"content":4675,"level":449},"/customization/custom-columns#client-side-lookup","Client-Side Lookup",[175,4104],"export function useShopProducts() {\n  const { items: categories } = await useCollectionQuery('shopCategories')\n\n  // Create lookup map\n  const categoryMap = computed(() =>\n    Object.fromEntries(categories.value.map(c => [c.id, c]))\n  )\n\n  const columns = [\n    { accessorKey: 'name', header: 'Product' },\n    { accessorKey: 'price', header: 'Price' },\n\n    // Look up category name\n    {\n      accessorKey: 'category',\n      header: 'Category',\n      cell: ({ row }) => categoryMap.value[row.original.categoryId]?.name || 'N/A'\n    }\n  ]\n\n  return { columns }\n}",{"id":4677,"title":4678,"titles":4679,"content":4680,"level":449},"/customization/custom-columns#server-side-join","Server-Side Join",[175,4104],"If your API returns joined data: const columns = [\n  { accessorKey: 'name', header: 'Product' },\n  { accessorKey: 'price', header: 'Price' },\n\n  // Access nested data directly\n  {\n    accessorKey: 'category.name',\n    header: 'Category'\n  }\n]",{"id":4682,"title":4683,"titles":4684,"content":4685,"level":391},"/customization/custom-columns#sortable-columns","Sortable Columns",[175],"Enable sorting on columns: const columns = [\n  {\n    accessorKey: 'name',\n    header: 'Name',\n    sortable: true\n  },\n  {\n    accessorKey: 'price',\n    header: 'Price',\n    sortable: true\n  },\n  {\n    accessorKey: 'createdAt',\n    header: 'Created',\n    sortable: true\n  }\n]",{"id":4687,"title":4688,"titles":4689,"content":4690,"level":391},"/customization/custom-columns#column-alignment","Column Alignment",[175],"Control text alignment using the cell function to wrap content with appropriate CSS classes: const columns = [\n  {\n    accessorKey: 'name',\n    header: 'Name'\n    // Default left alignment — no extra styling needed\n  },\n  {\n    accessorKey: 'price',\n    header: 'Price',\n    cell: ({ row }) => h('span', { class: 'text-right block' }, row.original.price)\n  },\n  {\n    accessorKey: 'status',\n    header: 'Status',\n    cell: ({ row }) => h('span', { class: 'text-center block' }, row.original.status)\n  }\n]",{"id":4692,"title":44,"titles":4693,"content":528,"level":391},"/customization/custom-columns#best-practices",[175],{"id":4695,"title":4696,"titles":4697,"content":4698,"level":449},"/customization/custom-columns#keep-cell-functions-simple","Keep Cell Functions Simple",[175,44],"// Good: Simple, readable\n{\n  accessorKey: 'status',\n  header: 'Status',\n  cell: ({ row }) => row.original.inStock ? 'Available' : 'Sold Out'\n}\n\n// Bad: Complex logic in cell\n{\n  accessorKey: 'status',\n  header: 'Status',\n  cell: ({ row }) => {\n    const now = new Date()\n    const created = new Date(row.original.createdAt)\n    const daysSince = (now - created) / (1000 * 60 * 60 * 24)\n    if (row.original.featured && daysSince \u003C 7) return 'New & Featured'\n    if (row.original.featured) return 'Featured'\n    if (daysSince \u003C 7) return 'New'\n    if (row.original.inStock) return 'Available'\n    return 'Sold Out'\n  }\n}\n\n// Better: Extract to a helper function\nconst getProductStatus = (product) => {\n  const now = new Date()\n  const created = new Date(product.createdAt)\n  const daysSince = (now - created) / (1000 * 60 * 60 * 24)\n\n  if (product.featured && daysSince \u003C 7) return 'New & Featured'\n  if (product.featured) return 'Featured'\n  if (daysSince \u003C 7) return 'New'\n  if (product.inStock) return 'Available'\n  return 'Sold Out'\n}\n\n{\n  accessorKey: 'status',\n  header: 'Status',\n  cell: ({ row }) => getProductStatus(row.original)\n}",{"id":4700,"title":4701,"titles":4702,"content":4703,"level":449},"/customization/custom-columns#use-components-for-complex-ui","Use Components for Complex UI",[175,44],"Move complex rendering to components: // Instead of complex cell logic, use a Vue component\n{\n  id: 'actions',\n  header: 'Actions',\n  cell: ({ row }) => h(resolveComponent('ProductActions'), { row: row.original })\n}",{"id":4705,"title":4706,"titles":4707,"content":4708,"level":449},"/customization/custom-columns#cache-lookups","Cache Lookups",[175,44],"Cache expensive computations: export function useShopProducts() {\n  const { items: categories } = await useCollectionQuery('shopCategories')\n\n  // Cache the lookup map\n  const categoryMap = computed(() =>\n    Object.fromEntries(categories.value.map(c => [c.id, c]))\n  )\n\n  // Reuse cached map in cell functions\n  const columns = [\n    {\n      accessorKey: 'category',\n      header: 'Category',\n      cell: ({ row }) => categoryMap.value[row.original.categoryId]?.name || 'N/A'\n    }\n  ]\n\n  return { columns }\n}",{"id":4710,"title":1562,"titles":4711,"content":4712,"level":391},"/customization/custom-columns#related-topics",[175],"Custom Components - Rich UI componentsWorking with Relations - Display related dataQueries - Fetch data for columns html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":180,"title":179,"titles":4714,"content":4715,"level":385},[],"Customize how your data displays - Card components, CardMini references, and responsive layouts",{"id":4717,"title":179,"titles":4718,"content":4719,"level":385},"/customization/layouts#layout-components",[],"Learn how to customize layouts in Nuxt Crouton - from responsive presets to custom Card components and CardMini reference displays. For general layout concepts and Nuxt UI layout components, see the Nuxt UI Layout documentation.",{"id":4721,"title":4722,"titles":4723,"content":4724,"level":391},"/customization/layouts#responsive-layout-presets","Responsive Layout Presets",[179],"Nuxt Crouton provides responsive layout presets that automatically adapt your data views to different screen sizes.",{"id":4726,"title":4727,"titles":4728,"content":4729,"level":449},"/customization/layouts#available-presets","Available Presets",[179,4722],"responsive (Default): Base (mobile): List layoutmd (tablet): Grid layoutlg (desktop): Table layoutBest for: General-purpose data that needs to work on all devices mobile-friendly: Base (mobile): List layoutlg (desktop): Table layoutBest for: Admin interfaces occasionally accessed on mobile compact: Base (mobile): List layoutxl (large desktop): Table layoutBest for: Data-dense applications optimized for large screens tree-default: All breakpoints: Tree layoutBest for: Hierarchical data with parent-child relationships",{"id":4731,"title":1608,"titles":4732,"content":4733,"level":449},"/customization/layouts#usage",[179,4722],"\u003Cscript setup lang=\"ts\">\nconst { items } = await useCollectionQuery('shopProducts')\nconst { columns } = useShopProducts()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- Responsive preset: list on mobile, table on desktop -->\n  \u003CCroutonCollection\n    :rows=\"items\"\n    :columns=\"columns\"\n    layout=\"responsive\"\n  />\n\u003C/template>",{"id":4735,"title":4736,"titles":4737,"content":4738,"level":449},"/customization/layouts#custom-responsive-layouts","Custom Responsive Layouts",[179,4722],"Define your own responsive behavior: \u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"items\"\n    :columns=\"columns\"\n    :layout=\"{\n      base: 'list',    // Mobile\n      sm: 'list',      // Small tablets\n      md: 'list',      // Tablets\n      lg: 'table',     // Desktops\n      xl: 'table',     // Large desktops\n      '2xl': 'table'   // Extra large\n    }\"\n  />\n\u003C/template>",{"id":4740,"title":4741,"titles":4742,"content":4743,"level":449},"/customization/layouts#breakpoints","Breakpoints",[179,4722],"Nuxt Crouton uses Tailwind breakpoints: base: \u003C 640pxsm: ≥ 640pxmd: ≥ 768pxlg: ≥ 1024pxxl: ≥ 1280px2xl: ≥ 1536px",{"id":4745,"title":4746,"titles":4747,"content":4748,"level":449},"/customization/layouts#why-list-layout-for-mobile","Why List Layout for Mobile?",[179,4722],"All presets use list layout for mobile devices because it's specifically optimized for small screens and touch interfaces: Touch-friendly - Large tap targets optimized for thumbsVertical scrolling - Natural scrolling pattern for mobileCompact display - Essential information without horizontal scrollingAutomatic field detection - Works with standard field names (name, email, avatar)",{"id":4750,"title":4751,"titles":4752,"content":4753,"level":391},"/customization/layouts#card-components","Card Components",[179],"Card components provide complete control over how collection items render in different layouts.",{"id":4755,"title":4756,"titles":4757,"content":4758,"level":449},"/customization/layouts#when-to-use-custom-cards","When to Use Custom Cards",[179,4751],"Use custom Card components when: Default rendering doesn't match your designYou need layout-specific stylingYou want to show different fields per layoutYou need custom interactions (click handlers, hover effects) Stick with defaults when: Basic field display is sufficientYou're prototyping or building quicklyYour data follows standard conventions",{"id":4760,"title":4761,"titles":4762,"content":4763,"level":449},"/customization/layouts#convention-based-discovery","Convention-Based Discovery",[179,4751],"Nuxt Crouton automatically discovers Card components using naming conventions: Collection: \"bookings\"\nComponent name: \"BookingsCard\"\n\nFile location: layers/bookings/app/components/Card.vue\nAuto-registered as: BookingsCard",{"id":4765,"title":4766,"titles":4767,"content":4768,"level":449},"/customization/layouts#card-variants-with-the-card-prop","Card Variants with the card Prop",[179,4751],"You can specify which card variant to use for any layout via the card prop on CroutonCollection or CroutonCollection: \u003Ctemplate>\n  \u003C!-- Default: uses {Collection}Card with layout prop -->\n  \u003CCroutonCollection layout=\"tree\" />\n  \u003CCroutonCollection layout=\"list\" />\n\n  \u003C!-- Specify card variant -->\n  \u003CCroutonCollection layout=\"tree\" card=\"CardSmall\" />\n  \u003CCroutonCollection layout=\"tree\" card=\"CardMini\" />\n  \u003CCroutonCollection layout=\"list\" card=\"CardMini\" />\n\u003C/template> Card Resolution Logic: card=\"CardSmall\" → {Collection}CardSmall (e.g., BookingsCardSmall)card=\"CardMini\" → {Collection}CardMiniNo card prop → {Collection}Card with layout prop (default behavior) This is particularly useful for tree layouts where you might want a compact card variant instead of the full card component: \u003Ctemplate>\n  \u003C!-- Tree layout with compact card variant -->\n  \u003CCroutonCollection\n    layout=\"tree\"\n    collection=\"pages\"\n    card=\"CardTree\"\n  />\n\u003C/template> Create the variant component in your collection layer: layers/pages/app/components/\n  ├── Card.vue       # Default card (list, grid layouts with size variants)\n  └── CardTree.vue   # Tree-specific compact card Important for Tree Layout: When using a custom card in tree layout, the card replaces only the content area (icon, label, badge) - NOT the drag handle, expand button, or actions menu. This preserves all tree interaction functionality: [drag handle] [expand btn] | [CARD CONTENT] | [actions menu]\n                           └─── replaced ───┘",{"id":4770,"title":4771,"titles":4772,"content":4773,"level":449},"/customization/layouts#props-interface","Props Interface",[179,4751],"interface CardProps {\n  item: any                                    // The data object\n  layout: 'list' | 'grid' | 'tree'            // Current layout context\n  collection: string                           // Collection name\n  size?: 'compact' | 'comfortable' | 'spacious' // Grid size variant\n  pending?: boolean                            // Loading state\n  error?: any                                  // Error state\n}",{"id":4775,"title":4776,"titles":4777,"content":4778,"level":449},"/customization/layouts#basic-example","Basic Example",[179,4751],"\u003Cscript setup lang=\"ts\">\ninterface Props {\n  item: any\n  layout: 'list' | 'grid'\n  collection: string\n  size?: 'compact' | 'comfortable' | 'spacious'\n}\n\nconst props = defineProps\u003CProps>()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- List: horizontal row -->\n  \u003Cdiv v-if=\"layout === 'list'\" class=\"flex items-center justify-between\">\n    \u003Cspan>{{ item.customerName }}\u003C/span>\n    \u003CUBadge>{{ item.status }}\u003C/UBadge>\n  \u003C/div>\n\n  \u003C!-- Grid: adapts to size prop -->\n  \u003CUCard\n    v-else-if=\"layout === 'grid'\"\n    :class=\"size === 'spacious' ? 'p-4' : 'p-3'\"\n  >\n    \u003Ch3 :class=\"size === 'spacious' ? 'text-lg font-semibold' : 'font-medium'\">\n      {{ item.customerName }}\n    \u003C/h3>\n    \u003Cp class=\"text-sm text-muted\">{{ item.date }}\u003C/p>\n    \u003Cdiv v-if=\"size === 'spacious'\" class=\"mt-2 space-y-1\">\n      \u003Cp class=\"text-sm\">{{ item.time }}\u003C/p>\n      \u003Cp class=\"text-sm\">{{ item.location }}\u003C/p>\n    \u003C/div>\n  \u003C/UCard>\n\u003C/template>",{"id":4780,"title":4781,"titles":4782,"content":4783,"level":449},"/customization/layouts#layout-specific-rendering","Layout-Specific Rendering",[179,4751],"List Layout - Best for scannable rows, mobile-first, quick actions: \u003Ctemplate>\n  \u003Cdiv v-if=\"layout === 'list'\" class=\"flex items-center justify-between gap-3\">\n    \u003Cdiv class=\"flex items-center gap-3\">\n      \u003CUIcon name=\"i-lucide-calendar\" class=\"text-primary\" />\n      \u003Cdiv>\n        \u003Cp class=\"font-medium\">{{ item.customerName }}\u003C/p>\n        \u003Cp class=\"text-sm text-muted\">{{ formatDate(item.date) }}\u003C/p>\n      \u003C/div>\n    \u003C/div>\n    \u003CUBadge :color=\"getStatusColor(item.status)\">\n      {{ item.status }}\n    \u003C/UBadge>\n  \u003C/div>\n\u003C/template> Grid Layout - Best for visual browsing, adapts to size prop: \u003Ctemplate>\n  \u003CUCard\n    v-else-if=\"layout === 'grid'\"\n    :class=\"[\n      'cursor-pointer hover:shadow-lg transition',\n      size === 'spacious' ? 'p-4' : 'p-3'\n    ]\"\n    @click=\"handleClick\"\n  >\n    \u003Ctemplate #header>\n      \u003Cdiv class=\"flex items-start justify-between\">\n        \u003Ch3 :class=\"size === 'spacious' ? 'font-semibold text-lg' : 'font-semibold truncate'\">\n          {{ item.customerName }}\n        \u003C/h3>\n        \u003CUBadge size=\"sm\" :color=\"getStatusColor(item.status)\">\n          {{ item.status }}\n        \u003C/UBadge>\n      \u003C/div>\n    \u003C/template>\n\n    \u003Cdiv class=\"space-y-2 text-sm\">\n      \u003Cdiv class=\"flex items-center gap-2\">\n        \u003CUIcon name=\"i-lucide-calendar\" class=\"text-muted\" />\n        \u003Cspan>{{ formatDate(item.date) }}\u003C/span>\n      \u003C/div>\n      \u003Cdiv class=\"flex items-center gap-2\">\n        \u003CUIcon name=\"i-lucide-clock\" class=\"text-muted\" />\n        \u003Cspan>{{ item.time }}\u003C/span>\n      \u003C/div>\n    \u003C/div>\n\n    \u003C!-- Extra details for spacious size -->\n    \u003Ctemplate v-if=\"size === 'spacious'\" #footer>\n      \u003Cdiv class=\"flex justify-between items-center\">\n        \u003Cspan class=\"text-xs text-muted\">\n          Created {{ formatRelative(item.createdAt) }}\n        \u003C/span>\n        \u003Cdiv class=\"flex gap-2\">\n          \u003CUButton size=\"xs\" variant=\"ghost\" @click.stop=\"handleEdit\">\n            Edit\n          \u003C/UButton>\n        \u003C/div>\n      \u003C/div>\n    \u003C/template>\n  \u003C/UCard>\n\u003C/template>",{"id":4785,"title":4786,"titles":4787,"content":4788,"level":449},"/customization/layouts#grid-size-variants","Grid Size Variants",[179,4751],"The size prop controls grid density: SizeColumnsSpacingBest Forcompact4 colsTight (gap-3, p-3)Thumbnails, quick scanningcomfortable3 colsMedium (gap-4, p-4)General purpose (default)spacious2-3 colsGenerous (gap-6, p-6)Detailed cards, dashboards \u003Ctemplate>\n  \u003C!-- Compact grid for thumbnails -->\n  \u003CCroutonCollection layout=\"grid\" grid-size=\"compact\" :rows=\"items\" />\n\n  \u003C!-- Default comfortable grid -->\n  \u003CCroutonCollection layout=\"grid\" :rows=\"items\" />\n\n  \u003C!-- Spacious grid for detailed cards -->\n  \u003CCroutonCollection layout=\"grid\" grid-size=\"spacious\" :rows=\"items\" />\n\u003C/template>",{"id":4790,"title":4791,"titles":4792,"content":4793,"level":391},"/customization/layouts#cardmini-components","CardMini Components",[179],"CardMini is a compact display component that shows related entities in tables and forms. It's a completely separate system from Card components.",{"id":4795,"title":4796,"titles":4797,"content":4798,"level":449},"/customization/layouts#what-is-cardmini","What is CardMini?",[179,4791],"When you have reference fields (relationships to other collections), CardMini automatically displays a preview of the referenced item: A booking referencing a user → Shows user's nameA product referencing a category → Shows category nameAn order referencing a customer → Shows customer details",{"id":4800,"title":4801,"titles":4802,"content":4803,"level":449},"/customization/layouts#default-behavior","Default Behavior",[179,4791],"By default, CardMini displays: The referenced item's title fieldA loading skeleton while fetchingMini action buttons on hover (update)Error state if item fails to load",{"id":4805,"title":64,"titles":4806,"content":4807,"level":449},"/customization/layouts#creating-custom-cardmini-components",[179,4791],"Customize the display for specific collections using convention-based discovery: Internal Collections: collections/locations/app/components/\n  └── CardMini.vue  ← Create this file External Collections: app/components/\n  └── UsersCardMini.vue  ← For :users collection",{"id":4809,"title":4810,"titles":4811,"content":4812,"level":449},"/customization/layouts#props-contract","Props Contract",[179,4791],"\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  item: any | null          // The fetched item data\n  pending: boolean          // Loading state\n  error: any | null         // Error state\n  id: string                // Item ID\n  collection: string        // Collection name\n  refresh: () => Promise\u003Cvoid>  // Refetch function\n}>()\n\u003C/script>",{"id":4814,"title":3367,"titles":4815,"content":4816,"level":449},"/customization/layouts#examples",[179,4791],"Basic Custom CardMini: \u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  item: any\n  pending: boolean\n  error: any\n  id: string\n  collection: string\n  refresh: () => Promise\u003Cvoid>\n}>()\n\nconst { open } = useCrouton()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"group relative\">\n    \u003CUSkeleton v-if=\"pending\" class=\"h-16 w-full rounded-md\" />\n\n    \u003Cdiv v-else-if=\"item\" class=\"border rounded-md p-3 bg-white dark:bg-gray-800\">\n      \u003Cdiv class=\"flex items-start gap-3\">\n        \u003CUIcon name=\"i-lucide-map-pin\" class=\"text-blue-500 mt-1\" />\n        \u003Cdiv class=\"flex-1\">\n          \u003Cdiv class=\"font-medium text-sm\">{{ item.name }}\u003C/div>\n          \u003Cdiv class=\"text-xs text-gray-500\">{{ item.address }}\u003C/div>\n          \u003CUBadge v-if=\"item.active\" color=\"green\" size=\"xs\" class=\"mt-1\">\n            Active\n          \u003C/UBadge>\n        \u003C/div>\n      \u003C/div>\n    \u003C/div>\n\n    \u003Cdiv v-else-if=\"error\" class=\"text-red-500 text-xs p-2 border border-red-200 rounded-md\">\n      Failed to load location\n    \u003C/div>\n\n    \u003CCroutonItemButtonsMini\n      v-if=\"item\"\n      update\n      @update=\"open('update', collection, [id])\"\n      class=\"absolute -top-1 right-2 transition delay-150 duration-300 ease-in-out group-hover:-translate-y-6 group-hover:scale-110\"\n    />\n  \u003C/div>\n\u003C/template> User CardMini with Avatar: \u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  item: any\n  pending: boolean\n  error: any\n  id: string\n  collection: string\n  refresh: () => Promise\u003Cvoid>\n}>()\n\nconst { open } = useCrouton()\n\nconst userInitials = computed(() => {\n  if (!props.item?.full_name) return '?'\n  return props.item.full_name\n    .split(' ')\n    .map((n: string) => n[0])\n    .join('')\n    .toUpperCase()\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"group relative\">\n    \u003CUSkeleton v-if=\"pending\" class=\"h-12 w-full rounded-lg\" />\n\n    \u003Cdiv v-else-if=\"item\" class=\"flex items-center gap-3 p-2 border rounded-lg bg-white dark:bg-gray-800\">\n      \u003C!-- Avatar -->\n      \u003Cdiv class=\"flex-shrink-0\">\n        \u003Cimg\n          v-if=\"item.avatar_url\"\n          :src=\"item.avatar_url\"\n          :alt=\"item.full_name\"\n          class=\"w-10 h-10 rounded-full\"\n        />\n        \u003Cdiv v-else class=\"w-10 h-10 rounded-full bg-blue-500 text-white flex items-center justify-center font-medium text-sm\">\n          {{ userInitials }}\n        \u003C/div>\n      \u003C/div>\n\n      \u003C!-- User Info -->\n      \u003Cdiv class=\"flex-1 min-w-0\">\n        \u003Cdiv class=\"font-medium text-sm truncate\">{{ item.full_name }}\u003C/div>\n        \u003Cdiv class=\"text-xs text-gray-500 truncate\">{{ item.email }}\u003C/div>\n      \u003C/div>\n\n      \u003CUBadge v-if=\"item.is_active\" color=\"green\" size=\"xs\">\n        Active\n      \u003C/UBadge>\n    \u003C/div>\n\n    \u003Cdiv v-else-if=\"error\" class=\"text-red-500 text-xs p-2\">\n      User not found\n    \u003C/div>\n\n    \u003CCroutonItemButtonsMini\n      v-if=\"item\"\n      update\n      @update=\"open('update', collection, [id])\"\n      class=\"absolute -top-1 right-2 transition delay-150 duration-300 ease-in-out group-hover:-translate-y-6 group-hover:scale-110\"\n    />\n  \u003C/div>\n\u003C/template>",{"id":4818,"title":4819,"titles":4820,"content":4821,"level":449},"/customization/layouts#naming-convention","Naming Convention",[179,4791],"The component name must match this format: Collection: users → Component: UsersCardMini.vueCollection: bookingsLocations → Component: BookingsLocationsCardMini.vueCollection: productCategories → Component: ProductCategoriesCardMini.vue Rules: Capitalize first letterAdd CardMini suffixPascalCase for multi-word collections",{"id":4823,"title":4824,"titles":4825,"content":4826,"level":391},"/customization/layouts#card-vs-cardmini","Card vs CardMini",[179],"Nuxt Crouton has two distinct card systems that serve different purposes:",{"id":4828,"title":4829,"titles":4830,"content":4831,"level":449},"/customization/layouts#cardvue-layout-rendering","Card.vue - Layout Rendering",[179,4824],"Purpose: Display collection items in list/grid/cards layoutsProps: item, layout, collectionLocation: Collection layer (Card.vue)Use case: Main collection views",{"id":4833,"title":4834,"titles":4835,"content":4836,"level":449},"/customization/layouts#cardmini-reference-display","CardMini - Reference Display",[179,4824],"Purpose: Display cross-collection references (e.g., show a user in a booking table)Props: id, collection, item, pending, errorLocation: Collection layer (CardMini.vue) or app (ItemCardMini.vue)Use case: Table cells, form fields, inline references They are completely separate systems: \u003C!-- Card: For layout rendering -->\n\u003CCard :item=\"booking\" layout=\"grid\" collection=\"bookings\" />\n\n\u003C!-- CardMini: For references -->\n\u003CItemCardMini :id=\"booking.userId\" collection=\"users\" />",{"id":4838,"title":44,"titles":4839,"content":528,"level":391},"/customization/layouts#best-practices",[179],{"id":4841,"title":4303,"titles":4842,"content":4843,"level":449},"/customization/layouts#responsive-layouts",[179,44],"✅ DO: Use presets for quick setup (responsive, mobile-friendly, compact)Test layouts on actual devices, not just browser resizeConsider mobile-first design principles ❌ DON'T: Mix too many different layouts (confuses users)Forget to test on real mobile devicesChange layout types too drastically between breakpoints",{"id":4845,"title":4751,"titles":4846,"content":4847,"level":449},"/customization/layouts#card-components-1",[179,44],"✅ DO: Handle both layout types (list, grid) with size variantsUse the size prop to adapt grid cards for different densitiesUse computed properties for derived dataHandle loading and error statesKeep layout logic in the Card component ❌ DON'T: Fetch additional data in Card components (should be pre-loaded)Make Card components too complexIgnore the layout or size propsDuplicate code across layouts (extract shared logic)",{"id":4849,"title":4791,"titles":4850,"content":4851,"level":449},"/customization/layouts#cardmini-components-1",[179,44],"✅ DO: Keep CardMini components compact (one or two lines of info)Handle loading, error, and empty statesUse loading skeletons for better UXInclude CroutonItemButtonsMini for consistencyTruncate long text to prevent overflow ❌ DON'T: Make CardMini too large (it's meant to be compact)Fetch additional data (use the provided item prop)Forget to handle pending and error statesRemove the mini action buttonsInclude heavy interactions (modals, forms)",{"id":4853,"title":36,"titles":4854,"content":528,"level":391},"/customization/layouts#troubleshooting",[179],{"id":4856,"title":4857,"titles":4858,"content":4859,"level":449},"/customization/layouts#custom-card-not-showing-up","Custom Card not showing up",[179,36],"Check file naming: Collection name: bookingsComponent file: BookingsCard.vue (exact name in Card.vue)Location: collections/bookings/app/components/Card.vue Check component is auto-imported: # Restart dev server to refresh auto-imports\npnpm dev",{"id":4861,"title":4862,"titles":4863,"content":4864,"level":449},"/customization/layouts#custom-cardmini-not-showing-up","Custom CardMini not showing up",[179,36],"Check file naming: Collection name: usersComponent file: UsersCardMini.vue (exact name)Location: app/components/ or collections/{name}/app/components/",{"id":4866,"title":4867,"titles":4868,"content":4869,"level":449},"/customization/layouts#layout-not-changing-on-resize","Layout not changing on resize",[179,36],"The component uses VueUse's useBreakpoints - ensure you're testing with actual browser window resize, not just DevTools responsive mode which can sometimes cache breakpoints.",{"id":4871,"title":4872,"titles":4873,"content":4874,"level":391},"/customization/layouts#typescript-support","TypeScript Support",[179],"// Card Props\ninterface CardProps {\n  item: any\n  layout: 'list' | 'grid' | 'tree'\n  collection: string\n  size?: 'compact' | 'comfortable' | 'spacious'  // Grid size variant\n  pending?: boolean\n  error?: any\n}\n\n// CardMini Props\ninterface CardMiniProps {\n  item: any | null\n  pending: boolean\n  error: any | null\n  id: string\n  collection: string\n  refresh: () => Promise\u003Cvoid>\n}\n\n// Layout Types\ntype LayoutType = 'table' | 'list' | 'grid' | 'tree' | 'kanban' | 'workspace'\ntype GridSize = 'compact' | 'comfortable' | 'spacious'\n\ninterface ResponsiveLayout {\n  base: LayoutType\n  sm?: LayoutType\n  md?: LayoutType\n  lg?: LayoutType\n  xl?: LayoutType\n  '2xl'?: LayoutType\n}\n\ntype LayoutPreset = 'responsive' | 'mobile-friendly' | 'compact' | 'tree-default'\n\n// Collection Props (subset for layouts)\ninterface CollectionProps {\n  layout?: LayoutType | ResponsiveLayout | LayoutPreset\n  gridSize?: GridSize  // Grid density (compact, comfortable, spacious)\n  card?: 'Card' | 'CardMini' | 'CardSmall' | 'CardTree' | string  // Card variant\n  // ... other props\n}",{"id":4876,"title":1562,"titles":4877,"content":4878,"level":391},"/customization/layouts#related-topics",[179],"Custom Components - Other customization patternsCustom Columns - Customizing table columnsWorking with Relations - Working with relationshipsTable Patterns - Table configuration and composition html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":192,"title":191,"titles":4880,"content":4881,"level":385},[],"Add multi-language support to your Crouton collections with automatic translation handling Status: Stable Add comprehensive multi-language support to your Nuxt Crouton applications with the @fyit/crouton-i18n addon package. This package provides team-specific translation overrides, development tools, and automatic locale management.",{"id":4883,"title":18,"titles":4884,"content":528,"level":391},"/features/internationalization#quick-start",[191],{"id":4886,"title":13,"titles":4887,"content":4888,"level":449},"/features/internationalization#installation",[191,18],"First, install the i18n package: pnpm add @fyit/crouton-i18n Then add it to your Nuxt config: // nuxt.config.ts\nexport default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-i18n'\n  ]\n}) Important: The i18n addon is a layer extension, not a standalone package. You must extend both the base layer and the i18n addon.",{"id":4890,"title":4891,"titles":4892,"content":4893,"level":449},"/features/internationalization#mark-fields-as-translatable","Mark Fields as Translatable",[191,18],"Configure which fields in your collections should be translatable: // crouton.config.js\nexport default {\n  translations: {\n    collections: {\n      products: ['name', 'description'],  // These fields are translatable\n      posts: ['title', 'body', 'excerpt']\n    }\n  }\n}",{"id":4895,"title":4896,"titles":4897,"content":4898,"level":449},"/features/internationalization#generated-forms-with-translations","Generated Forms with Translations",[191,18],"When you generate forms with translations enabled, Crouton automatically creates translation inputs: \u003C!-- Auto-generated when using --no-translations=false -->\n\u003Ctemplate>\n  \u003CUForm @submit=\"handleSubmit\">\n    \u003C!-- Regular fields -->\n    \u003CUFormField label=\"SKU\" name=\"sku\">\n      \u003CUInput v-model=\"state.sku\" />\n    \u003C/UFormField>\n\n    \u003C!-- Translatable fields -->\n    \u003CCroutonI18nInput\n      v-model=\"state.translations\"\n      :fields=\"['name', 'description']\"\n      :default-values=\"{\n        name: state.name,\n        description: state.description\n      }\"\n    />\n  \u003C/UForm>\n\u003C/template>",{"id":4900,"title":4901,"titles":4902,"content":4903,"level":449},"/features/internationalization#query-with-locale","Query with Locale",[191,18],"Queries automatically fetch data for the current locale: \u003Cscript setup lang=\"ts\">\nconst { locale } = useI18n()\n\n// Auto-fetches for current locale\nconst { items } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({ locale: locale.value }))\n})\n\n// Auto-refetches when locale changes!\n\u003C/script>",{"id":4905,"title":4906,"titles":4907,"content":4908,"level":449},"/features/internationalization#display-translated-fields","Display Translated Fields",[191,18],"Use the useEntityTranslations composable to display translated content: \u003Cscript setup lang=\"ts\">\nconst { t } = useEntityTranslations()\nconst product = { name: 'Product', translations: { fr: { name: 'Produit' } } }\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- Shows \"Product\" in English, \"Produit\" in French -->\n  \u003Cdiv>{{ t(product, 'name') }}\u003C/div>\n\u003C/template>",{"id":4910,"title":4911,"titles":4912,"content":4913,"level":391},"/features/internationalization#package-overview","Package Overview",[191],"Package: @fyit/crouton-i18nVersion: 0.1.0\nType: Nuxt Layer (Addon)\nDependencies: @nuxtjs/i18n v9.0.0",{"id":4915,"title":183,"titles":4916,"content":4917,"level":449},"/features/internationalization#features",[191,4911],"🌍 Multi-language input components with automatic locale switching🔄 Auto-sync with English as primary language📝 Team-specific translation overrides🎯 Built-in support for EN, NL, FR (configurable)⚡ Inherits all CRUD features from base layer🛠️ Development mode for inline translation editing💾 Database-backed translation management🎨 Rich text editor support for translated content",{"id":4919,"title":4920,"titles":4921,"content":4922,"level":391},"/features/internationalization#locale-file-sources","Locale File Sources",[191],"Crouton's i18n system merges translations from multiple sources. Understanding this merge order is essential for managing translations effectively.",{"id":4924,"title":4925,"titles":4926,"content":4927,"level":449},"/features/internationalization#translation-merge-order","Translation Merge Order",[191,4920],"Translations are loaded and merged in this order (last wins for conflicting keys): 1. @fyit/crouton/i18n/locales/          (Crouton component strings)\n2. @fyit/crouton-auth/i18n/locales/     (Auth strings: signIn, register, etc.)\n3. @fyit/crouton-admin/i18n/locales/    (Admin strings: superAdmin, etc.)\n4. @fyit/crouton-i18n/i18n/locales/     (i18n admin UI strings)\n5. layers/[domain]/i18n/locales/        (Domain-specific strings)\n6. app/i18n/locales/                    (App-level overrides)\n7. Database (translationsUi table)      (Runtime overrides)",{"id":4929,"title":4930,"titles":4931,"content":4932,"level":449},"/features/internationalization#translation-key-ownership","Translation Key Ownership",[191,4920],"PackageKeysExample@fyit/croutonCrouton component stringscrouton.table.search, crouton.form.save@fyit/crouton-authAuth stringsauth.signIn, auth.register, teams.create@fyit/crouton-adminAdmin stringssuperAdmin.dashboard, superAdmin.users@fyit/crouton-i18ni18n admin UIi18n.admin.addKey, i18n.admin.overridelayers/domainDomain-specificbookings.form.location, bookings.status.confirmed",{"id":4934,"title":4935,"titles":4936,"content":4937,"level":449},"/features/internationalization#resolution-order-runtime","Resolution Order (Runtime)",[191,4920],"When $t('common.save') is called: 1. DB: Team override for current team?\n   ↓ (not found)\n2. DB: System default?\n   ↓ (not found)\n3. Locale files (merged, last wins):\n   a. @fyit/crouton/i18n/locales/en.json\n   b. @fyit/crouton-auth/i18n/locales/en.json\n   c. layers/bookings/i18n/locales/en.json\n   d. app/i18n/locales/en.json (if exists)\n   ↓ (found in core)\n4. Return value: \"Save\"",{"id":4939,"title":4940,"titles":4941,"content":4942,"level":391},"/features/internationalization#seeding-translations-to-database","Seeding Translations to Database",[191],"The @fyit/crouton-i18n package provides a CLI command to populate your database with translations from locale files.",{"id":4944,"title":4945,"titles":4946,"content":4947,"level":449},"/features/internationalization#seed-command","Seed Command",[191,4940],"crouton-generate seed-translations",{"id":4949,"title":388,"titles":4950,"content":4951,"level":449},"/features/internationalization#what-it-does",[191,4940],"Reads locale files from all configured sourcesInserts translations into the translationsUi tableSets teamId: null (system default)Sets isOverrideable: true (allows team overrides)Skips existing keys (no duplicates on re-run)",{"id":4953,"title":4954,"titles":4955,"content":4956,"level":449},"/features/internationalization#when-to-run","When to Run",[191,4940],"Initial setup: After installing the i18n packageAfter updates: When locale files are updatedAfter adding layers: When new domain translations are added",{"id":4958,"title":3362,"titles":4959,"content":4960,"level":449},"/features/internationalization#options",[191,4940],"# Seed all locales (via API, requires dev server running)\ncrouton-generate seed-translations\n\n# Preview translations without seeding\ncrouton-generate seed-translations --dry-run\n\n# Output SQL statements instead of using API\ncrouton-generate seed-translations --sql\n\n# Seed from a specific layer only\ncrouton-generate seed-translations --layer=shop",{"id":4962,"title":4963,"titles":4964,"content":4965,"level":391},"/features/internationalization#default-locale-configuration","Default Locale Configuration",[191],"The package comes pre-configured with three locales: // Built-in configuration\n{\n  locales: [\n    { code: 'en', name: 'English', file: 'en.json' },\n    { code: 'nl', name: 'Nederlands', file: 'nl.json' },\n    { code: 'fr', name: 'Français', file: 'fr.json' }\n  ],\n  defaultLocale: 'en',\n  strategy: 'no_prefix'\n}",{"id":4967,"title":4968,"titles":4969,"content":4970,"level":449},"/features/internationalization#customizing-locales","Customizing Locales",[191,4963],"The recommended way to configure locales is via crouton.config.js. This flows through\nto the i18n layer, the CLI generator, and the language switcher UI automatically: // crouton.config.js\nexport default {\n  locales: ['en', 'de', 'es'],  // ISO 639-1 codes — names auto-resolved\n  defaultLocale: 'en',\n\n  // Or with explicit names:\n  // locales: [\n  //   { code: 'en', name: 'English' },\n  //   { code: 'de', name: 'Deutsch' },\n  //   { code: 'es', name: 'Español' }\n  // ],\n\n  features: { /* ... */ },\n  collections: [ /* ... */ ],\n  // ...\n} When no locales key is present, only ['en'] is configured by default. Add your desired locales explicitly. You can also override locales directly in nuxt.config.ts (this takes precedence over crouton.config.js for the i18n module): export default defineNuxtConfig({\n  i18n: {\n    locales: [\n      { code: 'en', name: 'English', file: 'en.json' },\n      { code: 'es', name: 'Español', file: 'es.json' },\n      { code: 'de', name: 'Deutsch', file: 'de.json' }\n    ]\n  }\n})",{"id":4972,"title":4973,"titles":4974,"content":4975,"level":449},"/features/internationalization#adding-translations-to-nuxt-layers","Adding Translations to Nuxt Layers",[191,4963],"Automatic Setup: When you generate collections with translations enabled, Crouton automatically:Creates the i18n/locales/ folder structureGenerates locale files for your configured localesAdds the correct i18n config to your layer's nuxt.config.ts If you need to manually configure i18n in a layer, use relative paths for langDir: // layers/my-layer/nuxt.config.ts\nexport default defineNuxtConfig({\n  i18n: {\n    locales: [\n      { code: 'en', file: 'en.json' },\n      { code: 'nl', file: 'nl.json' },\n      { code: 'fr', file: 'fr.json' }\n    ],\n    langDir: './locales'  // ✅ Relative to restructureDir 'i18n'\n  }\n}) Your locale files should be placed at: layers/my-layer/\n├── i18n/\n│   └── locales/\n│       ├── en.json\n│       ├── nl.json\n│       └── fr.json\n└── nuxt.config.ts Important: The langDir path is relative to the restructureDir (default: i18n). Use ./locales, NOT ./i18n/locales - the latter will result in a doubled path error. Critical: Never use absolute paths for langDir in layers. Absolute paths will work in development but fail in production.// ❌ WRONG - Will break in production\nimport { fileURLToPath } from 'node:url'\nimport { join } from 'node:path'\n\nconst currentDir = fileURLToPath(new URL('.', import.meta.url))\n\nexport default defineNuxtConfig({\n  i18n: {\n    langDir: join(currentDir, 'i18n/locales')  // ❌ Absolute path\n  }\n})",{"id":4977,"title":304,"titles":4978,"content":4979,"level":391},"/features/internationalization#components-reference",[191],"The package provides components with the CroutonI18n prefix for managing translations.",{"id":4981,"title":4982,"titles":4983,"content":4984,"level":449},"/features/internationalization#croutoni18ndisplay","CroutonI18nDisplay",[191,304],"Display translated content with automatic fallback to English.",{"id":4986,"title":4987,"titles":4988,"content":4989,"level":748},"/features/internationalization#props","Props",[191,304,4982],"interface DisplayProps {\n  translations: Record\u003Cstring, string>  // Translation values by locale\n  languages?: string[]                   // Override available languages\n}",{"id":4991,"title":183,"titles":4992,"content":4993,"level":748},"/features/internationalization#features-1",[191,304,4982],"Badge Display: Shows translation status for each languagePopover Preview: Displays translations up to 200 charactersModal View: For longer translationsCopy to Clipboard: Quick copy functionalityCharacter/Word Count: Metadata displayFallback Indication: Shows when using English fallback",{"id":4995,"title":1608,"titles":4996,"content":4997,"level":748},"/features/internationalization#usage",[191,304,4982],"\u003Cscript setup lang=\"ts\">\nconst product = {\n  name: 'Product',\n  translations: {\n    en: 'English Product Name',\n    nl: 'Nederlandse Productnaam',\n    fr: 'Nom du produit français'\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- Basic usage -->\n  \u003CCroutonI18nDisplay :translations=\"product.translations\" />\n\n  \u003C!-- Custom languages -->\n  \u003CCroutonI18nDisplay\n    :translations=\"product.translations\"\n    :languages=\"['en', 'nl']\"\n  />\n\u003C/template>",{"id":4999,"title":5000,"titles":5001,"content":5002,"level":748},"/features/internationalization#badge-colors","Badge Colors",[191,304,4982],"Primary: Translation exists for this localeNeutral: No translation availableError: Translation validation failed",{"id":5004,"title":5005,"titles":5006,"content":5007,"level":449},"/features/internationalization#croutoni18ninput","CroutonI18nInput",[191,304],"Multi-language input component for forms supporting both single and multi-field translations.",{"id":5009,"title":4987,"titles":5010,"content":5011,"level":748},"/features/internationalization#props-1",[191,304,5005],"interface InputProps {\n  modelValue: SingleFieldValue | MultiFieldValue | null\n  fields: string[]                          // Fields to translate\n  label?: string                            // Form label\n  error?: string | boolean                  // Validation error\n  defaultValues?: Record\u003Cstring, string>    // Default values for fallback\n  fieldComponents?: Record\u003Cstring, string>  // Custom components per field\n  showAiTranslate?: boolean                 // Enable AI translation suggestions\n  fieldType?: string                        // Field type context for AI (e.g., 'product_name', 'description')\n  collab?: CollabConnection                 // Collab connection for real-time block editing via Yjs\n  layout?: 'tabs' | 'side-by-side'          // Layout mode (default: 'tabs')\n  primaryLocale?: string                    // Primary locale for side-by-side layout (default: 'en')\n  secondaryLocale?: string                  // Secondary locale for side-by-side layout\n  fieldOptions?: Record\u003Cstring, FieldOptions> // Field-specific options like transforms\n  fieldGroups?: Record\u003Cstring, string>      // Group fields into collapsible sections (field name → group label)\n  defaultOpenGroups?: string[]              // Which groups start open (default: all)\n}\n\ntype SingleFieldValue = Record\u003Cstring, string>\ntype MultiFieldValue = Record\u003Cstring, Record\u003Cstring, string>>",{"id":5013,"title":183,"titles":5014,"content":5015,"level":748},"/features/internationalization#features-2",[191,304,5005],"Language Tabs: Switch between locales with visual indicatorsCompletion Status: Shows which locales are completeRequired Fields: English marked as requiredFallback Hints: Shows English text when editing other languagesCustom Components: Support for UInput, UTextarea, CroutonEditorSimpleValidation: Highlights errors for required fields",{"id":5017,"title":5018,"titles":5019,"content":5020,"level":748},"/features/internationalization#single-field-mode","Single Field Mode",[191,304,5005],"\u003Cscript setup lang=\"ts\">\nconst state = ref({\n  name: '',\n  translations: {} // { en: \"English\", nl: \"Nederlands\", fr: \"Français\" }\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonI18nInput\n    v-model=\"state.translations\"\n    :fields=\"[]\"\n  />\n\u003C/template>",{"id":5022,"title":5023,"titles":5024,"content":5025,"level":748},"/features/internationalization#multi-field-mode","Multi-Field Mode",[191,304,5005],"\u003Cscript setup lang=\"ts\">\nconst state = ref({\n  name: 'Product',\n  description: 'Description',\n  translations: {} // { en: { name: \"...\", description: \"...\" }, nl: { ... } }\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonI18nInput\n    v-model=\"state.translations\"\n    :fields=\"['name', 'description']\"\n    :default-values=\"{\n      name: state.name,\n      description: state.description\n    }\"\n  />\n\u003C/template>",{"id":5027,"title":5028,"titles":5029,"content":5030,"level":748},"/features/internationalization#custom-field-components","Custom Field Components",[191,304,5005],"\u003Cscript setup lang=\"ts\">\nconst state = ref({\n  title: '',\n  content: '\u003Cp>\u003C/p>',\n  translations: {}\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonI18nInput\n    v-model=\"state.translations\"\n    :fields=\"['title', 'content']\"\n    :default-values=\"{\n      title: state.title,\n      content: state.content\n    }\"\n    :field-components=\"{\n      content: 'CroutonEditorSimple',  // Rich text editor\n      title: 'UInput'                   // Default input\n    }\"\n  />\n\u003C/template> Supported Field Components: UInput - Single-line text (default)UTextarea - Multi-line textCroutonEditorSimple - Rich text editor",{"id":5032,"title":5033,"titles":5034,"content":5035,"level":449},"/features/internationalization#croutoni18ninputwitheditor","CroutonI18nInputWithEditor",[191,304],"Specialized input component with built-in rich text editor support.",{"id":5037,"title":4987,"titles":5038,"content":5039,"level":748},"/features/internationalization#props-2",[191,304,5033],"interface InputWithEditorProps {\n  modelValue: Record\u003Cstring, string>  // Translation values\n  fields: string[]                     // For backwards compatibility\n  label?: string\n  error?: string | boolean\n  useRichText?: boolean                // Enable rich text editor\n}",{"id":5041,"title":183,"titles":5042,"content":5043,"level":748},"/features/internationalization#features-3",[191,304,5033],"Rich Text Toggle: Switch between plain text and rich editorEnglish Reference: Shows original when editing translationsHeight Control: Fixed 256px height for editorVisual Indicators: Border colors for validation states",{"id":5045,"title":1608,"titles":5046,"content":5047,"level":748},"/features/internationalization#usage-1",[191,304,5033],"\u003Cscript setup lang=\"ts\">\nconst state = ref({\n  content: '\u003Cp>English content\u003C/p>',\n  translations: {}\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- With rich text editor -->\n  \u003CCroutonI18nInputWithEditor\n    v-model=\"state.translations\"\n    :fields=\"['content']\"\n    :use-rich-text=\"true\"\n  />\n\n  \u003C!-- Plain text input -->\n  \u003CCroutonI18nInputWithEditor\n    v-model=\"state.translations\"\n    :fields=\"['content']\"\n    :use-rich-text=\"false\"\n  />\n\u003C/template>",{"id":5049,"title":5050,"titles":5051,"content":5052,"level":449},"/features/internationalization#croutoni18nlanguageswitcher","CroutonI18nLanguageSwitcher",[191,304],"Dropdown selector for switching between available languages.",{"id":5054,"title":183,"titles":5055,"content":5056,"level":748},"/features/internationalization#features-4",[191,304,5050],"Locale Selection: Uses Nuxt UI's USelect with ghost variantUppercase Labels: Locale codes displayed in uppercase (e.g., EN, NL, FR)Locale Persistence: Saves locale preference to database for authenticated users",{"id":5058,"title":1608,"titles":5059,"content":5060,"level":748},"/features/internationalization#usage-2",[191,304,5050],"\u003Ctemplate>\n  \u003C!-- Basic usage -->\n  \u003CCroutonI18nLanguageSwitcher />\n\u003C/template> The component automatically: Reads available locales from i18n configUpdates the current locale on selectionPersists the locale to the database if the user is authenticatedTriggers reactive updates across the app",{"id":5062,"title":5063,"titles":5064,"content":5065,"level":449},"/features/internationalization#croutoni18nlanguageswitcherisland","CroutonI18nLanguageSwitcherIsland",[191,304],"Floating language switcher for overlay/island usage.",{"id":5067,"title":183,"titles":5068,"content":5069,"level":748},"/features/internationalization#features-5",[191,304,5063],"Fixed Position: Top-right corner (z-index 50)Dropdown Menu: UDropdownMenu with custom stylingDark Theme: Black background with hover effectsUppercase Labels: Language codes in uppercaseCheck Indicator: Shows current selection",{"id":5071,"title":1608,"titles":5072,"content":5073,"level":748},"/features/internationalization#usage-3",[191,304,5063],"\u003Ctemplate>\n  \u003C!-- Floating switcher in top-right -->\n  \u003CCroutonI18nLanguageSwitcherIsland />\n\u003C/template>",{"id":5075,"title":5076,"titles":5077,"content":5078,"level":449},"/features/internationalization#croutoni18nuiform","CroutonI18nUiForm",[191,304],"Form component for managing UI translation overrides.",{"id":5080,"title":4987,"titles":5081,"content":5082,"level":748},"/features/internationalization#props-3",[191,304,5076],"interface UiFormProps {\n  action: 'create' | 'update' | 'delete'\n  activeItem?: any                       // Item being edited\n  loading?: string                       // Loading state\n  collection: string                     // Collection name\n}",{"id":5084,"title":183,"titles":5085,"content":5086,"level":748},"/features/internationalization#features-6",[191,304,5076],"Delete Confirmation: Modal with warning iconTranslation Preview: Shows translation being deletedForm Validation: Zod schema validationKey Path Input: Auto-disabled when editingCategory Field: Organizes translations",{"id":5088,"title":1608,"titles":5089,"content":5090,"level":748},"/features/internationalization#usage-4",[191,304,5076],"\u003Cscript setup lang=\"ts\">\nconst action = ref\u003C'create' | 'update' | 'delete'>('create')\nconst activeItem = ref(null)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonI18nUiForm\n    :action=\"action\"\n    :active-item=\"activeItem\"\n    collection=\"translationsUi\"\n  />\n\u003C/template>",{"id":5092,"title":5093,"titles":5094,"content":5095,"level":748},"/features/internationalization#form-fields","Form Fields",[191,304,5076],"Key Path (required): Dot-notation key (e.g., table.search)Category (required): Grouping category (e.g., table)Translations (required): Multi-language valuesDescription (optional): Documentation for the translation",{"id":5097,"title":5098,"titles":5099,"content":5100,"level":449},"/features/internationalization#croutoni18nuilist","CroutonI18nUiList",[191,304],"List view for managing UI translations with Crouton table integration.",{"id":5102,"title":183,"titles":5103,"content":5104,"level":748},"/features/internationalization#features-7",[191,304,5098],"CroutonTable Integration: Uses standard table componentCustom Cell Rendering: Special handling for translation valuesCreate Button: Built-in creation workflowAuto-Refresh: Reactive updates",{"id":5106,"title":5107,"titles":5108,"content":5109,"level":748},"/features/internationalization#default-columns","Default Columns",[191,304,5098],"[\n  { key: 'keyPath', label: 'Key Path' },\n  { key: 'category', label: 'Category' },\n  { key: 'values', label: 'Translations' },\n  { key: 'description', label: 'Description' },\n  { key: 'actions', label: 'Actions' }\n]",{"id":5111,"title":1608,"titles":5112,"content":5113,"level":748},"/features/internationalization#usage-5",[191,304,5098],"\u003Ctemplate>\n  \u003CCroutonI18nUiList />\n\u003C/template> The component automatically: Fetches translations using useCollectionQuery('translationsUi')Displays translations with CroutonI18nDisplay componentProvides CRUD actions via CroutonTable",{"id":5115,"title":5116,"titles":5117,"content":5118,"level":449},"/features/internationalization#croutoni18ncardsmini","CroutonI18nCardsMini",[191,304],"Badge component showing single locale translation status.",{"id":5120,"title":4987,"titles":5121,"content":5122,"level":748},"/features/internationalization#props-4",[191,304,5116],"interface CardsMiniProps {\n  locale: string        // Locale code (en, nl, fr)\n  hasTranslation: boolean  // Whether translation exists\n}",{"id":5124,"title":183,"titles":5125,"content":5126,"level":748},"/features/internationalization#features-8",[191,304,5116],"Color Coding: Primary for exists, neutral for missingUppercase Labels: Locale codes in uppercaseSmall Size: Compact badge display",{"id":5128,"title":1608,"titles":5129,"content":5130,"level":748},"/features/internationalization#usage-6",[191,304,5116],"\u003Ctemplate>\n  \u003CCroutonI18nCardsMini locale=\"en\" :has-translation=\"true\" />\n  \u003CCroutonI18nCardsMini locale=\"nl\" :has-translation=\"false\" />\n\u003C/template>",{"id":5132,"title":5133,"titles":5134,"content":5135,"level":449},"/features/internationalization#croutoni18nlistcards","CroutonI18nListCards",[191,304],"Container component showing translation status for all locales.",{"id":5137,"title":4987,"titles":5138,"content":5139,"level":748},"/features/internationalization#props-5",[191,304,5133],"interface ListCardsProps {\n  item: any           // Entity with translations\n  fields: string[]    // Fields to check for translations\n}",{"id":5141,"title":183,"titles":5142,"content":5143,"level":748},"/features/internationalization#features-9",[191,304,5133],"Multi-Locale Display: Shows all configured localesField-Based Validation: Checks if any field has translationAuto-Layout: Flex gap layout",{"id":5145,"title":1608,"titles":5146,"content":5147,"level":748},"/features/internationalization#usage-7",[191,304,5133],"\u003Cscript setup lang=\"ts\">\nconst product = {\n  translations: {\n    en: { name: 'Product', description: 'Description' },\n    nl: { name: 'Product' }  // description missing\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonI18nListCards\n    :item=\"product\"\n    :fields=\"['name', 'description']\"\n  />\n  \u003C!-- Shows: EN (complete), NL (incomplete), FR (missing) -->\n\u003C/template>",{"id":5149,"title":5150,"titles":5151,"content":5152,"level":449},"/features/internationalization#croutoni18ndevmodetoggle","CroutonI18nDevModeToggle",[191,304],"Development tool for managing missing translations with inline editing.",{"id":5154,"title":183,"titles":5155,"content":5156,"level":748},"/features/internationalization#features-10",[191,304,5150],"Dev-Only: Only visible in development modeTranslation Scanner: Finds [key] patterns in DOMClick-to-Edit: Modal for adding missing translationsVisual Highlighting: Red background for missing translationsAuto-Refresh: Updates UI after saving",{"id":5158,"title":1608,"titles":5159,"content":5160,"level":748},"/features/internationalization#usage-8",[191,304,5150],"\u003Ctemplate>\n  \u003C!-- Add to your layout for dev mode -->\n  \u003CCroutonI18nDevModeToggle />\n\u003C/template>",{"id":5162,"title":5163,"titles":5164,"content":5165,"level":748},"/features/internationalization#workflow","Workflow",[191,304,5150],"Click \"Enable translation dev mode\"Scanner finds all [keyPath] patternsClick any highlighted translationModal opens for that keyEnter translationSave to databasePage refreshes with new translation",{"id":5167,"title":333,"titles":5168,"content":528,"level":391},"/features/internationalization#composables-reference",[191],{"id":5170,"title":5171,"titles":5172,"content":5173,"level":449},"/features/internationalization#uset","useT()",[191,333],"Enhanced translation composable with team override support and development mode.",{"id":5175,"title":5176,"titles":5177,"content":5178,"level":748},"/features/internationalization#type-signature","Type Signature",[191,333,5171],"interface TranslationOptions {\n  params?: Record\u003Cstring, any>\n  fallback?: string\n  category?: string\n  mode?: 'system' | 'team'\n  placeholder?: string\n}\n\nfunction useT(): {\n  t: (key: string, options?: TranslationOptions) => string\n  tString: (key: string, options?: TranslationOptions) => string\n  tContent: (entity: any, field: string, preferredLocale?: string) => string\n  tInfo: (key: string, options?: TranslationOptions) => TranslationInfo\n  hasTranslation: (key: string) => boolean\n  getAvailableLocales: () => string[]\n  getTranslationMeta: (key: string) => TranslationMeta\n  refreshTranslations: () => Promise\u003Cvoid>\n  locale: Ref\u003Cstring>\n  isDev: boolean\n  devModeEnabled: Ref\u003Cboolean>\n}",{"id":5180,"title":183,"titles":5181,"content":5182,"level":748},"/features/internationalization#features-11",[191,333,5171],"Team Overrides: Checks team translations before systemAuto-Caching: Caches team translations per team/localeParameter Substitution: {key} replacement in stringsFallback Support: Custom fallback valuesMissing Indicators: Returns [key] when not foundContent Translation: Direct entity field translationMetadata Access: Get translation info for admin UIs",{"id":5184,"title":4173,"titles":5185,"content":5186,"level":748},"/features/internationalization#basic-usage",[191,333,5171],"\u003Cscript setup lang=\"ts\">\nconst { t, tString, tContent } = useT()\n\n// Basic translation\nconst saveLabel = t('common.save')\n\n// With parameters\nconst greeting = t('messages.hello', {\n  params: { name: 'John' }\n})\n\n// String-only (for non-template contexts)\nconst title = tString('page.title')\n\n// Entity field translation\nconst productName = tContent(product, 'name')\n\u003C/script>",{"id":5188,"title":5189,"titles":5190,"content":5191,"level":748},"/features/internationalization#team-override-example","Team Override Example",[191,333,5171],"\u003Cscript setup lang=\"ts\">\nconst { t } = useT()\nconst { currentTeam } = useTeam()\n\n// 1. Checks team translations for current team\n// 2. Falls back to system translation\n// 3. Returns [key] if not found\nconst label = t('dashboard.welcome')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Ch1>{{ label }}\u003C/h1>\n\u003C/template>",{"id":5193,"title":5194,"titles":5195,"content":5196,"level":748},"/features/internationalization#content-translation","Content Translation",[191,333,5171],"\u003Cscript setup lang=\"ts\">\nconst { tContent } = useT()\nconst { locale } = useI18n()\n\nconst product = {\n  name: 'Product',\n  translations: {\n    en: { name: 'English Name', description: 'English Description' },\n    nl: { name: 'Nederlandse Naam' }\n  }\n}\n\n// In English: \"English Name\"\n// In Dutch: \"Nederlandse Naam\"\n// In French: \"English Name\" (fallback)\nconst name = tContent(product, 'name')\n\n// With explicit locale\nconst dutchName = tContent(product, 'name', 'nl')\n\u003C/script>",{"id":5198,"title":5199,"titles":5200,"content":5201,"level":449},"/features/internationalization#useentitytranslations","useEntityTranslations()",[191,333],"Simple composable for translating entity fields based on current locale.",{"id":5203,"title":5176,"titles":5204,"content":5205,"level":748},"/features/internationalization#type-signature-1",[191,333,5199],"function useEntityTranslations(): {\n  t: (entity: any, field: string) => string\n}",{"id":5207,"title":1608,"titles":5208,"content":5209,"level":748},"/features/internationalization#usage-9",[191,333,5199],"\u003Cscript setup lang=\"ts\">\nconst { t } = useEntityTranslations()\nconst { locale } = useI18n()\n\nconst product = {\n  name: 'Product',\n  description: 'English description',\n  translations: {\n    nl: {\n      name: 'Product',\n      description: 'Nederlandse beschrijving'\n    }\n  }\n}\n\n// Automatically uses current locale\nconst name = t(product, 'name')\nconst description = t(product, 'description')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch2>{{ name }}\u003C/h2>\n    \u003Cp>{{ description }}\u003C/p>\n  \u003C/div>\n\u003C/template>",{"id":5211,"title":5212,"titles":5213,"content":5214,"level":748},"/features/internationalization#fallback-behavior","Fallback Behavior",[191,333,5199],"// Priority:\n// 1. translations[currentLocale][field]\n// 2. entity[field] (root field value)\n// 3. Empty string\n\nconst product = {\n  name: 'Default Name',\n  translations: {\n    nl: { name: 'Nederlandse Naam' }\n  }\n}\n\n// In NL locale: \"Nederlandse Naam\"\n// In EN locale: \"Default Name\" (fallback to root)\n// In FR locale: \"Default Name\" (fallback to root)\nt(product, 'name')",{"id":5216,"title":5217,"titles":5218,"content":5219,"level":449},"/features/internationalization#translationsui-exports","translationsUi Exports",[191,333],"The useTranslationsUi file exports named constants for the translationsUi collection configuration. These are not a composable function — import the individual exports directly.",{"id":5221,"title":5222,"titles":5223,"content":5224,"level":748},"/features/internationalization#named-exports","Named Exports",[191,333,5217],"import {\n  translationsUiSchema,\n  TRANSLATIONS_UI_COLUMNS,\n  TRANSLATIONS_UI_DEFAULTS,\n  TRANSLATIONS_UI_PAGINATION,\n  translationsUiConfig,\n  TRANSLATIONS_UI_COLLECTION\n} from '#imports' translationsUiSchema: Zod validation schema for translation recordsTRANSLATIONS_UI_COLUMNS: Table column definitions for CroutonTableTRANSLATIONS_UI_DEFAULTS: Default form values for new translationsTRANSLATIONS_UI_PAGINATION: Default pagination settingstranslationsUiConfig: Full collection configuration object (includes schema as non-enumerable property)TRANSLATIONS_UI_COLLECTION: Collection name constant ('translationsUi')",{"id":5226,"title":1608,"titles":5227,"content":5228,"level":748},"/features/internationalization#usage-10",[191,333,5217],"\u003Cscript setup lang=\"ts\">\nconst state = ref({ ...TRANSLATIONS_UI_DEFAULTS })\n\nconst handleSubmit = async () => {\n  const valid = translationsUiSchema.parse(state.value)\n  await $fetch('/api/translations-ui', {\n    method: 'POST',\n    body: valid\n  })\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm :schema=\"translationsUiSchema\" :state=\"state\" @submit=\"handleSubmit\">\n    \u003CUFormField label=\"Key Path\" name=\"keyPath\">\n      \u003CUInput v-model=\"state.keyPath\" />\n    \u003C/UFormField>\n    \u003C!-- ... -->\n  \u003C/UForm>\n\u003C/template>",{"id":5230,"title":1603,"titles":5231,"content":528,"level":391},"/features/internationalization#database-schema",[191],{"id":5233,"title":5234,"titles":5235,"content":5236,"level":449},"/features/internationalization#translationsui-table","translationsUi Table",[191,1603],"The package adds a translations_ui table for storing system and team-specific translations. {\n  id: string (primary key)\n  userId: string (not null)\n  teamId: string (nullable)  // null = system translation\n  namespace: string (default: 'ui')\n  keyPath: string (not null)\n  category: string (not null)\n  values: Record\u003Cstring, string> (JSON)\n  description: string (nullable)\n  isOverrideable: boolean (default: true)\n  createdAt: timestamp\n  updatedAt: timestamp\n}",{"id":5238,"title":5239,"titles":5240,"content":5241,"level":748},"/features/internationalization#unique-constraint","Unique Constraint",[191,1603,5234],"unique(teamId, namespace, keyPath) This ensures one translation per team per key path.",{"id":5243,"title":5244,"titles":5245,"content":5246,"level":748},"/features/internationalization#translation-types","Translation Types",[191,1603,5234],"System Translations: teamId = null, isOverrideable = trueTeam Overrides: teamId = \u003Cteam-id>",{"id":5248,"title":5249,"titles":5250,"content":528,"level":391},"/features/internationalization#integration-examples","Integration Examples",[191],{"id":5252,"title":5253,"titles":5254,"content":5255,"level":449},"/features/internationalization#complete-product-management","Complete Product Management",[191,5249],"Basic form setup: \u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{ action: 'create' | 'update', item?: Product }>()\nconst { create, update } = useCollectionMutation('products')\n\nconst state = ref({\n  sku: props.item?.sku || '',\n  name: props.item?.name || '',\n  price: props.item?.price || 0,\n  translations: props.item?.translations || {}\n})\n\nconst handleSubmit = async () => {\n  if (props.action === 'create') {\n    await create(state.value)\n  } else {\n    await update(props.item!.id, state.value)\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm :state=\"state\" @submit=\"handleSubmit\">\n    \u003CUFormField label=\"SKU\" name=\"sku\" required>\n      \u003CUInput v-model=\"state.sku\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Name (English)\" name=\"name\" required>\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n\n    \u003CCroutonButton :action=\"action\" collection=\"products\" />\n  \u003C/UForm>\n\u003C/template> With translation inputs: \u003Ctemplate>\n  \u003CUForm :state=\"state\" @submit=\"handleSubmit\">\n    \u003C!-- Default language fields -->\n    \u003CUFormField label=\"Name (English)\" name=\"name\" required>\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Description (English)\" name=\"description\">\n      \u003CUTextarea v-model=\"state.description\" rows=\"3\" />\n    \u003C/UFormField>\n\n    \u003C!-- Translations component -->\n    \u003CUFormField label=\"Translations\">\n      \u003CCroutonI18nInput\n        v-model=\"state.translations\"\n        :fields=\"['name', 'description']\"\n        :default-values=\"{ name: state.name, description: state.description }\"\n        :field-components=\"{ description: 'UTextarea' }\"\n      />\n    \u003C/UFormField>\n\n    \u003CCroutonButton :action=\"action\" collection=\"products\" />\n  \u003C/UForm>\n\u003C/template>",{"id":5257,"title":5258,"titles":5259,"content":5260,"level":449},"/features/internationalization#product-display-with-translations","Product Display with Translations",[191,5249],"\u003Cscript setup lang=\"ts\">\nconst { locale } = useI18n()\nconst { items } = await useCollectionQuery('products', {\n  query: computed(() => ({\n    locale: locale.value\n  }))\n})\n\nconst { t } = useEntityTranslations()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"grid gap-4\">\n    \u003Cdiv v-for=\"product in items\" :key=\"product.id\" class=\"card\">\n      \u003C!-- Translated name -->\n      \u003Ch3>{{ t(product, 'name') }}\u003C/h3>\n\n      \u003C!-- Translated description -->\n      \u003Cp>{{ t(product, 'description') }}\u003C/p>\n\n      \u003C!-- Translation status badges -->\n      \u003CCroutonI18nListCards\n        :item=\"product\"\n        :fields=\"['name', 'description']\"\n      />\n\n      \u003C!-- Non-translated fields -->\n      \u003Cdiv class=\"text-sm text-gray-500\">\n        SKU: {{ product.sku }} | Price: {{ product.price }}\n      \u003C/div>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":5262,"title":5263,"titles":5264,"content":5265,"level":449},"/features/internationalization#admin-translation-management","Admin Translation Management",[191,5249],"\u003Cscript setup lang=\"ts\">\n// Full admin page for UI translations\ndefinePageMeta({\n  middleware: 'admin'\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"space-y-6\">\n    \u003Cdiv class=\"flex justify-between items-center\">\n      \u003Ch1>UI Translations\u003C/h1>\n      \u003CCroutonI18nLanguageSwitcher />\n    \u003C/div>\n\n    \u003C!-- Translation management table -->\n    \u003CCroutonI18nUiList />\n\n    \u003C!-- Dev mode toggle for testing -->\n    \u003CCroutonI18nDevModeToggle />\n  \u003C/div>\n\u003C/template>",{"id":5267,"title":44,"titles":5268,"content":528,"level":391},"/features/internationalization#best-practices",[191],{"id":5270,"title":5271,"titles":5272,"content":5273,"level":449},"/features/internationalization#_1-always-provide-english","1. Always Provide English",[191,44],"English is the fallback language - always provide English translations. \u003C!-- ✅ Good -->\n\u003CCroutonI18nInput\n  v-model=\"state.translations\"\n  :fields=\"['name']\"\n  :default-values=\"{ name: state.name }\"\n/>\n\n\u003C!-- ❌ Bad - No English default -->\n\u003CCroutonI18nInput\n  v-model=\"state.translations\"\n  :fields=\"['name']\"\n/>",{"id":5275,"title":5276,"titles":5277,"content":5278,"level":449},"/features/internationalization#_2-use-entity-translations-for-content","2. Use Entity Translations for Content",[191,44],"For content fields, store translations in the entity: // ✅ Good - Entity field translations\n{\n  id: '123',\n  name: 'Product',\n  translations: {\n    nl: { name: 'Product' },\n    fr: { name: 'Produit' }\n  }\n}\n\n// ❌ Bad - Don't use UI translations for content\n// UI translations are for interface labels only",{"id":5280,"title":5281,"titles":5282,"content":5283,"level":449},"/features/internationalization#_3-organize-translation-keys","3. Organize Translation Keys",[191,44],"Use dot notation and categories: // ✅ Good\n'common.save'\n'common.cancel'\n'table.search'\n'table.filter'\n'dashboard.title'\n\n// ❌ Bad\n'save'\n'cancelButton'\n'SearchTable'",{"id":5285,"title":5286,"titles":5287,"content":5288,"level":449},"/features/internationalization#_4-leverage-fallback-behavior","4. Leverage Fallback Behavior",[191,44],"\u003Cscript setup lang=\"ts\">\n// ✅ Good - Relies on automatic fallback\nconst { t } = useEntityTranslations()\nconst name = t(product, 'name')\n\n// ❌ Bad - Manual fallback logic\nconst name = product.translations?.[locale.value]?.name || product.name || ''\n\u003C/script>",{"id":5290,"title":5291,"titles":5292,"content":5293,"level":449},"/features/internationalization#_5-use-rich-text-for-long-content","5. Use Rich Text for Long Content",[191,44],"\u003C!-- ✅ Good - Rich text for long content -->\n\u003CCroutonI18nInput\n  v-model=\"state.translations\"\n  :fields=\"['content']\"\n  :field-components=\"{ content: 'CroutonEditorSimple' }\"\n/>\n\n\u003C!-- ❌ Bad - Plain text for HTML content -->\n\u003CCroutonI18nInput\n  v-model=\"state.translations\"\n  :fields=\"['content']\"\n/>",{"id":5295,"title":5296,"titles":5297,"content":5298,"level":449},"/features/internationalization#_6-enable-dev-mode-during-development","6. Enable Dev Mode During Development",[191,44],"\u003Ctemplate>\n  \u003C!-- ✅ Good - Always include in dev layout -->\n  \u003CCroutonI18nDevModeToggle />\n\n  \u003C!-- Quickly identify and fix missing translations -->\n\u003C/template>",{"id":5300,"title":36,"titles":5301,"content":528,"level":391},"/features/internationalization#troubleshooting",[191],{"id":5303,"title":5304,"titles":5305,"content":5306,"level":449},"/features/internationalization#translations-not-updating","Translations Not Updating",[191,36],"Problem: Changes to translations don't appear Solution: Clear cache and refresh const { refreshTranslations } = useT()\nawait refreshTranslations()",{"id":5308,"title":5309,"titles":5310,"content":5311,"level":449},"/features/internationalization#missing-translation-indicator","Missing Translation Indicator",[191,36],"Problem: Seeing [key.path] instead of translation Solutions: Check if translation exists in locale fileVerify key path is correctCheck team override isn't blocking system translationUse dev mode to identify missing translations \u003Ctemplate>\n  \u003CCroutonI18nDevModeToggle />\n  \u003C!-- Click highlighted translations to add them -->\n\u003C/template>",{"id":5313,"title":5314,"titles":5315,"content":5316,"level":449},"/features/internationalization#wrong-language-displayed","Wrong Language Displayed",[191,36],"Problem: Content shows in wrong language Solutions: Check current locale: const { locale } = useI18n()Verify query includes locale parameterCheck entity has translation for that locale \u003Cscript setup lang=\"ts\">\nconst { locale } = useI18n()\nconsole.log('Current locale:', locale.value)\n\n// Ensure query includes locale\nconst { items } = await useCollectionQuery('products', {\n  query: computed(() => ({ locale: locale.value }))\n})\n\u003C/script>",{"id":5318,"title":3680,"titles":5319,"content":5320,"level":391},"/features/internationalization#related-documentation",[191],"Collections & Layers - Collection basicsData Operations - Working with dataQuerying Data - Query patternsPackage Architecture - Understanding addons html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":196,"title":195,"titles":5322,"content":5323,"level":385},[],"Interactive maps with Mapbox GL JS for location-based features Status: Beta - This package is in beta and under active development. APIs may change in future releases. Use in production with caution and be prepared for potential breaking changes. The @fyit/crouton-maps package provides seamless map integration for your Nuxt Crouton applications using Mapbox GL JS. Add interactive maps, geocoding, and location-based features to your app.",{"id":5325,"title":5326,"titles":5327,"content":5328,"level":391},"/features/maps#package-information","Package Information",[195],"{\n  \"name\": \"@fyit/crouton-maps\",\n  \"version\": \"0.1.0\",\n  \"status\": \"BETA\"\n}",{"id":5330,"title":13,"titles":5331,"content":5332,"level":391},"/features/maps#installation",[195],"Install the beta package: pnpm add @fyit/crouton-maps Dependencies: This package uses nuxt-mapbox (v1.6.4+) and mapbox-gl (v3.0.0+) under the hood.",{"id":5334,"title":5335,"titles":5336,"content":528,"level":391},"/features/maps#setup","Setup",[195],{"id":5338,"title":5339,"titles":5340,"content":5341,"level":449},"/features/maps#_1-get-a-mapbox-access-token","1. Get a Mapbox Access Token",[195,5335],"Sign up for a free Mapbox account: https://account.mapbox.com/Get your access token: https://account.mapbox.com/access-tokens/Free tier includes:\n50,000 map loads/month100,000 geocoding requests/month",{"id":5343,"title":5344,"titles":5345,"content":5346,"level":449},"/features/maps#_2-configure-your-project","2. Configure Your Project",[195,5335],"Add the layer to your nuxt.config.ts: export default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-maps'  // Add maps layer\n  ]\n  // No runtimeConfig needed — the layer auto-configures from env vars\n})",{"id":5348,"title":5349,"titles":5350,"content":5351,"level":449},"/features/maps#_3-add-environment-variables","3. Add Environment Variables",[195,5335],"Create or update your .env file: # Server-side token (used by geocoding proxy, never sent to client)\nMAPBOX_TOKEN=sk.eyJ1IjoieW91ciIsImEiOiJ0b2tlbiJ9...\n\n# Optional: domain-restricted browser token for client-side tile loading\n# Falls back to MAPBOX_TOKEN if not set (fine for local dev)\nMAPBOX_PUBLIC_TOKEN=pk.eyJ1IjoieW91ciIsImEiOiJ0b2tlbiJ9... The layer automatically reads MAPBOX_TOKEN and MAPBOX_PUBLIC_TOKEN from your environment and configures both private (server-side geocoding) and public (client-side tile loading) runtime config. No manual runtimeConfig setup is required. To customize default map style, center, or zoom, you can optionally override in your nuxt.config.ts: runtimeConfig: {\n  public: {\n    mapbox: {\n      style: 'mapbox://styles/mapbox/dark-v11',\n      center: [4.9041, 52.3676],  // [lng, lat]\n      zoom: 10\n    }\n  }\n} Security: Never commit your .env file to version control. Add it to your .gitignore. For production, use a domain-restricted browser key for MAPBOX_PUBLIC_TOKEN.",{"id":5353,"title":5354,"titles":5355,"content":528,"level":391},"/features/maps#components","Components",[195],{"id":5357,"title":5358,"titles":5359,"content":5360,"level":449},"/features/maps#croutonmapsmap","CroutonMapsMap",[195,5354],"Interactive map component with loading states and error handling.",{"id":5362,"title":4987,"titles":5363,"content":5364,"level":748},"/features/maps#props",[195,5354,5358],"PropTypeDefaultDescriptionidstringauto-generatedMap container IDcenter[number, number]From configInitial center coordinates [lng, lat]zoomnumber12Initial zoom level (0-22)stylestringstreets-v12Mapbox style URL or presetheightstring'400px'Container height (CSS value)widthstring'100%'Container width (CSS value)classstring-Additional CSS classesflyToOnCenterChangebooleanfalseAnimate center changesflyToDurationnumber800Animation duration (ms)flyToEasingfunctioneaseInOutCubicEasing function",{"id":5366,"title":5367,"titles":5368,"content":5369,"level":748},"/features/maps#events","Events",[195,5354,5358],"EventPayloadDescription@loadmap: MapMap instance loaded and ready@errorerror: stringMap failed to load",{"id":5371,"title":5372,"titles":5373,"content":5374,"level":748},"/features/maps#slots","Slots",[195,5354,5358],"SlotPropsDescriptiondefault{ map: Map }Scoped slot for markers/popups",{"id":5376,"title":3404,"titles":5377,"content":5378,"level":748},"/features/maps#example",[195,5354,5358],"\u003Cscript setup lang=\"ts\">\nconst center = ref\u003C[number, number]>([-122.4194, 37.7749])\nconst zoom = ref(12)\n\nconst handleMapLoad = (map) => {\n  console.log('Map loaded:', map)\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonMapsMap\n    :center=\"center\"\n    :zoom=\"zoom\"\n    height=\"500px\"\n    @load=\"handleMapLoad\"\n  >\n    \u003Ctemplate #default=\"{ map }\">\n      \u003C!-- Add markers here -->\n    \u003C/template>\n  \u003C/CroutonMapsMap>\n\u003C/template>",{"id":5380,"title":5381,"titles":5382,"content":5383,"level":449},"/features/maps#croutonmapsmarker","CroutonMapsMarker",[195,5354],"Map marker with optional popup and smooth animations.",{"id":5385,"title":4987,"titles":5386,"content":5387,"level":748},"/features/maps#props-1",[195,5354,5381],"PropTypeRequiredDescriptionmapMap✅Map instance from parentposition[number, number]✅Marker position [lng, lat]colorstring-Marker color (hex/rgb/named)optionsMarkerOptions-Mapbox marker optionspopupTextstring-Plain text content for popup (rendered as text, not HTML)animateTransitionsbooleanundefinedSmooth position animation (effectively true; only disabled when explicitly false)animationDurationnumber800Animation duration (ms)animationEasingstring | function'easeInOutCubic'Easing preset or function",{"id":5389,"title":5367,"titles":5390,"content":5391,"level":748},"/features/maps#events-1",[195,5354,5381],"EventDescription@clickMarker clicked@dragStartDrag started (requires draggable: true)@dragDragging in progress@dragEndDrag ended",{"id":5393,"title":3404,"titles":5394,"content":5395,"level":748},"/features/maps#example-1",[195,5354,5381],"\u003Ctemplate>\n  \u003CCroutonMapsMap :center=\"center\" :zoom=\"12\" height=\"500px\">\n    \u003Ctemplate #default=\"{ map }\">\n      \u003CCroutonMapsMarker\n        :map=\"map\"\n        :position=\"[-122.4194, 37.7749]\"\n        color=\"#ef4444\"\n        popup-text=\"San Francisco\"\n        @click=\"handleMarkerClick\"\n      />\n    \u003C/template>\n  \u003C/CroutonMapsMap>\n\u003C/template>",{"id":5397,"title":5398,"titles":5399,"content":5400,"level":748},"/features/maps#draggable-markers","Draggable Markers",[195,5354,5381],"\u003Cscript setup lang=\"ts\">\nconst markerPosition = ref\u003C[number, number]>([-122.4194, 37.7749])\n\nconst handleDragEnd = () => {\n  console.log('New position:', markerPosition.value)\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonMapsMarker\n    :map=\"map\"\n    :position=\"markerPosition\"\n    :options=\"{ draggable: true }\"\n    @dragEnd=\"handleDragEnd\"\n  />\n\u003C/template>",{"id":5402,"title":5403,"titles":5404,"content":5405,"level":449},"/features/maps#croutonmapspopup","CroutonMapsPopup",[195,5354],"Standalone popup component for custom content.",{"id":5407,"title":4987,"titles":5408,"content":5409,"level":748},"/features/maps#props-2",[195,5354,5403],"PropTypeDefaultDescriptionmapMap-Map instance (required)position[number, number]-Popup position (required)optionsPopupOptions-Mapbox popup optionscloseButtonbooleantrueShow close buttoncloseOnClickbooleantrueClose on map clickmaxWidthstring'240px'Maximum width",{"id":5411,"title":5367,"titles":5412,"content":5413,"level":748},"/features/maps#events-2",[195,5354,5403],"EventDescription@openPopup opened@closePopup closed",{"id":5415,"title":5372,"titles":5416,"content":5417,"level":748},"/features/maps#slots-1",[195,5354,5403],"SlotDescriptiondefaultPopup content (supports Vue components)",{"id":5419,"title":3404,"titles":5420,"content":5421,"level":748},"/features/maps#example-2",[195,5354,5403],"\u003Ctemplate>\n  \u003CCroutonMapsMap :center=\"center\" :zoom=\"12\" height=\"500px\">\n    \u003Ctemplate #default=\"{ map }\">\n      \u003CCroutonMapsPopup\n        :map=\"map\"\n        :position=\"[-122.4194, 37.7749]\"\n        max-width=\"300px\"\n      >\n        \u003Cdiv class=\"p-4\">\n          \u003Ch3 class=\"font-bold\">Custom Popup\u003C/h3>\n          \u003Cp>Vue components work here!\u003C/p>\n          \u003CUButton size=\"xs\">Action\u003C/UButton>\n        \u003C/div>\n      \u003C/CroutonMapsPopup>\n    \u003C/template>\n  \u003C/CroutonMapsMap>\n\u003C/template>",{"id":5423,"title":5424,"titles":5425,"content":5426,"level":449},"/features/maps#croutonmapspreview","CroutonMapsPreview",[195,5354],"Compact map preview with modal expansion - perfect for forms and cards.",{"id":5428,"title":4987,"titles":5429,"content":5430,"level":748},"/features/maps#props-3",[195,5354,5424],"PropTypeDescriptionlocationstring | [number, number]Location as coordinates or JSON string",{"id":5432,"title":3404,"titles":5433,"content":5434,"level":748},"/features/maps#example-3",[195,5354,5424],"\u003Cscript setup lang=\"ts\">\nconst location = ref\u003C[number, number]>([-122.4194, 37.7749])\n// Or from a collection field\n// const location = ref(item.coordinates) // \"[lon, lat]\" string\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003C!-- Compact preview with click-to-expand -->\n    \u003CCroutonMapsPreview :location=\"location\" />\n  \u003C/div>\n\u003C/template> Use Case: Perfect for displaying locations in collection items, form previews, or dashboards. Shows a small map preview with an expand button for full-screen viewing.",{"id":5436,"title":5437,"titles":5438,"content":528,"level":391},"/features/maps#composables","Composables",[195],{"id":5440,"title":5441,"titles":5442,"content":5443,"level":449},"/features/maps#usemapconfig","useMapConfig()",[195,5437],"Access runtime configuration for Mapbox. const {\n  accessToken,  // string\n  style,        // string\n  center,       // [number, number] | undefined\n  zoom          // number\n} = useMapConfig()",{"id":5445,"title":3404,"titles":5446,"content":5447,"level":748},"/features/maps#example-4",[195,5437,5441],"\u003Cscript setup lang=\"ts\">\nconst config = useMapConfig()\n\nconsole.log('Using style:', config.style)\nconsole.log('Default center:', config.center)\n\u003C/script>",{"id":5449,"title":5450,"titles":5451,"content":5452,"level":449},"/features/maps#usegeocode","useGeocode()",[195,5437],"Forward and reverse geocoding with Mapbox API. const {\n  geocode,          // (query: string) => Promise\u003CGeocodeResult | null>\n  reverseGeocode,   // (coords: [number, number]) => Promise\u003CGeocodeResult | null>\n  loading,          // Readonly\u003CRef\u003Cboolean>>\n  error             // Readonly\u003CRef\u003Cstring | null>>\n} = useGeocode()",{"id":5454,"title":5455,"titles":5456,"content":5457,"level":748},"/features/maps#forward-geocoding-address-coordinates","Forward Geocoding (Address → Coordinates)",[195,5437,5450],"\u003Cscript setup lang=\"ts\">\nconst { geocode, loading, error } = useGeocode()\n\nconst address = ref('1600 Amphitheatre Parkway, Mountain View, CA')\nconst result = ref\u003CGeocodeResult | null>(null)\n\nconst searchAddress = async () => {\n  result.value = await geocode(address.value)\n  if (result.value) {\n    console.log('Coordinates:', result.value.coordinates)\n    console.log('Full address:', result.value.address)\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003CUInput\n      v-model=\"address\"\n      placeholder=\"Enter an address\"\n    />\n    \u003CUButton @click=\"searchAddress\" :loading=\"loading\">\n      Search\n    \u003C/UButton>\n\n    \u003Cdiv v-if=\"result\" class=\"p-4 bg-gray-50 rounded\">\n      \u003Cp class=\"font-semibold\">{{ result.address }}\u003C/p>\n      \u003Cp class=\"text-sm text-gray-600\">\n        {{ result.coordinates[0] }}, {{ result.coordinates[1] }}\n      \u003C/p>\n    \u003C/div>\n\n    \u003Cdiv v-if=\"error\" class=\"text-red-500\">\n      {{ error }}\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":5459,"title":5460,"titles":5461,"content":5462,"level":748},"/features/maps#reverse-geocoding-coordinates-address","Reverse Geocoding (Coordinates → Address)",[195,5437,5450],"\u003Cscript setup lang=\"ts\">\nconst { reverseGeocode } = useGeocode()\n\nconst coordinates = ref\u003C[number, number]>([-122.4194, 37.7749])\nconst address = ref('')\n\nconst findAddress = async () => {\n  const result = await reverseGeocode(coordinates.value)\n  if (result) {\n    address.value = result.address\n  }\n}\n\u003C/script>",{"id":5464,"title":5465,"titles":5466,"content":5467,"level":748},"/features/maps#geocoderesult-type","GeocodeResult Type",[195,5437,5450],"interface GeocodeResult {\n  coordinates: [number, number]\n  address: string\n  placeName: string\n  context?: {\n    postcode?: string\n    place?: string\n    region?: string\n    country?: string\n  }\n}",{"id":5469,"title":5470,"titles":5471,"content":5472,"level":449},"/features/maps#mapbox-styles","Mapbox Styles",[195,5437],"Named exports for working with Mapbox style presets. Import MAPBOX_STYLES (a const object of all style URLs) and getMapboxStyle() (a function that resolves a preset name or returns a custom URL as-is). import { MAPBOX_STYLES, getMapboxStyle } from '#imports'\n\n// Access style URLs directly\nMAPBOX_STYLES.dark   // 'mapbox://styles/mapbox/dark-v11'\n\n// Resolve a preset name or pass through a custom URL\ngetMapboxStyle('dark')                              // 'mapbox://styles/mapbox/dark-v11'\ngetMapboxStyle('mapbox://styles/username/custom')   // returns as-is",{"id":5474,"title":5475,"titles":5476,"content":5477,"level":748},"/features/maps#available-styles","Available Styles",[195,5437,5470],"PresetURLDescriptionstandardmapbox://styles/mapbox/standardNew 3D style (recommended)streetsmapbox://styles/mapbox/streets-v12Classic street mapoutdoorsmapbox://styles/mapbox/outdoors-v12Outdoor/hiking maplightmapbox://styles/mapbox/light-v11Light themedarkmapbox://styles/mapbox/dark-v11Dark themesatellitemapbox://styles/mapbox/satellite-v9Satellite imagerysatelliteStreetsmapbox://styles/mapbox/satellite-streets-v12Satellite + streetsnavigationDaymapbox://styles/mapbox/navigation-day-v1Navigation (day)navigationNightmapbox://styles/mapbox/navigation-night-v1Navigation (night)",{"id":5479,"title":3404,"titles":5480,"content":5481,"level":748},"/features/maps#example-5",[195,5437,5470],"\u003Cscript setup lang=\"ts\">\nimport { getMapboxStyle } from '#imports'\n\nconst selectedStyle = ref('dark')\n\nconst mapStyle = computed(() => getMapboxStyle(selectedStyle.value))\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cselect v-model=\"selectedStyle\">\n      \u003Coption value=\"streets\">Streets\u003C/option>\n      \u003Coption value=\"dark\">Dark\u003C/option>\n      \u003Coption value=\"satellite\">Satellite\u003C/option>\n    \u003C/select>\n\n    \u003CCroutonMapsMap\n      :style=\"mapStyle\"\n      :center=\"[-122.4194, 37.7749]\"\n      :zoom=\"12\"\n      height=\"500px\"\n    />\n  \u003C/div>\n\u003C/template>",{"id":5483,"title":5484,"titles":5485,"content":5486,"level":449},"/features/maps#usemarkercolor","useMarkerColor()",[195,5437],"Automatically sync marker colors with your Nuxt UI theme. const markerColor = useMarkerColor()  // Ref\u003Cstring> (hex color)",{"id":5488,"title":3404,"titles":5489,"content":5490,"level":748},"/features/maps#example-6",[195,5437,5484],"\u003Cscript setup lang=\"ts\">\nconst markerColor = useMarkerColor()\n// Automatically uses --ui-primary from your theme\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonMapsMarker\n    :map=\"map\"\n    :position=\"position\"\n    :color=\"markerColor\"\n  />\n\u003C/template>",{"id":5492,"title":1989,"titles":5493,"content":528,"level":391},"/features/maps#complete-examples",[195],{"id":5495,"title":5496,"titles":5497,"content":5498,"level":449},"/features/maps#interactive-search-with-map","Interactive Search with Map",[195,1989],"Combine geocoding with animated map navigation: Search with geocoding: \u003Cscript setup lang=\"ts\">\nconst { geocode, loading } = useGeocode()\nconst searchQuery = ref('')\nconst center = ref\u003C[number, number]>([-122.4194, 37.7749])\n\nconst handleSearch = async () => {\n  const result = await geocode(searchQuery.value)\n  if (result) {\n    center.value = result.coordinates\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003Cdiv class=\"flex gap-2\">\n      \u003CUInput\n        v-model=\"searchQuery\"\n        placeholder=\"Search for a place...\"\n        class=\"flex-1\"\n        @keyup.enter=\"handleSearch\"\n      />\n      \u003CUButton @click=\"handleSearch\" :loading=\"loading\">Search\u003C/UButton>\n    \u003C/div>\n\n    \u003CCroutonMapsMap :center=\"center\" :zoom=\"12\" height=\"500px\">\n      \u003Ctemplate #default=\"{ map }\">\n        \u003CCroutonMapsMarker :map=\"map\" :position=\"center\" color=\"#ef4444\" />\n      \u003C/template>\n    \u003C/CroutonMapsMap>\n  \u003C/div>\n\u003C/template> With smooth animations: \u003Cscript setup lang=\"ts\">\nconst mapInstance = ref(null)\n\nconst handleSearch = async () => {\n  const result = await geocode(searchQuery.value)\n  if (result && mapInstance.value) {\n    mapInstance.value.flyTo({\n      center: result.coordinates,\n      zoom: 14,\n      duration: 2000\n    })\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonMapsMap @load=\"(map) => mapInstance = map\">\n    \u003C!-- Map content -->\n  \u003C/CroutonMapsMap>\n\u003C/template>",{"id":5500,"title":5501,"titles":5502,"content":5503,"level":449},"/features/maps#store-locator","Store Locator",[195,1989],"Display collection items on a map: Store list: \u003Cscript setup lang=\"ts\">\nconst { items: stores } = useCollection('stores')\nconst selectedStore = ref(null)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003Cdiv\n      v-for=\"store in stores\"\n      :key=\"store.id\"\n      class=\"p-4 border rounded hover:bg-gray-50\"\n      :class=\"{ 'bg-blue-50': selectedStore?.id === store.id }\"\n    >\n      \u003Ch3 class=\"font-bold\">{{ store.name }}\u003C/h3>\n      \u003Cp class=\"text-sm text-gray-600\">{{ store.address }}\u003C/p>\n    \u003C/div>\n  \u003C/div>\n\u003C/template> Map with multiple markers: \u003Cscript setup lang=\"ts\">\nconst { items: stores } = useCollection('stores')\n\nconst storeMarkers = computed(() =>\n  stores.value.map(store => ({\n    id: store.id,\n    position: [store.longitude, store.latitude],\n    name: store.name,\n    address: store.address\n  }))\n)\n\nconst handleMarkerClick = (store) => {\n  selectedStore.value = store\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonMapsMap :center=\"[-120, 37]\" :zoom=\"6\" height=\"600px\">\n    \u003Ctemplate #default=\"{ map }\">\n      \u003CCroutonMapsMarker\n        v-for=\"marker in storeMarkers\"\n        :key=\"marker.id\"\n        :map=\"map\"\n        :position=\"marker.position\"\n        :popup-text=\"marker.name\"\n        @click=\"handleMarkerClick(marker)\"\n      />\n    \u003C/template>\n  \u003C/CroutonMapsMap>\n\u003C/template>",{"id":5505,"title":5506,"titles":5507,"content":5508,"level":449},"/features/maps#collection-with-map-field","Collection with Map Field",[195,1989],"Add location tracking to your collections: \u003Cscript setup lang=\"ts\">\nconst { items } = useCollection('events')\n\n// Assume your collection has latitude/longitude fields\nconst eventMarkers = computed(() =>\n  items.value\n    .filter(event => event.latitude && event.longitude)\n    .map(event => ({\n      id: event.id,\n      position: [event.longitude, event.latitude],\n      title: event.title,\n      date: event.date\n    }))\n)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonMapsMap :center=\"[-120, 37]\" :zoom=\"6\" height=\"600px\">\n    \u003Ctemplate #default=\"{ map }\">\n      \u003CCroutonMapsMarker\n        v-for=\"marker in eventMarkers\"\n        :key=\"marker.id\"\n        :map=\"map\"\n        :position=\"marker.position\"\n        :popup-text=\"marker.title\"\n      />\n    \u003C/template>\n  \u003C/CroutonMapsMap>\n\u003C/template>",{"id":5510,"title":4872,"titles":5511,"content":5512,"level":391},"/features/maps#typescript-support",[195],"All types are exported from the package: import type {\n  // Main types\n  MapConfig,\n  MapInstance,\n  PopupInstance,\n\n  // Options\n  UseMapOptions,\n\n  // Results\n  GeocodeResult,\n\n  // Animations\n  EasingFunction,\n  EasingPreset,\n  MarkerAnimationOptions,\n  MapFlyToOptions,\n\n  // Style presets\n  MapboxStylePreset,\n\n  // Re-exported Mapbox GL types\n  Map,\n  Marker,\n  Popup,\n  MapOptions,\n  MarkerOptions,\n  PopupOptions,\n  LngLatLike\n} from '@fyit/crouton-maps'",{"id":5514,"title":5515,"titles":5516,"content":5517,"level":391},"/features/maps#animation-system","Animation System",[195],"Both maps and markers support smooth animations.",{"id":5519,"title":5520,"titles":5521,"content":5522,"level":449},"/features/maps#map-animations-flyto","Map Animations (flyTo)",[195,5515],"\u003Cscript setup lang=\"ts\">\nconst center = ref\u003C[number, number]>([-122.4194, 37.7749])\n\nconst goToNewYork = () => {\n  center.value = [-74.0060, 40.7128]\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonMapsMap\n    :center=\"center\"\n    :zoom=\"12\"\n    height=\"500px\"\n    :flyToOnCenterChange=\"true\"\n    :flyToDuration=\"1500\"\n  />\n\u003C/template>",{"id":5524,"title":5525,"titles":5526,"content":5527,"level":449},"/features/maps#marker-animations","Marker Animations",[195,5515],"\u003Cscript setup lang=\"ts\">\nconst position = ref\u003C[number, number]>([-122.4194, 37.7749])\n\n// Position updates will animate smoothly\nconst moveMarker = () => {\n  position.value = [-122.4084, 37.7749]\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonMapsMarker\n    :map=\"map\"\n    :position=\"position\"\n    :animateTransitions=\"true\"\n    :animationDuration=\"1000\"\n    animationEasing=\"easeInOutCubic\"\n  />\n\u003C/template>",{"id":5529,"title":5530,"titles":5531,"content":5532,"level":449},"/features/maps#custom-easing-functions","Custom Easing Functions",[195,5515],"\u003Cscript setup lang=\"ts\">\n// Bounce easing\nconst bounceEasing = (t: number): number => {\n  return t \u003C 0.5\n    ? 8 * t * t * t * t\n    : 1 - 8 * (--t) * t * t * t\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonMapsMarker\n    :map=\"map\"\n    :position=\"position\"\n    :animationEasing=\"bounceEasing\"\n  />\n\u003C/template>",{"id":5534,"title":44,"titles":5535,"content":528,"level":391},"/features/maps#best-practices",[195],{"id":5537,"title":1364,"titles":5538,"content":5539,"level":449},"/features/maps#performance",[195,44],"Large Datasets: For 100+ markers, consider implementing marker clustering to improve performance. \u003Cscript setup lang=\"ts\">\n// Instead of rendering all markers\nconst visibleMarkers = computed(() => {\n  // Filter markers based on map bounds\n  return allMarkers.value.filter(marker => {\n    // Check if marker is in viewport\n    return isInBounds(marker.position)\n  })\n})\n\u003C/script>",{"id":5541,"title":2522,"titles":5542,"content":5543,"level":449},"/features/maps#error-handling",[195,44],"Always handle geocoding errors gracefully: \u003Cscript setup lang=\"ts\">\nconst { geocode, error } = useGeocode()\n\nconst search = async (query: string) => {\n  const result = await geocode(query)\n\n  if (error.value) {\n    // Show user-friendly error\n    toast.add({\n      title: 'Location not found',\n      description: 'Please try a different search term',\n      color: 'red'\n    })\n    return\n  }\n\n  if (!result) {\n    // No results found\n    toast.add({\n      title: 'No results',\n      description: 'Try being more specific',\n      color: 'amber'\n    })\n  }\n}\n\u003C/script>",{"id":5545,"title":5546,"titles":5547,"content":5548,"level":449},"/features/maps#api-rate-limits","API Rate Limits",[195,44],"Monitor your Mapbox usage: Free tier: 50,000 map loads/month, 100,000 geocoding requests/monthCheck usage: https://account.mapbox.com/ \u003Cscript setup lang=\"ts\">\n// Debounce geocoding requests\nimport { useDebounceFn } from '@vueuse/core'\n\nconst { geocode } = useGeocode()\n\nconst debouncedGeocode = useDebounceFn(async (query: string) => {\n  await geocode(query)\n}, 500) // Wait 500ms after user stops typing\n\u003C/script>",{"id":5550,"title":5551,"titles":5552,"content":5553,"level":449},"/features/maps#accessibility","Accessibility",[195,44],"Add proper labels and ARIA attributes: \u003Ctemplate>\n  \u003Cdiv role=\"region\" aria-label=\"Interactive map\">\n    \u003CCroutonMapsMap\n      :center=\"center\"\n      :zoom=\"12\"\n      height=\"500px\"\n    >\n      \u003Ctemplate #default=\"{ map }\">\n        \u003CCroutonMapsMarker\n          v-for=\"location in locations\"\n          :key=\"location.id\"\n          :map=\"map\"\n          :position=\"location.position\"\n          :aria-label=\"`Location: ${location.name}`\"\n        />\n      \u003C/template>\n    \u003C/CroutonMapsMap>\n  \u003C/div>\n\u003C/template>",{"id":5555,"title":36,"titles":5556,"content":528,"level":391},"/features/maps#troubleshooting",[195],{"id":5558,"title":5559,"titles":5560,"content":5561,"level":449},"/features/maps#map-not-displaying","Map Not Displaying",[195,36],"Symptoms: Blank container or loading spinner never disappears. Solutions: Check access token in .env:echo $MAPBOX_TOKEN\nVerify runtime config:const config = useMapConfig()\nconsole.log('Has token:', !!config.accessToken)\nEnsure container has explicit height:\u003CCroutonMapsMap height=\"500px\" /> \u003C!-- ✅ Good -->\n\u003CCroutonMapsMap /> \u003C!-- ❌ Bad - no height -->\nCheck browser console for errors",{"id":5563,"title":5564,"titles":5565,"content":5566,"level":449},"/features/maps#geocoding-failures","Geocoding Failures",[195,36],"Symptoms: useGeocode() returns null or errors. Solutions: Verify API limits: https://account.mapbox.com/Check network tab for 401/403 errorsValidate query formatting:\n// ✅ Good\nawait geocode('123 Main St, San Francisco, CA')\n\n// ❌ Bad\nawait geocode('') // Empty query",{"id":5568,"title":5569,"titles":5570,"content":5571,"level":449},"/features/maps#typescript-errors","TypeScript Errors",[195,36],"Symptoms: Type errors with Mapbox GL types. Solutions: Install peer dependencies:pnpm add -D mapbox-gl\nImport types from package:import type { LngLatLike } from '@fyit/crouton-maps'",{"id":5573,"title":975,"titles":5574,"content":5575,"level":449},"/features/maps#performance-issues",[195,36],"Symptoms: Slow rendering with many markers. Solutions: Implement marker filtering based on zoom levelUse marker clustering (consider future addon)Lazy load map component:\n\u003Cscript setup lang=\"ts\">\nconst MapComponent = defineAsyncComponent(() =>\n  import('#components').then(c => c.CroutonMapsMap)\n)\n\u003C/script>",{"id":5577,"title":5578,"titles":5579,"content":5580,"level":391},"/features/maps#roadmap","Roadmap",[195],"Planned features for future releases: Marker clustering for large datasets Drawing tools (polygons, circles, lines) Heatmap layer support 3D terrain and buildings Custom marker icons Route/directions integration GeoJSON layer support MapLibre GL JS option (non-Mapbox alternative) Beta Status: These features are planned but not guaranteed. API design may change based on community feedback.",{"id":5582,"title":5583,"titles":5584,"content":5585,"level":391},"/features/maps#resources","Resources",[195],"Mapbox Documentation: https://docs.mapbox.com/mapbox-gl-js/GitHub Issues: https://github.com/pmcp/nuxt-crouton/issuesPackage: https://www.npmjs.com/package/@fyit/crouton-maps",{"id":5587,"title":3680,"titles":5588,"content":5589,"level":391},"/features/maps#related-documentation",[195],"Collections & Layers - Collection basicsData Operations - Working with dataCustom Components - Form customization html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":200,"title":199,"titles":5591,"content":5592,"level":385},[],"Nuxt DevTools integration for visual inspection and debugging of Crouton collections The @fyit/crouton-devtools package provides a comprehensive Nuxt DevTools integration for visually inspecting, monitoring, and debugging your Crouton collections during development. Status: Beta - This package (v0.1.0) is experimental and under active development. The API may change between versions.",{"id":5594,"title":936,"titles":5595,"content":5596,"level":391},"/features/devtools#overview",[199],"Nuxt Crouton DevTools adds a dedicated \"Crouton\" tab to the Nuxt DevTools panel, providing real-time visibility into your CRUD collections, API endpoints, and database operations.",{"id":5598,"title":5599,"titles":5600,"content":5601,"level":449},"/features/devtools#key-features","Key Features",[199,936],"Collection Inspector - Browse and search all registered collectionsEndpoint Monitoring - Track API calls with timing and statusOperation Tracking - Monitor CRUD operations in real-timeRequest Execution - Test API endpoints directly from DevToolsZero Configuration - Automatically discovers your collections Development Only: The DevTools integration only runs in development mode (nuxt dev) and has zero impact on production builds.",{"id":5603,"title":13,"titles":5604,"content":528,"level":391},"/features/devtools#installation",[199],{"id":5606,"title":5607,"titles":5608,"content":5609,"level":449},"/features/devtools#_1-install-package","1. Install Package",[199,13],"pnpm add -D @fyit/crouton-devtools Pin Exact Version for Production: If using beta packages in production, pin the exact version to prevent unexpected breaking changes:pnpm add -D @fyit/crouton-devtools@0.1.0 --save-exact",{"id":5611,"title":5612,"titles":5613,"content":5614,"level":449},"/features/devtools#_2-add-module-to-configuration","2. Add Module to Configuration",[199,13],"Add the module to your nuxt.config.ts: export default defineNuxtConfig({\n  modules: [\n    '@nuxt/devtools',                              // Required\n    '@fyit/crouton-devtools'      // Add this\n  ],\n\n  devtools: {\n    enabled: true  // Ensure DevTools is enabled\n  }\n})",{"id":5616,"title":5617,"titles":5618,"content":5619,"level":449},"/features/devtools#_3-start-development-server","3. Start Development Server",[199,13],"pnpm dev Open Nuxt DevTools (look for the DevTools icon in the bottom-right corner) and find the Crouton tab. That's it! The DevTools integration automatically discovers your collections from app.config.croutonCollections and starts tracking operations.",{"id":5621,"title":4183,"titles":5622,"content":5623,"level":391},"/features/devtools#requirements",[199],"Nuxt: v4.0.0 or higher@nuxt/devtools: v3.0.0 or higher (installed automatically)@fyit/crouton: Any version",{"id":5625,"title":5626,"titles":5627,"content":528,"level":391},"/features/devtools#core-features","Core Features",[199],{"id":5629,"title":5630,"titles":5631,"content":5632,"level":449},"/features/devtools#_1-collection-inspector","1. Collection Inspector",[199,5626],"The Collection Inspector provides a visual overview of all registered collections in your application.",{"id":5634,"title":5635,"titles":5636,"content":5637,"level":748},"/features/devtools#what-youll-see","What You'll See",[199,5626,5630],"Each collection card displays: Collection Name - Human-readable nameAPI Path - Generated endpoint path (e.g., /api/crouton-collection/tasks)Layer Badge - Type indicator (internal/external/custom)Component Name - Associated Vue component (for internal collections)Description - Meta description (if configured)Collection Key - Unique identifier",{"id":5639,"title":5640,"titles":5641,"content":5642,"level":748},"/features/devtools#search-filter","Search & Filter",[199,5626,5630],"Use the search bar to quickly find collections: Search: \"tasks\"        → Finds tasks collection\nSearch: \"internal\"     → Shows all internal collections\nSearch: \"/api/users\"   → Finds by API path",{"id":5644,"title":5645,"titles":5646,"content":5647,"level":748},"/features/devtools#detail-view","Detail View",[199,5626,5630],"Click any collection card to view full configuration: Complete JSON schemaAll metadata fieldsCustom configurationDefault valuesColumn definitions Example Detail View: {\n  \"key\": \"tasks\",\n  \"name\": \"tasks\",\n  \"layer\": \"internal\",\n  \"apiPath\": \"/api/crouton-collection/tasks\",\n  \"componentName\": \"CroutonCollectionTasksCreate\",\n  \"meta\": {\n    \"label\": \"Tasks\",\n    \"description\": \"Project task management\",\n    \"icon\": \"i-lucide-check-circle\"\n  },\n  \"defaultValues\": {\n    \"status\": \"pending\"\n  },\n  \"columns\": [\"title\", \"status\", \"assignee\", \"dueDate\"]\n}",{"id":5649,"title":5650,"titles":5651,"content":5652,"level":449},"/features/devtools#_2-endpoint-monitoring","2. Endpoint Monitoring",[199,5626],"The DevTools integration automatically generates and lists all available CRUD endpoints for each collection.",{"id":5654,"title":5655,"titles":5656,"content":5657,"level":748},"/features/devtools#generated-endpoints","Generated Endpoints",[199,5626,5650],"For each collection, the following endpoints are documented: OperationMethodPathDescriptionList/SearchGET/api/crouton-collection/{name}/searchPaginated search with filtersGet SingleGET/api/crouton-collection/{name}/:idRetrieve one item by IDCreatePOST/api/crouton-collection/{name}Create new itemUpdatePATCH/api/crouton-collection/{name}/:idUpdate existing itemDeleteDELETE/api/crouton-collection/{name}/:idDelete item by ID",{"id":5659,"title":5660,"titles":5661,"content":5662,"level":748},"/features/devtools#endpoint-parameters","Endpoint Parameters",[199,5626,5650],"Each endpoint shows available parameters: List/Search Parameters: page (number) - Page number (default: 1)limit (number) - Items per page (default: 10)filter (json) - Filter object (e.g., {\"active\": true})sort (string) - Sort field (e.g., \"createdAt\") Get/Update/Delete Parameters: id (string, required) - Item ID (path parameter)",{"id":5664,"title":5665,"titles":5666,"content":5667,"level":748},"/features/devtools#rpc-interface","RPC Interface",[199,5626,5650],"Endpoints are accessible via the DevTools RPC interface: // Fetch all endpoints\nGET /__nuxt_crouton_devtools/api/endpoints\n\n// Response format\n{\n  \"success\": true,\n  \"data\": [\n    {\n      \"collection\": \"tasks\",\n      \"operation\": \"list\",\n      \"method\": \"GET\",\n      \"path\": \"/api/crouton-collection/tasks/search\",\n      \"params\": [...],\n      \"requiresBody\": false\n    },\n    // ... more endpoints\n  ],\n  \"count\": 20  // Total endpoints (5 per collection × 4 collections)\n}",{"id":5669,"title":5670,"titles":5671,"content":5672,"level":449},"/features/devtools#_3-operation-tracking","3. Operation Tracking",[199,5626],"The DevTools integration tracks all CRUD operations in real-time using an in-memory operation store.",{"id":5674,"title":5675,"titles":5676,"content":5677,"level":748},"/features/devtools#what-gets-tracked","What Gets Tracked",[199,5626,5670],"Every API call to a Crouton collection endpoint is logged with: {\n  id: \"op_1700000000000_abc123\",      // Unique operation ID\n  timestamp: 1700000000000,            // Unix timestamp (ms)\n  collection: \"tasks\",                 // Collection name\n  operation: \"create\",                 // Operation type\n  method: \"POST\",                      // HTTP method\n  path: \"/api/crouton-collection/tasks\", // Full path\n  status: 201,                         // HTTP status code\n  duration: 45,                        // Response time (ms)\n  teamContext: \"team_xyz\",             // Team ID (if applicable)\n  error: undefined                     // Error message (if failed)\n}",{"id":5679,"title":5680,"titles":5681,"content":5682,"level":748},"/features/devtools#operation-types","Operation Types",[199,5626,5670],"The tracker automatically detects operation types: list - GET requests to /search or base endpointget - GET requests with an ID parametercreate - POST requestsupdate - PATCH or PUT requestsdelete - DELETE requests",{"id":5684,"title":5685,"titles":5686,"content":5687,"level":748},"/features/devtools#circular-buffer","Circular Buffer",[199,5626,5670],"Operations are stored in a circular buffer (max 500 operations) to prevent memory issues during long development sessions.",{"id":5689,"title":5690,"titles":5691,"content":5692,"level":748},"/features/devtools#filtering-operations","Filtering Operations",[199,5626,5670],"Retrieve operations with filters: // Get all operations\nGET /__nuxt_crouton_devtools/api/operations\n\n// Filter by collection\nGET /__nuxt_crouton_devtools/api/operations?collection=tasks\n\n// Filter by operation type\nGET /__nuxt_crouton_devtools/api/operations?operation=create\n\n// Filter by status (success/error)\nGET /__nuxt_crouton_devtools/api/operations?status=error\n\n// Filter by timestamp (since)\nGET /__nuxt_crouton_devtools/api/operations?since=1700000000000\n\n// Combine filters\nGET /__nuxt_crouton_devtools/api/operations?collection=tasks&status=success",{"id":5694,"title":5695,"titles":5696,"content":5697,"level":748},"/features/devtools#operation-statistics","Operation Statistics",[199,5626,5670],"Get aggregated statistics: GET /__nuxt_crouton_devtools/api/operations/stats\n\n// Response\n{\n  \"success\": true,\n  \"data\": {\n    \"total\": 127,                      // Total operations tracked\n    \"byCollection\": {\n      \"tasks\": 45,\n      \"projects\": 38,\n      \"users\": 44\n    },\n    \"byOperation\": {\n      \"list\": 52,\n      \"get\": 31,\n      \"create\": 24,\n      \"update\": 15,\n      \"delete\": 5\n    },\n    \"successRate\": 94,                 // Percentage\n    \"avgDuration\": 38,                 // Average response time (ms)\n    \"successful\": 119,                 // Success count\n    \"failed\": 8                        // Failure count\n  }\n}",{"id":5699,"title":5700,"titles":5701,"content":5702,"level":748},"/features/devtools#clear-operation-history","Clear Operation History",[199,5626,5670],"Clear all tracked operations: POST /__nuxt_crouton_devtools/api/operations/clear",{"id":5704,"title":5705,"titles":5706,"content":5707,"level":449},"/features/devtools#_4-request-execution","4. Request Execution",[199,5626],"Test API endpoints directly from DevTools without writing code or using external tools like Postman.",{"id":5709,"title":5710,"titles":5711,"content":5712,"level":748},"/features/devtools#execute-requests","Execute Requests",[199,5626,5705],"Submit requests to any collection endpoint: POST /__nuxt_crouton_devtools/api/execute\n\n// Request body\n{\n  \"method\": \"POST\",\n  \"path\": \"/api/crouton-collection/tasks\",\n  \"params\": {\n    \"title\": \"Test Task\",\n    \"status\": \"pending\"\n  },\n  \"requestBody\": {\n    \"title\": \"Complete documentation\",\n    \"status\": \"in_progress\",\n    \"assignee\": \"user_123\",\n    \"dueDate\": \"2024-12-01\"\n  },\n  \"headers\": {\n    \"Authorization\": \"Bearer \u003Ctoken>\"\n  }\n}\n\n// Response\n{\n  \"success\": true,\n  \"status\": 201,\n  \"data\": {\n    \"id\": \"task_abc123\",\n    \"title\": \"Complete documentation\",\n    \"status\": \"in_progress\",\n    // ... rest of created task\n  },\n  \"duration\": 42  // Response time in ms\n}",{"id":5714,"title":5715,"titles":5716,"content":5717,"level":748},"/features/devtools#path-parameters","Path Parameters",[199,5626,5705],"The execution engine automatically replaces path parameters: // Request\n{\n  \"method\": \"GET\",\n  \"path\": \"/api/crouton-collection/tasks/:id\",\n  \"params\": {\n    \"id\": \"task_abc123\"\n  }\n}\n\n// Actual request sent to: /api/crouton-collection/tasks/task_abc123",{"id":5719,"title":5720,"titles":5721,"content":5722,"level":748},"/features/devtools#query-parameters","Query Parameters",[199,5626,5705],"For GET requests, non-path parameters become query strings: // Request\n{\n  \"method\": \"GET\",\n  \"path\": \"/api/crouton-collection/tasks/search\",\n  \"params\": {\n    \"page\": 2,\n    \"limit\": 20,\n    \"filter\": JSON.stringify({ active: true }),\n    \"sort\": \"-createdAt\"\n  }\n}\n\n// Actual request sent to:\n// /api/crouton-collection/tasks/search?page=2&limit=20&filter=%7B%22active%22%3Atrue%7D&sort=-createdAt",{"id":5724,"title":2522,"titles":5725,"content":5726,"level":748},"/features/devtools#error-handling",[199,5626,5705],"Failed requests return detailed error information: {\n  \"success\": false,\n  \"status\": 404,\n  \"error\": \"Item not found\",\n  \"data\": {\n    \"statusCode\": 404,\n    \"message\": \"Task with ID 'invalid_id' not found\"\n  },\n  \"duration\": 15\n}",{"id":5728,"title":5729,"titles":5730,"content":5731,"level":391},"/features/devtools#devtools-ui-components","DevTools UI Components",[199],"Internal Only: The following components are internal to the DevTools iframe UI. They are not importable or usable in your application — they power the DevTools panel interface only.",{"id":5733,"title":5734,"titles":5735,"content":5736,"level":449},"/features/devtools#collectioncard-component","CollectionCard Component",[199,5729],"Displays a single collection in a card format (internal to DevTools UI). Props: interface Props {\n  collection: CroutonCollection\n}\n\ninterface CroutonCollection {\n  key: string\n  name: string\n  layer?: string\n  apiPath?: string\n  componentName?: string | null\n  meta?: {\n    label?: string\n    description?: string\n    icon?: string\n  }\n  defaultValues?: Record\u003Cstring, any>\n  columns?: string[]\n  schema?: any\n} Events: @view-details - Emitted when card is clicked Features: Layer-based color coding (blue for external, green for internal, purple for custom)Displays description, component name, and keyHover effects and click handling",{"id":5738,"title":5739,"titles":5740,"content":5741,"level":449},"/features/devtools#collectiondetailmodal-component","CollectionDetailModal Component",[199,5729],"Shows full collection configuration in a modal (internal to DevTools UI). Props: interface Props {\n  modelValue?: boolean      // Controls visibility\n  collection?: CroutonCollection | null\n} Events: @update:modelValue - Two-way binding for visibility Features: Syntax-highlighted JSON displayCopy to clipboard functionalityScrollable content for large schemas",{"id":5743,"title":5744,"titles":5745,"content":5746,"level":449},"/features/devtools#main-devtools-view","Main DevTools View",[199,5729],"The main index page provides: Search bar with real-time filteringGrid layout of collection cardsEmpty state messagingLoading statesError handling",{"id":5748,"title":5749,"titles":5750,"content":5751,"level":391},"/features/devtools#rpc-api-reference","RPC API Reference",[199],"The DevTools integration exposes several RPC endpoints for programmatic access.",{"id":5753,"title":5754,"titles":5755,"content":5756,"level":449},"/features/devtools#collections-endpoint","Collections Endpoint",[199,5749],"GET /__nuxt_crouton_devtools/api/collections Retrieve all registered collections. Response: {\n  success: boolean\n  data: CroutonCollection[]\n  count: number\n}",{"id":5758,"title":5759,"titles":5760,"content":5761,"level":449},"/features/devtools#operations-endpoint","Operations Endpoint",[199,5749],"GET /__nuxt_crouton_devtools/api/operations Retrieve tracked operations with optional filters. Query Parameters: collection (string) - Filter by collection nameoperation (string) - Filter by operation typestatus (string) - Filter by status (\"success\" | \"error\")since (number) - Filter by timestamp (Unix ms) Response: {\n  success: boolean\n  data: Operation[]\n  count: number\n  filters: OperationFilters\n}",{"id":5763,"title":5764,"titles":5765,"content":5766,"level":449},"/features/devtools#operation-stats-endpoint","Operation Stats Endpoint",[199,5749],"GET /__nuxt_crouton_devtools/api/operations/stats Get aggregated operation statistics. Response: {\n  success: boolean\n  data: {\n    total: number\n    byCollection: Record\u003Cstring, number>\n    byOperation: Record\u003Cstring, number>\n    successRate: number\n    avgDuration: number\n    successful: number\n    failed: number\n  }\n}",{"id":5768,"title":5769,"titles":5770,"content":5771,"level":449},"/features/devtools#clear-operations-endpoint","Clear Operations Endpoint",[199,5749],"POST /__nuxt_crouton_devtools/api/operations/clear Clear all tracked operations. Response: {\n  success: boolean\n}",{"id":5773,"title":5774,"titles":5775,"content":5776,"level":449},"/features/devtools#endpoints-listing","Endpoints Listing",[199,5749],"GET /__nuxt_crouton_devtools/api/endpoints List all available CRUD endpoints. Response: {\n  success: boolean\n  data: Endpoint[]\n  count: number\n}\n\ninterface Endpoint {\n  collection: string\n  operation: string\n  method: string\n  path: string\n  params: Parameter[]\n  requiresBody: boolean\n  bodyDescription?: string\n}",{"id":5778,"title":5779,"titles":5780,"content":5781,"level":449},"/features/devtools#execute-request-endpoint","Execute Request Endpoint",[199,5749],"POST /__nuxt_crouton_devtools/api/execute Execute an API request. Request Body: {\n  method: string           // HTTP method\n  path: string            // Endpoint path (with :params)\n  params?: Record\u003Cstring, any>\n  requestBody?: any\n  headers?: Record\u003Cstring, string>\n} Response: {\n  success: boolean\n  status: number\n  data?: any\n  error?: string\n  duration: number  // ms\n}",{"id":5783,"title":5784,"titles":5785,"content":528,"level":391},"/features/devtools#architecture-implementation","Architecture & Implementation",[199],{"id":5787,"title":5788,"titles":5789,"content":5790,"level":449},"/features/devtools#module-structure","Module Structure",[199,5784],"The DevTools module is organized as follows: packages/crouton-devtools/\n├── src/\n│   ├── module.ts                              # Main Nuxt module\n│   ├── runtime/\n│   │   ├── client/                            # DevTools UI (iframe app)\n│   │   │   ├── app.vue                        # Root component\n│   │   │   ├── nuxt.config.ts                 # Client app config\n│   │   │   └── package.json\n│   │   ├── pages/\n│   │   │   └── data-browser.vue               # Data browser page\n│   │   ├── server-rpc/                        # RPC API handlers\n│   │   │   ├── client.ts                      # Client HTML server\n│   │   │   ├── collections.ts                 # Collections endpoint\n│   │   │   ├── endpoints.ts                   # Endpoints listing\n│   │   │   ├── operations.ts                  # Operations retrieval\n│   │   │   ├── operationStats.ts              # Statistics\n│   │   │   ├── clearOperations.ts             # Clear history\n│   │   │   ├── executeRequest.ts              # Request execution\n│   │   │   ├── events.ts                      # Query persisted events\n│   │   │   ├── eventsHealth.ts                # Events health stats\n│   │   │   ├── systemOperations.ts            # System operations\n│   │   │   ├── clearSystemOperations.ts       # Clear system ops\n│   │   │   └── generationHistory.ts           # Generation history\n│   │   └── server/\n│   │       ├── plugins/\n│   │       │   └── operationTracker.ts        # Nitro plugin (tracks API calls)\n│   │       ├── utils/\n│   │       │   ├── operationStore.ts          # In-memory operation storage\n│   │       │   └── systemOperationStore.ts    # System operation storage\n│   │       └── crouton-hooks.d.ts             # Hook type definitions\n├── playground/                                # Development playground\n└── package.json",{"id":5792,"title":1635,"titles":5793,"content":528,"level":449},"/features/devtools#how-it-works",[199,5784],{"id":5795,"title":5796,"titles":5797,"content":5798,"level":748},"/features/devtools#_1-module-registration","1. Module Registration",[199,5784,1635],"The module registers itself with Nuxt DevTools: // src/module.ts\nimport { addCustomTab } from '@nuxt/devtools-kit'\n\naddCustomTab(() => ({\n  name: 'crouton',\n  title: 'Crouton',\n  icon: 'carbon:data-table',\n  view: {\n    type: 'iframe',\n    src: '/__nuxt_crouton_devtools',  // Iframe URL\n  },\n}))",{"id":5800,"title":5801,"titles":5802,"content":5803,"level":748},"/features/devtools#_2-collection-discovery","2. Collection Discovery",[199,5784,1635],"At build time, the module reads collections from app.config.croutonCollections and stores them in Nitro runtime config: // src/module.ts (build time)\nnuxt.options.nitro.runtimeConfig.croutonCollections =\n  nuxt.options.appConfig?.croutonCollections || {} At runtime, the RPC handlers access the collections via useAppConfig() (which reflects the build-time data injected above): // src/runtime/server-rpc/collections.ts\nconst appConfig = useAppConfig()\nconst collections = appConfig.croutonCollections || {}",{"id":5805,"title":5670,"titles":5806,"content":5807,"level":748},"/features/devtools#_3-operation-tracking-1",[199,5784,1635],"A Nitro plugin intercepts all requests to /api/crouton-collection/* using the onAfterResponse hook: // src/runtime/server/plugins/operationTracker.ts\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('onAfterResponse', (event) => {\n    const path = event.path\n\n    // Only track Crouton collection API routes\n    if (!path.startsWith('/api/crouton-collection/')) {\n      return\n    }\n\n    // Extract metadata and record the operation\n    const collection = extractCollectionName(path)\n    const operation = detectOperation(method, path)\n\n    operationStore.add({\n      id: generateId(),\n      timestamp: Date.now(),\n      collection,\n      operation,\n      method,\n      path,\n      status: event.node.res.statusCode,\n      duration: Date.now() - startTime\n    })\n  })\n})",{"id":5809,"title":5810,"titles":5811,"content":5812,"level":748},"/features/devtools#_4-circular-buffer-storage","4. Circular Buffer Storage",[199,5784,1635],"Operations are stored in-memory with a circular buffer: // src/runtime/server/utils/operationStore.ts\nclass OperationStore {\n  private operations: Operation[] = []\n  private readonly maxSize = 500  // Prevent memory issues\n\n  add(operation: Operation): void {\n    this.operations.unshift(operation)\n    \n    // Maintain circular buffer\n    if (this.operations.length > this.maxSize) {\n      this.operations = this.operations.slice(0, this.maxSize)\n    }\n  }\n}",{"id":5814,"title":5815,"titles":5816,"content":5817,"level":748},"/features/devtools#_5-rpc-communication","5. RPC Communication",[199,5784,1635],"RPC endpoints handle communication between the DevTools iframe and the Nuxt server: // DevTools UI fetches data\nconst response = await $fetch('/__nuxt_crouton_devtools/api/collections')",{"id":5819,"title":5820,"titles":5821,"content":5822,"level":449},"/features/devtools#development-mode-only","Development Mode Only",[199,5784],"The module only activates in development: // src/module.ts\nasync setup(_options, nuxt) {\n  // Only enable in development mode\n  if (nuxt.options.dev === false) {\n    return\n  }\n  \n  // ... rest of setup\n} Production builds completely exclude the DevTools code.",{"id":5824,"title":5825,"titles":5826,"content":528,"level":391},"/features/devtools#usage-examples","Usage Examples",[199],{"id":5828,"title":5829,"titles":5830,"content":5831,"level":449},"/features/devtools#basic-setup","Basic Setup",[199,5825],"Minimal configuration to get started: // nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: [\n    '@nuxt/devtools',\n    '@fyit/crouton-devtools'\n  ],\n\n  devtools: {\n    enabled: true\n  }\n}) // app.config.ts\nexport default defineAppConfig({\n  croutonCollections: {\n    tasks: {\n      name: 'tasks',\n      layer: 'internal',\n      apiPath: '/api/crouton-collection/tasks',\n      meta: {\n        label: 'Tasks',\n        icon: 'i-lucide-check-circle'\n      }\n    }\n  }\n})",{"id":5833,"title":5834,"titles":5835,"content":5836,"level":449},"/features/devtools#viewing-collections","Viewing Collections",[199,5825],"Start dev server: pnpm devOpen Nuxt DevTools (icon in bottom-right)Click Crouton tabSee all registered collectionsClick a collection card to view details",{"id":5838,"title":5839,"titles":5840,"content":5841,"level":449},"/features/devtools#monitoring-operations","Monitoring Operations",[199,5825],"Use your app normally (create, edit, delete items)Check DevTools Crouton tabView real-time operation logsSee timing, status codes, and errors",{"id":5843,"title":5844,"titles":5845,"content":5846,"level":449},"/features/devtools#testing-endpoints","Testing Endpoints",[199,5825],"From the DevTools UI (or programmatically): // Create a task\nconst response = await $fetch('/__nuxt_crouton_devtools/api/execute', {\n  method: 'POST',\n  body: {\n    method: 'POST',\n    path: '/api/crouton-collection/tasks',\n    requestBody: {\n      title: 'Test task',\n      status: 'pending'\n    }\n  }\n})\n\nconsole.log(response)\n// {\n//   success: true,\n//   status: 201,\n//   data: { id: 'task_123', ... },\n//   duration: 42\n// }",{"id":5848,"title":5690,"titles":5849,"content":5850,"level":449},"/features/devtools#filtering-operations-1",[199,5825],"// Get failed operations\nconst errors = await $fetch('/__nuxt_crouton_devtools/api/operations?status=error')\n\n// Get recent task operations\nconst recentTasks = await $fetch(\n  '/__nuxt_crouton_devtools/api/operations?collection=tasks&since=' + \n  (Date.now() - 60000) // Last minute\n)",{"id":5852,"title":5853,"titles":5854,"content":5855,"level":449},"/features/devtools#viewing-statistics","Viewing Statistics",[199,5825],"const stats = await $fetch('/__nuxt_crouton_devtools/api/operations/stats')\n\nconsole.log(`Success rate: ${stats.data.successRate}%`)\nconsole.log(`Average response time: ${stats.data.avgDuration}ms`)\nconsole.log('Operations by collection:', stats.data.byCollection)",{"id":5857,"title":5858,"titles":5859,"content":528,"level":391},"/features/devtools#debugging-workflows","Debugging Workflows",[199],{"id":5861,"title":5862,"titles":5863,"content":5864,"level":449},"/features/devtools#_1-collection-configuration-issues","1. Collection Configuration Issues",[199,5858],"Problem: Collection not appearing in DevTools. Debug Steps: Open DevTools Crouton tabIf collection is missing, check app.config.croutonCollectionsEnsure collection key matches expected formatRefresh collections (reload DevTools tab) Common Issues: Collection not defined in app.config.tsTypo in collection keyMissing name field",{"id":5866,"title":5867,"titles":5868,"content":5869,"level":449},"/features/devtools#_2-api-endpoint-errors","2. API Endpoint Errors",[199,5858],"Problem: API calls failing with 404 or 500 errors. Debug Steps: Check Operations view in DevToolsFilter by status=errorInspect error messages and status codesVerify endpoint path matches generated routesUse Execute Request to test manually Common Issues: Incorrect API path configurationMissing server handlersTeam context not set correctly",{"id":5871,"title":5872,"titles":5873,"content":5874,"level":449},"/features/devtools#_3-performance-issues","3. Performance Issues",[199,5858],"Problem: Slow API responses. Debug Steps: View Operation StatsCheck avgDuration metricFilter operations by collection to isolate slow endpointsLook for operations with high duration values Common Issues: Database queries without indexesN+1 query problemsLarge payload sizes",{"id":5876,"title":5877,"titles":5878,"content":5879,"level":449},"/features/devtools#_4-filtering-not-working","4. Filtering Not Working",[199,5858],"Problem: Search results not showing expected items. Debug Steps: Clear operation historyTrigger operations againCheck filter parameters in RPC callsVerify filter objects are properly JSON-encoded Common Issues: Incorrect filter syntaxCase sensitivity issuesFilter field doesn't exist in schema",{"id":5881,"title":44,"titles":5882,"content":528,"level":391},"/features/devtools#best-practices",[199],{"id":5884,"title":5885,"titles":5886,"content":5887,"level":449},"/features/devtools#_1-monitor-operations-during-development","1. Monitor Operations During Development",[199,44],"Keep the DevTools Crouton tab open while developing: // You'll immediately see:\n- Which endpoints are being called\n- How long they take\n- Which ones fail\n- What errors occur",{"id":5889,"title":5890,"titles":5891,"content":5892,"level":449},"/features/devtools#_2-clear-operation-history-regularly","2. Clear Operation History Regularly",[199,44],"Prevent confusion from old operations: // Clear history when starting new feature\nawait $fetch('/__nuxt_crouton_devtools/api/operations/clear', {\n  method: 'POST'\n})",{"id":5894,"title":5895,"titles":5896,"content":5897,"level":449},"/features/devtools#_3-use-request-execution-for-testing","3. Use Request Execution for Testing",[199,44],"Test endpoints without writing code: // Instead of creating test files, use DevTools to:\n- Test new endpoints\n- Validate request/response formats\n- Check error handling\n- Verify filters and sorting",{"id":5899,"title":5900,"titles":5901,"content":5902,"level":449},"/features/devtools#_4-track-success-rates","4. Track Success Rates",[199,44],"Monitor operation statistics to catch issues early: // Check stats periodically\nconst stats = await $fetch('/__nuxt_crouton_devtools/api/operations/stats')\n\nif (stats.data.successRate \u003C 90) {\n  console.warn('High error rate detected!')\n}",{"id":5904,"title":5905,"titles":5906,"content":5907,"level":449},"/features/devtools#_5-isolate-collection-issues","5. Isolate Collection Issues",[199,44],"Filter operations by collection when debugging: // Focus on specific collection\nGET /__nuxt_crouton_devtools/api/operations?collection=tasks",{"id":5909,"title":36,"titles":5910,"content":528,"level":391},"/features/devtools#troubleshooting",[199],{"id":5912,"title":5913,"titles":5914,"content":5915,"level":449},"/features/devtools#devtools-tab-not-appearing","DevTools Tab Not Appearing",[199,36],"Issue: The \"Crouton\" tab doesn't show up in Nuxt DevTools. Solutions: Verify DevTools is enabled:// nuxt.config.ts\nexport default defineNuxtConfig({\n  devtools: {\n    enabled: true  // Must be true\n  }\n})\nCheck module is loaded:// nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: [\n    '@nuxt/devtools',  // Required\n    '@fyit/crouton-devtools'  // Must be present\n  ]\n})\nRestart dev server:# Stop server (Ctrl+C)\npnpm dev\nClear Nuxt cache:rm -rf .nuxt\npnpm dev",{"id":5917,"title":5918,"titles":5919,"content":5920,"level":449},"/features/devtools#collections-not-loading","Collections Not Loading",[199,36],"Issue: DevTools opens but shows \"No collections found\". Solutions: Check app.config.ts:// Ensure collections are defined\nexport default defineAppConfig({\n  croutonCollections: {\n    tasks: { ... }  // Must have at least one\n  }\n})\nVerify collection format:// Each collection needs minimum fields\n{\n  name: 'tasks',  // Required\n  // Other fields optional\n}\nCheck console for errors:Open browser DevToolsLook for RPC errorsCheck Network tab for failed requests",{"id":5922,"title":5923,"titles":5924,"content":5925,"level":449},"/features/devtools#operations-not-tracked","Operations Not Tracked",[199,36],"Issue: Operation tracking shows no data. Solutions: Ensure operations target correct paths:// Must start with /api/crouton-collection/\nGET /api/crouton-collection/tasks/search  ✅\nGET /api/custom-tasks                     ❌\nVerify the Nitro plugin is loaded:Check that the operationTracker plugin is registeredLook for operationTracker in Nuxt build outputCheck operation buffer:Max 500 operations storedOlder operations are droppedClear history and try again",{"id":5927,"title":5928,"titles":5929,"content":5930,"level":449},"/features/devtools#request-execution-fails","Request Execution Fails",[199,36],"Issue: Testing endpoints via Execute Request returns errors. Solutions: Verify endpoint exists:// Check endpoints list first\nGET /__nuxt_crouton_devtools/api/endpoints\nCheck request format:// Must include method and path\n{\n  \"method\": \"GET\",           // Required\n  \"path\": \"/api/...\",        // Required\n  \"params\": { ... },         // Optional\n  \"requestBody\": { ... }     // Optional (for POST/PATCH)\n}\nInspect error response:{\n  \"success\": false,\n  \"status\": 404,\n  \"error\": \"Not found\",  // Read this message\n  \"data\": { ... }        // Additional error details\n}",{"id":5932,"title":5933,"titles":5934,"content":5935,"level":449},"/features/devtools#high-memory-usage","High Memory Usage",[199,36],"Issue: DevTools integration consuming too much memory. Solutions: Clear operation history:POST /__nuxt_crouton_devtools/api/operations/clear\nRestart dev server:Operation buffer resets on restartMax 500 operations enforcedReduce operation tracking:Only use DevTools when neededClose DevTools tab when not debugging",{"id":5937,"title":4027,"titles":5938,"content":528,"level":391},"/features/devtools#performance-considerations",[199],{"id":5940,"title":5941,"titles":5942,"content":5943,"level":449},"/features/devtools#memory-usage","Memory Usage",[199,4027],"The operation store uses a circular buffer (max 500 operations) to prevent unbounded memory growth: // Automatic cleanup\nif (operations.length > maxSize) {\n  operations = operations.slice(0, maxSize)\n} Memory footprint: Each operation: ~200-500 bytesMax 500 operations: ~100-250 KBNegligible impact on development",{"id":5945,"title":5946,"titles":5947,"content":5948,"level":449},"/features/devtools#cpu-impact","CPU Impact",[199,4027],"Operation tracking has minimal overhead: Plugin execution: \u003C1ms per requestBuffer management: O(1) insert, O(n) filterStats calculation: O(n) but cached Best practice: Clear history periodically during long sessions.",{"id":5950,"title":5951,"titles":5952,"content":5953,"level":449},"/features/devtools#network-impact","Network Impact",[199,4027],"RPC calls are lightweight: Collections endpoint: ~1-10 KB (depending on config size)Operations endpoint: ~5-50 KB (depending on history)Stats endpoint: \u003C1 KBExecute endpoint: Variable (depends on response) Best practice: Use filtering to reduce response sizes.",{"id":5955,"title":5956,"titles":5957,"content":5958,"level":391},"/features/devtools#roadmap-future-features","Roadmap & Future Features",[199],"The DevTools integration is in Phase 1 (MVP). Future phases will add:",{"id":5960,"title":5961,"titles":5962,"content":5963,"level":449},"/features/devtools#phase-2-crud-operations-monitoring","Phase 2: CRUD Operations Monitoring",[199,5956],"Real-time operation logs (WebSocket)Request/response inspectionError stack tracesPerformance profiling",{"id":5965,"title":5966,"titles":5967,"content":5968,"level":449},"/features/devtools#phase-3-collection-data-browser","Phase 3: Collection Data Browser",[199,5956],"Browse collection data inlineInline editing capabilitiesBulk operationsExport/import data",{"id":5970,"title":5971,"titles":5972,"content":5973,"level":449},"/features/devtools#phase-4-generator-history","Phase 4: Generator History",[199,5956],"View generator runsRollback to previous versionsDiff view for changesRe-run generators",{"id":5975,"title":5976,"titles":5977,"content":5978,"level":449},"/features/devtools#phase-5-advanced-tools","Phase 5: Advanced Tools",[199,5956],"Schema validation debuggeri18n translation managerCollection graph visualizationPerformance recommendations Want to influence the roadmap? Provide feedback via GitHub Issues or Discussions.",{"id":5980,"title":5981,"titles":5982,"content":528,"level":391},"/features/devtools#migration-breaking-changes","Migration & Breaking Changes",[199],{"id":5984,"title":5985,"titles":5986,"content":5987,"level":449},"/features/devtools#v010-current","v0.1.0 (Current)",[199,5981],"Initial beta release. No migration needed.",{"id":5989,"title":5990,"titles":5991,"content":5992,"level":449},"/features/devtools#future-versions","Future Versions",[199,5981],"When breaking changes occur, this section will provide: List of breaking changesMigration stepsCode examples (before/after)Deprecation warnings Beta Stability: As a v0.x package, breaking changes may occur on any version bump. Always review release notes before upgrading.",{"id":5994,"title":5995,"titles":5996,"content":5997,"level":391},"/features/devtools#contributing","Contributing",[199],"The DevTools integration welcomes contributions!",{"id":5999,"title":6000,"titles":6001,"content":6002,"level":449},"/features/devtools#areas-for-improvement","Areas for Improvement",[199,5995],"UI/UX enhancements - Better visualizations, layoutsAdditional RPC endpoints - New debugging capabilitiesPerformance optimizations - Reduce overheadDocumentation - More examples and guides",{"id":6004,"title":6005,"titles":6006,"content":6007,"level":449},"/features/devtools#development-setup","Development Setup",[199,5995],"# Clone repository\ngit clone https://github.com/pmcp/nuxt-crouton.git\ncd nuxt-crouton/packages/crouton-devtools\n\n# Install dependencies\npnpm install\n\n# Start playground\npnpm dev\n\n# Build module\npnpm build",{"id":6009,"title":1402,"titles":6010,"content":6011,"level":449},"/features/devtools#testing",[199,5995],"# Run in playground\ncd playground\npnpm dev\n\n# Open DevTools and test features See CONTRIBUTING.md for full guidelines.",{"id":6013,"title":6014,"titles":6015,"content":528,"level":391},"/features/devtools#faq","FAQ",[199],{"id":6017,"title":6018,"titles":6019,"content":6020,"level":449},"/features/devtools#is-this-safe-to-use-in-production","Is this safe to use in production?",[199,6014],"No. The DevTools integration only runs in development mode (nuxt dev) and is completely excluded from production builds. It has zero impact on production performance or bundle size. However, since it's a beta package (v0.x), the API may change between versions. Use with caution.",{"id":6022,"title":6023,"titles":6024,"content":6025,"level":449},"/features/devtools#does-it-work-with-nuxt-3","Does it work with Nuxt 3?",[199,6014],"No. The DevTools integration requires Nuxt 4.0.0 or higher. It uses Nuxt 4-specific APIs and module patterns.",{"id":6027,"title":6028,"titles":6029,"content":6030,"level":449},"/features/devtools#can-i-use-it-without-nuxtdevtools","Can I use it without @nuxt/devtools?",[199,6014],"No. The package requires @nuxt/devtools to be installed and enabled. The DevTools integration appears as a custom tab within the Nuxt DevTools panel.",{"id":6032,"title":6033,"titles":6034,"content":6035,"level":449},"/features/devtools#does-it-track-production-data","Does it track production data?",[199,6014],"No. The module does not load in production environments. All tracking, monitoring, and debugging features are development-only.",{"id":6037,"title":6038,"titles":6039,"content":6040,"level":449},"/features/devtools#can-i-customize-the-devtools-ui","Can I customize the DevTools UI?",[199,6014],"Not currently. The UI is bundled with the package and not customizable. Future versions may support theming or plugins.",{"id":6042,"title":6043,"titles":6044,"content":6045,"level":449},"/features/devtools#does-it-affect-my-apps-performance","Does it affect my app's performance?",[199,6014],"In development, the impact is minimal: Plugin overhead: \u003C1ms per requestMemory usage: ~100-250 KB (circular buffer)No impact on production builds",{"id":6047,"title":6048,"titles":6049,"content":6050,"level":449},"/features/devtools#can-i-programmatically-access-operation-data","Can I programmatically access operation data?",[199,6014],"Yes! Use the RPC endpoints: // Fetch operations\nconst ops = await $fetch('/__nuxt_crouton_devtools/api/operations')\n\n// Get statistics\nconst stats = await $fetch('/__nuxt_crouton_devtools/api/operations/stats')",{"id":6052,"title":6053,"titles":6054,"content":6055,"level":449},"/features/devtools#what-data-is-stored-in-the-operation-buffer","What data is stored in the operation buffer?",[199,6014],"Only metadata is stored (no request/response bodies): Timestamp, collection name, operation typeHTTP method, path, status codeResponse durationTeam context (if applicable)Error message (if failed) Request/response bodies are not stored to minimize memory usage.",{"id":6057,"title":6058,"titles":6059,"content":6060,"level":449},"/features/devtools#can-i-export-operation-logs","Can I export operation logs?",[199,6014],"Not currently. This is a planned feature for Phase 2. For now, you can fetch operations via the RPC API and save manually: const ops = await $fetch('/__nuxt_crouton_devtools/api/operations')\nconsole.log(JSON.stringify(ops, null, 2))",{"id":6062,"title":6063,"titles":6064,"content":6065,"level":449},"/features/devtools#will-this-graduate-to-v10-stable","Will this graduate to v1.0 stable?",[199,6014],"If the DevTools integration proves valuable and gains community adoption, it will eventually graduate to v1.0 stable with a stable API and semantic versioning guarantees.",{"id":6067,"title":1007,"titles":6068,"content":528,"level":391},"/features/devtools#related-resources",[199],{"id":6070,"title":6071,"titles":6072,"content":6073,"level":449},"/features/devtools#nuxt-crouton-documentation","Nuxt Crouton Documentation",[199,1007],"Core Documentation - Main Crouton guideCollections - Understanding collectionsAPI Reference - Complete API docsFeatures - All features including experimental packages",{"id":6075,"title":6076,"titles":6077,"content":6078,"level":449},"/features/devtools#nuxt-devtools","Nuxt DevTools",[199,1007],"Nuxt DevTools Docs - Official DevTools guideDevTools Kit - Module API docsCustom Tabs - Creating DevTools integrations",{"id":6080,"title":6081,"titles":6082,"content":6083,"level":449},"/features/devtools#related-packages","Related Packages",[199,1007],"@fyit/crouton - Core library@fyit/crouton-cli - CLI generatorFeatures - All features including experimental packages",{"id":6085,"title":4050,"titles":6086,"content":6087,"level":391},"/features/devtools#summary",[199],"The @fyit/crouton-devtools package provides comprehensive development tooling for Crouton collections: Collection Inspector - Browse and search all registered collectionsEndpoint Monitoring - Track API calls with detailed timing and statusOperation Tracking - Monitor CRUD operations in real-time with statisticsRequest Execution - Test endpoints directly from DevToolsRPC API - Programmatic access to debugging dataZero Configuration - Automatically discovers collectionsDevelopment Only - Zero impact on production builds Next Steps: Install the package and add to your projectExplore collections in the DevTools panelMonitor operations during developmentTest endpoints without external toolsProvide feedback to shape future development Ready to debug? Install the package and open Nuxt DevTools to start inspecting your collections! html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":205,"title":204,"titles":6089,"content":6090,"level":385},[],"Interactive graph and DAG visualization for collections using Vue Flow Status: Beta - Feature-complete but may have minor API changes Add interactive graph visualizations and DAG rendering to your Nuxt Crouton collections with the @fyit/crouton-flow package. Perfect for workflow builders, decision trees, entity relationships, and any data with parent-child relationships.",{"id":6092,"title":18,"titles":6093,"content":528,"level":391},"/features/flow#quick-start",[204],{"id":6095,"title":13,"titles":6096,"content":6097,"level":449},"/features/flow#installation",[204,18],"pnpm add @fyit/crouton-flow Dependency: @fyit/crouton-flow extends @fyit/crouton-collab for real-time collaboration features (presence, sync, Durable Objects). When using sync mode, ensure @fyit/crouton-collab is also installed.",{"id":6099,"title":1789,"titles":6100,"content":6101,"level":449},"/features/flow#configuration",[204,18],"Add the layer to your nuxt.config.ts: export default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-flow'\n  ]\n})",{"id":6103,"title":4173,"titles":6104,"content":6105,"level":449},"/features/flow#basic-usage",[204,18],"\u003Cscript setup>\nconst { data: decisions } = await useCollectionQuery('decisions')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonFlow\n    :rows=\"decisions\"\n    collection=\"decisions\"\n    parent-field=\"parentId\"\n    position-field=\"position\"\n  />\n\u003C/template>",{"id":6107,"title":183,"titles":6108,"content":6109,"level":391},"/features/flow#features",[204],"Automatic edge generation from parentId field (tree/DAG structures)Dagre auto-layout for initial positioningDrag-and-drop node positioning with persistenceCustom node components per collectionControls, minimap, and background built-inDark mode supportReal-time collaboration with Yjs CRDTs (multiplayer sync)Presence indicators showing other users' cursors and selections",{"id":6111,"title":5354,"titles":6112,"content":528,"level":391},"/features/flow#components",[204],{"id":6114,"title":6115,"titles":6116,"content":6117,"level":449},"/features/flow#croutonflow","CroutonFlow",[204,5354],"The main wrapper component that renders your collection as an interactive graph.",{"id":6119,"title":4987,"titles":6120,"content":6121,"level":748},"/features/flow#props",[204,5354,6115],"PropTypeDefaultDescriptionrowsRecord\u003Cstring, unknown>[][]Collection data to display (not required when sync is enabled)collectionstringrequiredCollection name for mutations and component resolutionparentFieldstring'parentId'Field containing parent ID for edgespositionFieldstring'position'Field storing node { x, y } positionlabelFieldstring'title'Field to use as node labelcontrolsbooleantrueShow zoom/pan controlsminimapbooleanfalseShow minimap overlaybackgroundbooleantrueShow background patternbackgroundPattern'dots' | 'lines''dots'Background pattern typedraggablebooleantrueAllow node draggingfitViewOnMountbooleantrueFit graph to viewport on mountflowConfigFlowConfigundefinedAdvanced flow configurationsyncbooleanfalseEnable real-time multiplayer syncflowIdstringundefinedUnique ID for the sync room (required when sync is true)allowDropbooleanfalseAllow external items to be dropped onto the canvasallowedCollectionsstring[][]Collections allowed to be dropped (empty = all allowed)autoCreateOnDropbooleantrueAuto-create nodes when items are dropped (sync mode only)",{"id":6123,"title":5367,"titles":6124,"content":6125,"level":748},"/features/flow#events",[204,5354,6115],"EventPayloadDescriptionnodeClick(nodeId: string, data: Record\u003Cstring, unknown>, event: MouseEvent)Node was clickednodeDblClick(nodeId: string, data: Record\u003Cstring, unknown>)Node was double-clickednodeMove(nodeId: string, position: { x: number, y: number })Node was dragged to new positionedgeClick(edgeId: string)Edge was clickedselectionChange(selectedNodeIds: string[])Selected nodes changed (use with v-model:selected)nodeDrop(item: Record\u003Cstring, unknown>, position: { x, y }, collection: string)An external item was dropped onto the canvas (requires allowDrop)",{"id":6127,"title":3404,"titles":6128,"content":6129,"level":748},"/features/flow#example",[204,5354,6115],"\u003Ctemplate>\n  \u003CCroutonFlow\n    :rows=\"decisions\"\n    collection=\"decisions\"\n    parent-field=\"parentId\"\n    position-field=\"position\"\n    :controls=\"true\"\n    :minimap=\"true\"\n    @nodeClick=\"handleNodeClick\"\n    @nodeDblClick=\"openNodeDetail\"\n  />\n\u003C/template>\n\n\u003Cscript setup>\nconst handleNodeClick = (nodeId, data) => {\n  console.log('Clicked:', data.title)\n}\n\nconst openNodeDetail = (nodeId, data) => {\n  // Open slideover or modal\n  useCrouton().open('update', 'decisions', [nodeId])\n}\n\u003C/script>",{"id":6131,"title":6132,"titles":6133,"content":6134,"level":449},"/features/flow#croutonflownode","CroutonFlowNode",[204,5354],"The default node component used when no custom node exists. Displays the item's title/name in a styled card.",{"id":6136,"title":4987,"titles":6137,"content":6138,"level":748},"/features/flow#props-1",[204,5354,6132],"PropTypeDefaultDescriptiondataRecord\u003Cstring, unknown>requiredThe collection item datacollectionstringundefinedCollection name for component resolutionselectedbooleanfalseWhether node is selecteddraggingbooleanfalseWhether node is being draggedlabelstring''Override label text",{"id":6140,"title":6141,"titles":6142,"content":6143,"level":391},"/features/flow#custom-node-components","Custom Node Components",[204],"You can create custom node components for each collection. The flow layer will automatically detect and use them.",{"id":6145,"title":6146,"titles":6147,"content":6148,"level":449},"/features/flow#convention","Convention",[204,6141],"Create a component named [Collection]Node.vue in your app's components directory: app/components/\n└── DecisionsNode.vue    ← Custom node for \"decisions\" collection",{"id":6150,"title":6151,"titles":6152,"content":6153,"level":449},"/features/flow#example-custom-node","Example Custom Node",[204,6141],"\u003C!-- app/components/DecisionsNode.vue -->\n\u003Cscript setup lang=\"ts\">\nimport { Handle, Position } from '@vue-flow/core'\n\ninterface Props {\n  data: {\n    id: string\n    content: string\n    type: 'idea' | 'insight' | 'decision'\n    starred: boolean\n  }\n  selected?: boolean\n  dragging?: boolean\n}\n\nconst props = defineProps\u003CProps>()\n\nconst typeIcons = {\n  idea: 'i-lucide-lightbulb',\n  insight: 'i-lucide-eye',\n  decision: 'i-lucide-check-circle'\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv\n    class=\"decision-node\"\n    :class=\"{\n      'decision-node--selected': selected,\n      'decision-node--starred': data.starred\n    }\"\n  >\n    \u003CHandle type=\"target\" :position=\"Position.Top\" />\n\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003CUIcon :name=\"typeIcons[data.type]\" class=\"w-4 h-4\" />\n      \u003Cspan class=\"font-medium truncate\">{{ data.content }}\u003C/span>\n      \u003CUIcon\n        v-if=\"data.starred\"\n        name=\"i-lucide-star\"\n        class=\"w-4 h-4 text-yellow-500\"\n      />\n    \u003C/div>\n\n    \u003CHandle type=\"source\" :position=\"Position.Bottom\" />\n  \u003C/div>\n\u003C/template>\n\n\u003Cstyle scoped>\n.decision-node {\n  @apply px-4 py-2 rounded-lg border bg-white dark:bg-neutral-900;\n  @apply border-neutral-200 dark:border-neutral-700;\n  @apply shadow-sm min-w-[150px] max-w-[250px];\n}\n\n.decision-node--selected {\n  @apply border-primary-500 ring-2 ring-primary-500/20;\n}\n\n.decision-node--starred {\n  @apply bg-yellow-50 dark:bg-yellow-900/20;\n}\n\u003C/style>",{"id":6155,"title":220,"titles":6156,"content":6157,"level":391},"/features/flow#real-time-collaboration",[204],"Multiplayer Mode: Enable real-time collaboration where multiple users can edit the same flow simultaneously with live cursor tracking and presence indicators.",{"id":6159,"title":936,"titles":6160,"content":6161,"level":449},"/features/flow#overview",[204,220],"The flow layer supports real-time multiplayer editing using: Yjs CRDTs for conflict-free collaborative editingCloudflare Durable Objects for WebSocket-based syncPresence awareness showing other users' cursors and selections",{"id":6163,"title":6164,"titles":6165,"content":6166,"level":449},"/features/flow#enabling-sync-mode","Enabling Sync Mode",[204,220],"Add the sync and flowId props to enable multiplayer: \u003Cscript setup>\nconst projectId = useRoute().params.id\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonFlow\n    collection=\"decisions\"\n    sync\n    :flow-id=\"projectId\"\n  />\n\u003C/template> When sync is enabled: Node positions sync in real-time across all connected clientsNode CRUD operations are broadcast to all usersUser presence (cursors, selections) is visible to everyoneChanges persist automatically to the database",{"id":6168,"title":1635,"titles":6169,"content":6170,"level":449},"/features/flow#how-it-works",[204,220],"┌─────────────────────────────────────────────────────────────┐\n│                        Clients                              │\n│  ┌──────────┐  ┌──────────┐  ┌──────────┐                  │\n│  │ Client A │  │ Client B │  │ Client C │                  │\n│  └────┬─────┘  └────┬─────┘  └────┬─────┘                  │\n│       └─────────────┼─────────────┘                         │\n│                     │ WebSocket                             │\n│                     ▼                                       │\n│  ┌─────────────────────────────────────────────────────┐   │\n│  │           Cloudflare Durable Object                 │   │\n│  │                 (CollabRoom)                          │   │\n│  │  - Manages Yjs Y.Doc per flow                       │   │\n│  │  - Merges updates from all clients                  │   │\n│  │  - Persists to D1 on changes                        │   │\n│  └──────────────────────┬──────────────────────────────┘   │\n│                         │                                   │\n│                         ▼                                   │\n│  ┌─────────────────────────────────────────────────────┐   │\n│  │                 D1 (SQLite)                         │   │\n│  │  yjs_collab_states (Yjs blob) + collection tables     │   │\n│  └─────────────────────────────────────────────────────┘   │\n└─────────────────────────────────────────────────────────────┘",{"id":6172,"title":6173,"titles":6174,"content":6175,"level":449},"/features/flow#sync-mode-vs-standard-mode","Sync Mode vs Standard Mode",[204,220],"FeatureStandard ModeSync ModePosition persistenceDebounced API callsReal-time via YjsMulti-user editingNot supportedFull supportPresence indicatorsNot availableUsers, cursors, selectionsOffline supportCrouton cacheYjs CRDT merge on reconnectInfrastructureStandard APIDurable Objects + WebSocket",{"id":6177,"title":6178,"titles":6179,"content":6180,"level":449},"/features/flow#infrastructure-setup","Infrastructure Setup",[204,220],"Sync mode requires Cloudflare Durable Objects. Add to your wrangler.toml: [[durable_objects.bindings]]\nname = \"COLLAB_ROOMS\"\nclass_name = \"CollabRoom\"\n\n[[migrations]]\ntag = \"collab-v1\"\nnew_classes = [\"CollabRoom\"] Run the D1 migration to create the state table: CREATE TABLE IF NOT EXISTS yjs_collab_states (\n  room_type TEXT NOT NULL,\n  room_id TEXT NOT NULL,\n  state BLOB NOT NULL,\n  version INTEGER DEFAULT 1,\n  created_at INTEGER DEFAULT (unixepoch()),\n  updated_at INTEGER DEFAULT (unixepoch()),\n  PRIMARY KEY (room_type, room_id)\n); Enable WebSocket in your Nuxt config: export default defineNuxtConfig({\n  nitro: {\n    experimental: {\n      websocket: true\n    }\n  }\n})",{"id":6182,"title":6183,"titles":6184,"content":528,"level":391},"/features/flow#sync-mode-components","Sync Mode Components",[204],{"id":6186,"title":6187,"titles":6188,"content":6189,"level":449},"/features/flow#collabpresence","CollabPresence",[204,6183],"Displays presence indicators for other connected users. Provided by @fyit/crouton-collab. \u003Ctemplate>\n  \u003CCollabPresence\n    :users=\"users\"\n    :max-visible=\"4\"\n    size=\"sm\"\n  />\n\u003C/template>",{"id":6191,"title":4987,"titles":6192,"content":6193,"level":748},"/features/flow#props-2",[204,6183,6187],"PropTypeDefaultDescriptionusersCollabAwarenessState[]requiredArray of connected usersmaxVisiblenumber5Max avatars before showing +N overflowsize'xs' | 'sm' | 'md''sm'Avatar sizeshowTooltipbooleantrueShow user name on hover Features: Stacked user avatars with initials and colorsOverflow indicator when more users than maxVisibleTooltip with user name on hover",{"id":6195,"title":6196,"titles":6197,"content":6198,"level":449},"/features/flow#collabstatus","CollabStatus",[204,6183],"Shows the current sync connection state. Provided by @fyit/crouton-collab. \u003Ctemplate>\n  \u003CCollabStatus\n    :connected=\"connected\"\n    :synced=\"synced\"\n    :error=\"error\"\n    :show-label=\"true\"\n  />\n\u003C/template>",{"id":6200,"title":4987,"titles":6201,"content":6202,"level":748},"/features/flow#props-3",[204,6183,6196],"PropTypeDefaultDescriptionconnectedbooleanrequiredWebSocket connectedsyncedbooleanrequiredInitial sync completeerrorError | nullnullConnection error if anyshowLabelbooleantrueShow text label next to dot States: Green (synced): Fully operationalYellow pulsing (syncing): WebSocket connected, waiting for initial syncGray (disconnected): Connection lost, will auto-reconnectRed (error): Connection error (tooltip shows error message)",{"id":6204,"title":5437,"titles":6205,"content":528,"level":391},"/features/flow#composables",[204],{"id":6207,"title":6208,"titles":6209,"content":6210,"level":449},"/features/flow#useflowdata","useFlowData",[204,5437],"Converts collection rows into Vue Flow nodes and edges. const { nodes, edges, getNode, getItem } = useFlowData(\n  computed(() => rows),\n  {\n    parentField: 'parentId',\n    positionField: 'position',\n    labelField: 'title'\n  }\n)",{"id":6212,"title":3362,"titles":6213,"content":6214,"level":748},"/features/flow#options",[204,5437,6208],"OptionTypeDefaultDescriptionparentFieldstring'parentId'Field containing parent IDpositionFieldstring'position'Field containing { x, y }labelFieldstring'title'Field for node label",{"id":6216,"title":6217,"titles":6218,"content":6219,"level":748},"/features/flow#returns","Returns",[204,5437,6208],"PropertyTypeDescriptionnodesComputedRef\u003CNode[]>Vue Flow nodesedgesComputedRef\u003CEdge[]>Vue Flow edgesgetNode(id: string) => NodeGet node by IDgetItem(id: string) => TGet original item by ID",{"id":6221,"title":6222,"titles":6223,"content":6224,"level":449},"/features/flow#useflowlayout","useFlowLayout",[204,5437],"Provides dagre-based automatic layout for graphs. const { applyLayout, applyLayoutToNew, needsLayout } = useFlowLayout({\n  direction: 'TB',      // TB, LR, BT, RL\n  nodeSpacing: 50,      // Horizontal spacing\n  rankSpacing: 100      // Vertical spacing\n})\n\n// Apply layout if needed\nif (needsLayout(nodes)) {\n  const layoutedNodes = applyLayout(nodes, edges)\n}",{"id":6226,"title":6217,"titles":6227,"content":6228,"level":748},"/features/flow#returns-1",[204,5437,6222],"PropertyTypeDescriptionapplyLayout(nodes: Node[], edges: Edge[]) => Node[]Apply dagre layout to all nodesapplyLayoutToNew(nodes: Node[], edges: Edge[]) => Node[]Apply layout only to nodes without existing positionsneedsLayout(nodes: Node[]) => booleanCheck if any nodes need layout (missing positions)",{"id":6230,"title":6231,"titles":6232,"content":6233,"level":449},"/features/flow#useflowmutation","useFlowMutation",[204,5437],"Handles position persistence via the crouton mutation system. const { updatePosition, updatePositions, pending, error } = useFlowMutation(\n  'decisions',\n  'position'  // Position field name\n)\n\n// Update a node's position\nawait updatePosition('node-123', { x: 100, y: 200 })",{"id":6235,"title":6236,"titles":6237,"content":6238,"level":449},"/features/flow#usedebouncedpositionupdate","useDebouncedPositionUpdate",[204,5437],"Debounced version for drag operations (prevents excessive API calls). const { debouncedUpdate, pending, error } = useDebouncedPositionUpdate(\n  'decisions',\n  'position',\n  500  // Debounce delay in ms\n)\n\n// Called on each drag event\ndebouncedUpdate(nodeId, { x, y })",{"id":6240,"title":6241,"titles":6242,"content":6243,"level":449},"/features/flow#useflowsync","useFlowSync",[204,5437],"Real-time flow synchronization via Yjs. Handles WebSocket connection, node CRUD, and presence. const {\n  // State (readonly)\n  nodes,        // Ref\u003CYjsFlowNode[]>\n  connected,    // ComputedRef\u003Cboolean>\n  synced,       // ComputedRef\u003Cboolean>\n  error,        // ComputedRef\u003CError | null>\n  users,        // ComputedRef\u003CCollabAwarenessState[]>\n  user,         // Current user info\n\n  // Node operations\n  createNode,\n  updateNode,\n  updatePosition,\n  deleteNode,\n  getNode,\n\n  // Presence\n  updateCursor,\n  selectNode,\n  updateGhostNode,\n  clearGhostNode,\n\n  // Connection\n  connect,\n  disconnect,\n\n  // Advanced (Yjs internals)\n  ydoc,\n  nodesMap\n} = useFlowSync({\n  flowId: 'flow-123',\n  collection: 'decisions'\n})",{"id":6245,"title":3362,"titles":6246,"content":6247,"level":748},"/features/flow#options-1",[204,5437,6241],"OptionTypeRequiredDescriptionflowIdstringYesUnique identifier for the sync roomcollectionstringYesCollection name for persistence",{"id":6249,"title":6217,"titles":6250,"content":6251,"level":748},"/features/flow#returns-2",[204,5437,6241],"State PropertyTypeDescriptionnodesReadonly\u003CRef\u003CYjsFlowNode[]>>All nodes in the flowconnectedComputedRef\u003Cboolean>WebSocket connectedsyncedComputedRef\u003Cboolean>Initial sync completeerrorComputedRef\u003CError | null>Connection errorusersComputedRef\u003CCollabAwarenessState[]>Connected usersuserComputedRef\u003C{id, name, color} | null>Current user (from session) Node Operations MethodSignatureDescriptioncreateNode(data: Partial\u003CYjsFlowNode>) => stringCreate node, returns IDupdateNode(id: string, updates: Partial\u003CYjsFlowNode>) => voidUpdate node fieldsupdatePosition(id: string, position: {x, y}) => voidUpdate node positiondeleteNode(id: string) => voidDelete nodegetNode(id: string) => YjsFlowNode | undefinedGet node by ID Presence MethodSignatureDescriptionupdateCursor(cursor: {x, y} | null) => voidUpdate cursor positionselectNode(nodeId: string | null) => voidBroadcast node selectionupdateGhostNode(ghost: YjsGhostNode | null) => voidShow drag preview to othersclearGhostNode() => voidClear ghost node",{"id":6253,"title":6254,"titles":6255,"content":6256,"level":748},"/features/flow#example-custom-sync-controls","Example: Custom Sync Controls",[204,5437,6241],"\u003Cscript setup>\nconst {\n  nodes,\n  connected,\n  synced,\n  users,\n  createNode,\n  deleteNode\n} = useFlowSync({\n  flowId: props.flowId,\n  collection: 'decisions'\n})\n\nconst addNode = () => {\n  createNode({\n    title: 'New Decision',\n    position: { x: 100, y: 100 }\n  })\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"flex items-center gap-2\">\n    \u003Cdiv class=\"flex items-center gap-1\">\n      \u003Cspan\n        class=\"w-2 h-2 rounded-full\"\n        :class=\"connected && synced ? 'bg-green-500' : 'bg-yellow-500'\"\n      />\n      \u003Cspan class=\"text-sm\">{{ users.length }} online\u003C/span>\n    \u003C/div>\n    \u003CUButton @click=\"addNode\">Add Node\u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":6258,"title":6259,"titles":6260,"content":6261,"level":449},"/features/flow#useflowpresence","useFlowPresence",[204,5437],"Helper composable for presence UI. Provides utilities for rendering user indicators. const {\n  otherUsers,           // Other connected users (excludes current)\n  getUsersSelectingNode,  // Get users selecting a specific node\n  getNodePresenceStyle    // Get style for node presence border\n} = useFlowPresence({\n  users: computed(() => syncState.users),\n  currentUserId: currentUser.id\n})",{"id":6263,"title":3362,"titles":6264,"content":6265,"level":748},"/features/flow#options-2",[204,5437,6259],"OptionTypeDescriptionusersRef\u003CCollabAwarenessState[]>Connected users from useFlowSynccurrentUserIdstringCurrent user's ID to exclude",{"id":6267,"title":6217,"titles":6268,"content":6269,"level":748},"/features/flow#returns-3",[204,5437,6259],"PropertyTypeDescriptionotherUsersComputedRef\u003CCollabAwarenessState[]>Users excluding currentgetUserColor(userId: string) => stringGet assigned color for a usergetUsersSelectingNode(nodeId: string) => ComputedRef\u003CCollabAwarenessState[]>Users selecting a nodegetNodePresenceStyle(nodeId: string) => ComputedRef\u003CCSSProperties>Presence border style",{"id":6271,"title":6272,"titles":6273,"content":6274,"level":748},"/features/flow#example-node-presence-indicator","Example: Node Presence Indicator",[204,5437,6259],"\u003Cscript setup>\nconst { getNodePresenceStyle } = useFlowPresence({\n  users: users,\n  currentUserId: currentUser.id\n})\n\n// Get style for a specific node\nconst nodeStyle = getNodePresenceStyle('node-123')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv :style=\"nodeStyle\">\n    \u003C!-- Node content with presence border -->\n  \u003C/div>\n\u003C/template>",{"id":6276,"title":6277,"titles":6278,"content":528,"level":391},"/features/flow#data-model","Data Model",[204],{"id":6280,"title":6281,"titles":6282,"content":6283,"level":449},"/features/flow#position-field","Position Field",[204,6277],"Add a position field to your collection schema to persist node positions: {\n  \"position\": {\n    \"type\": \"json\",\n    \"meta\": {\n      \"description\": \"Node position for flow visualization\"\n    }\n  }\n} The position is stored as: {\n  position: {\n    x: 150,\n    y: 200\n  }\n}",{"id":6285,"title":6286,"titles":6287,"content":6288,"level":449},"/features/flow#parent-field","Parent Field",[204,6277],"The flow layer builds edges from parentId relationships: {\n  \"parentId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"decisions\",\n    \"meta\": {\n      \"description\": \"Parent decision for tree structure\"\n    }\n  }\n}",{"id":6290,"title":6291,"titles":6292,"content":6293,"level":391},"/features/flow#flow-configuration","Flow Configuration",[204],"For advanced configuration, use the flowConfig prop: \u003CCroutonFlow\n  :rows=\"decisions\"\n  collection=\"decisions\"\n  :flow-config=\"{\n    direction: 'LR',      // Left to right layout\n    nodeSpacing: 80,\n    rankSpacing: 150,\n    autoLayout: 'dagre'\n  }\"\n/> OptionTypeDefaultDescriptiondirection'TB' | 'LR' | 'BT' | 'RL''TB'Layout directionnodeSpacingnumber50Horizontal spacing between nodesrankSpacingnumber100Vertical spacing between ranksautoLayout'dagre' | 'none''dagre'Auto-layout algorithmpositionFieldstring'position'Position storage field",{"id":6295,"title":3367,"titles":6296,"content":528,"level":391},"/features/flow#examples",[204],{"id":6298,"title":6299,"titles":6300,"content":6301,"level":449},"/features/flow#decision-tree","Decision Tree",[204,3367],"\u003Cscript setup>\nconst { data: decisions } = await useCollectionQuery('decisions', {\n  sort: { field: 'createdAt', direction: 'asc' }\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"h-[600px]\">\n    \u003CCroutonFlow\n      :rows=\"decisions\"\n      collection=\"decisions\"\n      parent-field=\"parentId\"\n      :minimap=\"true\"\n      @nodeDblClick=\"(id) => useCrouton().open('update', 'decisions', [id])\"\n    />\n  \u003C/div>\n\u003C/template>",{"id":6303,"title":6304,"titles":6305,"content":6306,"level":449},"/features/flow#workflow-builder","Workflow Builder",[204,3367],"\u003Cscript setup>\nconst { data: steps } = await useCollectionQuery('workflowSteps')\n\nconst handleConnect = async (sourceId: string, targetId: string) => {\n  // Create edge relationship\n  await useCollectionMutation('workflowSteps').update(targetId, {\n    parentId: sourceId\n  })\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonFlow\n    :rows=\"steps\"\n    collection=\"workflowSteps\"\n    :flow-config=\"{ direction: 'LR' }\"\n    :controls=\"true\"\n  >\n    \u003C!-- Custom slot content -->\n    \u003Ctemplate #default>\n      \u003Cdiv class=\"absolute top-4 right-4\">\n        \u003CUButton @click=\"addStep\">Add Step\u003C/UButton>\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonFlow>\n\u003C/template>",{"id":6308,"title":6309,"titles":6310,"content":6311,"level":449},"/features/flow#multiplayer-flow","Multiplayer Flow",[204,3367],"Real-time collaborative editing with presence: \u003Cscript setup>\nconst route = useRoute()\nconst { user: currentUser } = useUserSession()\n\n// Use sync mode for real-time collaboration\nconst {\n  nodes,\n  connected,\n  synced,\n  users,\n  error,\n  createNode\n} = useFlowSync({\n  flowId: route.params.id,\n  collection: 'decisions'\n})\n\n// Presence helpers\nconst { otherUsers } = useFlowPresence({\n  users,\n  currentUserId: currentUser.value?.id\n})\n\nconst addDecision = () => {\n  createNode({\n    title: 'New Decision',\n    position: { x: Math.random() * 400, y: Math.random() * 300 }\n  })\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"h-screen relative\">\n    \u003C!-- Main flow with sync enabled -->\n    \u003CCroutonFlow\n      collection=\"decisions\"\n      sync\n      :flow-id=\"route.params.id\"\n      :minimap=\"true\"\n    />\n\n    \u003C!-- Connection status -->\n    \u003CCollabStatus\n      :connected=\"connected\"\n      :synced=\"synced\"\n      :error=\"error\"\n    />\n\n    \u003C!-- Presence overlay -->\n    \u003CCollabPresence\n      :users=\"otherUsers\"\n    />\n\n    \u003C!-- Toolbar -->\n    \u003Cdiv class=\"absolute top-4 left-4 flex items-center gap-4\">\n      \u003Cdiv class=\"flex items-center gap-2 bg-white/80 dark:bg-neutral-900/80 px-3 py-1.5 rounded-lg\">\n        \u003Cdiv\n          class=\"w-2 h-2 rounded-full\"\n          :class=\"connected && synced ? 'bg-green-500' : 'bg-yellow-500'\"\n        />\n        \u003Cspan class=\"text-sm\">{{ otherUsers.length + 1 }} online\u003C/span>\n      \u003C/div>\n      \u003CUButton @click=\"addDecision\" icon=\"i-lucide-plus\">\n        Add Decision\n      \u003C/UButton>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":6313,"title":6314,"titles":6315,"content":6316,"level":391},"/features/flow#styling","Styling",[204],"The flow component supports dark mode automatically. Override styles using CSS: /* Custom node styles */\n:deep(.vue-flow__node) {\n  /* Your styles */\n}\n\n/* Custom edge styles */\n:deep(.vue-flow__edge-path) {\n  stroke: theme('colors.primary.500');\n  stroke-width: 2px;\n}\n\n/* Selected state */\n:deep(.vue-flow__node.selected) {\n  /* Selected node styles */\n}",{"id":6318,"title":4341,"titles":6319,"content":6320,"level":391},"/features/flow#related",[204],"Vue Flow DocumentationDagre LayoutYjs Documentation - CRDT library for real-time syncCloudflare Durable Objects - WebSocket infrastructure html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":209,"title":208,"titles":6322,"content":6323,"level":385},[],"Chat, completion, and multi-provider AI support for Nuxt Crouton The AI package extends Nuxt Crouton with AI-powered chat and text completion functionality. Built on the Vercel AI SDK, it provides streaming chat interfaces, multi-provider support (OpenAI and Anthropic), and ready-to-use Vue components.",{"id":6325,"title":936,"titles":6326,"content":528,"level":391},"/features/ai#overview",[208],{"id":6328,"title":5326,"titles":6329,"content":6330,"level":449},"/features/ai#package-information",[208,936],"Package: @fyit/crouton-aiVersion: 0.1.0Type: Nuxt Layer / Addon PackageRepository: nuxt-crouton monorepo",{"id":6332,"title":6333,"titles":6334,"content":6335,"level":449},"/features/ai#whats-included","What's Included",[208,936],"Composables (4): useChat() - Streaming chat with conversation historyuseCompletion() - Single-turn text completionuseAIProvider() - Provider and model configurationuseTranslationSuggestion() - AI-powered translation suggestions Components (5): AIChatbox - Complete chat interface with messages and inputAIMessage - Individual message bubble componentAIInput - Message input with send buttonAIPageGenerator - AI-powered page content generationAITranslateButton - One-click AI translation trigger Server Utilities: createAIProvider() - Server-side provider factoryAuto-detection for provider from model IDStreaming and non-streaming response support Server API Endpoints (in /api/ai/): translate.post.ts - Translate text contenttranslate-blocks.post.ts - Translate block-based contentgenerate-email-template.post.ts - Generate email template contentgenerate-page.post.ts - Generate page content Note: The package does not ship a chat.post.ts endpoint. The useChat composable defaults to /api/ai/chat, but you must create this endpoint in your own app (see Server Usage for examples). Integration: Chat conversations schema for persistenceMulti-provider support (OpenAI, Anthropic)Team context integration (when using crouton-auth)",{"id":6337,"title":5599,"titles":6338,"content":6339,"level":391},"/features/ai#key-features",[208],"Real-time Streaming - Messages stream token-by-token for responsive UXMulti-Provider - Switch between OpenAI and Anthropic seamlesslyAuto-Detection - Provider auto-detected from model ID (gpt-* → OpenAI, claude-* → Anthropic)Ready-to-Use Components - Drop-in chat interface componentsConversation Persistence - Schema for storing chat historyTeam Integration - Automatic team scoping when using crouton-authType-Safe - Full TypeScript support with exported types",{"id":6341,"title":13,"titles":6342,"content":528,"level":391},"/features/ai#installation",[208],{"id":6344,"title":426,"titles":6345,"content":6346,"level":449},"/features/ai#prerequisites",[208,13],"Before installing, ensure you have: Nuxt 4.0+@fyit/crouton installedAn API key for at least one provider (OpenAI or Anthropic)",{"id":6348,"title":6349,"titles":6350,"content":6351,"level":449},"/features/ai#install-package","Install Package",[208,13],"pnpm add @fyit/crouton-ai",{"id":6353,"title":436,"titles":6354,"content":6355,"level":449},"/features/ai#configure-nuxt",[208,13],"Add the AI layer to your nuxt.config.ts: export default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-ai'\n  ],\n  runtimeConfig: {\n    // Server-side (private)\n    openaiApiKey: '',      // Set via NUXT_OPENAI_API_KEY\n    anthropicApiKey: '',   // Set via NUXT_ANTHROPIC_API_KEY\n\n    // Client-side (public)\n    public: {\n      croutonAI: {\n        defaultProvider: 'openai',\n        defaultModel: 'gpt-4o'\n      }\n    }\n  }\n})",{"id":6357,"title":6358,"titles":6359,"content":6360,"level":449},"/features/ai#environment-variables","Environment Variables",[208,13],"Create or update your .env file: # For OpenAI models (gpt-4o, gpt-4-turbo, o1, etc.)\nNUXT_OPENAI_API_KEY=sk-...\n\n# For Anthropic models (claude-sonnet-4, claude-opus-4, etc.)\nNUXT_ANTHROPIC_API_KEY=sk-ant-... You only need API keys for providers you plan to use. If you only use OpenAI models, you don't need an Anthropic key.",{"id":6362,"title":18,"titles":6363,"content":528,"level":391},"/features/ai#quick-start",[208],{"id":6365,"title":6366,"titles":6367,"content":6368,"level":449},"/features/ai#basic-chat","Basic Chat",[208,18],"Create a simple chat page: \u003Ctemplate>\n  \u003Cdiv class=\"max-w-2xl mx-auto p-4\">\n    \u003Ch1 class=\"text-2xl font-bold mb-4\">AI Chat\u003C/h1>\n    \u003Cdiv class=\"h-[600px]\">\n      \u003CAIChatbox\n        system-prompt=\"You are a helpful assistant.\"\n        placeholder=\"Ask me anything...\"\n      />\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":6370,"title":6371,"titles":6372,"content":6373,"level":449},"/features/ai#custom-chat-implementation","Custom Chat Implementation",[208,18],"For more control, use the useChat composable directly: \u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003C!-- Messages -->\n    \u003Cdiv v-for=\"message in messages\" :key=\"message.id\" class=\"p-3 rounded-lg\"\n         :class=\"message.role === 'user' ? 'bg-blue-100 ml-12' : 'bg-gray-100 mr-12'\">\n      \u003Cp class=\"text-sm font-medium\">{{ message.role }}\u003C/p>\n      \u003Cp>{{ message.content }}\u003C/p>\n    \u003C/div>\n\n    \u003C!-- Input -->\n    \u003Cform @submit.prevent=\"handleSubmit\" class=\"flex gap-2\">\n      \u003Cinput\n        v-model=\"input\"\n        placeholder=\"Type a message...\"\n        class=\"flex-1 px-4 py-2 border rounded-lg\"\n        :disabled=\"isLoading\"\n      />\n      \u003Cbutton\n        type=\"submit\"\n        :disabled=\"isLoading || !input.trim()\"\n        class=\"px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50\"\n      >\n        {{ isLoading ? 'Sending...' : 'Send' }}\n      \u003C/button>\n    \u003C/form>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { messages, input, handleSubmit, isLoading } = useChat({\n  model: 'gpt-4o',\n  systemPrompt: 'You are a helpful assistant.',\n  onFinish: (message) => {\n    console.log('Response complete:', message.content)\n  },\n  onError: (error) => {\n    console.error('Chat error:', error)\n  }\n})\n\u003C/script>",{"id":6375,"title":5437,"titles":6376,"content":528,"level":391},"/features/ai#composables",[208],{"id":6378,"title":6379,"titles":6380,"content":6381,"level":449},"/features/ai#usechat","useChat()",[208,5437],"The primary composable for chat functionality. Wraps the Vercel AI SDK's useChat with Crouton-specific defaults.",{"id":6383,"title":3362,"titles":6384,"content":6385,"level":748},"/features/ai#options",[208,5437,6379],"interface AIChatOptions {\n  /** API endpoint for chat (default: '/api/ai/chat') */\n  api?: string\n  /** Unique identifier for the chat session */\n  id?: string\n  /** Provider to use (e.g., 'openai', 'anthropic') */\n  provider?: string\n  /** Model to use (e.g., 'gpt-4o', 'claude-sonnet-4-20250514') */\n  model?: string\n  /** System prompt to set context */\n  systemPrompt?: string\n  /** Initial messages to populate the chat */\n  initialMessages?: AIMessage[]\n  /** Initial input value */\n  initialInput?: string\n  /** Additional body parameters to send with each request */\n  body?: Record\u003Cstring, unknown>\n  /** Additional headers to send with each request */\n  headers?: Record\u003Cstring, string> | Headers\n  /** Credentials mode for fetch requests */\n  credentials?: 'omit' | 'same-origin' | 'include'\n  /** Callback when a message is complete */\n  onFinish?: (message: AIMessage) => void\n  /** Callback when an error occurs */\n  onError?: (error: Error) => void\n  /** Callback when a response is received */\n  onResponse?: (response: Response) => void | Promise\u003Cvoid>\n}",{"id":6387,"title":6217,"titles":6388,"content":6389,"level":748},"/features/ai#returns",[208,5437,6379],"{\n  // Core state\n  messages: ComputedRef\u003CAIMessage[]>     // Conversation history\n  input: Ref\u003Cstring>                     // Current input value\n  isLoading: ComputedRef\u003Cboolean>        // Whether request is in progress\n  error: Ref\u003CError | undefined>          // Current error state\n  status: Ref\u003C'idle' | 'streaming' | 'submitted'>\n\n  // Actions\n  handleSubmit: () => void               // Submit current input\n  stop: () => void                       // Stop streaming response\n  reload: () => void                     // Regenerate last response\n  append: (message: AIMessage) => void   // Add message to history\n  setMessages: (messages: AIMessage[]) => void\n\n  // Crouton helpers\n  clearMessages: () => void              // Clear all messages\n  exportMessages: () => AIMessage[]      // Export for persistence\n  importMessages: (messages: AIMessage[]) => void  // Restore messages\n}",{"id":6391,"title":5825,"titles":6392,"content":6393,"level":748},"/features/ai#usage-examples",[208,5437,6379],"With System Prompt: const { messages, input, handleSubmit, isLoading } = useChat({\n  model: 'gpt-4o',\n  systemPrompt: `You are an expert customer support agent for Acme Corp.\n    Be helpful, friendly, and concise.\n    Always greet customers by name when provided.`\n}) With Initial Messages: const { messages, handleSubmit } = useChat({\n  initialMessages: [\n    { id: '1', role: 'assistant', content: 'Hello! How can I help you today?' }\n  ]\n}) With Callbacks: const { messages, handleSubmit } = useChat({\n  onFinish: (message) => {\n    // Save to database, track analytics, etc.\n    saveConversation(messages.value)\n  },\n  onError: (error) => {\n    toast.add({\n      title: 'Error',\n      description: error.message,\n      color: 'red'\n    })\n  }\n})",{"id":6395,"title":6396,"titles":6397,"content":6398,"level":449},"/features/ai#usecompletion","useCompletion()",[208,5437],"For single-turn text completion without conversation history.",{"id":6400,"title":3362,"titles":6401,"content":6402,"level":748},"/features/ai#options-1",[208,5437,6396],"interface AICompletionOptions {\n  /** API endpoint for completion (default: '/api/ai/completion') */\n  api?: string\n  /** Provider to use */\n  provider?: string\n  /** Model to use */\n  model?: string\n  /** Additional body parameters */\n  body?: Record\u003Cstring, unknown>\n  /** Additional headers */\n  headers?: Record\u003Cstring, string> | Headers\n  /** Credentials mode */\n  credentials?: 'omit' | 'same-origin' | 'include'\n  /** Callback when completion is finished */\n  onFinish?: (completion: string) => void\n  /** Callback when an error occurs */\n  onError?: (error: Error) => void\n}",{"id":6404,"title":6217,"titles":6405,"content":6406,"level":748},"/features/ai#returns-1",[208,5437,6396],"{\n  completion: Ref\u003Cstring>       // Current completion text\n  complete: (prompt: string) => Promise\u003Cvoid>  // Trigger completion\n  input: Ref\u003Cstring>            // Input value\n  isLoading: Ref\u003Cboolean>       // Loading state\n  error: Ref\u003CError | undefined> // Error state\n  stop: () => void              // Stop generation\n  setCompletion: (text: string) => void\n\n  // Crouton helpers\n  clearCompletion: () => void   // Clear completion text\n}",{"id":6408,"title":6409,"titles":6410,"content":6411,"level":748},"/features/ai#usage-example","Usage Example",[208,5437,6396],"\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003CUTextarea v-model=\"textToSummarize\" placeholder=\"Paste text to summarize...\" />\n    \u003CUButton @click=\"summarize\" :loading=\"isLoading\">\n      Summarize\n    \u003C/UButton>\n    \u003Cdiv v-if=\"completion\" class=\"p-4 bg-gray-100 rounded-lg\">\n      \u003Ch3 class=\"font-medium mb-2\">Summary:\u003C/h3>\n      \u003Cp>{{ completion }}\u003C/p>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst textToSummarize = ref('')\nconst { completion, complete, isLoading } = useCompletion({\n  model: 'gpt-4o-mini'  // Use faster model for summaries\n})\n\nconst summarize = async () => {\n  await complete(`Please summarize the following text in 2-3 sentences:\\n\\n${textToSummarize.value}`)\n}\n\u003C/script>",{"id":6413,"title":6414,"titles":6415,"content":6416,"level":449},"/features/ai#useaiprovider","useAIProvider()",[208,5437],"Access provider configuration and model information.",{"id":6418,"title":6217,"titles":6419,"content":6420,"level":748},"/features/ai#returns-2",[208,5437,6414],"{\n  /** Default provider from config */\n  defaultProvider: ComputedRef\u003Cstring>\n  /** Default model from config */\n  defaultModel: ComputedRef\u003Cstring>\n  /** List of all available providers */\n  providers: AIProvider[]\n  /** Model information by ID */\n  models: Record\u003Cstring, AIModel>\n\n  // Helper functions\n  getProvider: (providerId: string) => AIProvider | undefined\n  getModel: (modelId: string) => AIModel | undefined\n  getModelsForProvider: (providerId: string) => AIModel[]\n  isModelFromProvider: (modelId: string, providerId: string) => boolean\n  detectProviderFromModel: (modelId: string) => string | undefined\n}",{"id":6422,"title":6409,"titles":6423,"content":6424,"level":748},"/features/ai#usage-example-1",[208,5437,6414],"\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003CUFormField label=\"Provider\">\n      \u003CUSelectMenu v-model=\"selectedProvider\" :items=\"providers\" value-key=\"id\" label-key=\"name\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Model\">\n      \u003CUSelectMenu v-model=\"selectedModel\" :items=\"availableModels\" value-key=\"id\" label-key=\"name\" />\n    \u003C/UFormField>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { providers, getModelsForProvider, defaultProvider, defaultModel } = useAIProvider()\n\nconst selectedProvider = ref(defaultProvider.value)\nconst selectedModel = ref(defaultModel.value)\n\nconst availableModels = computed(() =>\n  getModelsForProvider(selectedProvider.value)\n)\n\u003C/script>",{"id":6426,"title":5354,"titles":6427,"content":6428,"level":391},"/features/ai#components",[208],"All components are auto-imported with the AI prefix.",{"id":6430,"title":6431,"titles":6432,"content":6433,"level":449},"/features/ai#aichatbox","AIChatbox",[208,5354],"Complete chat interface with messages area, error handling, and input.",{"id":6435,"title":4987,"titles":6436,"content":6437,"level":748},"/features/ai#props",[208,5354,6431],"interface AIChatboxProps {\n  /** API endpoint for chat (default: '/api/ai/chat') */\n  api?: string\n  /** System prompt to set context */\n  systemPrompt?: string\n  /** Placeholder text for input */\n  placeholder?: string\n  /** Message shown when there are no messages */\n  emptyMessage?: string\n  /** Provider to use */\n  provider?: string\n  /** Model to use */\n  model?: string\n  /** Initial messages */\n  initialMessages?: AIMessage[]\n}",{"id":6439,"title":5367,"titles":6440,"content":6441,"level":748},"/features/ai#events",[208,5354,6431],"@finish: (message: AIMessage) => void  // Emitted when response completes\n@error: (error: Error) => void         // Emitted on errors",{"id":6443,"title":6444,"titles":6445,"content":6446,"level":748},"/features/ai#exposed-methods","Exposed Methods",[208,5354,6431],"The component exposes its internal state for programmatic control: \u003Ctemplate>\n  \u003Cdiv>\n    \u003CAIChatbox ref=\"chatbox\" />\n    \u003CUButton @click=\"clearChat\">Clear Chat\u003C/UButton>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst chatbox = ref()\n\nconst clearChat = () => {\n  chatbox.value?.clearMessages()\n}\n\u003C/script>",{"id":6448,"title":1608,"titles":6449,"content":6450,"level":748},"/features/ai#usage",[208,5354,6431],"Basic: \u003Ctemplate>\n  \u003Cdiv class=\"h-[600px]\">\n    \u003CAIChatbox system-prompt=\"You are a helpful coding assistant.\" />\n  \u003C/div>\n\u003C/template> With Custom Model: \u003Ctemplate>\n  \u003CAIChatbox\n    model=\"claude-sonnet-4-20250514\"\n    system-prompt=\"You are Claude, a helpful AI assistant.\"\n    placeholder=\"Chat with Claude...\"\n  />\n\u003C/template>",{"id":6452,"title":6453,"titles":6454,"content":6455,"level":449},"/features/ai#aimessage","AIMessage",[208,5354],"Individual message bubble component.",{"id":6457,"title":4987,"titles":6458,"content":6459,"level":748},"/features/ai#props-1",[208,5354,6453],"interface AIMessageProps {\n  /** The message to display */\n  message: AIMessage\n  /** Whether this message is currently streaming */\n  isStreaming?: boolean\n}",{"id":6461,"title":1608,"titles":6462,"content":6463,"level":748},"/features/ai#usage-1",[208,5354,6453],"\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003CAIMessage\n      v-for=\"message in messages\"\n      :key=\"message.id\"\n      :message=\"message\"\n      :is-streaming=\"isLoading && message === messages[messages.length - 1]\"\n    />\n  \u003C/div>\n\u003C/template>",{"id":6465,"title":6466,"titles":6467,"content":6468,"level":449},"/features/ai#aiinput","AIInput",[208,5354],"Message input with send button.",{"id":6470,"title":4987,"titles":6471,"content":6472,"level":748},"/features/ai#props-2",[208,5354,6466],"interface AIInputProps {\n  /** Current input value (v-model) */\n  modelValue?: string\n  /** Whether the input is in loading state */\n  loading?: boolean\n  /** Placeholder text */\n  placeholder?: string\n  /** Whether the input is disabled */\n  disabled?: boolean\n}",{"id":6474,"title":5367,"titles":6475,"content":6476,"level":748},"/features/ai#events-1",[208,5354,6466],"@update:modelValue: (value: string) => void\n@submit: () => void  // Emitted when user submits (Enter or click)",{"id":6478,"title":1608,"titles":6479,"content":6480,"level":748},"/features/ai#usage-2",[208,5354,6466],"\u003Ctemplate>\n  \u003CAIInput\n    v-model=\"input\"\n    :loading=\"isLoading\"\n    placeholder=\"Type your message...\"\n    @submit=\"handleSubmit\"\n  />\n\u003C/template>",{"id":6482,"title":6483,"titles":6484,"content":528,"level":391},"/features/ai#server-usage","Server Usage",[208],{"id":6486,"title":6487,"titles":6488,"content":6489,"level":449},"/features/ai#creating-ai-endpoints","Creating AI Endpoints",[208,6483],"The package provides a createAIProvider() factory for server-side AI operations.",{"id":6491,"title":6492,"titles":6493,"content":6494,"level":748},"/features/ai#basic-chat-endpoint","Basic Chat Endpoint",[208,6483,6487],"// server/api/ai/chat.post.ts\n// createAIProvider is auto-imported when extending the layer\nimport { streamText } from 'ai'\n\nexport default defineEventHandler(async (event) => {\n  const { messages, model } = await readBody(event)\n  const ai = createAIProvider(event)\n\n  const result = await streamText({\n    model: ai.model(model || 'gpt-4o'),\n    messages\n  })\n\n  return result.toDataStreamResponse()\n})",{"id":6496,"title":6497,"titles":6498,"content":6499,"level":748},"/features/ai#with-system-prompt","With System Prompt",[208,6483,6487],"// server/api/ai/chat.post.ts\n// createAIProvider is auto-imported when extending the layer\nimport { streamText } from 'ai'\n\nexport default defineEventHandler(async (event) => {\n  const { messages, model, systemPrompt } = await readBody(event)\n  const ai = createAIProvider(event)\n\n  // Prepend system message if provided\n  const allMessages = systemPrompt\n    ? [{ role: 'system', content: systemPrompt }, ...messages]\n    : messages\n\n  const result = await streamText({\n    model: ai.model(model || 'gpt-4o'),\n    messages: allMessages\n  })\n\n  return result.toDataStreamResponse()\n})",{"id":6501,"title":6502,"titles":6503,"content":6504,"level":748},"/features/ai#non-streaming-response","Non-Streaming Response",[208,6483,6487],"// server/api/ai/generate.post.ts\n// createAIProvider is auto-imported when extending the layer\nimport { generateText } from 'ai'\n\nexport default defineEventHandler(async (event) => {\n  const { prompt, model } = await readBody(event)\n  const ai = createAIProvider(event)\n\n  const result = await generateText({\n    model: ai.model(model || 'gpt-4o'),\n    prompt\n  })\n\n  return { text: result.text }\n})",{"id":6506,"title":6507,"titles":6508,"content":6509,"level":449},"/features/ai#provider-auto-detection","Provider Auto-Detection",[208,6483],"The ai.model() function automatically detects the provider from the model ID: const ai = createAIProvider(event)\n\n// OpenAI models\nai.model('gpt-4o')           // → Uses OpenAI\nai.model('gpt-4-turbo')      // → Uses OpenAI\nai.model('o1')               // → Uses OpenAI\nai.model('o1-mini')          // → Uses OpenAI\nai.model('o3-mini')          // → Uses OpenAI\n\n// Anthropic models\nai.model('claude-sonnet-4-20250514')    // → Uses Anthropic\nai.model('claude-opus-4-20250514')      // → Uses Anthropic\nai.model('claude-3-5-sonnet-20241022')  // → Uses Anthropic",{"id":6511,"title":6512,"titles":6513,"content":6514,"level":449},"/features/ai#accessing-providers-directly","Accessing Providers Directly",[208,6483],"For advanced use cases, access providers directly: const ai = createAIProvider(event)\n\n// Get OpenAI provider\nconst openai = ai.openai()\nconst gpt4 = openai('gpt-4o')\n\n// Get Anthropic provider\nconst anthropic = ai.anthropic()\nconst claude = anthropic('claude-sonnet-4-20250514')",{"id":6516,"title":6517,"titles":6518,"content":528,"level":391},"/features/ai#providers","Providers",[208],{"id":6520,"title":6521,"titles":6522,"content":6523,"level":449},"/features/ai#openai","OpenAI",[208,6517],"Supported Models: ModelDescriptiongpt-4oMost capable, great for complex tasksgpt-4o-miniFast and cost-effective for simpler tasksgpt-4-turboHigh capability with larger context windowo1Advanced reasoning model for complex problemso1-miniFast reasoning model Configuration: NUXT_OPENAI_API_KEY=sk-...",{"id":6525,"title":6526,"titles":6527,"content":6528,"level":449},"/features/ai#anthropic","Anthropic",[208,6517],"Supported Models: ModelDescriptionclaude-sonnet-4-20250514Balanced performance and speedclaude-opus-4-20250514Most capable Anthropic modelclaude-3-5-sonnet-20241022Previous generation, reliable performance Configuration: NUXT_ANTHROPIC_API_KEY=sk-ant-...",{"id":6530,"title":6531,"titles":6532,"content":6533,"level":391},"/features/ai#conversation-persistence","Conversation Persistence",[208],"The package includes a JSON schema for generating a chat conversations collection with the Crouton generator.",{"id":6535,"title":6536,"titles":6537,"content":6538,"level":449},"/features/ai#generate-chat-conversations-collection","Generate Chat Conversations Collection",[208,6531],"pnpm crouton generate ai chatConversations \\\n  --fields-file=node_modules/@fyit/crouton-ai/schemas/chat-conversations.json \\\n  --dialect=sqlite This creates a collection with these fields: FieldTypeDescriptiontitlestringConversation titlemessagesjsonArray of chat messagesproviderstringAI provider usedmodelstringModel identifiersystemPrompttextSystem prompt usedmetadatajsonAdditional metadatamessageCountnumberCached message countlastMessageAtdateLast message timestamp",{"id":6540,"title":6541,"titles":6542,"content":6543,"level":449},"/features/ai#saving-conversations","Saving Conversations",[208,6531],"\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\nconst teamId = route.params.team as string\n\nconst { messages, input, handleSubmit, exportMessages } = useChat({\n  model: 'gpt-4o',\n  onFinish: async (message) => {\n    // Auto-save after each response\n    await saveConversation()\n  }\n})\n\nconst conversationId = ref\u003Cstring | null>(null)\n\nconst saveConversation = async () => {\n  const payload = {\n    title: messages.value[0]?.content.slice(0, 50) || 'New Conversation',\n    messages: exportMessages(),\n    provider: 'openai',\n    model: 'gpt-4o',\n    messageCount: messages.value.length,\n    lastMessageAt: new Date()\n  }\n\n  if (conversationId.value) {\n    await $fetch(`/api/teams/${teamId}/chatConversations/${conversationId.value}`, {\n      method: 'PUT',\n      body: payload\n    })\n  } else {\n    const result = await $fetch(`/api/teams/${teamId}/chatConversations`, {\n      method: 'POST',\n      body: payload\n    })\n    conversationId.value = result.id\n  }\n}\n\u003C/script>",{"id":6545,"title":6546,"titles":6547,"content":6548,"level":449},"/features/ai#loading-conversations","Loading Conversations",[208,6531],"\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\nconst teamId = route.params.team as string\nconst conversationId = route.params.id as string\n\nconst { messages, importMessages } = useChat()\n\n// Load existing conversation\nconst { data: conversation } = await useFetch(\n  `/api/teams/${teamId}/chatConversations/${conversationId}`\n)\n\n// Restore messages\nif (conversation.value?.messages) {\n  importMessages(conversation.value.messages)\n}\n\u003C/script>",{"id":6550,"title":6551,"titles":6552,"content":6553,"level":391},"/features/ai#types","Types",[208],"All types are exported for use in your application: import type {\n  AIMessage,\n  AIProvider,\n  AIModel,\n  AIChatOptions,\n  AICompletionOptions,\n  AIChatboxProps,\n  AIMessageProps,\n  AIInputProps\n} from '@fyit/crouton-ai/types'",{"id":6555,"title":6453,"titles":6556,"content":6557,"level":449},"/features/ai#aimessage-1",[208,6551],"interface AIMessage {\n  /** Unique identifier for the message */\n  id: string\n  /** The role of the message sender */\n  role: 'user' | 'assistant' | 'system'\n  /** The content of the message */\n  content: string\n  /** When the message was created */\n  createdAt?: Date\n}",{"id":6559,"title":44,"titles":6560,"content":528,"level":391},"/features/ai#best-practices",[208],{"id":6562,"title":6563,"titles":6564,"content":6565,"level":449},"/features/ai#api-key-security","API Key Security",[208,44],"Never expose API keys on the client. API keys should only be used in server-side code. // nuxt.config.ts\nexport default defineNuxtConfig({\n  runtimeConfig: {\n    // Server-only (not exposed to client)\n    openaiApiKey: '',      // Set via NUXT_OPENAI_API_KEY\n    anthropicApiKey: '',   // Set via NUXT_ANTHROPIC_API_KEY\n\n    // Public (safe for client)\n    public: {\n      croutonAI: {\n        defaultProvider: 'openai',\n        defaultModel: 'gpt-4o'\n      }\n    }\n  }\n})",{"id":6567,"title":276,"titles":6568,"content":6569,"level":449},"/features/ai#rate-limiting",[208,44],"Implement rate limiting on your API endpoints: // server/api/ai/chat.post.ts\n// createAIProvider is auto-imported when extending the layer\nimport { streamText } from 'ai'\n\nexport default defineEventHandler(async (event) => {\n  // Get user/team from session\n  const user = await requireAuth(event)\n\n  // Check rate limit (implement your own logic)\n  const { allowed, remaining } = await checkRateLimit(user.id, 'ai-chat')\n  if (!allowed) {\n    throw createError({\n      status: 429,\n      statusText: 'Rate limit exceeded. Please try again later.'\n    })\n  }\n\n  const { messages, model } = await readBody(event)\n  const ai = createAIProvider(event)\n\n  const result = await streamText({\n    model: ai.model(model || 'gpt-4o'),\n    messages\n  })\n\n  return result.toDataStreamResponse()\n})",{"id":6571,"title":2522,"titles":6572,"content":6573,"level":449},"/features/ai#error-handling",[208,44],"Always handle errors gracefully: \u003Cscript setup lang=\"ts\">\nconst { messages, handleSubmit, error } = useChat({\n  onError: (err) => {\n    // Log for debugging\n    console.error('Chat error:', err)\n\n    // Show user-friendly message\n    toast.add({\n      title: 'Something went wrong',\n      description: 'Please try again or contact support if the problem persists.',\n      color: 'red'\n    })\n  }\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003C!-- Show error state -->\n    \u003CUAlert v-if=\"error\" color=\"red\" :title=\"error.message\" />\n\n    \u003C!-- Chat interface -->\n    \u003CAIChatbox />\n  \u003C/div>\n\u003C/template>",{"id":6575,"title":6576,"titles":6577,"content":6578,"level":449},"/features/ai#cost-optimization","Cost Optimization",[208,44],"Choose the right model for the task: // For simple tasks, use mini models\nconst { complete } = useCompletion({\n  model: 'gpt-4o-mini'  // Faster, cheaper\n})\n\n// For complex reasoning, use full models\nconst { messages, handleSubmit } = useChat({\n  model: 'gpt-4o'  // More capable\n})\n\n// For advanced reasoning, use o1 models\nconst { messages, handleSubmit } = useChat({\n  model: 'o1'  // Best reasoning, highest cost\n})",{"id":6580,"title":36,"titles":6581,"content":528,"level":391},"/features/ai#troubleshooting",[208],{"id":6583,"title":6584,"titles":6585,"content":6586,"level":449},"/features/ai#api-key-not-working","API Key Not Working",[208,36],"Check environment variable names: # Correct\nNUXT_OPENAI_API_KEY=sk-...\nNUXT_ANTHROPIC_API_KEY=sk-ant-...\n\n# Wrong (missing NUXT_ prefix)\nOPENAI_API_KEY=sk-... Verify key in runtime config: // server/api/debug.get.ts (development only!)\nexport default defineEventHandler((event) => {\n  const config = useRuntimeConfig()\n  return {\n    hasOpenAI: !!config.openaiApiKey,\n    hasAnthropic: !!config.anthropicApiKey\n  }\n})",{"id":6588,"title":6589,"titles":6590,"content":6591,"level":449},"/features/ai#streaming-not-working","Streaming Not Working",[208,36],"Ensure your API endpoint returns a data stream response: // Correct\nreturn result.toDataStreamResponse()\n\n// Wrong - returns object, not stream\nreturn { text: result.text }",{"id":6593,"title":6594,"titles":6595,"content":6596,"level":449},"/features/ai#messages-not-displaying","Messages Not Displaying",[208,36],"Check that your messages have the correct structure: // Correct\nconst message: AIMessage = {\n  id: 'unique-id',\n  role: 'user',  // or 'assistant' or 'system'\n  content: 'Hello!'\n}\n\n// Wrong - missing required fields\nconst message = {\n  text: 'Hello!'\n}",{"id":6598,"title":5569,"titles":6599,"content":6600,"level":449},"/features/ai#typescript-errors",[208,36],"Run typecheck after adding the package: npx nuxt typecheck Common issues: Missing type importsUsing old AI SDK typesIncorrect message structure",{"id":6602,"title":1007,"titles":6603,"content":6604,"level":391},"/features/ai#related-resources",[208],"Vercel AI SDK DocumentationOpenAI API DocumentationAnthropic API DocumentationCollection Generator - Generate chat conversations collectionBase Package - Core Crouton functionality",{"id":6606,"title":3685,"titles":6607,"content":6608,"level":391},"/features/ai#version-history",[208],"v0.1.0 (Current) Initial releaseuseChat, useCompletion, useAIProvider composablesAIChatbox, AIMessage, AIInput componentsServer-side createAIProvider factoryOpenAI and Anthropic provider supportChat conversations schema for persistenceTeam context integration html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":213,"title":212,"titles":6610,"content":6611,"level":385},[],"Three-tier admin system with user, team admin, and super admin dashboards Nuxt Crouton provides a three-tier admin architecture with clear separation of concerns: TierRoute PatternPurposeAccessUser/dashboard/[team]/*User-facing featuresAny team memberAdmin/admin/[team]/*Team managementTeam admins/ownersSuper Admin/super-admin/*System managementApp owner only Separate package: @fyit/crouton-admin is a separate package, but it is automatically included when you install the @fyit/crouton meta-package. You do not need to add it to your extends array manually.",{"id":6613,"title":13,"titles":6614,"content":6615,"level":391},"/features/admin#installation",[212],"The admin functionality is included automatically via the @fyit/crouton meta-package: pnpm add @fyit/crouton export default defineNuxtConfig({\n  extends: ['@fyit/crouton'],  // Includes auth, admin, i18n\n\n  croutonAuth: {\n    teams: {\n      allowCreate: true,\n      showSwitcher: true\n    }\n  }\n})",{"id":6617,"title":183,"titles":6618,"content":528,"level":391},"/features/admin#features",[212],{"id":6620,"title":6621,"titles":6622,"content":6623,"level":449},"/features/admin#dashboard-stats","Dashboard Stats",[212,183],"View key metrics at a glance: Total users and new signupsBanned users countTotal teams/organizationsActive sessionsSuper admin count",{"id":6625,"title":6626,"titles":6627,"content":6628,"level":449},"/features/admin#user-management","User Management",[212,183],"Full user lifecycle management: List users with search and filters (status, super admin)Create users with password and role assignmentBan users with reason and optional durationUnban users to restore accessDelete users permanently",{"id":6630,"title":6631,"titles":6632,"content":6633,"level":449},"/features/admin#team-oversight","Team Oversight",[212,183],"View and monitor all teams: List all teams with member countsView team details and membersMonitor team activity",{"id":6635,"title":6636,"titles":6637,"content":6638,"level":449},"/features/admin#user-impersonation","User Impersonation",[212,183],"Debug issues by becoming any user: Start impersonation sessionVisual banner showing impersonation statusStop impersonation to return to admin account",{"id":6640,"title":6641,"titles":6642,"content":528,"level":391},"/features/admin#route-architecture","Route Architecture",[212],{"id":6644,"title":6645,"titles":6646,"content":6647,"level":449},"/features/admin#user-tier-dashboardteam","User Tier (/dashboard/[team]/*)",[212,6641],"Routes for regular team members: PagePathPurposeTeam Selection/dashboardSelect which team to accessUser Dashboard/dashboard/[team]User home/overviewProfile/dashboard/[team]/profileMy profile settingsSettings/dashboard/[team]/settingsMy preferencesApp Routes/dashboard/[team]/{app}Auto-discovered from apps",{"id":6649,"title":6650,"titles":6651,"content":6652,"level":449},"/features/admin#admin-tier-adminteam","Admin Tier (/admin/[team]/*)",[212,6641],"Routes for team administrators: PagePathPurposeAdmin Dashboard/admin/[team]Team admin overviewCollections/admin/[team]/collectionsCRUD collection managementMembers/admin/[team]/team/Team member managementInvitations/admin/[team]/team/invitationsPending invitationsSettings/admin/[team]/team/settingsTeam settingsDomains/admin/[team]/team/domainsCustom domain managementLook & Feel/admin/[team]/team/look-and-feelBranding and appearanceApp Admin Routes/admin/[team]/{app}Auto-discovered from apps Protected by team-admin middleware.",{"id":6654,"title":6655,"titles":6656,"content":6657,"level":449},"/features/admin#super-admin-tier-super-admin","Super Admin Tier (/super-admin/*)",[212,6641],"Routes for the app owner (system-wide management): PagePathPurposeDashboard/super-adminStats overview and quick actionsUsers/super-admin/usersUser managementTeams/super-admin/teamsTeam oversight Protected by super-admin middleware.",{"id":6659,"title":6660,"titles":6661,"content":6662,"level":391},"/features/admin#middleware","Middleware",[212],"Two middleware options for different access levels:",{"id":6664,"title":6665,"titles":6666,"content":6667,"level":449},"/features/admin#team-admin","team-admin",[212,6660],"Requires user to be a team admin or owner. \u003Cscript setup lang=\"ts\">\ndefinePageMeta({\n  middleware: ['auth', 'team-admin']\n})\n\u003C/script>",{"id":6669,"title":6670,"titles":6671,"content":6672,"level":449},"/features/admin#super-admin","super-admin",[212,6660],"Requires user to be the app owner (super admin). Redirects non-admins to the home page. \u003Cscript setup lang=\"ts\">\ndefinePageMeta({\n  middleware: 'super-admin'\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>Custom super admin page content\u003C/div>\n\u003C/template>",{"id":6674,"title":6675,"titles":6676,"content":6677,"level":391},"/features/admin#app-auto-discovery","App Auto-Discovery",[212],"Apps can register their routes to appear automatically in sidebars using croutonApps:",{"id":6679,"title":6680,"titles":6681,"content":6682,"level":449},"/features/admin#registering-app-routes","Registering App Routes",[212,6675],"export default defineAppConfig({\n  croutonApps: {\n    bookings: {\n      id: 'bookings',\n      name: 'Bookings',\n      icon: 'i-lucide-calendar',\n\n      // User-facing routes (appear in /dashboard/[team]/ sidebar)\n      dashboardRoutes: [\n        {\n          path: 'bookings',\n          label: 'bookings.myBookings.title',  // Translation key\n          icon: 'i-lucide-calendar'\n        }\n      ],\n\n      // Admin routes (appear in /admin/[team]/ sidebar)\n      adminRoutes: [\n        {\n          path: 'bookings',\n          label: 'bookings.admin.title',\n          icon: 'i-lucide-calendar'\n        }\n      ],\n\n      // Settings pages (appear in /admin/[team]/settings/)\n      settingsRoutes: [\n        {\n          path: 'email-templates',\n          label: 'bookings.settings.emailTemplates',\n          icon: 'i-lucide-mail'\n        }\n      ]\n    }\n  }\n})",{"id":6684,"title":6685,"titles":6686,"content":6687,"level":449},"/features/admin#using-usecroutonapps","Using useCroutonApps",[212,6675],"const {\n  apps,              // All registered apps\n  dashboardRoutes,   // All user-facing routes\n  adminRoutes,       // All admin routes\n  settingsRoutes,    // All settings routes\n  getApp,            // Get app by ID\n  hasApp             // Check if app exists\n} = useCroutonApps()\n\n// Build navigation\nconst navItems = dashboardRoutes.value.map(route => ({\n  label: t(route.label),\n  icon: route.icon,\n  to: `/dashboard/${teamSlug}/${route.path}`\n}))",{"id":6689,"title":6690,"titles":6691,"content":6692,"level":449},"/features/admin#route-types","Route Types",[212,6675],"interface CroutonAppRoute {\n  path: string          // Route path segment\n  label: string         // Translation key\n  icon?: string         // Heroicon name\n  hidden?: boolean      // Hide from sidebar\n  children?: CroutonAppRoute[]\n}\n\ninterface CroutonAppConfig {\n  id: string\n  name: string\n  icon?: string\n  dashboardRoutes?: CroutonAppRoute[]\n  adminRoutes?: CroutonAppRoute[]\n  settingsRoutes?: CroutonAppRoute[]\n}",{"id":6694,"title":5437,"titles":6695,"content":528,"level":391},"/features/admin#composables",[212],{"id":6697,"title":6698,"titles":6699,"content":6700,"level":449},"/features/admin#useadminusers","useAdminUsers()",[212,5437],"User management operations. const {\n  users,           // List of users\n  total,           // Total count\n  page,            // Current page\n  loading,         // Loading state\n  error,           // Error message\n\n  getUsers,        // Fetch users with filters\n  getUser,         // Get user detail\n  createUser,      // Create new user\n  banUser,         // Ban a user\n  unbanUser,       // Unban a user\n  deleteUser       // Delete user permanently\n} = useAdminUsers()\n\n// Fetch users with filters\nawait getUsers({\n  page: 1,\n  pageSize: 20,\n  search: 'john',\n  status: 'active',     // 'all' | 'active' | 'banned'\n  superAdmin: false,\n  sortBy: 'createdAt',\n  sortOrder: 'desc'\n})\n\n// Ban a user\nawait banUser('user-id', {\n  reason: 'Spam activity',\n  duration: 168  // hours (null = permanent)\n})",{"id":6702,"title":6703,"titles":6704,"content":6705,"level":449},"/features/admin#useadminteams","useAdminTeams()",[212,5437],"Team management operations. const {\n  teams,           // List of teams\n  total,           // Total count\n  loading,         // Loading state\n  error,           // Error message\n\n  getTeams,        // Fetch teams with filters\n  getTeam          // Get team detail with members\n} = useAdminTeams()\n\n// Fetch teams\nawait getTeams({\n  page: 1,\n  pageSize: 20,\n  search: 'acme',\n  personal: false,\n  sortBy: 'memberCount',\n  sortOrder: 'desc'\n})",{"id":6707,"title":6708,"titles":6709,"content":6710,"level":449},"/features/admin#useadminstats","useAdminStats()",[212,5437],"Dashboard statistics with optional auto-refresh. const {\n  stats,           // AdminStats object\n  loading,         // Loading state\n  error,           // Error message\n\n  refresh,         // Fetch/refresh stats\n  startAutoRefresh,// Start auto-refresh\n  stopAutoRefresh  // Stop auto-refresh\n} = useAdminStats({ autoRefresh: true, refreshInterval: 30000 })\n\n// Stats include:\n// - totalUsers, newUsersToday, newUsersWeek\n// - bannedUsers, superAdminCount\n// - totalTeams, newTeamsWeek\n// - activeSessions",{"id":6712,"title":6713,"titles":6714,"content":6715,"level":449},"/features/admin#useimpersonation","useImpersonation()",[212,5437],"User impersonation for debugging. const {\n  isImpersonating,     // Boolean: currently impersonating\n  impersonatedUser,    // User being impersonated\n  originalAdminId,     // Original admin's ID\n  loading,             // Loading state\n\n  startImpersonation,  // Start impersonating a user\n  stopImpersonation,   // Return to admin account\n  checkStatus          // Check current status\n} = useImpersonation()\n\n// Start impersonating\nawait startImpersonation('user-id')\n\n// Stop impersonating\nawait stopImpersonation()",{"id":6717,"title":5354,"titles":6718,"content":528,"level":391},"/features/admin#components",[212],{"id":6720,"title":6721,"titles":6722,"content":6723,"level":449},"/features/admin#dashboard-components","Dashboard Components",[212,5354],"ComponentPurposeAdminDashboardFull dashboard with stats and quick actionsAdminStatsCardIndividual stat card with icon and trend \u003Ctemplate>\n  \u003CAdminDashboard\n    :stats=\"preloadedStats\"\n    :show-quick-actions=\"true\"\n    @navigate=\"handleNavigation\"\n  />\n\u003C/template>",{"id":6725,"title":6726,"titles":6727,"content":6728,"level":449},"/features/admin#user-components","User Components",[212,5354],"ComponentPurposeAdminUserListPaginated user table with search and filtersAdminUserActionsDropdown menu for user actionsAdminUserBanFormForm for banning with reason and durationAdminUserCreateFormForm for creating new users \u003Ctemplate>\n  \u003CAdminUserList\n    :page-size=\"20\"\n    @user-selected=\"handleSelect\"\n  />\n\u003C/template>",{"id":6730,"title":6731,"titles":6732,"content":6733,"level":449},"/features/admin#global-components","Global Components",[212,5354],"ComponentPurposeImpersonationBannerTop banner when impersonating a user Add to your app layout to show impersonation status: \u003Ctemplate>\n  \u003Cdiv>\n    \u003CImpersonationBanner />\n    \u003Cslot />\n  \u003C/div>\n\u003C/template>",{"id":6735,"title":6736,"titles":6737,"content":528,"level":391},"/features/admin#api-endpoints","API Endpoints",[212],{"id":6739,"title":6740,"titles":6741,"content":6742,"level":449},"/features/admin#users","Users",[212,6736],"MethodEndpointPurposeGET/api/admin/usersList users with paginationGET/api/admin/users/[id]Get user detailPOST/api/admin/users/createCreate new userPOST/api/admin/users/banBan a userPOST/api/admin/users/unbanUnban a userDELETE/api/admin/users/deleteDelete a user",{"id":6744,"title":6745,"titles":6746,"content":6747,"level":449},"/features/admin#teams","Teams",[212,6736],"MethodEndpointPurposeGET/api/admin/teamsList teamsGET/api/admin/teams/[id]Get team with members",{"id":6749,"title":6750,"titles":6751,"content":6752,"level":449},"/features/admin#stats","Stats",[212,6736],"MethodEndpointPurposeGET/api/admin/statsGet dashboard statistics",{"id":6754,"title":6755,"titles":6756,"content":6757,"level":449},"/features/admin#impersonation","Impersonation",[212,6736],"MethodEndpointPurposePOST/api/admin/impersonate/startStart impersonationPOST/api/admin/impersonate/stopStop impersonationGET/api/admin/impersonate/statusCheck impersonation status",{"id":6759,"title":292,"titles":6760,"content":528,"level":391},"/features/admin#server-utilities",[212],{"id":6762,"title":6763,"titles":6764,"content":6765,"level":449},"/features/admin#requiresuperadminevent","requireSuperAdmin(event)",[212,292],"Server-side authorization check. Use in custom admin endpoints. export default defineEventHandler(async (event) => {\n  // Auto-imported server utility — no import needed\n  // Throws 403 if not super admin\n  const { user: adminUser } = await requireSuperAdmin(event)\n\n  // Your admin logic here\n  return { admin: adminUser.name }\n})",{"id":6767,"title":6768,"titles":6769,"content":6770,"level":391},"/features/admin#making-a-user-super-admin","Making a User Super Admin",[212],"Super admin privileges are stored in the superAdmin field on the user table.",{"id":6772,"title":6773,"titles":6774,"content":6775,"level":449},"/features/admin#via-database-migration","Via Database Migration",[212,6768],"import { db } from '@fyit/crouton-auth'\nimport { user } from '@fyit/crouton-auth/server/database/schema/auth'\nimport { eq } from 'drizzle-orm'\n\nawait db.update(user)\n  .set({ superAdmin: true })\n  .where(eq(user.email, 'admin@example.com'))",{"id":6777,"title":6778,"titles":6779,"content":6780,"level":449},"/features/admin#via-seed-script","Via Seed Script",[212,6768],"import { db } from '@fyit/crouton-auth'\nimport { user } from '@fyit/crouton-auth/server/database/schema/auth'\n\nawait db.insert(user).values({\n  id: 'admin-id',\n  email: 'admin@example.com',\n  name: 'Admin User',\n  emailVerified: true,\n  superAdmin: true\n})",{"id":6782,"title":1789,"titles":6783,"content":6784,"level":391},"/features/admin#configuration",[212],"export default defineNuxtConfig({\n  runtimeConfig: {\n    public: {\n      crouton: {\n        admin: {\n          // Super admin route prefix (default: /super-admin)\n          routePrefix: '/super-admin',\n\n          // Enable impersonation feature\n          impersonation: true,\n\n          // Stats auto-refresh interval (ms)\n          statsRefreshInterval: 30000\n        }\n      }\n    }\n  }\n})",{"id":6786,"title":6787,"titles":6788,"content":6789,"level":391},"/features/admin#ban-durations","Ban Durations",[212],"Pre-defined ban duration options: DurationHours1 hour124 hours247 days16830 days72090 days2160Permanentnull",{"id":6791,"title":1383,"titles":6792,"content":528,"level":391},"/features/admin#customization",[212],{"id":6794,"title":6795,"titles":6796,"content":6797,"level":449},"/features/admin#override-pages","Override Pages",[212,1383],"Create your own page at the same path to override: \u003Cscript setup lang=\"ts\">\ndefinePageMeta({ middleware: 'super-admin' })\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>My Custom Super Admin Dashboard\u003C/h1>\n    \u003C!-- Your custom content -->\n  \u003C/div>\n\u003C/template>",{"id":6799,"title":6800,"titles":6801,"content":6802,"level":449},"/features/admin#override-impersonation-banner","Override Impersonation Banner",[212,1383],"Create your own ImpersonationBanner.vue: \u003Cscript setup lang=\"ts\">\nconst { isImpersonating, impersonatedUser, stopImpersonation } = useImpersonation()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"isImpersonating\" class=\"my-custom-banner\">\n    Impersonating: {{ impersonatedUser?.name }}\n    \u003Cbutton @click=\"stopImpersonation\">Stop\u003C/button>\n  \u003C/div>\n\u003C/template>",{"id":6804,"title":6551,"titles":6805,"content":528,"level":391},"/features/admin#types",[212],{"id":6807,"title":6808,"titles":6809,"content":6810,"level":449},"/features/admin#adminstats","AdminStats",[212,6551],"interface AdminStats {\n  totalUsers: number\n  newUsersToday: number\n  newUsersWeek: number\n  bannedUsers: number\n  totalTeams: number\n  newTeamsWeek: number\n  activeSessions: number\n  superAdminCount: number\n}",{"id":6812,"title":6813,"titles":6814,"content":6815,"level":449},"/features/admin#banpayload","BanPayload",[212,6551],"interface BanPayload {\n  userId: string\n  reason: string\n  duration: number | null  // hours, null = permanent\n}",{"id":6817,"title":6818,"titles":6819,"content":6820,"level":449},"/features/admin#impersonationstate","ImpersonationState",[212,6551],"interface ImpersonationState {\n  isImpersonating: boolean\n  originalAdminId: string | null\n  impersonatedUser: {\n    id: string\n    name: string\n    email: string\n  } | null\n}",{"id":6822,"title":6823,"titles":6824,"content":6825,"level":391},"/features/admin#security-considerations","Security Considerations",[212],"Super admin access is required for all admin endpointsCannot ban other super admins - protects against admin lockoutCannot ban yourself - prevents self-lockoutImpersonation is logged - original admin ID stored in sessionSessions are invalidated when a user is banned",{"id":6827,"title":4341,"titles":6828,"content":6829,"level":391},"/features/admin#related",[212],"Team-Based Auth - Authentication with teamsPackage Architecture - Understanding the ecosystem html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":217,"title":216,"titles":6831,"content":6832,"level":385},[],"Export collection data to CSV and JSON formats for reporting, backups, and data migration.",{"id":6834,"title":216,"titles":6835,"content":6836,"level":385},"/features/export#data-export",[],"Export collection data to CSV and JSON formats for reporting, backups, and data migration. Status: Stable",{"id":6838,"title":936,"titles":6839,"content":6840,"level":391},"/features/export#overview",[216],"The export feature provides a composable and a ready-to-use button component for exporting collection data. It works with useCollectionQuery for client-side data or can fetch directly from the API for server-filtered exports. Features: CSV and JSON export formatsCustom field selection and labelingField transformationsRow transformationsMetadata and ID exclusionServer-side query exportsi18n support for labels",{"id":6842,"title":18,"titles":6843,"content":528,"level":391},"/features/export#quick-start",[216],{"id":6845,"title":6846,"titles":6847,"content":6848,"level":449},"/features/export#basic-export-with-component","Basic Export with Component",[216,18],"\u003Cscript setup lang=\"ts\">\nconst { items } = await useCollectionQuery('products')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonExportButton\n    collection=\"products\"\n    :rows=\"items\"\n  />\n\u003C/template>",{"id":6850,"title":6851,"titles":6852,"content":6853,"level":449},"/features/export#basic-export-with-composable","Basic Export with Composable",[216,18],"\u003Cscript setup lang=\"ts\">\nconst { items } = await useCollectionQuery('products')\nconst { exportCSV, exportJSON } = useCollectionExport('products')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"flex gap-2\">\n    \u003CUButton @click=\"exportCSV(items)\">Export CSV\u003C/UButton>\n    \u003CUButton @click=\"exportJSON(items)\">Export JSON\u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":6855,"title":6856,"titles":6857,"content":528,"level":391},"/features/export#composable-api","Composable API",[216],{"id":6859,"title":6860,"titles":6861,"content":6862,"level":449},"/features/export#usecollectionexportcollection","useCollectionExport(collection)",[216,6856],"interface UseCollectionExportReturn {\n  exportCSV: (rows: any[], options?: ExportOptions) => void\n  exportJSON: (rows: any[], options?: ExportOptions) => void\n  exportWithQuery: (\n    format: 'csv' | 'json',\n    query?: Record\u003Cstring, any>,\n    options?: ExportOptions\n  ) => Promise\u003Cvoid>\n  isExporting: Ref\u003Cboolean>\n}",{"id":6864,"title":6865,"titles":6866,"content":6867,"level":449},"/features/export#export-options","Export Options",[216,6856],"interface ExportOptions {\n  // Field selection\n  fields?: (string | ExportField)[]\n  excludeFields?: string[]\n\n  // What to include\n  includeId?: boolean        // default: false\n  includeMetadata?: boolean  // default: false (createdAt, updatedAt, etc.)\n\n  // Transformations\n  transformRow?: (row: any) => any\n\n  // Output\n  filename?: string          // default: collection name\n  dateFormat?: 'iso' | 'locale' | 'timestamp'\n  flattenNested?: boolean    // Stringify nested objects\n}\n\ninterface ExportField {\n  key: string\n  label?: string\n  transform?: (value: any, row: any) => any\n}",{"id":6869,"title":3367,"titles":6870,"content":528,"level":391},"/features/export#examples",[216],{"id":6872,"title":6873,"titles":6874,"content":6875,"level":449},"/features/export#custom-fields-and-labels","Custom Fields and Labels",[216,3367],"const { exportCSV } = useCollectionExport('products')\n\nexportCSV(items.value, {\n  fields: [\n    { key: 'name', label: 'Product Name' },\n    { key: 'price', label: 'Price ($)', transform: (v) => v.toFixed(2) },\n    { key: 'category', label: 'Category' }\n  ],\n  filename: 'product-report'\n})",{"id":6877,"title":6878,"titles":6879,"content":6880,"level":449},"/features/export#flatten-nested-data","Flatten Nested Data",[216,3367],"const { exportCSV } = useCollectionExport('orders')\n\nexportCSV(orders.value, {\n  transformRow: (row) => ({\n    ...row,\n    customerName: row.customer?.name,\n    customerEmail: row.customer?.email,\n    items: row.items?.map(i => i.name).join(', ')\n  }),\n  excludeFields: ['customer']  // Exclude original nested object\n})",{"id":6882,"title":6883,"titles":6884,"content":6885,"level":449},"/features/export#export-with-current-filters","Export with Current Filters",[216,3367],"const searchQuery = ref('')\nconst statusFilter = ref('active')\n\nconst query = computed(() => ({\n  search: searchQuery.value,\n  status: statusFilter.value\n}))\n\nconst { items } = await useCollectionQuery('products', { query })\nconst { exportWithQuery, isExporting } = useCollectionExport('products')\n\n// Export ALL data matching filters (not just current page)\nasync function handleExport() {\n  await exportWithQuery('csv', query.value, {\n    filename: `products-${statusFilter.value}`\n  })\n}",{"id":6887,"title":6888,"titles":6889,"content":6890,"level":449},"/features/export#include-metadata","Include Metadata",[216,3367],"const { exportCSV } = useCollectionExport('products')\n\nexportCSV(items.value, {\n  includeId: true,\n  includeMetadata: true,  // Includes createdAt, updatedAt, etc.\n  dateFormat: 'locale'    // Format dates for local timezone\n})",{"id":6892,"title":6893,"titles":6894,"content":528,"level":391},"/features/export#component-api","Component API",[216],{"id":6896,"title":6897,"titles":6898,"content":6899,"level":449},"/features/export#croutonexportbutton","CroutonExportButton",[216,6893],"\u003CCroutonExportButton\n  collection=\"products\"\n  :rows=\"items\"\n  :formats=\"['csv', 'json']\"\n  :options=\"{ excludeFields: ['internalCode'] }\"\n  variant=\"ghost\"\n  size=\"sm\"\n  @export=\"handleExport\"\n  @error=\"handleError\"\n/>",{"id":6901,"title":4987,"titles":6902,"content":6903,"level":748},"/features/export#props",[216,6893,6897],"PropTypeDefaultDescriptioncollectionstringrequiredCollection namerowsany[]requiredData to exportformats('csv' | 'json')[]['csv', 'json']Available formatsoptionsExportOptions{}Export optionsvariantstring'ghost'Button variantsizestring'sm'Button sizecolorstring-Button colordisabledbooleanfalseDisable button",{"id":6905,"title":5367,"titles":6906,"content":6907,"level":748},"/features/export#events",[216,6893,6897],"EventPayloadDescriptionexport[format, rows]Fired on successful exporterror[Error]Fired on export error",{"id":6909,"title":5372,"titles":6910,"content":6911,"level":748},"/features/export#slots",[216,6893,6897],"\u003CCroutonExportButton collection=\"products\" :rows=\"items\">\n  Download Report\n\u003C/CroutonExportButton>",{"id":6913,"title":5249,"titles":6914,"content":528,"level":391},"/features/export#integration-examples",[216],{"id":6916,"title":6917,"titles":6918,"content":6919,"level":449},"/features/export#table-header-with-export","Table Header with Export",[216,5249],"\u003Cscript setup lang=\"ts\">\nconst { items } = await useCollectionQuery('products')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection :rows=\"items\" collection=\"products\">\n    \u003Ctemplate #header>\n      \u003Cdiv class=\"flex justify-between items-center p-4\">\n        \u003Ch2 class=\"text-lg font-semibold\">Products\u003C/h2>\n        \u003Cdiv class=\"flex gap-2\">\n          \u003CCroutonExportButton\n            collection=\"products\"\n            :rows=\"items\"\n            :options=\"{ excludeFields: ['teamId'] }\"\n          />\n          \u003CUButton @click=\"open('create', 'products')\">\n            Add Product\n          \u003C/UButton>\n        \u003C/div>\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonCollection>\n\u003C/template>",{"id":6921,"title":6922,"titles":6923,"content":6924,"level":449},"/features/export#export-selected-rows","Export Selected Rows",[216,5249],"\u003Cscript setup lang=\"ts\">\nconst { items } = await useCollectionQuery('products')\nconst { exportCSV } = useCollectionExport('products')\n\nconst selectedRows = ref\u003Cstring[]>([])\n\nfunction exportSelected() {\n  const selected = items.value.filter(item =>\n    selectedRows.value.includes(item.id)\n  )\n  exportCSV(selected, { filename: 'selected-products' })\n}\n\u003C/script>",{"id":6926,"title":6927,"titles":6928,"content":6929,"level":391},"/features/export#default-exclusions","Default Exclusions",[216],"The following fields are excluded by default: FieldReasonOverrideteamIdInternal identifierN/A (always excluded)idOften not needed in exportsincludeId: truecreatedAtMetadataincludeMetadata: trueupdatedAtMetadataincludeMetadata: truecreatedByMetadataincludeMetadata: trueupdatedByMetadataincludeMetadata: true",{"id":6931,"title":6932,"titles":6933,"content":6934,"level":391},"/features/export#csv-handling","CSV Handling",[216],"The export handles special characters properly: Commas: Values containing commas are quotedQuotes: Double quotes are escaped as \"\"Newlines: Multi-line values are quotedUnicode: UTF-8 BOM is added for Excel compatibility",{"id":6936,"title":6937,"titles":6938,"content":6939,"level":391},"/features/export#i18n-support","i18n Support",[216],"The component uses translation keys for labels: KeyDefaultexport.buttonExportexport.csvExport as CSVexport.jsonExport as JSONexport.noDataNo data to exportexport.successExport completedexport.errorExport failed",{"id":6941,"title":6942,"titles":6943,"content":6944,"level":391},"/features/export#limitations","Limitations",[216],"Maximum 10,000 rows for exportWithQuery (configurable server-side)Large exports may cause browser memory pressureNo Excel (XLSX) format (coming in future release)",{"id":6946,"title":4341,"titles":6947,"content":6948,"level":391},"/features/export#related",[216],"useCollectionQuery - Data fetchingCollections - Understanding collections html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":221,"title":220,"titles":6950,"content":6951,"level":385},[],"Enable multiple users to edit content simultaneously with live presence Status: Stable - Complete and ready for production use Add real-time collaboration to your Nuxt Crouton applications with the @fyit/crouton-collab package. Built on Yjs CRDTs for conflict-free synchronization, it supports rich text editing, flow graphs, and any collaborative data structure.",{"id":6953,"title":18,"titles":6954,"content":528,"level":391},"/features/collaboration#quick-start",[220],{"id":6956,"title":13,"titles":6957,"content":6958,"level":449},"/features/collaboration#installation",[220,18],"pnpm add @fyit/crouton-collab",{"id":6960,"title":1789,"titles":6961,"content":6962,"level":449},"/features/collaboration#configuration",[220,18],"Add the layer to your nuxt.config.ts: export default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-collab'\n  ]\n})",{"id":6964,"title":4173,"titles":6965,"content":6966,"level":449},"/features/collaboration#basic-usage",[220,18],"\u003Cscript setup>\n// For rich text editors (pages)\nconst { ydoc, yxmlFragment, connected, users } = useCollabEditor({\n  roomId: 'page-123',\n  roomType: 'page'\n})\n\n// For flow graphs\nconst { ymap, data, connected, users } = useCollabSync({\n  roomId: 'flow-123',\n  roomType: 'flow',\n  structure: 'map'\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003CCollabIndicator\n      :connected=\"connected\"\n      :synced=\"true\"\n      :users=\"users\"\n    />\n    \u003C!-- Your editor content -->\n  \u003C/div>\n\u003C/template>",{"id":6968,"title":5437,"titles":6969,"content":528,"level":391},"/features/collaboration#composables",[220],{"id":6971,"title":6972,"titles":6973,"content":6974,"level":449},"/features/collaboration#usecollabeditor","useCollabEditor",[220,5437],"Ready-to-use setup for TipTap collaborative editing. const {\n  // Connection state\n  connected,      // Whether WebSocket is connected\n  synced,         // Whether initial sync is complete\n  error,          // Connection error, if any\n\n  // Yjs for TipTap\n  ydoc,           // Y.Doc instance\n  yxmlFragment,   // Y.XmlFragment for content\n\n  // Presence\n  user,           // Current user\n  users,          // All users in room\n  otherUsers,     // Other users (excluding self)\n\n  // For TipTap extensions\n  provider,       // { awareness: { setLocalStateField, ... } }\n\n  // Actions\n  connect,        // Manually connect\n  disconnect,     // Manually disconnect\n  updateCursor,   // Update cursor position\n  updateSelection // Update text selection\n} = useCollabEditor({\n  roomId: 'page-123',\n  roomType: 'page',        // default\n  field: 'content',        // default\n  user: { name: 'Alice', color: '#ff0000' }\n}) TipTap Integration: import { Collaboration } from '@tiptap/extension-collaboration'\nimport { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'\n\nconst editor = useEditor({\n  extensions: [\n    Collaboration.configure({\n      document: ydoc,\n      field: 'content'\n    }),\n    CollaborationCursor.configure({\n      provider,\n      user: { name: 'Alice', color: '#ff0000' }\n    })\n  ]\n})",{"id":6976,"title":6977,"titles":6978,"content":6979,"level":449},"/features/collaboration#usecollabsync","useCollabSync",[220,5437],"General-purpose Yjs synchronization for any data structure. const {\n  // Connection state\n  connected, synced, error,\n\n  // Yjs structures (one populated based on structure option)\n  ymap,           // Y.Map | null\n  yarray,         // Y.Array | null\n  yxmlFragment,   // Y.XmlFragment | null\n  ytext,          // Y.Text | null\n\n  // Reactive data\n  data,           // Ref\u003CRecord\u003Cstring, unknown>> - for Y.Map\n  arrayData,      // Ref\u003Cunknown[]> - for Y.Array\n\n  // Users in room\n  users,\n\n  // Actions\n  connect, disconnect\n} = useCollabSync({\n  roomId: 'room-123',\n  roomType: 'flow',\n  structure: 'map',       // 'map' | 'array' | 'xmlFragment' | 'text'\n  structureName: 'nodes'  // optional, defaults to roomType\n})",{"id":6981,"title":6982,"titles":6983,"content":6984,"level":449},"/features/collaboration#usecollabpresence","useCollabPresence",[220,5437],"Track cursor positions, selections, and presence. const {\n  user,               // Current user\n  users,              // All users\n  otherUsers,         // Other users (excluding self)\n\n  // Actions\n  updateCursor,       // (cursor) => void\n  updateSelection,    // (selection) => void\n  selectNode,         // (nodeId) => void\n  updateGhostNode,    // (ghostNode) => void\n\n  // Utilities\n  getUsersSelectingNode,  // (nodeId) => CollabAwarenessState[]\n  getUserColor,           // (userId) => string\n  getNodePresenceStyle    // (nodeId) => { boxShadow?, borderColor? }\n} = useCollabPresence({\n  connection,         // from useCollabConnection\n  user: { name: 'Alice' }\n})",{"id":6986,"title":6987,"titles":6988,"content":6989,"level":449},"/features/collaboration#usecollabroomusers","useCollabRoomUsers",[220,5437],"Poll room users via HTTP (for global presence in lists). const {\n  users,          // All users in room\n  otherUsers,     // Excluding current user\n  count,          // Total count\n  otherCount,     // Other users count\n  loading,\n  error,\n  isPolling,\n\n  // Actions\n  refresh,        // Manual refresh\n  startPolling,   // Start polling\n  stopPolling     // Stop polling\n} = useCollabRoomUsers({\n  roomId: 'page-123',\n  roomType: 'page',\n  pollInterval: 5000,     // default: 5 seconds\n  currentUserId: user?.id,\n  excludeSelf: true,      // default\n  immediate: true         // default: start on mount\n})",{"id":6991,"title":5354,"titles":6992,"content":528,"level":391},"/features/collaboration#components",[220],{"id":6994,"title":6196,"titles":6995,"content":6996,"level":449},"/features/collaboration#collabstatus",[220,5354],"Connection status indicator with colored dot. \u003CCollabStatus\n  :connected=\"connected\"\n  :synced=\"synced\"\n  :error=\"error\"\n  :show-label=\"true\"\n/> PropTypeDefaultDescriptionconnectedbooleanrequiredWebSocket connectedsyncedbooleanrequiredInitial sync completeerrorError | nullnullConnection errorshowLabelbooleantrueShow text label Status Colors: 🟢 Green: synced and ready🟡 Yellow (pulsing): connecting or syncing🔴 Red: error⚪ Gray: disconnected",{"id":6998,"title":6187,"titles":6999,"content":7000,"level":449},"/features/collaboration#collabpresence",[220,5354],"Stacked user avatars with overflow indicator. \u003CCollabPresence\n  :users=\"otherUsers\"\n  :max-visible=\"5\"\n  size=\"sm\"\n  :show-tooltip=\"true\"\n/> PropTypeDefaultDescriptionusersCollabAwarenessState[]requiredUsers to displaymaxVisiblenumber5Max avatars before +Nsize'xs' | 'sm' | 'md''sm'Avatar sizeshowTooltipbooleantrueShow name on hover",{"id":7002,"title":7003,"titles":7004,"content":7005,"level":449},"/features/collaboration#collabindicator","CollabIndicator",[220,5354],"Combined status + presence for toolbars. \u003CCollabIndicator\n  :connected=\"connected\"\n  :synced=\"synced\"\n  :error=\"error\"\n  :users=\"otherUsers\"\n  :max-visible-users=\"3\"\n/>",{"id":7007,"title":7008,"titles":7009,"content":7010,"level":449},"/features/collaboration#collabeditingbadge","CollabEditingBadge",[220,5354],"Shows \"X editing\" badge on collection list items. \u003CCollabEditingBadge\n  room-id=\"page-123\"\n  room-type=\"page\"\n  :current-user-id=\"currentUser?.id\"\n  :poll-interval=\"5000\"\n  size=\"xs\"\n  :show-avatars=\"true\"\n/> PropTypeDefaultDescriptionroomIdstringrequiredRoom ID to checkroomTypestring'page'Room typecurrentUserIdstring-Exclude self from countpollIntervalnumber5000Poll interval in mssize'xs' | 'sm' | 'md''xs'Badge sizeshowAvatarsbooleantrueShow avatars on hover",{"id":7012,"title":7013,"titles":7014,"content":7015,"level":449},"/features/collaboration#collabcursors","CollabCursors",[220,5354],"Remote cursor overlay for canvas/editor content. \u003Cdiv class=\"relative\">\n  \u003CCollabCursors\n    :users=\"otherUsers\"\n    :show-labels=\"true\"\n    :offset-x=\"0\"\n    :offset-y=\"0\"\n  />\n  \u003C!-- Your content here -->\n\u003C/div>",{"id":7017,"title":7018,"titles":7019,"content":7020,"level":391},"/features/collaboration#global-presence-in-lists","Global Presence in Lists",[220],"Show \"X people editing\" badges in collection lists: \u003CCroutonCollection\n  collection=\"pages\"\n  :rows=\"pages\"\n  :show-collab-presence=\"true\"\n/>\n\n\u003C!-- With custom configuration -->\n\u003CCroutonCollection\n  collection=\"pages\"\n  :rows=\"pages\"\n  :show-collab-presence=\"{\n    roomType: 'page',\n    currentUserId: currentUser?.id,\n    pollInterval: 10000,\n    getRoomId: (row, collection) => `${collection}-${row.id}`\n  }\"\n/>",{"id":7022,"title":85,"titles":7023,"content":7024,"level":391},"/features/collaboration#architecture",[220],"The collaboration package uses a layered architecture: ┌─────────────────────────────────────────────────────┐\n│                     Clients                          │\n│  ┌─────────┐  ┌─────────┐  ┌─────────┐              │\n│  │ User A  │  │ User B  │  │ User C  │              │\n│  └────┬────┘  └────┬────┘  └────┬────┘              │\n│       └────────────┼────────────┘                    │\n│                    │ WebSocket                       │\n│                    ▼                                 │\n│  ┌───────────────────────────────────────────────┐  │\n│  │           /api/collab/[roomId]/ws             │  │\n│  │           ?type=page|flow|document            │  │\n│  └───────────────────────────────────────────────┘  │\n│                    │                                 │\n│                    ▼                                 │\n│  ┌───────────────────────────────────────────────┐  │\n│  │              CollabRoom DO                    │  │\n│  │  ┌─────────┐  ┌──────────┐  ┌──────────────┐ │  │\n│  │  │  Y.Doc  │  │ Sessions │  │  Awareness   │ │  │\n│  │  │ (CRDT)  │  │(WebSockets)│  │(Presence)  │ │  │\n│  │  └────┬────┘  └──────────┘  └──────────────┘ │  │\n│  └───────┼───────────────────────────────────────┘  │\n│          │                                           │\n│          ▼                                           │\n│  ┌───────────────────────────────────────────────┐  │\n│  │              D1: yjs_collab_states            │  │\n│  │  room_type │ room_id │ state │ version        │  │\n│  └───────────────────────────────────────────────┘  │\n└─────────────────────────────────────────────────────┘",{"id":7026,"title":7027,"titles":7028,"content":7029,"level":449},"/features/collaboration#room-types","Room Types",[220,85],"TypeUse CaseYjs StructurepageTipTap editor contentY.XmlFragmentflowNode graphsY.MapdocumentPlain textY.TextgenericCustomAny",{"id":7031,"title":7032,"titles":7033,"content":528,"level":391},"/features/collaboration#cloudflare-configuration","Cloudflare Configuration",[220],{"id":7035,"title":7036,"titles":7037,"content":7038,"level":449},"/features/collaboration#wranglertoml","wrangler.toml",[220,7032],"[[durable_objects.bindings]]\nname = \"COLLAB_ROOMS\"\nclass_name = \"CollabRoom\"\n\n[[migrations]]\ntag = \"collab-v1\"\nnew_classes = [\"CollabRoom\"]",{"id":7040,"title":7041,"titles":7042,"content":7043,"level":449},"/features/collaboration#d1-migration","D1 Migration",[220,7032],"npx wrangler d1 execute \u003CDB_NAME> \\\n  --file=./packages/nuxt-crouton-collab/server/database/migrations/0001_yjs_collab_states.sql",{"id":7045,"title":7046,"titles":7047,"content":528,"level":391},"/features/collaboration#websocket-protocol","WebSocket Protocol",[220],{"id":7049,"title":7050,"titles":7051,"content":7052,"level":449},"/features/collaboration#binary-messages","Binary Messages",[220,7046],"Raw Uint8Array containing Yjs update data.",{"id":7054,"title":7055,"titles":7056,"content":7057,"level":449},"/features/collaboration#json-messages","JSON Messages",[220,7046],"// Awareness update\n{\n  type: 'awareness',\n  userId: 'user-123',\n  state: {\n    user: { id: 'user-123', name: 'Alice', color: '#ff0000' },\n    cursor: { x: 100, y: 200 },\n    selection: { anchor: 10, head: 20 }\n  }\n}\n\n// Ping/Pong for connection health\n{ type: 'ping' }\n{ type: 'pong' }",{"id":7059,"title":7060,"titles":7061,"content":7062,"level":391},"/features/collaboration#types-reference","Types Reference",[220],"interface CollabUser {\n  id: string\n  name: string\n  color: string\n}\n\ninterface CollabAwarenessState {\n  user: CollabUser\n  cursor: { x: number; y: number } | null\n  selection?: { anchor: number; head: number } | null\n  selectedNodeId?: string | null\n  ghostNode?: { id: string; position: { x: number; y: number } } | null\n  [key: string]: unknown  // Extensible\n}\n\ninterface CollabConnectionState {\n  connected: boolean\n  synced: boolean\n  error: Error | null\n}\n\ntype CollabStructure = 'map' | 'array' | 'xmlFragment' | 'text'",{"id":7064,"title":6736,"titles":7065,"content":7066,"level":391},"/features/collaboration#api-endpoints",[220],"EndpointMethodDescription/api/collab/[roomId]/ws?type=XGETWebSocket upgrade/api/collab/[roomId]/users?type=XGETGet current users",{"id":7068,"title":7069,"titles":7070,"content":7071,"level":449},"/features/collaboration#users-endpoint-response","Users Endpoint Response",[220,6736],"{\n  \"users\": [\n    {\n      \"user\": { \"id\": \"user-123\", \"name\": \"Alice\", \"color\": \"#ff0000\" },\n      \"cursor\": { \"x\": 100, \"y\": 200 }\n    }\n  ],\n  \"count\": 1\n}",{"id":7073,"title":44,"titles":7074,"content":7075,"level":391},"/features/collaboration#best-practices",[220],"Use appropriate room types - Match the room type to your data structureExclude self from counts - Use currentUserId to exclude the current userThrottle awareness updates - Don't send cursor updates on every mouse moveHandle disconnections gracefully - Show status indicators to usersClean up on unmount - The composables handle this automatically",{"id":7077,"title":36,"titles":7078,"content":528,"level":391},"/features/collaboration#troubleshooting",[220],{"id":7080,"title":7081,"titles":7082,"content":7083,"level":449},"/features/collaboration#connection-issues","Connection Issues",[220,36],"Check browser WebSocket tab in DevToolsVerify type and roomId query paramsCheck Cloudflare Durable Object logsVerify D1 migration was run",{"id":7085,"title":7086,"titles":7087,"content":7088,"level":449},"/features/collaboration#debug-room-users","Debug Room Users",[220,36],"# Via curl (when server is running)\ncurl https://your-app.com/api/collab/room-123/users?type=page",{"id":7090,"title":7091,"titles":7092,"content":7093,"level":449},"/features/collaboration#common-errors","Common Errors",[220,36],"ErrorCauseSolutionWebSocket connection failedServer not runningCheck server logsRoom not foundMissing D1 migrationRun migration SQLUser not syncingWrong room typeVerify type parameter html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":225,"title":224,"titles":7095,"content":7096,"level":385},[],"Email infrastructure with Vue Email templates and Resend delivery",{"id":7098,"title":224,"titles":7099,"content":7100,"level":385},"/features/email#email",[],"Status: Stable ✅ The Email feature provides server-side email utilities and client-side flow components for verification, magic links, password resets, and team invitations. Works standalone or integrates with @fyit/crouton-auth.",{"id":7102,"title":7103,"titles":7104,"content":7105,"level":391},"/features/email#enable","Enable",[224],"The email feature is included in the core @fyit/crouton package. Ensure it is in your extends: // nuxt.config.ts\nexport default defineNuxtConfig({\n  extends: ['@fyit/crouton']\n})",{"id":7107,"title":6358,"titles":7108,"content":7109,"level":391},"/features/email#environment-variables",[224],"# Required\nRESEND_API_KEY=re_xxx\n\n# Optional (can be set in nuxt.config.ts)\nEMAIL_FROM=noreply@example.com\nEMAIL_FROM_NAME=My App\nEMAIL_REPLY_TO=support@example.com",{"id":7111,"title":1789,"titles":7112,"content":7113,"level":391},"/features/email#configuration",[224],"// nuxt.config.ts\nexport default defineNuxtConfig({\n  extends: ['@fyit/crouton'],\n\n  runtimeConfig: {\n    email: {\n      resendApiKey: process.env.RESEND_API_KEY,\n      from: 'noreply@example.com',\n      fromName: 'My App',\n      replyTo: 'support@example.com'\n    },\n    public: {\n      crouton: {\n        email: {\n          brand: {\n            name: 'My App',\n            logoUrl: 'https://example.com/logo.png',\n            primaryColor: '#0F766E',\n            url: 'https://example.com'\n          },\n          verification: {\n            codeLength: 6,\n            codeExpiry: 10, // minutes\n            resendCooldown: 60 // seconds\n          },\n          magicLink: {\n            expiry: 10, // minutes\n            resendCooldown: 60 // seconds\n          }\n        }\n      }\n    }\n  }\n})",{"id":7115,"title":292,"titles":7116,"content":528,"level":391},"/features/email#server-utilities",[224],{"id":7118,"title":7119,"titles":7120,"content":7121,"level":449},"/features/email#email-service","Email Service",[224,292],"import { useEmailService } from '#crouton-email/server/utils/email'\n\nconst emailService = useEmailService()\n\n// Send raw email\nconst result = await emailService.send({\n  to: 'user@example.com',\n  subject: 'Hello',\n  html: '\u003Cp>Hello World\u003C/p>'\n})",{"id":7123,"title":7124,"titles":7125,"content":7126,"level":449},"/features/email#convenience-senders","Convenience Senders",[224,292],"import {\n  sendVerificationEmail,\n  sendMagicLink,\n  sendPasswordReset,\n  sendTeamInvite,\n  sendWelcome\n} from '#crouton-email/server/utils/senders'\n\n// Send verification code email\nawait sendVerificationEmail({\n  to: 'user@example.com',\n  code: '123456',\n  name: 'John'\n})\n\n// Send magic link\nawait sendMagicLink({\n  to: 'user@example.com',\n  link: 'https://app.com/auth/magic?token=xxx',\n  name: 'John'\n})\n\n// Send password reset\nawait sendPasswordReset({\n  to: 'user@example.com',\n  link: 'https://app.com/auth/reset?token=xxx',\n  name: 'John'\n})\n\n// Send team invitation\nawait sendTeamInvite({\n  to: 'user@example.com',\n  link: 'https://app.com/invite/accept?token=xxx',\n  inviterName: 'Jane',\n  teamName: 'Acme Inc',\n  role: 'member'\n})\n\n// Send welcome email\nawait sendWelcome({\n  to: 'user@example.com',\n  name: 'John',\n  getStartedLink: 'https://app.com/getting-started'\n})",{"id":7128,"title":7129,"titles":7130,"content":528,"level":391},"/features/email#client-components","Client Components",[224],{"id":7132,"title":7133,"titles":7134,"content":7135,"level":449},"/features/email#emailverificationflow","EmailVerificationFlow",[224,7129],"Complete verification code input flow with resend functionality. \u003Ctemplate>\n  \u003CEmailVerificationFlow\n    :email=\"userEmail\"\n    @verified=\"handleVerified\"\n    @resend=\"handleResend\"\n    @error=\"handleError\"\n  />\n\u003C/template> PropTypeDefaultDescriptionemailstringrequiredEmail being verifiedcodeLengthnumber6Code lengthresendCooldownnumber60Cooldown in secondsloadingbooleanfalseLoading state while verifyingerrorstring''External error message EventPayloadDescriptionverifiedcode: stringCode enteredresend-Resend requestederrorerror: stringError occurred",{"id":7137,"title":7138,"titles":7139,"content":7140,"level":449},"/features/email#emailmagiclinksent","EmailMagicLinkSent",[224,7129],"\"Check your email\" message with resend option. \u003Ctemplate>\n  \u003CEmailMagicLinkSent\n    :email=\"userEmail\"\n    @resend=\"handleResend\"\n    @change-email=\"handleChangeEmail\"\n  />\n\u003C/template> PropTypeDefaultDescriptionemailstringrequiredEmail where link was sentresendCooldownnumber60Cooldown in secondsloadingbooleanfalseLoading state while resendingerrorstring''External error message",{"id":7142,"title":7143,"titles":7144,"content":7145,"level":449},"/features/email#emailresendbutton","EmailResendButton",[224,7129],"Timer-based resend button with cooldown. \u003Ctemplate>\n  \u003CEmailResendButton\n    :cooldown=\"60\"\n    :loading=\"isResending\"\n    @resend=\"handleResend\"\n  />\n\u003C/template>",{"id":7147,"title":7148,"titles":7149,"content":7150,"level":449},"/features/email#emailinput","EmailInput",[224,7129],"Email input with validation. \u003Ctemplate>\n  \u003CEmailInput\n    v-model=\"email\"\n    :error=\"emailError\"\n    placeholder=\"Enter your email\"\n  />\n\u003C/template>",{"id":7152,"title":7153,"titles":7154,"content":7155,"level":391},"/features/email#email-templates","Email Templates",[224],"Built-in Vue Email templates: TemplatePurposePropsBaseLayout.vueShared layoutbrandName, logoUrl, primaryColorVerification.vueVerification codecode, name, expiryMinutesMagicLink.vueMagic link loginlink, name, expiryMinutesPasswordReset.vuePassword resetlink, name, expiryMinutesTeamInvite.vueTeam invitationlink, inviterName, teamName, roleWelcome.vueWelcome emailname, getStartedLink",{"id":7157,"title":7158,"titles":7159,"content":7160,"level":391},"/features/email#integration-with-auth","Integration with Auth",[224],"When using with @fyit/crouton-auth, the auth package automatically uses email utilities for: Email verification on signupMagic link authenticationPassword reset emailsTeam invitation emails export default defineNuxtConfig({\n  extends: ['@fyit/crouton']\n  // Email config will be used automatically by auth\n})",{"id":7162,"title":7163,"titles":7164,"content":7165,"level":391},"/features/email#standalone-usage","Standalone Usage",[224],"Works without auth. Implement your own endpoints: // server/api/auth/verify.post.ts\nexport default defineEventHandler(async (event) => {\n  const { email } = await readBody(event)\n\n  // Generate your verification code\n  const code = generateCode()\n\n  // Save code to your database\n  await saveVerificationCode(email, code)\n\n  // Send email using crouton-email\n  const { sendVerificationEmail } = await import('#crouton-email/server/utils/senders')\n  await sendVerificationEmail({ to: email, code })\n\n  return { success: true }\n})",{"id":7167,"title":7168,"titles":7169,"content":7170,"level":391},"/features/email#custom-templates","Custom Templates",[224],"Create your own templates: Create template in server/emails/MyEmail.vueUse the BaseLayout component for consistent stylingImport and use your template in sender functions \u003C!-- server/emails/OrderConfirmation.vue -->\n\u003Cscript setup lang=\"ts\">\n// Note: This path works via filesystem resolution but is not an explicit\n// package.json export of @fyit/crouton-email. It relies on the layer's\n// file structure being accessible at build time.\nimport BaseLayout from '@fyit/crouton-email/server/emails/BaseLayout.vue'\n\ndefineProps\u003C{\n  orderNumber: string\n  items: Array\u003C{ name: string; price: number }>\n  total: number\n}>()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CBaseLayout brand-name=\"My Store\">\n    \u003Ch1>Order Confirmed!\u003C/h1>\n    \u003Cp>Order #{{ orderNumber }}\u003C/p>\n    \u003C!-- ... -->\n  \u003C/BaseLayout>\n\u003C/template>",{"id":7172,"title":7173,"titles":7174,"content":7175,"level":391},"/features/email#testing-emails","Testing Emails",[224],"Use Resend's test mode: # Use Resend's test API key for development\nRESEND_API_KEY=re_test_xxx",{"id":7177,"title":3170,"titles":7178,"content":7179,"level":391},"/features/email#use-cases",[224],"Authentication flows: Verification codes, magic links, password resetsTeam management: Invitations, role changes, notificationsTransactional emails: Order confirmations, receipts, updatesUser engagement: Welcome emails, onboarding sequences",{"id":7181,"title":4341,"titles":7182,"content":7183,"level":391},"/features/email#related",[224],"Authentication - Auth integrationPackage Architecture - Package details html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":229,"title":228,"titles":7185,"content":7186,"level":385},[],"CMS-like page management with block-based editing and custom domains",{"id":7188,"title":228,"titles":7189,"content":7190,"level":385},"/features/pages#pages-cms",[],"Status: Stable ✅ The Pages feature provides a CMS-like page management system with page types, block-based editing, tree organization, and custom domain support.",{"id":7192,"title":7103,"titles":7193,"content":7194,"level":391},"/features/pages#enable",[228],"Add the pages and editor layers to your nuxt.config.ts: // nuxt.config.ts\nexport default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-editor'  // Required for rich text blocks\n  ]\n})",{"id":7196,"title":5599,"titles":7197,"content":7198,"level":391},"/features/pages#key-features",[228],"Page Types: Regular pages, app-specific pages (booking calendar, product list)Block Editor: Hero sections, feature grids, CTAs, cards, rich textTree Organization: Hierarchical page ordering with drag-and-dropCustom Domains: Automatic team resolution from custom domainsPublic Routes: /[team]/[locale]/[...slug] for public pages",{"id":7200,"title":7201,"titles":7202,"content":7203,"level":391},"/features/pages#url-structure","URL Structure",[228],"RoutePurpose/[team]/[...slug]Public page display (no locale)/[team]/[locale]/[...slug]Public page display (with locale)/[team]/Homepage (empty slug)/admin/[team]/pagesAdmin page management",{"id":7205,"title":7206,"titles":7207,"content":7208,"level":391},"/features/pages#page-types","Page Types",[228],"Apps can register custom page types that appear in the page type selector.",{"id":7210,"title":7211,"titles":7212,"content":7213,"level":449},"/features/pages#built-in-types","Built-in Types",[228,7206],"core:regular - Standard rich text page",{"id":7215,"title":7216,"titles":7217,"content":7218,"level":449},"/features/pages#registering-custom-types","Registering Custom Types",[228,7206],"// app.config.ts\nexport default defineAppConfig({\n  croutonApps: {\n    bookings: {\n      id: 'bookings',\n      name: 'Bookings',\n      pageTypes: [\n        {\n          id: 'calendar',\n          name: 'Booking Calendar',\n          component: 'CroutonBookingsCalendar',\n          icon: 'i-lucide-calendar',\n          category: 'customer',\n          description: 'Interactive calendar for bookings'\n        }\n      ]\n    }\n  }\n})",{"id":7220,"title":7221,"titles":7222,"content":7223,"level":449},"/features/pages#using-page-types","Using Page Types",[228,7206],"const {\n  pageTypes,           // All aggregated page types (apps + publishable collections)\n  getPageType          // Get by fullId (e.g., 'bookings:calendar')\n} = usePageTypes()",{"id":7225,"title":7226,"titles":7227,"content":7228,"level":391},"/features/pages#block-editor","Block Editor",[228],"The block-based editor uses TipTap and Nuxt UI page components.",{"id":7230,"title":7231,"titles":7232,"content":7233,"level":449},"/features/pages#block-types","Block Types",[228,7226],"BlockComponentPurposeheroBlockUPageHeroTitle, description, CTA, imagesectionBlockUPageSectionFeature grid with iconsctaBlockUPageCTACall-to-action bannercardGridBlockUPageGrid + UPageCardGrid of cardsseparatorBlockUSeparatorVisual dividerrichTextBlockprose divStandard text contentfaqBlockFAQFrequently asked questions accordiontwoColumnBlockTwoColumnTwo-column layoutimageBlockImageImage with captionvideoBlockVideoEmbedded videofileBlockFileFile download/attachmentembedBlockEmbedExternal embed (iframe)buttonRowBlockButtonRowRow of action buttonsstatsBlockStatsStatistics/metrics displaygalleryBlockGalleryImage gallery gridlogoBlockLogoLogo cloud/displaycollectionBlockCollectionDynamic collection listingaddonBlockAddonAddon/extension content",{"id":7235,"title":7236,"titles":7237,"content":7238,"level":449},"/features/pages#using-the-editor","Using the Editor",[228,7226],"\u003Ctemplate>\n  \u003CCroutonPagesBlockContent\n    v-model=\"content\"\n    placeholder=\"Type / to insert a block...\"\n  />\n\u003C/template>",{"id":7240,"title":7241,"titles":7242,"content":7243,"level":391},"/features/pages#content-format","Content Format",[228],"Content auto-detects format: JSON with type: 'doc' - Renders as blocksHTML string - Renders as legacy contentEmpty - Shows empty state interface PageBlockContent {\n  type: 'doc'\n  content: PageBlock[]\n}\n\ninterface PageBlock {\n  type: 'heroBlock' | 'sectionBlock' | 'ctaBlock' | 'cardGridBlock' | 'separatorBlock' | 'richTextBlock' | 'faqBlock' | 'twoColumnBlock' | 'imageBlock' | 'videoBlock' | 'fileBlock' | 'embedBlock' | 'buttonRowBlock' | 'statsBlock' | 'galleryBlock' | 'logoBlock' | 'collectionBlock' | 'addonBlock'\n  attrs: Record\u003Cstring, any>\n}",{"id":7245,"title":7246,"titles":7247,"content":7248,"level":391},"/features/pages#page-schema","Page Schema",[228],"interface PageRecord {\n  id: string\n  teamId: string\n  title: string\n  slug: string\n  pageType: string        // 'core:regular' or 'appId:pageTypeId'\n  content?: string        // For regular pages (JSON blocks or HTML)\n  config?: object         // For app pages (type-specific settings)\n  status: 'draft' | 'published' | 'archived'\n  visibility: 'public' | 'members' | 'hidden'\n  showInNavigation: boolean\n  parentId?: string       // For hierarchy\n  order: number           // For sorting\n  path?: string           // Materialized path\n  depth?: number          // Nesting level\n}",{"id":7250,"title":7251,"titles":7252,"content":7253,"level":391},"/features/pages#custom-domains","Custom Domains",[228],"The pages package supports custom domain resolution.",{"id":7255,"title":1635,"titles":7256,"content":7257,"level":449},"/features/pages#how-it-works",[228,7251],"Custom Domain Request: booking.acme.com/about\n    │\n    ▼\nDomain resolver middleware\n    │ looks up 'booking.acme.com' in domain table\n    │ finds: organizationId → org with slug 'acme'\n    ▼\nURL Rewrite: /about → /acme/about\n    │\n    ▼\nNormal routing: [team]/[...slug].vue",{"id":7259,"title":7260,"titles":7261,"content":7262,"level":449},"/features/pages#domain-context","Domain Context",[228,7251],"const {\n  isCustomDomain,    // Whether request is from custom domain\n  resolvedDomain,    // The custom domain hostname\n  resolvedTeamId,    // Team ID from domain lookup\n  hideTeamInUrl,     // true on custom domains\n  hostname,          // Current hostname\n  isAppDomain        // Whether hostname is known app domain\n} = useDomainContext()",{"id":7264,"title":1789,"titles":7265,"content":7266,"level":449},"/features/pages#configuration",[228,7251],"runtimeConfig: {\n  public: {\n    croutonPages: {\n      // Domains to skip (not custom domains)\n      appDomains: ['myapp.com', 'staging.myapp.com'],\n      debug: false\n    }\n  }\n}",{"id":7268,"title":7269,"titles":7270,"content":7271,"level":391},"/features/pages#navigation","Navigation",[228],"Build navigation from published pages: const {\n  navigation,        // Hierarchical navigation tree\n  flatNavigation,    // Flat list of all nav items\n  isLoading,         // Loading state\n  currentPage,       // Current active page\n  isActive,          // Check if nav item is active\n  refresh,           // Refresh navigation data\n  team               // Current team slug\n} = useNavigation()",{"id":7273,"title":5354,"titles":7274,"content":7275,"level":391},"/features/pages#components",[228],"ComponentPurposeCroutonPagesRendererRenders page based on typeCroutonPagesRegularContentRich text content displayCroutonPagesBlockContentBlock-based content display and editorCroutonPagesFormPage creation/editing form (registered via manifest, not shipped as a standalone file)",{"id":7277,"title":7278,"titles":7279,"content":7280,"level":391},"/features/pages#admin-page-list","Admin Page List",[228],"\u003Ctemplate>\n  \u003CCroutonCollection\n    collection=\"pagesPages\"\n    layout=\"tree\"\n    :columns=\"['title', 'slug', 'pageType', 'status']\"\n  />\n\u003C/template>",{"id":7282,"title":7283,"titles":7284,"content":7285,"level":391},"/features/pages#rendering-pages","Rendering Pages",[228],"\u003Ctemplate>\n  \u003CCroutonPagesRenderer :page=\"pageData\" />\n\u003C/template>",{"id":7287,"title":6736,"titles":7288,"content":7289,"level":391},"/features/pages#api-endpoints",[228],"EndpointMethodPurpose/api/teams/[id]/pagesGETList published pages (for navigation)/api/teams/[id]/pages/[...slug]GETGet single page by slug (catch-all)",{"id":7291,"title":3170,"titles":7292,"content":7293,"level":391},"/features/pages#use-cases",[228],"Marketing sites: Landing pages, about, contactDocumentation: Knowledge base, help centerPortals: Customer dashboards, member areasMulti-tenant apps: Team-specific pages with custom domains",{"id":7295,"title":4341,"titles":7296,"content":7297,"level":391},"/features/pages#related",[228],"Rich Text Editor - Editor integrationCollaboration - Real-time editingPackage Architecture - Package details html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":233,"title":232,"titles":7299,"content":528,"level":385},[],{"id":7301,"title":232,"titles":7302,"content":7303,"level":385},"/features/bookings#bookings",[],"Booking system for slot-based appointments and inventory-based reservations. Status: Beta -- API may change between minor releases.",{"id":7305,"title":936,"titles":7306,"content":7307,"level":391},"/features/bookings#overview",[232],"The Bookings package (@fyit/crouton-bookings) provides a complete booking system supporting two modes: Slot mode (default): Fixed time slots with optional per-slot capacity (courts, rooms, appointments)Inventory mode: Quantity-based pool (equipment rentals, bike pools) Features: Cart system with batch checkoutSchedule rules (open days, per-slot day schedules, blocked date ranges)Monthly booking limits per location per userOptional email notifications (confirmations, reminders, cancellations, follow-ups)Group-based bookings (e.g. age groups)Map integration (optional, via @fyit/crouton-maps)i18n support (EN, NL, FR)",{"id":7309,"title":18,"titles":7310,"content":7311,"level":391},"/features/bookings#quick-start",[232],"Add the bookings layer to your nuxt.config.ts: export default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',           // Core\n    '@fyit/crouton-bookings',  // Bookings layer\n    './layers/bookings'        // Generated collections\n  ]\n}) Dependencies: Regular: @fyit/crouton-editor (installed automatically)Peer: @fyit/crouton-core, @nuxtjs/i18n, @internationalized/date, @vueuse/core, zodOptional peer: @fyit/crouton-maps (for map display in panel)",{"id":7313,"title":7314,"titles":7315,"content":528,"level":391},"/features/bookings#booking-modes","Booking Modes",[232],{"id":7317,"title":7318,"titles":7319,"content":7320,"level":449},"/features/bookings#slot-mode-default","Slot Mode (Default)",[232,7314],"Each location defines named time slots with optional capacity: // Location with slots (capacity defaults to 1)\nconst location = {\n  title: 'Tennis Court A',\n  slots: [\n    { id: 'morning', label: '09:00 - 10:00' },\n    { id: 'midday', label: '10:00 - 11:00' },\n    { id: 'afternoon', label: '14:00 - 15:00', capacity: 4 }\n  ]\n} When capacity > 1, the UI shows \"X left\" / \"Full\" badges. If no slots are defined, an implicit \"All Day\" slot is used.",{"id":7322,"title":7323,"titles":7324,"content":7325,"level":449},"/features/bookings#inventory-mode","Inventory Mode",[232,7314],"Quantity-based pool where users book units per day: const location = {\n  title: 'Kayak Rental',\n  inventoryMode: true,\n  quantity: 20 // 20 kayaks available per day\n}",{"id":7327,"title":5437,"titles":7328,"content":528,"level":391},"/features/bookings#composables",[232],{"id":7330,"title":7331,"titles":7332,"content":7333,"level":449},"/features/bookings#usebookingcart","useBookingCart()",[232,5437],"The primary composable for the customer booking flow. Manages form state, cart persistence (localStorage), availability checking, and batch submission. const {\n  // State\n  cart,                    // Ref\u003CCartItem[]> - persisted in localStorage\n  formState,               // reactive { locationId, date, slotIds, groupId, quantity, editingBookingId }\n  isOpen,                  // Ref\u003Cboolean> - sidebar open state\n  isCartOpen,              // Ref\u003Cboolean> - cart drawer open state\n  isExpanded,              // Ref\u003Cboolean> - expanded mode (with map)\n  activeTab,               // Ref\u003Cstring> - 'book' | 'my-bookings'\n  isSubmitting,            // Ref\u003Cboolean>\n  availabilityLoading,     // Ref\u003Cboolean>\n  cartPulse,               // Ref\u003Cnumber> - increments on add for animation\n\n  // Mode detection\n  isInventoryMode,         // ComputedRef\u003Cboolean>\n  inventoryQuantity,       // ComputedRef\u003Cnumber>\n\n  // Monthly limit\n  monthlyBookingLimit,     // ComputedRef\u003Cnumber | null>\n  monthlyBookingRemaining, // ComputedRef\u003Cnumber | null>\n\n  // Groups\n  enableGroups,            // ComputedRef\u003Cboolean>\n  groupOptions,            // ComputedRef\u003CArray>\n\n  // Locations & slots\n  locations,               // Ref\u003CLocationData[]>\n  locationsStatus,         // fetch status\n  selectedLocation,        // ComputedRef\u003CLocationData | null>\n  allSlots,                // ComputedRef\u003CSlotItem[]>\n  availableSlots,          // ComputedRef\u003CSlotItem[]> - filtered by rules + bookings\n  rawSlots,                // ComputedRef\u003CSlotItem[]> - from location config\n  isSlotDisabled,          // (slotId: string) => boolean\n  getSlotRemaining,        // (slotId: string) => number\n  getSlotCapacity,         // (slotId: string) => number\n\n  // Calendar helpers (API + cart combined)\n  hasBookingsOnDate,       // (date: Date) => boolean\n  isDateFullyBooked,       // (date: Date) => boolean\n  getBookedSlotLabelsForDate, // (date: Date) => string[]\n  getBookedSlotsForDate,   // (date: Date) => string[]\n  getInventoryAvailability, // (date: Date) => { available, remaining, total, bookedCount }\n\n  // Schedule rules\n  isDateUnavailable,       // (date: Date | DateValue) => boolean\n  getBlockedReason,        // (date: Date | DateValue) => string | null\n\n  // My bookings\n  myBookings,              // Ref\u003CBookingData[]>\n  myBookingsStatus,\n  refreshMyBookings,\n\n  // Computed\n  canAddToCart,            // ComputedRef\u003Cboolean>\n  cartCount,               // ComputedRef\u003Cnumber>\n  upcomingBookingsCount,   // ComputedRef\u003Cnumber>\n\n  // Actions\n  addToCart,               // () => void\n  toggleSlot,              // (slotId: string) => void\n  removeFromCart,          // (id: string) => void\n  clearCart,               // () => void\n  submitAll,               // () => Promise\u003Cresult | null>\n  resetForm,               // () => void\n  cancelBooking,           // (bookingId: string) => Promise\u003Cboolean>\n  deleteBooking,           // (bookingId: string) => Promise\u003Cboolean>\n  fetchAvailability,       // (startDate: Date, endDate: Date) => void\n\n  // Signals\n  lastBookingCreatedAt,    // Ref\u003Cnumber | null>\n  lastCreatedBookingIds,   // Ref\u003Cstring[]>\n} = useBookingCart()",{"id":7335,"title":7336,"titles":7337,"content":7338,"level":449},"/features/bookings#usebookingavailabilitylocationid-location","useBookingAvailability(locationId, location)",[232,5437],"Lower-level composable for checking slot/inventory availability. Used internally by useBookingCart and available standalone for admin-side checks. const {\n  loading,\n  availabilityData,        // Ref\u003CAvailabilityData>\n  isInventoryMode,\n  inventoryQuantity,\n  allSlots,\n  locationSlots,\n  fetchAvailability,       // (startDate: Date, endDate: Date, excludeBookingId?: string) => void\n  getBookedSlotsForDate,   // (date: Date | DateValue) => string[]\n  getSlotBookedCountForDate, // (date: Date | DateValue, slotId: string) => number\n  getSlotRemainingForDate, // (date: Date | DateValue, slotId: string) => number\n  getAvailableSlotsForDate, // (date: Date | DateValue) => SlotItem[]\n  getBookedCountForDate,   // (date: Date | DateValue) => number\n  getInventoryAvailability, // (date: Date | DateValue, quantityOverride?: number) => InventoryAvailability\n  hasBookingsOnDate,       // (date: Date | DateValue) => boolean\n  isDateFullyBooked,       // (date: Date | DateValue) => boolean\n\n  // Schedule rules (pass-through from useScheduleRules)\n  isDateUnavailable,\n  isSlotAvailableByRules,\n  getBlockedReason,\n  getRuleBlockedSlotIds,\n} = useBookingAvailability(\n  locationId,  // Ref\u003Cstring | null>\n  location     // Ref\u003CLocationWithInventory | null | undefined>\n)",{"id":7340,"title":7341,"titles":7342,"content":7343,"level":449},"/features/bookings#usebookingslistoptions","useBookingsList(options?)",[232,5437],"Fetches and manages the bookings list with settings and locations. Supports personal and team-wide scopes. const {\n  bookings,           // ComputedRef\u003CBooking[]> - sorted by date\n  calendarBookings,   // ComputedRef\u003CBooking[]> - all team bookings for calendar indicators\n  settings,           // ComputedRef\u003CSettingsData | null>\n  locations,          // ComputedRef\u003CLocationData[]>\n  loading,            // ComputedRef\u003Cboolean>\n  error,              // ComputedRef\u003CError | null>\n  refresh,            // () => Promise\u003Cvoid>\n} = useBookingsList({ scope: 'personal' }) // or 'team'",{"id":7345,"title":7346,"titles":7347,"content":7348,"level":449},"/features/bookings#usescheduleruleslocation","useScheduleRules(location)",[232,5437],"Evaluates location schedule rules client-side. Checks open days, per-slot day schedules, and blocked date ranges. const {\n  openDays,              // ComputedRef\u003Cnumber[] | null>\n  slotSchedule,          // ComputedRef\u003CSlotSchedule | null>\n  blockedDates,          // ComputedRef\u003CBlockedDateItem[]>\n  isLocationOpenOnDate,  // (date: Date | DateValue) => boolean\n  isDateBlocked,         // (date: Date | DateValue, slotId?: string) => boolean\n  isDateUnavailable,     // (date: Date | DateValue) => boolean\n  isSlotAvailableByRules, // (slotId: string, date: Date | DateValue) => boolean\n  getBlockedReason,      // (date: Date | DateValue) => string | null\n  getRuleBlockedSlotIds, // (date: Date | DateValue) => string[]\n} = useScheduleRules(location) // Ref\u003CScheduleRuleLocation | null | undefined> Rule precedence (all must pass for a slot to be available): Location open on this weekday (openDays)Slot scheduled for this weekday (slotSchedule, falls back to openDays)Date not in a blocked range (blockedDates)",{"id":7350,"title":7351,"titles":7352,"content":7353,"level":449},"/features/bookings#usebookingemail","useBookingEmail()",[232,5437],"Check email status and resend emails for bookings. const {\n  isEmailEnabled,  // ComputedRef\u003Cboolean>\n  resendEmail,     // (teamId, bookingId, triggerType) => Promise\u003C{ success, error? }>\n} = useBookingEmail()",{"id":7355,"title":7356,"titles":7357,"content":7358,"level":449},"/features/bookings#usebookingslots","useBookingSlots()",[232,5437],"Utility functions for parsing and labeling slots. const {\n  parseSlotIds,       // (slot: string | string[] | null) => string[]\n  parseLocationSlots, // (location: { slots? }) => SlotItem[]\n  getSlotLabel,       // (slotId: string, slots: SlotItem[]) => string\n} = useBookingSlots()",{"id":7360,"title":7361,"titles":7362,"content":7363,"level":449},"/features/bookings#usebookingoptions","useBookingOptions()",[232,5437],"Fetches booking settings and provides translated label lookups for statuses and groups. const {\n  statuses,          // ComputedRef\u003COptionItem[]>\n  groups,            // ComputedRef\u003COptionItem[]>\n  pending,\n  error,\n  refresh,\n  getStatusLabel,    // (statusValue: string) => string\n  getGroupLabel,     // (groupId: string) => string\n  getTranslatedLabel, // (item: OptionItem) => string\n} = useBookingOptions()",{"id":7365,"title":7366,"titles":7367,"content":7368,"level":449},"/features/bookings#usebookingssettings","useBookingsSettings()",[232,5437],"Returns the bookings settings collection configuration (schema, columns, defaults). const {\n  name,          // 'bookingsSettings'\n  layer,         // 'bookings'\n  apiPath,       // 'bookings-settings'\n  schema,        // Zod schema\n  defaultValues, // { statuses: [], enableGroups: false, groups: [] }\n  columns,\n} = useBookingsSettings()",{"id":7370,"title":7371,"titles":7372,"content":7373,"level":449},"/features/bookings#usebookingmonthlylimit","useBookingMonthlyLimit(...)",[232,5437],"Tracks and enforces monthly booking limits per user per location. Used internally by useBookingCart. const {\n  monthlyBookingCount,        // Ref\u003Cnumber>\n  monthlyBookingCountLoading, // Ref\u003Cboolean>\n  cartCountForLocationMonth,  // ComputedRef\u003Cnumber>\n  monthlyBookingRemaining,    // ComputedRef\u003Cnumber | null>\n  fetchMonthlyBookingCount,   // () => Promise\u003Cvoid>\n} = useBookingMonthlyLimit(teamId, locationId, selectedDate, maxBookingsPerMonth, cart, editingBookingId)",{"id":7375,"title":7376,"titles":7377,"content":7378,"level":449},"/features/bookings#usebookingemailvariables","useBookingEmailVariables()",[232,5437],"Provides email template variables and demo data for preview rendering. const {\n  variables,        // EditorVariable[] - all available template variables\n  demoData,         // object with demo customer, booking, location, team data\n  getPreviewValues, // () => Record\u003Cstring, string>\n} = useBookingEmailVariables()",{"id":7380,"title":5354,"titles":7381,"content":7382,"level":391},"/features/bookings#components",[232],"All components use the prefix CroutonBookings (configured in nuxt.config.ts).",{"id":7384,"title":7385,"titles":7386,"content":7387,"level":449},"/features/bookings#main-components","Main Components",[232,5354],"ComponentPurposeCroutonBookingsPanelMain booking panel with tabs (book / my-bookings), filters, calendar, and list. Calendar functionality is integrated into this component.CroutonBookingsListScrollable bookings list with inline creation, date grouping, and highlightingCroutonBookingsBookingCardIndividual booking card showing date, slot, location, status, and email actions (includes inline activity timeline)CroutonBookingsBookingCreateCardInline booking creation form",{"id":7389,"title":7390,"titles":7391,"content":7392,"level":449},"/features/bookings#supporting-components","Supporting Components",[232,5354],"ComponentPurposeCroutonBookingsWeekStripHorizontal week date navigation stripCroutonBookingsDateBadgeDate display badge with formattingCroutonBookingsPanelFiltersFilter controls for status and locationCroutonBookingsLocationCardLocation card with detailsCroutonBookingsLocationFormLocation editing formCroutonBookingsSlotIndicatorVisual slot capacity indicatorCroutonBookingsOpenDaysPickerDay-of-week picker for location schedulingCroutonBookingsScheduleGridPer-slot day-of-week schedule grid editorCroutonBookingsBlockedDateInputBlocked date range inputCroutonBookingsAvailabilityPreviewPreview of slot availability",{"id":7394,"title":7395,"titles":7396,"content":7397,"level":449},"/features/bookings#content-block-components","Content Block Components",[232,5354],"ComponentPurposeCroutonBookingsBlocksBookingBlockViewBooking block for content pagesCroutonBookingsBlocksBookingBlockRenderBooking block renderer",{"id":7399,"title":7400,"titles":7401,"content":528,"level":449},"/features/bookings#key-component-props","Key Component Props",[232,5354],{"id":7403,"title":7404,"titles":7405,"content":7406,"level":748},"/features/bookings#croutonbookingspanel","CroutonBookingsPanel",[232,5354,7400],"PropTypeDefaultDescriptionbookingsBooking[]auto-fetchedBookings data (external or auto-fetched via useBookingsList)locationsLocationData[]auto-fetchedLocations datasettingsSettingsData | nullauto-fetchedSettings dataloadingbooleanfalseLoading state (when providing external data)errorError | null-Error stateinitialFiltersPartial\u003CFilterState>{}Initial filter statetitlestring-Header title (empty string hides header)emptyMessagestring-Empty state messagescope'personal' | 'team''personal'Booking scope",{"id":7408,"title":7409,"titles":7410,"content":7411,"level":748},"/features/bookings#croutonbookingsbookingcard","CroutonBookingsBookingCard",[232,5354,7400],"PropTypeDefaultDescriptionbookingBookingrequiredBooking data with relationshighlightedbooleanfalseVisual highlight statejustCreatedbooleanfalseTemporary highlight for new bookings (fades)sendingEmailTypeEmailTriggerType | nullnullCurrently sending email type",{"id":7413,"title":6736,"titles":7414,"content":7415,"level":391},"/features/bookings#api-endpoints",[232],"PathMethodPurpose/api/crouton-bookings/teams/[id]/availabilityGETBooked slots for a date range (query: locationId, startDate, endDate)/api/crouton-bookings/teams/[id]/customer-bookingsGETCurrent user's bookings with email stats/api/crouton-bookings/teams/[id]/customer-bookings-batchPOSTSubmit cart (batch checkout)/api/crouton-bookings/teams/[id]/customer-locationsGETLocations accessible to current user/api/crouton-bookings/teams/[id]/admin-bookingsGETAll team bookings (team members)/api/crouton-bookings/teams/[id]/monthly-booking-countGETUser's monthly booking count for a location/api/crouton-bookings/teams/[id]/bookings/[bookingId]PATCHUpdate a booking/api/crouton-bookings/teams/[id]/bookings/[bookingId]/resend-emailPOSTResend email for a booking",{"id":7417,"title":7418,"titles":7419,"content":7420,"level":449},"/features/bookings#chart-endpoints","Chart Endpoints",[232,6736],"PathMethodPurpose/api/crouton-bookings/teams/[id]/charts/bookings-by-dateGETBooking counts by date/api/crouton-bookings/teams/[id]/charts/bookings-by-groupGETBooking counts by group/api/crouton-bookings/teams/[id]/charts/bookings-by-locationGETBooking counts by location/api/crouton-bookings/teams/[id]/charts/bookings-by-slotGETBooking counts by slot/api/crouton-bookings/teams/[id]/charts/bookings-by-statusGETBooking counts by status",{"id":7422,"title":7423,"titles":7424,"content":7425,"level":391},"/features/bookings#database-schemas","Database Schemas",[232],"The package uses tables prefixed with bookings: Schema FileTable NameKey Fieldsbooking.jsonbookingsBookingslocation (ref), date, slot (JSON array), quantity, group, statuslocation.jsonbookingsLocationstitle, color, street/zip/city, slots (repeater), openDays, slotSchedule, blockedDates, inventoryMode, quantity, maxBookingsPerMonthsettings.jsonbookingsSettingsstatuses (repeater), groups (repeater)email-template.jsonbookingsEmailtemplatesname, subject, body (richtext), fromEmail, triggerType, recipientType, isActive, daysOffset, locationIdemail-log.jsonbookingsEmaillogsbookingId (ref), templateId (ref), recipientEmail, triggerType, status, sentAt, error",{"id":7427,"title":1789,"titles":7428,"content":7429,"level":391},"/features/bookings#configuration",[232],"// nuxt.config.ts\nexport default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-bookings',\n    './layers/bookings'        // Generated collections\n  ],\n\n  // Optional: Enable email notifications\n  runtimeConfig: {\n    croutonBookings: {\n      email: { enabled: true }\n    },\n    public: {\n      croutonBookings: {\n        email: { enabled: true }\n      }\n    }\n  }\n})",{"id":7431,"title":7153,"titles":7432,"content":7433,"level":391},"/features/bookings#email-templates",[232],"When email is enabled (croutonBookings.email.enabled: true), manage templates at /dashboard/[team]/settings/email-templates.",{"id":7435,"title":7436,"titles":7437,"content":7438,"level":449},"/features/bookings#trigger-types","Trigger Types",[232,7153],"TriggerWhen Sentbooking_createdWhen a new booking is madereminder_beforeBefore the booking date (use daysOffset, e.g. -1 for day before)booking_cancelledWhen a booking is cancelledfollow_up_afterAfter the booking date (use daysOffset, e.g. 1 for day after)",{"id":7440,"title":7441,"titles":7442,"content":7443,"level":449},"/features/bookings#template-variables","Template Variables",[232,7153],"VariableDescriptionExample{{customer_name}}Customer's nameEmma van der Berg{{customer_email}}Customer's emailemma.vanderberg@gmail.com{{booking_date}}Formatted booking dateFriday, January 24, 2025{{booking_slot}}Time slot(s)14:00 - 15:00{{booking_reference}}Booking reference numberBK-2025-0124{{location_name}}Location nameCourt A{{location_title}}Location titleTennis Court A - Indoor{{location_street}}Street addressSportlaan 42{{location_city}}CityAmsterdam{{location_address}}Full addressSportlaan 42, 1081 KL Amsterdam{{location_content}}Location descriptionIndoor court with professional lighting...{{team_name}}Team/business nameAmsterdam Tennis Club{{team_email}}Team contact emailinfo@amsterdamtennis.nl{{team_phone}}Team contact phone+31 20 987 6543 The template editor supports rich text editing via TipTap, variable autocomplete (type {{), live preview with demo data, translation tabs (EN, NL, FR), and per-location or global templates.",{"id":7445,"title":7446,"titles":7447,"content":7448,"level":391},"/features/bookings#i18n","i18n",[232],"Translation keys are namespaced under bookings.*: bookings.sidebar.*    - Sidebar UI\nbookings.cart.*       - Cart UI\nbookings.form.*       - Form labels\nbookings.status.*     - Booking statuses\nbookings.confirm.*    - Confirmation dialogs\nbookings.buttons.*    - Action buttons\nbookings.meta.*       - Metadata labels\nbookings.common.*     - Common terms Locale files are in i18n/locales/{en,nl,fr}.json and auto-merge when the layer is extended.",{"id":7450,"title":3170,"titles":7451,"content":7452,"level":391},"/features/bookings#use-cases",[232],"Sports facilities: Court reservations with per-slot capacityHealthcare: Appointment scheduling with open day rulesServices: Salon bookings, consultationsRentals: Equipment pools with inventory modeEvents: Workshops, classes with group bookings and monthly limits",{"id":7454,"title":4341,"titles":7455,"content":7456,"level":391},"/features/bookings#related",[232],"Email -- Email notificationsAuthentication -- User authenticationPackage Architecture -- Package details html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":237,"title":236,"titles":7458,"content":7459,"level":385},[],"Event-based Point of Sale system for markets and pop-up events.",{"id":7461,"title":236,"titles":7462,"content":7463,"level":385},"/features/sales#sales-pos",[],"Event-based Point of Sale system for markets and pop-up events. Status: Beta -- under active development. API may change.",{"id":7465,"title":936,"titles":7466,"content":7467,"level":391},"/features/sales#overview",[236],"The Sales package (@fyit/crouton-sales) provides a complete Point of Sale system designed for pop-up events, markets, and temporary retail situations. It includes customer-facing order interfaces, cart management, helper (volunteer/staff) authentication, and optional thermal receipt printing. Features: Event-based product and category managementShopping cart with quantity controls and product optionsPIN-based helper/volunteer authentication (via @fyit/crouton-auth)Client selector with create-on-typeOffline awareness bannerOptional ESC/POS thermal receipt printingi18n support (English, Dutch, French)",{"id":7469,"title":18,"titles":7470,"content":528,"level":391},"/features/sales#quick-start",[236],{"id":7472,"title":7473,"titles":7474,"content":7475,"level":449},"/features/sales#_1-install-and-configure","1. Install and Configure",[236,18],"Add the sales layer to your nuxt.config.ts: export default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-auth',   // Required for helper authentication\n    '@fyit/crouton-sales',\n    './layers/sales'        // Generated layer (see step 3)\n  ]\n})",{"id":7477,"title":7478,"titles":7479,"content":7480,"level":449},"/features/sales#_2-copy-schemas","2. Copy Schemas",[236,18],"Copy the JSON schema files from the package to your project's schemas/ directory: cp node_modules/@fyit/crouton-sales/schemas/*.json ./schemas/",{"id":7482,"title":7483,"titles":7484,"content":7485,"level":449},"/features/sales#_3-generate-collections","3. Generate Collections",[236,18],"Configure crouton.config.js with sales as the layer name, then generate: pnpm crouton config This generates the ./layers/sales/ layer with collection composables like useSalesProducts(), useSalesCategories(), etc.",{"id":7487,"title":7488,"titles":7489,"content":7490,"level":449},"/features/sales#_4-run-migrations","4. Run Migrations",[236,18],"npx nuxt db:generate\nnpx nuxt db:migrate",{"id":7492,"title":7493,"titles":7494,"content":7495,"level":449},"/features/sales#_5-use-the-order-interface","5. Use the Order Interface",[236,18],"\u003Cscript setup lang=\"ts\">\nconst eventId = 'your-event-id'\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CSalesClientOrderInterface :event-id=\"eventId\" />\n\u003C/template>",{"id":7497,"title":5437,"titles":7498,"content":528,"level":391},"/features/sales#composables",[236],{"id":7500,"title":7501,"titles":7502,"content":7503,"level":449},"/features/sales#useposorderoptions","usePosOrder(options?)",[236,5437],"Cart management, price calculation, and checkout. This composable manages the full cart lifecycle. const {\n  // State (reactive)\n  cartItems,           // Readonly\u003CRef\u003CCartItem[]>>\n  selectedEventId,     // Ref\u003Cstring | null>\n  selectedClientId,    // Ref\u003Cstring | null>\n  selectedClientName,  // Ref\u003Cstring | null>\n  overallRemarks,      // Ref\u003Cstring | null>\n  isPersonnel,         // Ref\u003Cboolean>\n  isCheckingOut,       // Readonly\u003CRef\u003Cboolean>>\n\n  // Computed\n  cartTotal,           // ComputedRef\u003Cnumber>\n  cartItemCount,       // ComputedRef\u003Cnumber>\n  isEmpty,             // ComputedRef\u003Cboolean>\n\n  // Methods\n  addToCart,            // (product, remarks?, selectedOptions?) => void\n  removeFromCart,       // (index: number) => void\n  updateQuantity,      // (index: number, quantity: number) => void\n  clearCart,            // () => void\n  getItemPrice,        // (item: CartItem) => number\n  getItemTotal,        // (item: CartItem) => number\n  checkout,            // () => Promise\u003CCreateOrderResponse>\n} = usePosOrder(options?)",{"id":7505,"title":3362,"titles":7506,"content":7507,"level":748},"/features/sales#options",[236,5437,7501],"interface UsePosOrderOptions {\n  /** API base path for orders, defaults to '/api/sales/events' */\n  apiBasePath?: string\n  /** Whether to trigger print queue after checkout */\n  enablePrinting?: boolean\n}",{"id":7509,"title":1608,"titles":7510,"content":7511,"level":748},"/features/sales#usage",[236,5437,7501],"const { cartItems, addToCart, selectedEventId, checkout } = usePosOrder()\n\n// Set the event context\nselectedEventId.value = 'event-123'\n\n// Add a product (increments quantity if already in cart)\naddToCart(product)\n\n// Add with remarks and options (always added as new line item)\naddToCart(product, 'Extra sauce', ['option-1', 'option-2'])\n\n// Checkout -- creates order via POST to apiBasePath/{eventId}/orders\nconst response = await checkout()\n// response: { order: { id, eventOrderNumber, status }, items, eventOrderNumber }",{"id":7513,"title":7514,"titles":7515,"content":7516,"level":748},"/features/sales#calculateitempriceitem","calculateItemPrice(item)",[236,5437,7501],"Utility function defined inside usePosOrder.ts that calculates an item's price including option price modifiers. It is exported alongside the composable and auto-imported when the @fyit/crouton-sales layer is extended -- no import statement is needed: // Auto-imported in Nuxt layer context -- no import needed\nconst price = calculateItemPrice(cartItem)",{"id":7518,"title":7519,"titles":7520,"content":7521,"level":449},"/features/sales#usehelperauth","useHelperAuth()",[236,5437],"PIN-based authentication for event helpers (volunteers, staff). Wraps @fyit/crouton-auth's scoped access system. const {\n  // State (reactive)\n  isHelper,            // ComputedRef\u003Cboolean>\n  helperName,          // ComputedRef\u003Cstring>\n  eventId,             // ComputedRef\u003Cstring>\n  teamId,              // ComputedRef\u003Cstring>\n  token,               // ComputedRef\u003Cstring>\n  helperSession,       // Readonly\u003CRef\u003CHelperSession | null>>\n  isLoading,           // Readonly\u003CRef\u003Cboolean>>\n  error,               // Readonly\u003CRef\u003Cstring | null>>\n\n  // Methods\n  login,               // (options: HelperLoginOptions) => Promise\u003Cboolean>\n  logout,              // () => Promise\u003Cvoid>\n  validateToken,       // () => Promise\u003Cboolean>\n  loadSession,         // () => HelperSession | null\n  setSession,          // (session: HelperSession) => void\n  clearSession,        // () => void\n} = useHelperAuth()",{"id":7523,"title":7524,"titles":7525,"content":7526,"level":748},"/features/sales#login-options","Login Options",[236,5437,7519],"interface HelperLoginOptions {\n  teamId: string\n  eventId: string\n  pin: string\n  helperName?: string   // For new helpers\n  helperId?: string     // For returning helpers\n}",{"id":7528,"title":1608,"titles":7529,"content":7530,"level":748},"/features/sales#usage-1",[236,5437,7519],"const { isHelper, helperName, login, logout } = useHelperAuth()\n\n// Login with PIN\nconst success = await login({\n  teamId: 'team-123',\n  eventId: 'event-456',\n  pin: '1234',\n  helperName: 'John'\n})\n\nif (isHelper.value) {\n  console.log(`Welcome, ${helperName.value}!`)\n}\n\nawait logout() Sessions are stored in localStorage and a cookie (pos-helper-token) with an 8-hour expiry. The composable automatically loads existing sessions on client-side mount.",{"id":7532,"title":5354,"titles":7533,"content":7534,"level":391},"/features/sales#components",[236],"All components are auto-imported with the Sales prefix.",{"id":7536,"title":7537,"titles":7538,"content":7539,"level":449},"/features/sales#customer-facing-client","Customer-Facing (Client/)",[236,5354],"ComponentPropsDescriptionSalesClientOrderInterfaceeventId (required), productsCollection?, categoriesCollection?Main order page -- combines category tabs, product grid, cart sidebar, options modal, and mobile drawerSalesClientProductListproductsProduct grid with inline option selection (single/multi-select)SalesClientCategoryTabscategories, modelValue, productCountsCategory tab navigation with product countsSalesClientCartitems, total, disabled, clientRequired?, hasClient?Shopping cart with quantity controls. Emits: updateQuantity, remove, checkout, clearSalesClientCartTotalcount, total, size? ('sm' or 'lg')Order total display with animated item count badgeSalesClientProductOptionsSelectmodelValue, options, multipleAllowed?Product option picker (grid of selectable cards)SalesClientSelectorclients, useReusableClients, highlight?, clientId?, clientName?, collectionName?Client selector -- either a searchable dropdown with create-on-type or a free-text inputSalesClientOfflineBanner(none)Shows a warning banner when the browser is offline",{"id":7541,"title":7542,"titles":7543,"content":7544,"level":449},"/features/sales#orders-pos","Orders (Pos/)",[236,5354],"ComponentPropsDescriptionSalesPosOrdersListeventId?, collectionName?, showReprint?, printApiBasePath?, refreshInterval?Orders table with status filter tabs, auto-refresh toggle, and optional reprint button",{"id":7546,"title":7547,"titles":7548,"content":7549,"level":449},"/features/sales#admin-admin","Admin (Admin/)",[236,5354],"ComponentPropsDescriptionSalesAdminPosSidebarbasePath (required), showPrinters?, showHelpers?, additionalItems?Vertical navigation menu for POS admin (events, products, categories, locations, clients)",{"id":7551,"title":7552,"titles":7553,"content":7554,"level":449},"/features/sales#print-settings-settings-opt-in","Print Settings (Settings/) -- Opt-in",[236,5354],"ComponentPropsDescriptionSalesSettingsReceiptSettingsModalmodelValue, apiEndpointModal for customizing receipt text (headers, footer, section titles)SalesSettingsPrintPreviewModalmodelValue, printer, testPrintApiBase, receiptSettings, locationName?Receipt preview modal with visual preview and test print button",{"id":7556,"title":7557,"titles":7558,"content":7559,"level":391},"/features/sales#schemas-collections","Schemas (Collections)",[236],"The package provides JSON schemas for 10 collections. All use the sales prefix when generated.",{"id":7561,"title":7562,"titles":7563,"content":7564,"level":449},"/features/sales#core-collections","Core Collections",[236,7557],"SchemaTable NameKey Fieldsevents.jsonsalesEventstitle, slug, startDate, endDate, status, isCurrent, helperPinproducts.jsonsalesProductseventId, categoryId, locationId, title, price, isActive, hasOptions, options (repeater)categories.jsonsalesCategorieseventId, title, displayOrderorders.jsonsalesOrderseventId, clientId, clientName, eventOrderNumber, status, isPersonnel, overallRemarksorderItems.jsonsalesOrderitemsorderId, productId, quantity, unitPrice, totalPrice, remarks, selectedOptionslocations.jsonsalesLocationseventId, titleclients.jsonsalesClientstitle, isReusableeventSettings.jsonsalesEventsettingseventId, settingKey, settingValue",{"id":7566,"title":7567,"titles":7568,"content":7569,"level":449},"/features/sales#print-collections-opt-in","Print Collections (Opt-in)",[236,7557],"SchemaTable NameKey Fieldsprinters.jsonsalesPrinterseventId, locationId, title, ipAddress, port, showPrices, isActiveprintQueues.jsonsalesPrintqueueseventId, orderId, printerId, status (0=pending, 1=printing, 2=done, 9=error), printData, printMode",{"id":7571,"title":292,"titles":7572,"content":7573,"level":391},"/features/sales#server-utilities",[236],"The package provides server-side utilities for thermal receipt printing. These are imported from the package but require your generated layer's database tables to function.",{"id":7575,"title":7576,"titles":7577,"content":7578,"level":449},"/features/sales#receipt-formatter","Receipt Formatter",[236,292],"Note: These server utilities are auto-imported by Nuxt when you extend the @fyit/crouton-sales layer. The import paths shown below are for reference only -- in practice, you do not need explicit import statements in your Nuxt server routes. // Auto-imported in Nuxt server context -- no import needed\n// Internal path (for reference): @fyit/crouton-sales/server/utils/receipt-formatter\n\n// Format a receipt -- returns { base64: string, rawBuffer: Buffer }\nconst receipt = formatReceipt({\n  orderNumber: 42,\n  orderId: 'order-123',\n  teamName: 'My Team',\n  eventName: 'Summer Market',\n  items: [{ name: 'Coffee', quantity: 2, price: 3.50 }],\n  total: 7.00,\n  printMode: 'receipt',  // 'kitchen' or 'receipt'\n  showPrices: true,\n  createdAt: new Date()\n})\n\n// Generate a test receipt\nconst testReceipt = formatTestReceipt('Kitchen Printer', '192.168.1.100')",{"id":7580,"title":7581,"titles":7582,"content":7583,"level":449},"/features/sales#print-queue-service","Print Queue Service",[236,292],"// Auto-imported in Nuxt server context -- no import needed\n// Internal path (for reference): @fyit/crouton-sales/server/utils/print-queue-service\n\n// Generate print jobs (returns data only -- you insert into your DB)\nconst jobs = generatePrintJobsForOrder(orderOptions, orderItems, printers)\n\nfor (const job of jobs) {\n  await db.insert(salesPrintqueues).values({\n    teamId, eventId, orderId,\n    printerId: job.printerId,\n    printData: job.printData,\n    printMode: job.printMode,\n    status: PRINT_STATUS.PENDING,\n    retryCount: 0\n  })\n}",{"id":7585,"title":7586,"titles":7587,"content":7588,"level":391},"/features/sales#api-routes","API Routes",[236],"This package does not ship API routes. API routes are generated per-app via the crouton CLI when you run pnpm crouton config. The generated ./layers/sales/ layer provides standard CRUD endpoints for each collection. For the print server (polling jobs, updating status), see the endpoint templates in @fyit/crouton-sales/server/api/sales/print-server/README.md. These templates must be copied and adapted to your project.",{"id":7590,"title":7591,"titles":7592,"content":7593,"level":391},"/features/sales#helper-authentication-flow","Helper Authentication Flow",[236],"Admin creates an event with a helperPin field (up to 6 characters)Helper opens the POS interface and enters the event PIN + their nameThe app calls POST /api/teams/{teamId}/pos-events/{eventId}/helper-loginA scoped access token is created (8-hour expiry) and stored in a cookie + localStorageServer-side validation uses @fyit/crouton-auth's scoped access: // Auto-imported when extending @fyit/crouton-auth layer -- no import needed\n// Internal path (for reference): @fyit/crouton-auth/server/utils/scoped-access\nexport default defineEventHandler(async (event) => {\n  const access = await requireScopedAccess(event, 'pos-helper-token')\n  // access.displayName, access.resourceId, access.organizationId, access.role\n}) requireScopedAccess is auto-imported via Nitro when your app extends the @fyit/crouton-auth layer. The layer configures nitro.imports.dirs to include its server/utils/ directory. If you are not extending the layer and instead importing directly, note that @fyit/crouton-auth/server is not an exported path -- use the granular export @fyit/crouton-auth/server/utils/scoped-access instead.",{"id":7595,"title":1789,"titles":7596,"content":7597,"level":391},"/features/sales#configuration",[236],"export default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-auth',\n    '@fyit/crouton-sales',\n    './layers/sales'\n  ]\n}) The package requires @fyit/crouton-core and @fyit/crouton-auth as peer dependencies. The @nuxtjs/i18n module is included automatically. For thermal printing, node-thermal-printer is an optional peer dependency.",{"id":7599,"title":7446,"titles":7600,"content":7601,"level":391},"/features/sales#i18n",[236],"The package ships translations for English (en), Dutch (nl), and French (fr). Translation keys are namespaced under sales.* (e.g., sales.cart.empty, sales.orders.title, sales.sidebar.events).",{"id":7603,"title":4341,"titles":7604,"content":7605,"level":391},"/features/sales#related",[236],"Authentication - User and scoped access authenticationPackage Architecture - Package details html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":241,"title":240,"titles":7607,"content":7608,"level":385},[],"Rich text editing for Nuxt Crouton collections, powered by Nuxt UI's UEditor (TipTap-based).",{"id":7610,"title":240,"titles":7611,"content":7612,"level":385},"/features/rich-text#rich-text-editor",[],"Rich text editing for Nuxt Crouton collections, powered by Nuxt UI's UEditor (TipTap-based). Status: Beta",{"id":7614,"title":936,"titles":7615,"content":7616,"level":391},"/features/rich-text#overview",[240],"The @fyit/crouton-editor package is a Nuxt layer that wraps Nuxt UI's UEditor with Crouton-specific defaults, variable insertion support, and live preview functionality. Features: WYSIWYG editing via Nuxt UI's UEditor5 components: simple editor, block editor, variable menu, preview, and editor-with-previewVariable insertion ({{variable_name}}) with live preview interpolationBlock editor with custom NodeView support and property panelsImage upload (toolbar, paste, drag-and-drop)Optional AI translation integrationOptional real-time collaboration via Yjs",{"id":7618,"title":18,"titles":7619,"content":528,"level":391},"/features/rich-text#quick-start",[240],{"id":7621,"title":13,"titles":7622,"content":7623,"level":449},"/features/rich-text#installation",[240,18],"pnpm add @fyit/crouton-editor Peer dependencies: @nuxt/ui v4.3.0+ (provides the TipTap-based UEditor)@fyit/crouton-core@nuxt/icon v1.0.0+",{"id":7625,"title":1789,"titles":7626,"content":7627,"level":449},"/features/rich-text#configuration",[240,18],"Add the editor layer to your nuxt.config.ts: export default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-editor'\n  ]\n})",{"id":7629,"title":4173,"titles":7630,"content":7631,"level":449},"/features/rich-text#basic-usage",[240,18],"\u003Cscript setup lang=\"ts\">\nconst content = ref('\u003Cp>Hello world!\u003C/p>')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonEditorSimple v-model=\"content\" />\n\u003C/template>",{"id":7633,"title":5354,"titles":7634,"content":7635,"level":391},"/features/rich-text#components",[240],"All components auto-register with the CroutonEditor prefix.",{"id":7637,"title":7638,"titles":7639,"content":7640,"level":449},"/features/rich-text#croutoneditorsimple","CroutonEditorSimple",[240,5354],"The main editor component. Wraps UEditor with a fixed toolbar and a bubble toolbar that appears on text selection.",{"id":7642,"title":4987,"titles":7643,"content":7644,"level":748},"/features/rich-text#props",[240,5354,7638],"PropTypeDefaultDescriptionmodelValuestring | null''Content (v-model)placeholderstring-Placeholder textcontentType'html' | 'markdown' | 'json''html'Output formateditablebooleantrueEnable/disable editingautofocusboolean | 'start' | 'end' | 'all' | number-Focus behaviorshowToolbarbooleantrueShow the fixed toolbarshowBubbleToolbarbooleantrueShow bubble toolbar on text selectionextensionsany[]-Additional TipTap extensionsenableTranslationAIbooleanfalseShow AI translation button (requires @fyit/crouton-ai)translationContextTranslationContext-Context for AI translationonTranslationAccept(text: string) => void-Callback when translation is acceptedenableImageUploadbooleanfalseEnable image upload via toolbar, paste, and drag-drop",{"id":7646,"title":5367,"titles":7647,"content":7648,"level":748},"/features/rich-text#events",[240,5354,7638],"EventPayloadDescriptionupdate:modelValuestringContent changedcreate{ editor: Editor }Editor instance createdupdate{ editor: Editor }Content updatedtranslationAcceptstringTranslation accepted by user",{"id":7650,"title":7651,"titles":7652,"content":7653,"level":748},"/features/rich-text#toolbar","Toolbar",[240,5354,7638],"The fixed toolbar includes: undo/redo, headings (H1-H3), lists (bullet/ordered), blockquote, code block, horizontal rule, bold, italic, underline, strikethrough, inline code, highlight, and link. The bubble toolbar appears on text selection with a \"Turn into\" dropdown, text formatting marks, and link insertion.",{"id":7655,"title":3367,"titles":7656,"content":7657,"level":748},"/features/rich-text#examples",[240,5354,7638],"In a form: \u003Cscript setup lang=\"ts\">\nconst state = ref({\n  title: '',\n  content: '\u003Cp>\u003C/p>'\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm :state=\"state\" @submit=\"handleSubmit\">\n    \u003CUFormField label=\"Title\" name=\"title\">\n      \u003CUInput v-model=\"state.title\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Content\" name=\"content\">\n      \u003CCroutonEditorSimple v-model=\"state.content\" />\n    \u003C/UFormField>\n\n    \u003CUButton type=\"submit\">Save\u003C/UButton>\n  \u003C/UForm>\n\u003C/template> With image upload: \u003CCroutonEditorSimple\n  v-model=\"content\"\n  enable-image-upload\n/> When enableImageUpload is true, the toolbar shows an \"Insert Image\" button. Pasting or dragging images into the editor also triggers an upload to /api/upload-image. With AI translation: \u003CCroutonEditorSimple\n  v-model=\"content\"\n  enable-translation-ai\n  :translation-context=\"{\n    sourceText: selectedText,\n    sourceLanguage: 'en',\n    targetLanguage: 'nl',\n    fieldType: 'description'\n  }\"\n  @translation-accept=\"handleTranslation\"\n/>",{"id":7659,"title":7660,"titles":7661,"content":7662,"level":449},"/features/rich-text#croutoneditorblocks","CroutonEditorBlocks",[240,5354],"A block-based editor built on UEditor with slash command support, custom NodeView blocks, and a property panel system. Designed for structured page content. Supports real-time collaboration via Yjs.",{"id":7664,"title":4987,"titles":7665,"content":7666,"level":748},"/features/rich-text#props-1",[240,5354,7660],"PropTypeDefaultDescriptionmodelValuestring | TipTapDoc | null''Content (v-model)placeholderstring-Placeholder textcontentType'html' | 'markdown' | 'json''json'Output formateditablebooleantrueEnable/disable editingautofocusboolean | 'start' | 'end' | 'all' | number-Focus behaviorextensionsany[]-Custom TipTap block extensionsshowToolbarbooleantrueShow the toolbarshowBubbleToolbarbooleantrueShow bubble toolbar on selectionsuggestionItemsBlockSuggestionItem[][]Block items for slash command menuyxmlFragmentY.XmlFragment-Yjs fragment for real-time collaborationcollabProvider{ awareness: any }-Collaboration provider for cursor awarenesscollabUser{ name: string; color?: string }-User info for collaboration cursors",{"id":7668,"title":5367,"titles":7669,"content":7670,"level":748},"/features/rich-text#events-1",[240,5354,7660],"EventPayloadDescriptionupdate:modelValuestring | TipTapDocContent changedcreate{ editor: Editor }Editor instance createdupdate{ editor: Editor }Content updatedblock:select{ node, pos } | nullBlock selected/deselectedblock:edit{ node, pos }Block edit requested (property panel)",{"id":7672,"title":5372,"titles":7673,"content":7674,"level":748},"/features/rich-text#slots",[240,5354,7660],"The property-panel slot provides a way to render a custom property panel for editing block attributes: \u003CCroutonEditorBlocks\n  v-model=\"content\"\n  :extensions=\"[MyBlockExtension]\"\n  :suggestion-items=\"blockItems\"\n  content-type=\"json\"\n>\n  \u003Ctemplate #property-panel=\"{ selectedNode, isOpen, close, updateAttrs, deleteBlock }\">\n    \u003CUSlideover :model-value=\"isOpen\" @update:model-value=\"!$event && close()\">\n      \u003Ctemplate #content>\n        \u003CMyPropertyPanel\n          v-if=\"selectedNode\"\n          :node=\"selectedNode.node\"\n          @update=\"updateAttrs\"\n          @delete=\"deleteBlock\"\n          @close=\"close\"\n        />\n      \u003C/template>\n    \u003C/USlideover>\n  \u003C/template>\n\u003C/CroutonEditorBlocks> Slot PropTypeDescriptionselectedNode{ node, pos } | nullCurrently selected blockisOpenbooleanProperty panel open stateclose() => voidClose the panelupdateAttrs(attrs: Record\u003Cstring, unknown>) => voidUpdate block attributesdeleteBlock() => voidDelete selected block",{"id":7676,"title":7677,"titles":7678,"content":7679,"level":748},"/features/rich-text#blocksuggestionitem","BlockSuggestionItem",[240,5354,7660],"interface BlockSuggestionItem {\n  type: string          // Block type name (e.g., 'heroBlock')\n  label: string         // Display label in menu\n  description?: string  // Description shown in menu\n  icon?: string         // Icon name (e.g., 'i-lucide-layout-template')\n  category?: string     // Category for grouping\n  command: string       // TipTap command name (e.g., 'insertHeroBlock')\n}",{"id":7681,"title":7682,"titles":7683,"content":7684,"level":449},"/features/rich-text#croutoneditorvariables","CroutonEditorVariables",[240,5354],"A variable insertion menu using Nuxt UI's UEditorMentionMenu. Triggered by typing {{ in the editor. Renders variables with optional category grouping.",{"id":7686,"title":4987,"titles":7687,"content":7688,"level":748},"/features/rich-text#props-2",[240,5354,7682],"PropTypeDefaultDescriptioneditorEditor-TipTap editor instance (from UEditor slot)variablesEditorVariable[][]Flat list of variablesgroupsEditorVariableGroup[]-Grouped variables (alternative to flat list)charstring'{{'Trigger character",{"id":7690,"title":1608,"titles":7691,"content":7692,"level":748},"/features/rich-text#usage",[240,5354,7682],"Pass the editor instance from the UEditor slot: \u003CCroutonEditorSimple v-model=\"content\">\n  \u003Ctemplate #default=\"{ editor }\">\n    \u003CCroutonEditorVariables\n      :editor=\"editor\"\n      :variables=\"emailVariables\"\n    />\n  \u003C/template>\n\u003C/CroutonEditorSimple>\n\n\u003Cscript setup lang=\"ts\">\nconst emailVariables = [\n  { name: 'customer_name', label: 'Customer Name', sample: 'John Doe' },\n  { name: 'booking_date', label: 'Booking Date', sample: 'January 15, 2024' },\n]\n\u003C/script> Variables with a category property are automatically grouped under category headers.",{"id":7694,"title":7695,"titles":7696,"content":7697,"level":449},"/features/rich-text#croutoneditorpreview","CroutonEditorPreview",[240,5354],"Live preview component with variable interpolation. Displays content with {{variables}} replaced by provided values or sample values from variable definitions.",{"id":7699,"title":4987,"titles":7700,"content":7701,"level":748},"/features/rich-text#props-3",[240,5354,7695],"PropTypeDefaultDescriptioncontentstring | Record\u003Cstring, any>-Raw content with {{variables}}, or TipTap JSON documenttitlestring'Preview'Panel titlevaluesRecord\u003Cstring, string>-Values for interpolationvariablesEditorVariable[]-Variable definitions (for sample values)mode'inline' | 'panel''panel'Display modeexpandablebooleantrueAllow expanding to modalshowVariableCountbooleantrueShow variable count in headercontainerClassstring-Custom container CSS classcontentClassstring-Custom content area CSS class",{"id":7703,"title":7704,"titles":7705,"content":7706,"level":748},"/features/rich-text#modes","Modes",[240,5354,7695],"panel (default): Full preview with header, variable count indicator, and optional expand-to-modal button.inline: Compact 40px-high thumbnail with an eye button to open a modal for full view.",{"id":7708,"title":1608,"titles":7709,"content":7710,"level":748},"/features/rich-text#usage-1",[240,5354,7695],"\u003CCroutonEditorPreview\n  :content=\"emailBody\"\n  :variables=\"emailVariables\"\n  :values=\"{ customer_name: 'Jane Smith' }\"\n  title=\"Email Preview\"\n/>",{"id":7712,"title":7713,"titles":7714,"content":7715,"level":449},"/features/rich-text#croutoneditorwithpreview","CroutonEditorWithPreview",[240,5354],"Combined editor and preview with tab-based layout. Shows an \"Editor\" tab and a \"Preview\" tab, with quick-insert variable chips above the editor.",{"id":7717,"title":4987,"titles":7718,"content":7719,"level":748},"/features/rich-text#props-4",[240,5354,7713],"PropTypeDefaultDescriptionmodelValuestring | null''Content (v-model)variablesEditorVariable[][]Variables for insertionpreviewValuesRecord\u003Cstring, string>-Override sample values in previewpreviewTitlestring'Preview'Preview panel titlecontentType'html' | 'markdown' | 'json''html'Output formatplaceholderstring-Placeholder texteditablebooleantrueEnable/disable editingshowVariableChipsbooleantrueShow quick-insert variable chips above editorextensionsany[]-Additional TipTap extensions",{"id":7721,"title":5367,"titles":7722,"content":7723,"level":748},"/features/rich-text#events-2",[240,5354,7713],"EventPayloadDescriptionupdate:modelValuestringContent changed",{"id":7725,"title":1608,"titles":7726,"content":7727,"level":748},"/features/rich-text#usage-2",[240,5354,7713],"\u003Cscript setup lang=\"ts\">\nconst emailVariables = [\n  { name: 'customer_name', label: 'Customer Name', category: 'customer', sample: 'John Doe' },\n  { name: 'booking_date', label: 'Booking Date', category: 'booking', sample: 'Monday, January 15, 2024' },\n  { name: 'location_name', label: 'Location', category: 'location', sample: 'Main Office' },\n]\n\nconst sampleData = {\n  customer_name: 'Jane Smith',\n  booking_date: 'Tuesday, January 16, 2024',\n  location_name: 'Downtown Branch'\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonEditorWithPreview\n    v-model=\"emailBody\"\n    :variables=\"emailVariables\"\n    :preview-values=\"sampleData\"\n    preview-title=\"Email Preview\"\n    placeholder=\"Write your email template...\"\n  />\n\u003C/template>",{"id":7729,"title":5437,"titles":7730,"content":528,"level":391},"/features/rich-text#composables",[240],{"id":7732,"title":7733,"titles":7734,"content":7735,"level":449},"/features/rich-text#useeditorvariables","useEditorVariables",[240,5437],"Utilities for working with {{variable}} placeholders in editor content. const {\n  interpolate,\n  extractVariables,\n  getSampleValues,\n  findUndefinedVariables\n} = useEditorVariables() FunctionSignatureDescriptioninterpolate(content: string, values: Record\u003Cstring, string>) => stringReplace {{vars}} with valuesextractVariables(content: string) => string[]Get unique variable names from contentgetSampleValues(variables: EditorVariable[]) => Record\u003Cstring, string>Get sample values from variable definitionsfindUndefinedVariables(content: string, variables: EditorVariable[]) => string[]Find variables used in content but not defined Examples: const { interpolate, extractVariables } = useEditorVariables()\n\n// Interpolate content\nconst rendered = interpolate(\n  'Hello {{customer_name}}!',\n  { customer_name: 'John' }\n)\n// Result: \"Hello John!\"\n\n// Extract variables\nconst vars = extractVariables('Hello {{name}}, your booking is on {{date}}')\n// Result: ['name', 'date']",{"id":7737,"title":6551,"titles":7738,"content":7739,"level":391},"/features/rich-text#types",[240],"import type { EditorVariable, EditorVariableGroup } from '#crouton-editor/types/editor'\n\ninterface EditorVariable {\n  name: string        // Variable name: \"customer_name\"\n  label: string       // Display label: \"Customer Name\"\n  description?: string // Help text\n  icon?: string       // Lucide icon name\n  category?: string   // Grouping: \"customer\", \"booking\"\n  sample?: string     // Sample value for preview\n}\n\ninterface EditorVariableGroup {\n  label: string\n  variables: EditorVariable[]\n}",{"id":7741,"title":1789,"titles":7742,"content":7743,"level":391},"/features/rich-text#configuration-1",[240],"The layer auto-registers components with the CroutonEditor prefix at priority 1 (overriding stubs from crouton-core) and auto-imports composables: // nuxt.config.ts (layer internals)\nexport default defineNuxtConfig({\n  components: {\n    dirs: [{\n      path: 'app/components',\n      prefix: 'CroutonEditor',\n      global: true,\n      priority: 1\n    }]\n  },\n  imports: {\n    dirs: ['app/composables']\n  },\n  alias: {\n    '#crouton-editor': 'app'\n  }\n})",{"id":7745,"title":4538,"titles":7746,"content":7747,"level":391},"/features/rich-text#generator-integration",[240],"Mark fields in your collection schema to use the editor automatically: {\n  \"collections\": {\n    \"posts\": {\n      \"fields\": {\n        \"content\": {\n          \"type\": \"text\",\n          \"meta\": {\n            \"label\": \"Content\",\n            \"component\": \"CroutonEditorSimple\"\n          }\n        }\n      }\n    }\n  }\n} Generated form components will render CroutonEditorSimple for that field. Generated list components will use CroutonEditorPreview for rich text columns.",{"id":7749,"title":4548,"titles":7750,"content":7751,"level":391},"/features/rich-text#database-storage",[240],"The editor outputs HTML by default. Store it in a TEXT column: export const blogPosts = sqliteTable('blog_posts', {\n  id: text('id').primaryKey(),\n  title: text('title').notNull(),\n  content: text('content').notNull(),\n  createdAt: integer('createdAt', { mode: 'timestamp' })\n})",{"id":7753,"title":7754,"titles":7755,"content":7756,"level":391},"/features/rich-text#displaying-content","Displaying Content",[240],"Render editor HTML safely on the frontend: \u003Ctemplate>\n  \u003Cdiv class=\"prose dark:prose-invert\" v-html=\"post.content\" />\n\u003C/template> Security: Always sanitize HTML on the backend before saving to prevent XSS attacks.",{"id":7758,"title":4341,"titles":7759,"content":7760,"level":391},"/features/rich-text#related",[240],"Internationalization - Multilingual content supportGenerator Schema Format - Field metadata options html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .su27w, html code.shiki .su27w{--shiki-light:#916B53;--shiki-default:#916B53;--shiki-dark:#916B53}",{"id":245,"title":244,"titles":7762,"content":7763,"level":385},[],"Centralized asset management with NuxtHub blob storage Status: Experimental - The @fyit/crouton-assets package is in active development. APIs may change before the stable release. Use with caution in production. The assets package extends Nuxt Crouton with a centralized media library system, providing full-featured asset management with team-based ownership, rich metadata tracking, and NuxtHub blob storage integration.",{"id":7765,"title":936,"titles":7766,"content":528,"level":391},"/features/assets#overview",[244],{"id":7768,"title":5326,"titles":7769,"content":7770,"level":449},"/features/assets#package-information",[244,936],"Package: @fyit/crouton-assetsVersion: 0.1.0 (BETA)Type: Nuxt Layer / Addon PackageRepository: nuxt-crouton monorepo",{"id":7772,"title":6333,"titles":7773,"content":7774,"level":449},"/features/assets#whats-included",[244,936],"Components (7): CroutonAssetsPicker - Browse and select assets with type filtering (images, documents, video, audio)CroutonAssetsUploader - Upload files with optional crop step and metadata formCroutonAssetsLibrary - Full asset library view with grid/list layoutCroutonAssetsCard - Asset preview card for list/grid displayCroutonAssetsAssetTile - Compact asset tile with selection supportCroutonAssetsForm - Asset creation form with crop and metadataCroutonAssetsFormUpdate - Asset metadata update form (alt text, translations) Composable (1): useAssetUpload() - Programmatic asset upload/delete with progress tracking Integration: NuxtHub blob storage configuration (auto-enabled via layer)Reference schema for collection generationAuto-detection for asset reference fieldsimage and file first-class CLI field types",{"id":7776,"title":5599,"titles":7777,"content":7778,"level":391},"/features/assets#key-features",[244],"📸 Centralized Library - Single source of truth for all media uploads🎯 Visual Picker - Browse assets with thumbnail grid, search, and type filtering📊 Rich Metadata - Track filename, size, MIME type, category, dimensions, alt text👥 Team-Scoped - Assets automatically scoped to teams/organizations🔍 Search & Filter - Filter by type (images, documents, video, audio) and search by name✂️ Image Cropping - Built-in crop support via cropperjs v2 (aspect ratios, rotate, zoom)♿ Accessibility - Alt text support with i18n integration🔄 Reusable - Reference same asset across multiple collections📁 Multi-File Support - Images, PDFs, video, audio, and documents⚡ Edge Storage - Powered by NuxtHub blob storage on Cloudflare",{"id":7780,"title":13,"titles":7781,"content":528,"level":391},"/features/assets#installation",[244],{"id":7783,"title":426,"titles":7784,"content":7785,"level":449},"/features/assets#prerequisites",[244,13],"Before installing, ensure you have: Nuxt 4.0+@fyit/crouton-core installed (includes NuxtHub blob support)@vueuse/core ^11.0.0 or higher",{"id":7787,"title":6349,"titles":7788,"content":1587,"level":449},"/features/assets#install-package",[244,13],{"id":7790,"title":436,"titles":7791,"content":7792,"level":449},"/features/assets#configure-nuxt",[244,13],"Add the assets layer to your nuxt.config.ts: export default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-assets'  // Add assets layer\n  ],\n  hub: {\n    blob: true  // REQUIRED: Enable NuxtHub blob storage\n  }\n}) Important: NuxtHub blob storage (hub.blob: true) is required for the assets package to function.",{"id":7794,"title":7795,"titles":7796,"content":7797,"level":449},"/features/assets#generate-assets-collection","Generate Assets Collection",[244,13],"The package provides tools (components and composables), but you need to generate the actual collection in your project: crouton-generate core assets \\\n  --fields-file=node_modules/@fyit/crouton-assets/assets-schema.json \\\n  --dialect=sqlite This creates: layers/core/collections/assets/\n├── Form.vue          # CRUD form with asset metadata\n├── List.vue          # Asset library list view\n├── CardMini.vue      # Asset preview card\n├── index.ts          # Exports\n├── schema.ts         # Zod validation\n├── drizzle.ts        # Database schema\n└── api/\n    └── [...].ts      # CRUD endpoints",{"id":7799,"title":85,"titles":7800,"content":528,"level":391},"/features/assets#architecture",[244],{"id":7802,"title":1635,"titles":7803,"content":7804,"level":449},"/features/assets#how-it-works",[244,85],"The assets package follows a toolkit pattern - it provides reusable components and composables that work with your generated collection: Base Package (@fyit/crouton)Core upload infrastructurePOST /api/upload-image - Upload to blobGET /images/[pathname] - Serve from blobBasic upload componentsAssets Package (@fyit/crouton-assets) - This PackageReusable components and composablesCroutonAssetsPicker - Visual selectorCroutonAssetsUploader - Upload + metadata formuseAssetUpload() - Programmatic APIassets-schema.json - Reference schemaYour Project (Generated Collection)layers/core/collections/assets/CRUD forms and API endpointsDatabase tables and validationTeam-scoped asset management",{"id":7806,"title":1640,"titles":7807,"content":7808,"level":449},"/features/assets#upload-flow",[244,85],"1. User selects file in CroutonAssetsUploader\n   ↓\n2. File uploaded to NuxtHub blob storage\n   → POST /api/upload-image\n   → Returns pathname (e.g., \"uploads/abc123.jpg\")\n   ↓\n3. Asset record created in database\n   → POST /api/teams/[teamId]/assets\n   → Stores: filename, pathname, contentType, size, alt, etc.\n   ↓\n4. Asset now available in media library\n   → GET /api/teams/[teamId]/assets\n   → CroutonAssetsPicker displays it",{"id":7810,"title":1603,"titles":7811,"content":7812,"level":449},"/features/assets#database-schema",[244,85],"The generated assets collection includes these fields: {\n  id: string              // Unique identifier (primaryKey)\n  teamId: string          // Team/organization ownership (required)\n  userId: string          // User who uploaded (required)\n  filename: string        // Original filename (required)\n  pathname: string        // Blob storage path (required)\n  contentType: string     // MIME type (image/jpeg, etc)\n  size: number            // File size in bytes\n  category: string        // 'image' | 'video' | 'audio' | 'document' | 'other'\n  width: number           // Image width in px (0 for non-images)\n  height: number          // Image height in px (0 for non-images)\n  alt: string             // Alt text for accessibility\n  uploadedAt: Date        // Upload timestamp\n  createdAt: Date         // Record created (auto)\n  updatedAt: Date         // Record updated (auto)\n  updatedBy: string       // Last modifier\n}",{"id":7814,"title":5354,"titles":7815,"content":528,"level":391},"/features/assets#components",[244],{"id":7817,"title":7818,"titles":7819,"content":7820,"level":449},"/features/assets#croutonassetspicker","CroutonAssetsPicker",[244,5354],"Browse and select assets from your media library.",{"id":7822,"title":4987,"titles":7823,"content":7824,"level":748},"/features/assets#props",[244,5354,7818],"interface Props {\n  collection?: string  // Collection name (default: 'crouton-assets')\n  crop?: boolean | { aspectRatio?: number }  // Enable image cropping\n}\n\n// v-model\nmodelValue: string  // Selected asset ID",{"id":7826,"title":183,"titles":7827,"content":7828,"level":748},"/features/assets#features",[244,5354,7818],"Grid View: Thumbnail grid with 4-column layoutSearch: Real-time filtering by filename or alt textUpload: Inline upload button opens modal with uploaderSelection: Visual feedback with border and checkmarkAuto-refresh: Refreshes list after uploadLoading States: Skeleton loading for pending data",{"id":7830,"title":1608,"titles":7831,"content":7832,"level":748},"/features/assets#usage",[244,5354,7818],"In Forms (Schema Definition) The easiest way is to reference assets in your collection schema: {\n  \"imageId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"assets\",\n    \"meta\": {\n      \"component\": \"CroutonAssetsPicker\",\n      \"label\": \"Featured Image\",\n      \"area\": \"main\"\n    }\n  }\n} Auto-Detection: If your refTarget points to assets, images, files, or media, the generator automatically uses CroutonAssetsPicker - no need to specify the component! Direct Usage \u003Ctemplate>\n  \u003CUFormField label=\"Product Image\" name=\"imageId\">\n    \u003CCroutonAssetsPicker v-model=\"state.imageId\" />\n  \u003C/UFormField>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst state = ref({\n  imageId: ''\n})\n\n// Access selected asset ID\nwatch(() => state.imageId, (newId) => {\n  console.log('Selected asset:', newId)\n})\n\u003C/script> Custom Collection \u003Ctemplate>\n  \u003CCroutonAssetsPicker \n    v-model=\"selectedId\" \n    collection=\"productImages\" \n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedId = ref('')\n\u003C/script>",{"id":7834,"title":5367,"titles":7835,"content":7836,"level":748},"/features/assets#events",[244,5354,7818],"// Emitted when asset is selected (v-model update)\n@update:modelValue: (assetId: string) => void\n\n// Emitted when asset is confirmed with full asset data\n@select: (asset: Record\u003Cstring, any>) => void",{"id":7838,"title":7839,"titles":7840,"content":7841,"level":748},"/features/assets#component-source","Component Source",[244,5354,7818],"Located at: packages/crouton-assets/app/components/Picker.vue Key Implementation Details: Uses useFetch() to load assets from generated APIFilters assets client-side with computed propertyModal integration with CroutonAssetsUploaderTeam ID from route params (useRoute().params.team)",{"id":7843,"title":7844,"titles":7845,"content":7846,"level":449},"/features/assets#croutonassetsuploader","CroutonAssetsUploader",[244,5354],"Upload files with metadata form (alt text, filename display).",{"id":7848,"title":4987,"titles":7849,"content":7850,"level":748},"/features/assets#props-1",[244,5354,7844],"interface Props {\n  collection?: string  // Collection name (default: 'crouton-assets')\n  crop?: boolean | { aspectRatio?: number | AspectRatioPreset }  // Enable image cropping\n}\n\ntype AspectRatioPreset = 'free' | '1:1' | '16:9' | '4:3' | '3:2'",{"id":7852,"title":5367,"titles":7853,"content":7854,"level":748},"/features/assets#events-1",[244,5354,7844],"@uploaded: (assetId: string) => void  // Emitted after successful upload",{"id":7856,"title":183,"titles":7857,"content":7858,"level":748},"/features/assets#features-1",[244,5354,7844],"File Selection: Uses CroutonImageUpload for file pickerPreview: Image preview before uploadMetadata Form: Alt text input fieldFile Info: Displays filename, size, MIME typeUpload State: Loading indicator during uploadTwo-Step Process:\nUpload file to blob storageCreate asset record in database",{"id":7860,"title":1608,"titles":7861,"content":7862,"level":748},"/features/assets#usage-1",[244,5354,7844],"In Modal \u003Ctemplate>\n  \u003Cdiv>\n    \u003CUButton @click=\"showUploader = true\">\n      Upload New Asset\n    \u003C/UButton>\n\n    \u003CUModal v-model=\"showUploader\">\n      \u003Ctemplate #content=\"{ close }\">\n        \u003Cdiv class=\"p-6\">\n          \u003Ch3 class=\"text-lg font-semibold mb-4\">Upload New Asset\u003C/h3>\n          \u003CCroutonAssetsUploader @uploaded=\"handleUploaded(close)\" />\n        \u003C/div>\n      \u003C/template>\n    \u003C/UModal>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst showUploader = ref(false)\n\nconst handleUploaded = async (close: () => void, assetId: string) => {\n  console.log('Uploaded asset ID:', assetId)\n  // Optionally refresh your asset list\n  close()\n  showUploader.value = false\n}\n\u003C/script> Standalone \u003Ctemplate>\n  \u003Cdiv class=\"max-w-md mx-auto\">\n    \u003CCroutonAssetsUploader @uploaded=\"onAssetUploaded\" />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst onAssetUploaded = (assetId: string) => {\n  console.log('New asset:', assetId)\n  // Navigate to asset or show success message\n  navigateTo(`/assets/${assetId}`)\n}\n\u003C/script>",{"id":7864,"title":7839,"titles":7865,"content":7866,"level":748},"/features/assets#component-source-1",[244,5354,7844],"Located at: packages/crouton-assets/app/components/Uploader.vue Upload Process: User selects file via CroutonImageUploadFile preview and metadata form appearsUser enters alt text (optional)Click \"Upload Asset\"File uploads to blob storage (POST /api/upload-image)Asset record created in database (POST /api/teams/[id]/assets)@uploaded event emitted with asset IDForm resets",{"id":7868,"title":7869,"titles":7870,"content":528,"level":391},"/features/assets#composable","Composable",[244],{"id":7872,"title":7873,"titles":7874,"content":7875,"level":449},"/features/assets#useassetupload","useAssetUpload()",[244,7869],"Programmatic asset upload handling for custom workflows.",{"id":7877,"title":7878,"titles":7879,"content":7880,"level":748},"/features/assets#api","API",[244,7869,7873],"const {\n  uploadAsset,\n  uploadAssets,\n  uploading,\n  error\n} = useAssetUpload()",{"id":7882,"title":6217,"titles":7883,"content":7884,"level":748},"/features/assets#returns",[244,7869,7873],"{\n  // Upload single asset\n  uploadAsset: (\n    file: File,\n    metadata?: AssetMetadata,\n    collection?: string\n  ) => Promise\u003CUploadAssetResult>\n\n  // Upload multiple assets in parallel\n  uploadAssets: (\n    files: File[],\n    metadata?: AssetMetadata,\n    collection?: string\n  ) => Promise\u003CUploadAssetResult[]>\n\n  // Delete an asset's blob file\n  deleteAssetFile: (pathname: string) => Promise\u003Cvoid>\n\n  // Reactive state\n  uploading: Readonly\u003CRef\u003Cboolean>>\n  error: Readonly\u003CRef\u003CError | null>>\n  progress: Readonly\u003CRef\u003Cnumber>>  // 0-100\n}",{"id":7886,"title":6551,"titles":7887,"content":7888,"level":748},"/features/assets#types",[244,7869,7873],"interface AssetMetadata {\n  alt?: string\n  filename?: string\n  translations?: Record\u003Cstring, { alt?: string }>\n}\n\ninterface UploadAssetResult {\n  id: string\n  pathname: string\n  filename: string\n  contentType: string\n  size: number\n  alt?: string\n}",{"id":7890,"title":5825,"titles":7891,"content":7892,"level":748},"/features/assets#usage-examples",[244,7869,7873],"Simple Upload \u003Cscript setup lang=\"ts\">\nconst { uploadAsset, uploading, error } = useAssetUpload()\n\nconst handleFileInput = async (event: Event) => {\n  const file = (event.target as HTMLInputElement).files?.[0]\n  if (!file) return\n\n  try {\n    const asset = await uploadAsset(file, {\n      alt: 'User uploaded image',\n      filename: file.name\n    })\n\n    console.log('Upload successful:', asset.id)\n  } catch (err) {\n    console.error('Upload failed:', error.value)\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cinput type=\"file\" @change=\"handleFileInput\" :disabled=\"uploading\" />\n    \u003Cp v-if=\"uploading\">Uploading...\u003C/p>\n    \u003Cp v-if=\"error\" class=\"text-red-500\">{{ error.message }}\u003C/p>\n  \u003C/div>\n\u003C/template> Drag-and-Drop Upload \u003Cscript setup lang=\"ts\">\nconst { uploadAsset, uploading } = useAssetUpload()\n\nconst handleDrop = async (event: DragEvent) => {\n  event.preventDefault()\n  const file = event.dataTransfer?.files[0]\n  if (!file) return\n\n  const asset = await uploadAsset(file, {\n    alt: 'Drag-and-drop upload'\n  })\n\n  console.log('Dropped file uploaded:', asset.id)\n}\n\nconst handleDragOver = (event: DragEvent) => {\n  event.preventDefault()\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv\n    @drop=\"handleDrop\"\n    @dragover=\"handleDragOver\"\n    class=\"border-2 border-dashed rounded-lg p-12 text-center\"\n    :class=\"uploading ? 'opacity-50' : 'hover:border-primary-500'\"\n  >\n    \u003Cp v-if=\"!uploading\">Drop files here to upload\u003C/p>\n    \u003Cp v-else>Uploading...\u003C/p>\n  \u003C/div>\n\u003C/template> Batch Upload \u003Cscript setup lang=\"ts\">\nconst { uploadAssets, uploading } = useAssetUpload()\n\nconst handleMultipleFiles = async (event: Event) => {\n  const files = Array.from((event.target as HTMLInputElement).files || [])\n  if (files.length === 0) return\n\n  try {\n    const assets = await uploadAssets(files, {\n      alt: 'Batch uploaded images'\n    })\n\n    console.log(`Uploaded ${assets.length} assets`)\n    assets.forEach(asset => {\n      console.log(`- ${asset.filename}: ${asset.id}`)\n    })\n  } catch (err) {\n    console.error('Batch upload failed')\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cinput \n      type=\"file\" \n      multiple \n      @change=\"handleMultipleFiles\"\n      :disabled=\"uploading\"\n    />\n    \u003Cp v-if=\"uploading\">Uploading multiple files...\u003C/p>\n  \u003C/div>\n\u003C/template> Custom Collection \u003Cscript setup lang=\"ts\">\nconst { uploadAsset } = useAssetUpload()\n\nconst uploadProductImage = async (file: File) => {\n  // Upload to custom collection\n  const asset = await uploadAsset(\n    file,\n    { alt: 'Product image' },\n    'productImages'  // Custom collection\n  )\n\n  return asset.id\n}\n\u003C/script>",{"id":7894,"title":2522,"titles":7895,"content":7896,"level":748},"/features/assets#error-handling",[244,7869,7873],"The composable catches errors and stores them in the error ref: \u003Cscript setup lang=\"ts\">\nconst { uploadAsset, error } = useAssetUpload()\n\nconst upload = async (file: File) => {\n  try {\n    await uploadAsset(file)\n  } catch (err) {\n    // error.value is also set\n    console.error('Upload failed:', error.value?.message)\n    \n    // Show toast notification\n    useToast().add({\n      title: 'Upload Failed',\n      description: error.value?.message || 'Unknown error',\n      color: 'red'\n    })\n  }\n}\n\u003C/script>",{"id":7898,"title":7899,"titles":7900,"content":7901,"level":391},"/features/assets#nuxthub-blob-storage","NuxtHub Blob Storage",[244],"The assets package relies on NuxtHub's blob storage for file management.",{"id":7903,"title":1789,"titles":7904,"content":7905,"level":449},"/features/assets#configuration",[244,7899],"Required in nuxt.config.ts: export default defineNuxtConfig({\n  hub: {\n    blob: true  // Enable blob storage\n  }\n})",{"id":7907,"title":7908,"titles":7909,"content":7910,"level":449},"/features/assets#how-blob-storage-works","How Blob Storage Works",[244,7899],"Upload Endpoint (provided by base package): // POST /api/upload-image (authenticated)\n// Receives: FormData with 'file' (or 'image') field\n// Returns: { pathname, contentType, size, filename }\n// Supports: images, PDFs, video, audio, documents\n// Configurable via runtimeConfig.public.croutonUpload Delete Endpoint (provided by base package): // DELETE /api/upload-image (authenticated)\n// Receives: { pathname: string }\n// Returns: { success: true, pathname } Serving Route (provided by base package): // GET /images/[pathname]\n// Fetches from blob storage\n// Serves file with correct content-type\n// Cache headers: public, max-age=31536000, immutable",{"id":7912,"title":1297,"titles":7913,"content":7914,"level":449},"/features/assets#file-organization",[244,7899],"Files are stored with unique pathnames: uploads/\n├── team-123/\n│   ├── abc123.jpg\n│   ├── def456.png\n│   └── ghi789.webp\n└── team-456/\n    ├── jkl012.jpg\n    └── mno345.png",{"id":7916,"title":7917,"titles":7918,"content":7919,"level":449},"/features/assets#benefits-of-edge-storage","Benefits of Edge Storage",[244,7899],"Global CDN: Fast delivery worldwideAutomatic Scaling: No storage limitsCost Effective: Pay per usageCloudflare Integration: Seamless with NuxtHubNo Configuration: Works out of the box",{"id":7921,"title":1650,"titles":7922,"content":528,"level":391},"/features/assets#common-patterns",[244],{"id":7924,"title":1654,"titles":7925,"content":7926,"level":449},"/features/assets#product-with-image",[244,1650],"Schema Definition: {\n  \"name\": {\n    \"type\": \"string\",\n    \"meta\": { \"required\": true, \"area\": \"main\" }\n  },\n  \"description\": {\n    \"type\": \"string\",\n    \"meta\": { \"component\": \"Textarea\", \"area\": \"main\" }\n  },\n  \"imageId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"assets\",\n    \"meta\": {\n      \"label\": \"Product Image\",\n      \"area\": \"sidebar\"\n    }\n  },\n  \"price\": {\n    \"type\": \"number\",\n    \"meta\": { \"required\": true, \"area\": \"sidebar\" }\n  }\n} Generated Form: Automatically includes asset picker thanks to auto-detection! Display Product with Image: \u003Cscript setup lang=\"ts\">\nconst route = useRoute()\nconst teamId = route.params.team as string\nconst productId = route.params.id as string\n\n// Fetch product\nconst { data: product } = await useFetch(\n  `/api/teams/${teamId}/products/${productId}`\n)\n\n// Fetch referenced asset\nconst { data: asset } = await useFetch(\n  () => product.value?.imageId \n    ? `/api/teams/${teamId}/assets/${product.value.imageId}`\n    : null,\n  { watch: [() => product.value?.imageId] }\n)\n\nconst imageUrl = computed(() => \n  asset.value?.pathname ? `/images/${asset.value.pathname}` : '/placeholder.png'\n)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"product\">\n    \u003Cimg \n      :src=\"imageUrl\" \n      :alt=\"asset?.alt || product.name\"\n      class=\"w-full h-64 object-cover rounded-lg\"\n    />\n    \u003Ch1>{{ product.name }}\u003C/h1>\n    \u003Cp>{{ product.description }}\u003C/p>\n    \u003Cp class=\"text-2xl font-bold\">${{ product.price }}\u003C/p>\n  \u003C/div>\n\u003C/template>",{"id":7928,"title":7929,"titles":7930,"content":7931,"level":449},"/features/assets#avatar-upload-simple","Avatar Upload (Simple)",[244,1650],"For user avatars, you might prefer the simple approach without the asset library: \u003Ctemplate>\n  \u003CCroutonAvatarUpload\n    v-model=\"avatarUrl\"\n    @file-selected=\"handleUpload\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst user = useCurrentUser()\nconst avatarUrl = ref(user.value?.avatar || '/default-avatar.png')\n\nconst handleUpload = async (file: File | null) => {\n  if (!file) return\n\n  const formData = new FormData()\n  formData.append('image', file)\n\n  const pathname = await $fetch('/api/upload-image', {\n    method: 'POST',\n    body: formData\n  })\n\n  avatarUrl.value = `/images/${pathname}`\n\n  // Update user profile\n  await $fetch(`/api/users/${user.value.id}`, {\n    method: 'PATCH',\n    body: { avatar: pathname }\n  })\n}\n\u003C/script>",{"id":7933,"title":1659,"titles":7934,"content":7935,"level":449},"/features/assets#multiple-images-gallery",[244,1650],"Schema with Multiple Assets: {\n  \"title\": { \"type\": \"string\", \"meta\": { \"required\": true } },\n  \"featuredImageId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"assets\",\n    \"meta\": { \"label\": \"Featured Image\" }\n  },\n  \"galleryImageIds\": {\n    \"type\": \"json\",\n    \"meta\": {\n      \"label\": \"Gallery Images\",\n      \"component\": \"MultiAssetPicker\"\n    }\n  }\n} Custom Multi-Picker Component: \u003C!-- components/MultiAssetPicker.vue -->\n\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003Cdiv class=\"flex flex-wrap gap-2\">\n      \u003Cdiv \n        v-for=\"(assetId, index) in selectedIds\"\n        :key=\"assetId\"\n        class=\"relative group\"\n      >\n        \u003Cimg \n          :src=\"`/images/${getAssetPathname(assetId)}`\"\n          class=\"w-24 h-24 object-cover rounded\"\n        />\n        \u003Cbutton\n          @click=\"removeAsset(index)\"\n          class=\"absolute -top-2 -right-2 bg-red-500 rounded-full p-1\"\n        >\n          \u003CUIcon name=\"i-lucide-x\" class=\"w-4 h-4 text-white\" />\n        \u003C/button>\n      \u003C/div>\n    \u003C/div>\n\n    \u003CUButton @click=\"showPicker = true\">\n      Add Images\n    \u003C/UButton>\n\n    \u003CUModal v-model=\"showPicker\">\n      \u003Ctemplate #content=\"{ close }\">\n        \u003Cdiv class=\"p-6\">\n          \u003Ch3 class=\"text-lg font-semibold mb-4\">Select Images\u003C/h3>\n          \u003CCroutonAssetsPicker v-model=\"tempSelection\" />\n          \u003Cdiv class=\"flex justify-end gap-2 mt-4\">\n            \u003CUButton variant=\"ghost\" @click=\"close\">Cancel\u003C/UButton>\n            \u003CUButton @click=\"addAsset(close)\">Add\u003C/UButton>\n          \u003C/div>\n        \u003C/div>\n      \u003C/template>\n    \u003C/UModal>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedIds = defineModel\u003Cstring[]>({ default: () => [] })\nconst showPicker = ref(false)\nconst tempSelection = ref('')\n\nconst addAsset = (close: () => void) => {\n  if (tempSelection.value && !selectedIds.value.includes(tempSelection.value)) {\n    selectedIds.value.push(tempSelection.value)\n  }\n  tempSelection.value = ''\n  close()\n}\n\nconst removeAsset = (index: number) => {\n  selectedIds.value.splice(index, 1)\n}\n\nconst getAssetPathname = (assetId: string) => {\n  // Fetch pathname from asset - implement caching\n  return assetId\n}\n\u003C/script>",{"id":7937,"title":7938,"titles":7939,"content":7940,"level":449},"/features/assets#reusable-assets-across-collections","Reusable Assets Across Collections",[244,1650],"// Same asset used in multiple places\n{\n  product1: { imageId: 'asset-abc123' },\n  product2: { imageId: 'asset-abc123' },\n  blogPost: { featuredImageId: 'asset-abc123' }\n} Benefits: Saves storage space (file stored once)Consistent images across applicationUpdate alt text once, reflects everywhereCentralized asset management",{"id":7942,"title":44,"titles":7943,"content":528,"level":391},"/features/assets#best-practices",[244],{"id":7945,"title":7946,"titles":7947,"content":7948,"level":449},"/features/assets#alt-text-guidelines","Alt Text Guidelines",[244,44],"Always provide descriptive alt text for accessibility and SEO: Good Alt Text: await uploadAsset(file, {\n  alt: 'Red Nike Air Max 90 sneakers on white background, side view'\n}) Bad Alt Text: await uploadAsset(file, {\n  alt: 'image'  // Too generic\n})\nawait uploadAsset(file, {\n  alt: ''  // Missing\n}) Benefits of Good Alt Text: Improves accessibility for screen readersBoosts SEO rankingsMakes search more effectiveHelps content discovery",{"id":7950,"title":7951,"titles":7952,"content":7953,"level":449},"/features/assets#file-naming-conventions","File Naming Conventions",[244,44],"Use descriptive, consistent filenames: // Good\nawait uploadAsset(file, {\n  filename: 'red-nike-air-max-90-side-view.jpg',\n  alt: 'Red Nike Air Max 90, side view'\n})\n\n// Avoid\nawait uploadAsset(file, {\n  filename: 'IMG_1234.jpg',  // Not descriptive\n  alt: ''\n})",{"id":7955,"title":7956,"titles":7957,"content":7958,"level":449},"/features/assets#team-based-organization","Team-Based Organization",[244,44],"Assets are automatically scoped to teams via teamId: // Team A's assets\nGET /api/teams/team-123/assets\n// Returns only team-123's assets\n\n// Team B's assets\nGET /api/teams/team-456/assets\n// Returns only team-456's assets No manual filtering needed - the generated API handles team scoping.",{"id":7960,"title":4232,"titles":7961,"content":7962,"level":449},"/features/assets#search-optimization",[244,44],"Make assets easy to find: await uploadAsset(file, {\n  filename: 'summer-collection-beach-sunset.jpg',\n  alt: 'Summer collection photo shoot at beach during golden hour sunset'\n}) Search will match: \"summer\" → Found via filename or alt\"beach\" → Found via alt text\"sunset\" → Found via both\"collection\" → Found via filename",{"id":7964,"title":36,"titles":7965,"content":528,"level":391},"/features/assets#troubleshooting",[244],{"id":7967,"title":7968,"titles":7969,"content":7970,"level":449},"/features/assets#assets-not-displaying-in-picker","Assets Not Displaying in Picker",[244,36],"Check NuxtHub blob storage is enabled: // nuxt.config.ts\nexport default defineNuxtConfig({\n  hub: {\n    blob: true  // Must be true\n  }\n}) Verify assets collection exists: ls layers/core/collections/assets\n# Should show: _Form.vue, List.vue, drizzle.ts, etc. Check team ID in route: The picker needs team in route params: // Route must include team\n/teams/:team/products\n/teams/:team/blog-posts",{"id":7972,"title":1729,"titles":7973,"content":7974,"level":449},"/features/assets#upload-fails",[244,36],"Check file size limits: NuxtHub blob storage has default limits. For large files, configure limits. Verify blob storage permissions: Ensure your NuxtHub project has blob storage enabled in the dashboard. Review server logs: # Check for errors\npnpm dev\n# Look for upload errors in console",{"id":7976,"title":7977,"titles":7978,"content":7979,"level":449},"/features/assets#images-not-serving","Images Not Serving",[244,36],"Verify pathname is correct: console.log('Pathname:', asset.pathname)\nconsole.log('Full URL:', `/images/${asset.pathname}`) Test serving route directly: curl http://localhost:3000/images/uploads/team-123/abc123.jpg Check blob storage: In NuxtHub dashboard, verify the file exists in blob storage.",{"id":7981,"title":5569,"titles":7982,"content":7983,"level":449},"/features/assets#typescript-errors",[244,36],"Run typecheck after generation: npx nuxt typecheck Common issues: Missing teamId in route paramsIncorrect asset ID type (should be string)Missing await on async operations",{"id":7985,"title":40,"titles":7986,"content":528,"level":391},"/features/assets#migration-guide",[244],{"id":7988,"title":7989,"titles":7990,"content":7991,"level":449},"/features/assets#from-direct-url-storage","From Direct URL Storage",[244,40],"If you're currently storing image URLs directly in your database: 1. Update Schema {\n  \"name\": { \"type\": \"string\" },\n- \"imageUrl\": { \"type\": \"string\" }\n+ \"imageId\": {\n+   \"type\": \"string\",\n+   \"refTarget\": \"assets\"\n+ }\n} 2. Generate Assets Collection crouton-generate core assets \\\n  --fields-file=node_modules/@fyit/crouton-assets/assets-schema.json 3. Migrate Existing Data Create migration script: // scripts/migrate-to-assets.ts\nimport { db } from '~/server/db'\nimport { products, assets } from '~/server/db/schema'\n\nconst allProducts = await db.select().from(products)\n\nfor (const product of allProducts) {\n  if (!product.imageUrl) continue\n\n  // Extract pathname from URL\n  const pathname = product.imageUrl.replace('/images/', '')\n\n  // Create asset record\n  const [asset] = await db.insert(assets).values({\n    id: generateId(),\n    teamId: product.teamId,\n    userId: product.userId || 'system',\n    filename: pathname.split('/').pop() || 'unknown.jpg',\n    pathname,\n    contentType: 'image/jpeg',  // Detect from file extension\n    size: 0,  // Could fetch from blob\n    uploadedAt: product.createdAt,\n    alt: product.name  // Use product name as fallback alt\n  }).returning()\n\n  // Update product reference\n  await db\n    .update(products)\n    .set({ imageId: asset.id })\n    .where(eq(products.id, product.id))\n}\n\nconsole.log(`Migrated ${allProducts.length} products`) 4. Regenerate Collection crouton-generate products shopProducts --overwrite",{"id":7993,"title":285,"titles":7994,"content":528,"level":391},"/features/assets#api-reference",[244],{"id":7996,"title":7997,"titles":7998,"content":7999,"level":449},"/features/assets#component-props","Component Props",[244,285],"CroutonAssetsPicker {\n  collection?: string      // Default: 'crouton-assets'\n  modelValue?: string      // v-model (asset ID)\n  crop?: boolean | { aspectRatio?: number }  // Enable image cropping\n}\n// Events:\n// @update:modelValue(assetId: string)\n// @select(asset: Record\u003Cstring, any>) CroutonAssetsUploader {\n  collection?: string      // Default: 'crouton-assets'\n  crop?: boolean | { aspectRatio?: number | AspectRatioPreset }  // Enable image cropping\n}\n// Events:\n// @uploaded(assetId: string)",{"id":8001,"title":8002,"titles":8003,"content":8004,"level":449},"/features/assets#composable-types","Composable Types",[244,285],"// useAssetUpload() return type\ninterface UseAssetUploadReturn {\n  uploadAsset: (\n    file: File,\n    metadata?: AssetMetadata,\n    collection?: string\n  ) => Promise\u003CUploadAssetResult>\n\n  uploadAssets: (\n    files: File[],\n    metadata?: AssetMetadata,\n    collection?: string\n  ) => Promise\u003CUploadAssetResult[]>\n\n  deleteAssetFile: (pathname: string) => Promise\u003Cvoid>\n\n  uploading: Readonly\u003CRef\u003Cboolean>>\n  error: Readonly\u003CRef\u003CError | null>>\n  progress: Readonly\u003CRef\u003Cnumber>>  // 0-100\n}\n\ninterface AssetMetadata {\n  alt?: string\n  filename?: string\n  translations?: Record\u003Cstring, { alt?: string }>\n}\n\ninterface UploadAssetResult {\n  id: string\n  pathname: string\n  filename: string\n  contentType: string\n  size: number\n  alt?: string\n}",{"id":8006,"title":8007,"titles":8008,"content":8009,"level":391},"/features/assets#known-limitations-beta","Known Limitations (BETA)",[244],"Active Development: These limitations may be addressed in future releases. No Automatic Thumbnails - Large images are served at full resolution. Consider generating thumbnails separately.Basic Search - Client-side filtering by filename and alt text only. For advanced search, implement server-side filtering.Team Scoping Required - Assets must be scoped to teams. For global assets, create a \"global\" team or adjust the schema.",{"id":8011,"title":8012,"titles":8013,"content":8014,"level":391},"/features/assets#roadmap-future-versions","Roadmap (Future Versions)",[244],"Planned Features: Image transformation and optimizationAutomatic thumbnail generationFolder/tag organizationAdvanced search with filtersAsset usage tracking (where asset is referenced)Bulk operations (delete, move, tag)Storage analytics API Stability: Current API will remain backward compatibleMajor changes will follow semantic versioningMigration guides provided for breaking changes",{"id":8016,"title":1007,"titles":8017,"content":8018,"level":391},"/features/assets#related-resources",[244],"Asset Management Guide - Complete usage guideNuxtHub Blob Storage - Storage documentationComponent Generator - Collection generationBase Package - Core upload infrastructure",{"id":8020,"title":8021,"titles":8022,"content":8023,"level":391},"/features/assets#feedback","Feedback",[244],"This is a BETA package. Your feedback is valuable! Issues: GitHub IssuesDiscussions: GitHub DiscussionsFeature Requests: Tag with nuxt-crouton-assets",{"id":8025,"title":3685,"titles":8026,"content":8027,"level":391},"/features/assets#version-history",[244],"v0.1.0 (Current - BETA) Initial beta releaseCroutonAssetsPicker componentCroutonAssetsUploader componentuseAssetUpload() composableNuxtHub blob storage integrationAuto-detection for asset referencesReference schema for generation html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":249,"title":248,"titles":8029,"content":8030,"level":385},[],"Complete reference for @fyit/crouton-events package - Event tracking and audit trails Status: Experimental - This package is in active development (v0.1.0). APIs and features may change. Use in production with caution and expect potential breaking changes in future releases. The @fyit/crouton-events package provides comprehensive event tracking and audit trails for Nuxt Crouton applications. Automatically tracks all CREATE, UPDATE, and DELETE operations across collections with smart diff tracking and zero-configuration setup.",{"id":8032,"title":4911,"titles":8033,"content":8034,"level":391},"/features/events#package-overview",[248],"Package: @fyit/crouton-eventsVersion: 0.1.0 (BETA)\nType: Nuxt Layer (Addon)Dependencies: @fyit/crouton-core",{"id":8036,"title":5599,"titles":8037,"content":8038,"level":449},"/features/events#key-features",[248,4911],"⚡ Zero Configuration: Auto-tracks all collection mutations via hooks🎯 Smart Diff Tracking: Stores only changed fields to minimize storage👤 User Attribution: Captures user ID and username at event time📸 Historical Snapshots: Preserves user data for accurate audit trails🗑️ Auto-Cleanup: Configurable retention policy prevents database bloat🔍 Rich Querying: Filter by collection, operation, user, or date🚨 Error Handling: Development-friendly toasts with production safety📊 Health Monitoring: Track success/failure rates in real-time🛠️ Standard Collection: Generated UI components for viewing events",{"id":8040,"title":13,"titles":8041,"content":528,"level":391},"/features/events#installation",[248],{"id":8043,"title":426,"titles":8044,"content":8045,"level":449},"/features/events#prerequisites",[248,13],"You must have the base @fyit/crouton-core package installed first. # Install both base and events addon\npnpm add @fyit/crouton-core @fyit/crouton-events",{"id":8047,"title":5829,"titles":8048,"content":8049,"level":449},"/features/events#basic-setup",[248,13],"Add both layers to your nuxt.config.ts: // nuxt.config.ts\nexport default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton-core',    // Base layer (required)\n    '@fyit/crouton-events'   // Events addon\n  ]\n}) That's it! Events are now automatically tracked for all collection mutations. Important: The events addon is a layer extension, not a standalone package. You must extend both the base @fyit/crouton-core layer and the events addon.",{"id":8051,"title":1789,"titles":8052,"content":8053,"level":391},"/features/events#configuration",[248],"Customize event tracking behavior in your nuxt.config.ts: export default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton-core',\n    '@fyit/crouton-events'\n  ],\n\n  runtimeConfig: {\n    public: {\n      croutonEvents: {\n        // Enable/disable tracking globally\n        enabled: true,\n\n        // Store username snapshot for audit trail\n        snapshotUserName: true,\n\n        // Error handling configuration\n        errorHandling: {\n          mode: 'toast',        // 'silent' | 'toast' | 'throw'\n          logToConsole: true    // Log errors to console\n        },\n\n        // Automatic cleanup of old events\n        retention: {\n          enabled: true,        // Enable auto-cleanup\n          days: 90,            // Keep events for 90 days\n          maxEvents: 100000    // Or maximum number of events\n        }\n      }\n    }\n  }\n})",{"id":8055,"title":3236,"titles":8056,"content":8057,"level":449},"/features/events#configuration-options",[248,1789],"OptionTypeDefaultDescriptionenabledbooleantrueEnable/disable event tracking globallysnapshotUserNamebooleantrueStore username at time of eventerrorHandling.modestring'toast'How to handle tracking errors: 'silent', 'toast', or 'throw'errorHandling.logToConsolebooleantrueLog errors to consoleretention.enabledbooleantrueEnable automatic cleanupretention.daysnumber90Keep events for N daysretention.maxEventsnumber100000Maximum number of events to keep",{"id":8059,"title":8060,"titles":8061,"content":528,"level":391},"/features/events#event-schema","Event Schema",[248],{"id":8063,"title":8064,"titles":8065,"content":8066,"level":449},"/features/events#database-structure","Database Structure",[248,8060],"Each tracked event is stored with the following structure: interface CroutonEvent {\n  // Core identification\n  id: string\n  timestamp: string | Date\n\n  // Operation details\n  operation: 'create' | 'update' | 'delete'\n  collectionName: string\n  itemId: string\n\n  // User attribution\n  userId: string\n  userName: string  // Snapshot at time of event\n\n  // Field-level changes\n  changes: EventChange[]\n\n  // Optional metadata\n  metadata?: Record\u003Cstring, unknown>\n}\n\ninterface EventChange {\n  fieldName: string\n  oldValue: string | null  // JSON stringified\n  newValue: string | null  // JSON stringified\n}",{"id":8068,"title":8069,"titles":8070,"content":8071,"level":449},"/features/events#schema-fields","Schema Fields",[248,8060],"FieldTypeRequiredDescriptiontimestampstring | Date✅When the event occurredoperationstring✅Type of operation: create, update, deletecollectionNamestring✅Name of the collection modifieditemIdstring✅ID of the item created/updated/deleteduserIdstring✅ID of the user who performed the actionuserNamestring✅Name of user at time of event (historical snapshot)changesJSON✅Array of field-level changesmetadataJSON❌Additional context (Record\u003Cstring, unknown>)",{"id":8073,"title":8074,"titles":8075,"content":8076,"level":449},"/features/events#smart-diff-logic","Smart Diff Logic",[248,8060],"The package intelligently tracks only changed fields to minimize storage: CREATE Operation: Stores all fields as \"new\" (oldValue = null)Excludes internal fields: id, createdAt, updatedAt, createdBy, updatedBy, teamId, owner UPDATE Operation: Compares before and after statesStores only fields where values changedUses JSON stringify comparison for deep equality DELETE Operation: Stores all fields as \"removed\" (newValue = null)Excludes internal fields",{"id":8078,"title":5437,"titles":8079,"content":528,"level":391},"/features/events#composables",[248],{"id":8081,"title":8082,"titles":8083,"content":8084,"level":449},"/features/events#usecroutoneventtracker","useCroutonEventTracker",[248,5437],"Core composable for manual event tracking with smart diff calculation. const { track, trackInBackground } = useCroutonEventTracker()",{"id":8086,"title":8087,"titles":8088,"content":8089,"level":748},"/features/events#trackoptions","track(options)",[248,5437,8082],"Track an event synchronously (awaitable). Parameters: interface TrackEventOptions {\n  operation: 'create' | 'update' | 'delete'\n  collection: string\n  itemId?: string\n  itemIds?: string[]\n  data?: any           // For create: new item data\n  updates?: any        // For update: fields being updated\n  result?: any         // Result after operation\n  beforeData?: any     // State before operation (for update/delete)\n} Usage: // Manual tracking (usually not needed - auto-tracking via plugin)\ntry {\n  await track({\n    operation: 'update',\n    collection: 'users',\n    itemId: 'user-123',\n    beforeData: { name: 'John', email: 'john@example.com' },\n    result: { name: 'Jane', email: 'john@example.com' }\n  })\n} catch (error) {\n  console.error('Tracking failed:', error)\n}",{"id":8091,"title":8092,"titles":8093,"content":8094,"level":748},"/features/events#trackinbackgroundoptions","trackInBackground(options)",[248,5437,8082],"Track an event asynchronously (fire and forget with error handling). Usage: // Non-blocking tracking\ntrackInBackground({\n  operation: 'create',\n  collection: 'posts',\n  data: { title: 'New Post', content: '...' }\n}) Note: You rarely need to use this composable directly. The plugin automatically tracks all collection mutations via the crouton:mutation hook.",{"id":8096,"title":8097,"titles":8098,"content":8099,"level":449},"/features/events#usecroutonevents","useCroutonEvents",[248,5437],"Composable for querying events with filtering and enrichment options. const { data, pending, error, refresh } = useCroutonEvents(options)",{"id":8101,"title":3362,"titles":8102,"content":8103,"level":748},"/features/events#options",[248,5437,8097],"interface UseCroutonEventsOptions {\n  teamId?: string              // Override team context\n  enrichUserData?: boolean     // Join with users table (future)\n  \n  filters?: {\n    collectionName?: string    // Filter by collection\n    operation?: 'create' | 'update' | 'delete'\n    userId?: string            // Filter by user\n    dateFrom?: Date            // Events after this date\n    dateTo?: Date              // Events before this date\n  }\n  \n  pagination?: {\n    page?: number              // Page number (default: 1)\n    pageSize?: number          // Items per page (default: 50)\n  }\n}",{"id":8105,"title":4173,"titles":8106,"content":8107,"level":748},"/features/events#basic-usage",[248,5437,8097],"// Get all events for current team\nconst { data: events, pending } = useCroutonEvents()\n\n// Filter by collection\nconst { data: userEvents } = useCroutonEvents({\n  filters: {\n    collectionName: 'users'\n  }\n})",{"id":8109,"title":8110,"titles":8111,"content":8112,"level":748},"/features/events#filter-by-operation","Filter by Operation",[248,5437,8097],"// Get only UPDATE operations\nconst { data: updates } = useCroutonEvents({\n  filters: {\n    operation: 'update'\n  }\n})",{"id":8114,"title":8115,"titles":8116,"content":8117,"level":748},"/features/events#filter-by-date-range","Filter by Date Range",[248,5437,8097],"// Get events from last 7 days\nconst sevenDaysAgo = new Date()\nsevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7)\n\nconst { data: recentEvents } = useCroutonEvents({\n  filters: {\n    dateFrom: sevenDaysAgo\n  }\n})",{"id":8119,"title":8120,"titles":8121,"content":8122,"level":748},"/features/events#combined-filters","Combined Filters",[248,5437,8097],"// Complex query: user updates in posts collection, last 30 days\nconst { data: events } = useCroutonEvents({\n  filters: {\n    collectionName: 'posts',\n    operation: 'update',\n    userId: 'user-123',\n    dateFrom: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)\n  },\n  pagination: {\n    page: 1,\n    pageSize: 100\n  }\n})",{"id":8124,"title":8125,"titles":8126,"content":8127,"level":748},"/features/events#user-data-enrichment-future","User Data Enrichment (Future)",[248,5437,8097],"// Join with users table to get current user data\nconst { data: enrichedEvents } = useCroutonEvents({\n  enrichUserData: true\n})\n\n// enrichedEvents[0].userName = \"John Smith\" (at time of event)\n// enrichedEvents[0].user.currentName = \"Jane Doe\" (current)\n// enrichedEvents[0].user.email = \"jane@example.com\" BETA Note: The enrichUserData option is planned but not yet fully implemented. Currently returns events without user JOIN.",{"id":8129,"title":8130,"titles":8131,"content":8132,"level":449},"/features/events#event-tracking-health-state","Event Tracking Health State",[248,5437],"The event-listener plugin tracks health internally via useState('crouton-events-health'). There is no standalone composable — access the state directly.",{"id":8134,"title":8135,"titles":8136,"content":8137,"level":748},"/features/events#state-shape","State Shape",[248,5437,8130],"interface CroutonEventsHealth {\n  total: number           // Total tracking attempts\n  failed: number          // Failed tracking attempts\n  lastError: string | null\n  lastErrorTime: Date | null\n}",{"id":8139,"title":1608,"titles":8140,"content":8141,"level":748},"/features/events#usage",[248,5437,8130],"\u003Cscript setup lang=\"ts\">\nconst health = useState\u003C{\n  total: number\n  failed: number\n  lastError: string | null\n  lastErrorTime: Date | null\n}>('crouton-events-health')\n\nconst failureRate = computed(() => {\n  if (!health.value?.total) return 0\n  return (health.value.failed / health.value.total) * 100\n})\n\nconst isHealthy = computed(() => failureRate.value \u003C 10)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"health\">\n    \u003Ch3>Event Tracking Health\u003C/h3>\n    \u003Cdiv>Total Events: {{ health.total }}\u003C/div>\n    \u003Cdiv>Failed: {{ health.failed }}\u003C/div>\n    \u003Cdiv>Failure Rate: {{ failureRate.toFixed(2) }}%\u003C/div>\n    \u003Cdiv>Status: {{ isHealthy ? 'Healthy' : 'Degraded' }}\u003C/div>\n\n    \u003Cdiv v-if=\"health.lastError\">\n      \u003Cp>Last Error: {{ health.lastError }}\u003C/p>\n      \u003Cp>Time: {{ health.lastErrorTime }}\u003C/p>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":8143,"title":5354,"titles":8144,"content":8145,"level":391},"/features/events#components",[248],"The package provides purpose-built components for displaying event data:",{"id":8147,"title":8148,"titles":8149,"content":8150,"level":449},"/features/events#croutonactivitylog","CroutonActivityLog",[248,5354],"Full activity log page with stats, filters, pagination, and export.",{"id":8152,"title":4987,"titles":8153,"content":8154,"level":748},"/features/events#props",[248,5354,8148],"PropTypeDefaultDescriptioncollectionstring—Filter to specific collectionuserIdstring—Filter to specific userpageSizenumber50Items per pageshowFiltersbooleantrueShow filters barshowPaginationbooleantrueShow paginationemptyMessagestring'No activity found'Empty state message \u003Ctemplate>\n  \u003CCroutonActivityLog />\n\u003C/template>",{"id":8156,"title":8157,"titles":8158,"content":8159,"level":449},"/features/events#croutonactivitytimeline","CroutonActivityTimeline",[248,5354],"Timeline visualization that groups events by date (Today, Yesterday, etc.).",{"id":8161,"title":4987,"titles":8162,"content":8163,"level":748},"/features/events#props-1",[248,5354,8157],"PropTypeDefaultDescriptioneventsCroutonEvent[][]Events to displayloadingbooleanfalseShow loading stateemptyMessagestring'No activity found'Empty state message \u003Ctemplate>\n  \u003CCroutonActivityTimeline :events=\"events\" @event-click=\"handleClick\" />\n\u003C/template>",{"id":8165,"title":8166,"titles":8167,"content":8168,"level":449},"/features/events#croutonactivitytimelineitem","CroutonActivityTimelineItem",[248,5354],"Individual event row with operation badge, timestamp, user, and expandable changes.",{"id":8170,"title":4987,"titles":8171,"content":8172,"level":748},"/features/events#props-2",[248,5354,8166],"PropTypeDefaultDescriptioneventCroutonEvent—Event to displayexpandedbooleanfalseWhether changes are expanded",{"id":8174,"title":8175,"titles":8176,"content":8177,"level":449},"/features/events#croutonactivityfilters","CroutonActivityFilters",[248,5354],"Filter controls for collection, operation, user, and date range presets.",{"id":8179,"title":4987,"titles":8180,"content":8181,"level":748},"/features/events#props-3",[248,5354,8175],"PropTypeDefaultDescriptionmodelValueFilterState—Filter state (v-model)collectionsstring[][]Available collection namesusersArray\u003C{ id: string, name: string }>[]Available users",{"id":8183,"title":8184,"titles":8185,"content":8186,"level":449},"/features/events#croutoneventdetail","CroutonEventDetail",[248,5354],"Modal showing full event details with metadata and a changes diff table.",{"id":8188,"title":4987,"titles":8189,"content":8190,"level":748},"/features/events#props-4",[248,5354,8184],"PropTypeDefaultDescriptionmodelValueboolean—Modal open state (v-model)eventCroutonEvent—Event to display",{"id":8192,"title":8193,"titles":8194,"content":8195,"level":449},"/features/events#croutoneventchangestable","CroutonEventChangesTable",[248,5354],"Before/after diff table for field-level changes.",{"id":8197,"title":4987,"titles":8198,"content":8199,"level":748},"/features/events#props-5",[248,5354,8193],"PropTypeDefaultDescriptionchangesEventChange[]—Array of field changesoperationEventOperation—Operation type (affects column visibility)",{"id":8201,"title":8202,"titles":8203,"content":528,"level":391},"/features/events#query-patterns","Query Patterns",[248],{"id":8205,"title":8206,"titles":8207,"content":8208,"level":449},"/features/events#using-standard-collection-query","Using Standard Collection Query",[248,8202],"Since events are a Crouton collection, you can use the standard useCollectionQuery composable: Query Patterns: For complete useCollectionQuery patterns, see Querying Data. // Events can be queried like any other collection\nconst { data: events, pending } = await useCollectionQuery('collectionEvents', {\n  teamId: currentTeam.id,\n  filters: { collectionName: 'users', operation: 'create' }\n})",{"id":8210,"title":8211,"titles":8212,"content":8213,"level":449},"/features/events#direct-api-access","Direct API Access",[248,8202],"Events are accessible via the standard Crouton API endpoints: // GET /api/teams/:teamId/crouton-events — query tracked events\nconst response = await $fetch(`/api/teams/${teamId}/crouton-events`, {\n  params: {\n    collectionName: 'users',\n    operation: 'update',\n    page: 1,\n    pageSize: 50\n  }\n})\n\n// POST /api/teams/:teamId/crouton-collection-events — manual event tracking only\nconst response = await $fetch(`/api/teams/${teamId}/crouton-collection-events`, {\n  method: 'POST',\n  body: {\n    operation: 'update',\n    collection: 'users',\n    itemId: 'user-123'\n  }\n})",{"id":8215,"title":85,"titles":8216,"content":528,"level":391},"/features/events#architecture",[248],{"id":8218,"title":1635,"titles":8219,"content":8220,"level":449},"/features/events#how-it-works",[248,85],"The event tracking system uses a hook-based architecture: Core Hooks: nuxt-crouton emits crouton:mutation hooks after successful CRUD operationsEvent Listener Plugin: This package subscribes to those hooks via a Nuxt pluginSmart Diff: Calculates field-level changes (oldValue → newValue)Async Tracking: Events tracked in background without blocking user operationsStorage: Events stored in same database as collections (NuxtHub D1/SQLite)",{"id":8222,"title":8223,"titles":8224,"content":8225,"level":449},"/features/events#auto-tracking-plugin","Auto-Tracking Plugin",[248,85],"The event-listener.ts plugin automatically subscribes to collection mutations: // Runs automatically - no configuration needed\nnuxtApp.hooks.hook('crouton:mutation', async (event) => {\n  // Track event in background\n  await track({\n    operation: event.operation,\n    collection: event.collection,\n    itemId: event.itemId,\n    data: event.data,\n    result: event.result,\n    beforeData: event.beforeData\n  })\n})",{"id":8227,"title":8228,"titles":8229,"content":8230,"level":449},"/features/events#performance-characteristics","Performance Characteristics",[248,85],"Non-blocking: Events tracked asynchronously after mutation completesMinimal overhead: Smart diff stores only changed fieldsIndexed queries: Fast filtering by collection, user, dateAuto-cleanup: Configurable retention prevents database bloat",{"id":8232,"title":8233,"titles":8234,"content":8235,"level":748},"/features/events#storage-estimates","Storage Estimates",[248,85,8228],"OperationTypical SizeDescriptionCREATE~500 bytesAll fields stored as newUPDATE~200-400 bytesOnly changed fieldsDELETE~150 bytesMinimal metadata10,000 events≈ 3-5 MBTotal database impact",{"id":8237,"title":2522,"titles":8238,"content":8239,"level":391},"/features/events#error-handling",[248],"The package uses environment-aware error handling:",{"id":8241,"title":8242,"titles":8243,"content":8244,"level":449},"/features/events#development-mode","Development Mode",[248,2522],"⚠️ Toast Notifications: Failed tracking shows visible toast📝 Console Logging: Full error details logged to console🎯 Error Details: Stack traces and context included ⚠️ Event tracking failed\nDescription: Network error or validation failure",{"id":8246,"title":8247,"titles":8248,"content":8249,"level":449},"/features/events#production-mode","Production Mode",[248,2522],"🔇 Silent Logging: Errors logged to console only🚫 No User Disruption: Failed tracking never blocks operations📊 Health Monitoring: Use useState('crouton-events-health') to detect issues",{"id":8251,"title":8252,"titles":8253,"content":8254,"level":449},"/features/events#error-handling-configuration","Error Handling Configuration",[248,2522],"export default defineNuxtConfig({\n  runtimeConfig: {\n    public: {\n      croutonEvents: {\n        errorHandling: {\n          mode: 'toast',        // Development-friendly\n          logToConsole: true\n        }\n      }\n    }\n  }\n})\n\n// Options for mode:\n// 'silent'  - No UI feedback, console logging only\n// 'toast'   - Show toast in dev, silent in prod\n// 'throw'   - Throw errors (not recommended - blocks operations)",{"id":8256,"title":8257,"titles":8258,"content":528,"level":391},"/features/events#data-retention-cleanup","Data Retention & Cleanup",[248],{"id":8260,"title":8261,"titles":8262,"content":8263,"level":449},"/features/events#automatic-cleanup","Automatic Cleanup",[248,8257],"The package includes a cleanup utility to prevent database bloat: // Server-side utility (auto-imported in Nuxt server context)\nconst result = await cleanupOldEvents()\n\nconsole.log(result)\n// {\n//   deletedCount: 5234,\n//   oldestRemaining: Date('2025-08-18T...'),\n//   totalRemaining: 94766\n// }",{"id":8265,"title":8266,"titles":8267,"content":8268,"level":449},"/features/events#cleanup-options","Cleanup Options",[248,8257],"interface CleanupOptions {\n  retentionDays?: number   // Override config setting\n  maxEvents?: number       // Override config setting\n  dryRun?: boolean        // Preview without deleting\n}\n\n// Dry run to see what would be deleted\nconst preview = await cleanupOldEvents({ dryRun: true })\n\n// Custom retention (keep only 30 days)\nconst result = await cleanupOldEvents({ retentionDays: 30 })\n\n// Limit by count (keep max 50k events)\nconst result = await cleanupOldEvents({ maxEvents: 50000 })",{"id":8270,"title":8271,"titles":8272,"content":8273,"level":449},"/features/events#cleanup-strategy","Cleanup Strategy",[248,8257],"The cleanup utility uses a two-phase approach: Phase 1: Age-based deletionDeletes events older than retentionDaysExample: 90 days (default)Phase 2: Count-based deletionIf total still exceeds maxEvents, delete oldest eventsDeletes in batches of 1000 to avoid query limitsCapped at 5000 deletions per run to avoid full-table scans on large tables. If excess remains, the next scheduled cleanup run continues where this one left off. // Cleanup process\n1. Count total events: 125,000\n2. Delete events > 90 days: -20,000 (105,000 remaining)\n3. Check max limit: 105,000 > 100,000\n4. Delete 5,000 oldest events: -5,000 (100,000 remaining)\n5. Result: 25,000 deleted, 100,000 remaining",{"id":8275,"title":8276,"titles":8277,"content":8278,"level":449},"/features/events#scheduled-cleanup-nuxthub","Scheduled Cleanup (NuxtHub)",[248,8257],"You can schedule automatic cleanup using NuxtHub's scheduled tasks: // server/tasks/cleanup-events.ts\nexport default defineTask({\n  meta: {\n    name: 'cleanup-old-events',\n    description: 'Remove old event tracking data'\n  },\n  \n  // Run daily at 3 AM\n  run: async () => {\n    const result = await cleanupOldEvents()\n    \n    return {\n      result: 'success',\n      deletedCount: result.deletedCount,\n      remaining: result.totalRemaining\n    }\n  }\n})",{"id":8280,"title":8281,"titles":8282,"content":528,"level":391},"/features/events#migration-stability","Migration & Stability",[248],{"id":8284,"title":8285,"titles":8286,"content":8287,"level":449},"/features/events#beta-stability-warning","Beta Stability Warning",[248,8281],"This package is in active development (v0.1.0 BETA). Be aware of: What's Stable: ✅ Core event tracking and storage✅ Smart diff calculation✅ Auto-tracking plugin✅ Basic querying and filtering✅ Cleanup utilities What May Change: ⚠️ User data enrichment API⚠️ Advanced query filters⚠️ Event schema (additional fields)⚠️ Configuration structure⚠️ Component APIs",{"id":8289,"title":8290,"titles":8291,"content":8292,"level":449},"/features/events#migration-expectations","Migration Expectations",[248,8281],"When upgrading between beta versions: Schema Changes: May require database migrationsConfig Changes: Runtime config structure may evolveAPI Changes: Composable signatures may changeBreaking Changes: Expect breaking changes until v1.0 Recommended Approach: Pin to specific version in package.jsonTest thoroughly before upgradingReview changelog for breaking changesConsider event data as audit logs (preserve on migration)",{"id":8294,"title":44,"titles":8295,"content":528,"level":391},"/features/events#best-practices",[248],{"id":8297,"title":8298,"titles":8299,"content":8300,"level":449},"/features/events#when-to-use-events","When to Use Events",[248,44],"Good Use Cases: ✅ Audit trails for compliance✅ User activity monitoring✅ Change history for important records✅ Debugging data issues✅ Analytics and reporting Not Recommended For: ❌ Real-time notifications (use WebSockets)❌ Undo/redo functionality (too expensive)❌ Version control (consider separate versioning system)❌ High-frequency events (> 1000/sec)",{"id":8302,"title":4246,"titles":8303,"content":8304,"level":449},"/features/events#performance-tips",[248,44],"Configure Retention Aggressivelyretention: {\n  days: 30,        // Keep only recent data\n  maxEvents: 50000 // Limit total events\n}\nUse Specific Filters// Good: Specific filters reduce result set\nfilters: {\n  collectionName: 'users',\n  operation: 'update',\n  dateFrom: last7Days\n}\n\n// Bad: Fetching all events\nconst { data } = useCroutonEvents()\nPaginate Resultspagination: {\n  page: 1,\n  pageSize: 50  // Don't fetch thousands at once\n}\nIndex Frequently Queried FieldscollectionNameuserIdtimestampoperation",{"id":8306,"title":6823,"titles":8307,"content":8308,"level":449},"/features/events#security-considerations",[248,44],"Sensitive Data// Avoid tracking sensitive fields\n// Consider excluding fields like passwords, tokens, etc.\n// The smart diff will store field values as JSON\nUser Attribution// userName snapshot helps with audit trails\n// But consider privacy implications\nsnapshotUserName: true  // Default\nAccess Control// Events inherit team-based access control\n// Only users in the team can view events\n// Enforce via middleware in custom endpoints",{"id":8310,"title":36,"titles":8311,"content":528,"level":391},"/features/events#troubleshooting",[248],{"id":8313,"title":8314,"titles":8315,"content":8316,"level":449},"/features/events#events-not-being-tracked","Events Not Being Tracked",[248,36],"Check 1: Verify configuration const config = useRuntimeConfig()\nconsole.log(config.public.croutonEvents?.enabled)  // Should be true Check 2: Verify user session const { user } = useUserSession()\nconsole.log(user.value)  // Should exist Check 3: Check health monitoring \u003Cscript setup lang=\"ts\">\nconst health = useState('crouton-events-health')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"health\">\n    Failed: {{ health.failed }} / {{ health.total }}\n    Last error: {{ health.lastError }}\n  \u003C/div>\n\u003C/template>",{"id":8318,"title":8319,"titles":8320,"content":8321,"level":449},"/features/events#high-failure-rate","High Failure Rate",[248,36],"Possible Causes: Network issuesDatabase connection problemsInvalid user sessionValidation errors Debug Steps: // Enable console logging\ncroutonEvents: {\n  errorHandling: {\n    logToConsole: true\n  }\n}\n\n// Check browser console for errors\n// Check network tab for failed API calls",{"id":8323,"title":8324,"titles":8325,"content":8326,"level":449},"/features/events#events-not-appearing-in-list","Events Not Appearing in List",[248,36],"Check 1: Verify team context const { getTeamId } = useTeamContext()\nconsole.log(getTeamId())  // Should match event teamId Check 2: Check filters // Remove filters to see all events\nconst { data } = useCroutonEvents({\n  filters: {}  // No filters\n})",{"id":8328,"title":8329,"titles":8330,"content":8331,"level":449},"/features/events#database-growing-too-large","Database Growing Too Large",[248,36],"Solution 1: Run manual cleanup // In server endpoint or task\nconst result = await cleanupOldEvents() Solution 2: Adjust retention croutonEvents: {\n  retention: {\n    days: 30,         // Shorter retention\n    maxEvents: 10000  // Lower limit\n  }\n}",{"id":8333,"title":285,"titles":8334,"content":528,"level":391},"/features/events#api-reference",[248],{"id":8336,"title":6551,"titles":8337,"content":8338,"level":449},"/features/events#types",[248,285],"// Core event type\ninterface CroutonEvent {\n  id: string\n  timestamp: string | Date\n  operation: 'create' | 'update' | 'delete'\n  collectionName: string\n  itemId: string\n  userId: string\n  userName: string\n  changes: EventChange[]\n  metadata?: Record\u003Cstring, unknown>\n}\n\n// Change record\ninterface EventChange {\n  fieldName: string\n  oldValue: string | null  // JSON stringified\n  newValue: string | null  // JSON stringified\n}\n\n// Tracking options\ninterface TrackEventOptions {\n  operation: 'create' | 'update' | 'delete'\n  collection: string\n  itemId?: string\n  itemIds?: string[]\n  data?: any\n  updates?: any\n  result?: any\n  beforeData?: any\n}\n\n// Query options\ninterface UseCroutonEventsOptions {\n  teamId?: string\n  enrichUserData?: boolean\n  filters?: {\n    collectionName?: string\n    operation?: 'create' | 'update' | 'delete'\n    userId?: string\n    dateFrom?: Date\n    dateTo?: Date\n  }\n  pagination?: {\n    page?: number\n    pageSize?: number\n  }\n}\n\n// Cleanup options\ninterface CleanupOptions {\n  retentionDays?: number\n  maxEvents?: number\n  dryRun?: boolean\n}\n\n// Cleanup result\ninterface CleanupResult {\n  deletedCount: number\n  oldestRemaining: Date | null\n  totalRemaining: number\n}\n\n// Health monitoring\ninterface CroutonEventsHealth {\n  total: number\n  failed: number\n  lastError: string | null\n  lastErrorTime: Date | null\n}",{"id":8340,"title":1007,"titles":8341,"content":8342,"level":391},"/features/events#related-resources",[248],"Nuxt Crouton Core DocumentationCollection ManagementTeam ContextUser Sessions",{"id":8344,"title":8345,"titles":8346,"content":8347,"level":391},"/features/events#support-feedback","Support & Feedback",[248],"This is a beta package. We welcome feedback and bug reports: GitHub Issues: nuxt-crouton/issuesDiscussions: nuxt-crouton/discussions When reporting issues, include: Package version (v0.1.0)Nuxt Crouton versionError messages from consoleHealth monitoring statsSteps to reproduce html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":184,"title":188,"titles":8349,"content":8350,"level":385},[],"This section covers advanced features in Nuxt Crouton. Each feature has a stability status indicating its readiness for production use.",{"id":8352,"title":188,"titles":8353,"content":8350,"level":385},"/features#features-overview",[],{"id":8355,"title":8356,"titles":8357,"content":8358,"level":391},"/features#stability-status","Stability Status",[188],"All features in this section are labeled with their current stability status: Stability LegendStable ✅ - Production-ready, fully tested, breaking changes rareBeta 🔬 - Feature-complete but may have minor changes, safe for non-critical useExperimental ⚠️ - Under active development, API may change, use with caution",{"id":8360,"title":3431,"titles":8361,"content":528,"level":391},"/features#available-features",[188],{"id":8363,"title":8364,"titles":8365,"content":8366,"level":449},"/features#_1-internationalization-i18n","1. Internationalization (i18n)",[188,3431],"File: 1.internationalization.md | Status: Stable ✅ Multi-language support for your Nuxt Crouton applications: Translatable fields in collectionsLanguage switching UITranslation management interfaceFallback language supportIntegration with Nuxt i18n module Use case: Multi-language applications, global products, localized content",{"id":8368,"title":8369,"titles":8370,"content":8371,"level":449},"/features#_2-rich-text-editor","2. Rich Text Editor",[188,3431],"File: 6.rich-text.md | Status: Beta 🔬 Rich text editing capabilities: WYSIWYG editor integrationMarkdown supportCustom formatting optionsImage embeddingCode blocks and syntax highlighting Use case: Blog posts, documentation, content management systems",{"id":8373,"title":8374,"titles":8375,"content":8376,"level":449},"/features#_3-assets-file-management","3. Assets & File Management",[188,3431],"File: 7.assets.md | Status: Experimental ⚠️ File upload and asset management: File upload fieldsImage optimizationAsset storage (local, cloud)File type validationAsset gallery views Use case: Image galleries, document management, media libraries Note: API may change in future releases. Use storage adapters for production.",{"id":8378,"title":8379,"titles":8380,"content":8381,"level":449},"/features#_4-ai-integration","4. AI Integration",[188,3431],"File: 13.ai.md | Status: Stable ✅ AI chat and completion capabilities: useChat() composable for streaming conversationsuseCompletion() for text generationAIChatbox, AIMessage, AIInput componentsMulti-provider support (OpenAI, Anthropic)Server-side provider factoryChat conversation persistence schema Use case: AI assistants, chatbots, content generation, code assistants",{"id":8383,"title":8384,"titles":8385,"content":8386,"level":449},"/features#_5-events-system","5. Events System",[188,3431],"File: 9.events.md | Status: Experimental ⚠️ Event-driven architecture for collections: Lifecycle hooks (beforeCreate, afterUpdate, etc.)Custom event handlersEvent bus integrationWebhooks supportReal-time updates Use case: Audit logging, notifications, data synchronization, workflow automation Note: API under active development. Event patterns may change.",{"id":8388,"title":8389,"titles":8390,"content":8391,"level":449},"/features#_6-maps-integration","6. Maps Integration",[188,3431],"File: 10.maps.md | Status: Beta 🔬 Location and map features: Map field typesLocation picker UIGeocoding supportMap providers (Leaflet, Mapbox, Google Maps)Custom map styles Use case: Store locators, delivery tracking, location-based services",{"id":8393,"title":8394,"titles":8395,"content":8396,"level":449},"/features#_7-devtools","7. DevTools",[188,3431],"File: 11.devtools.md | Status: Beta 🔬 Enhanced development experience: Collection inspectorSchema validatorGenerated code viewerPerformance profilerDatabase query inspector Use case: Debugging, optimization, understanding generated code",{"id":8398,"title":8399,"titles":8400,"content":8401,"level":449},"/features#_8-flow-visualization","8. Flow Visualization",[188,3431],"File: 12.flow.md | Status: Beta 🔬 Interactive graph visualization with real-time collaboration: DAG/tree visualization with Vue FlowDrag-and-drop node positioning with persistenceReal-time multiplayer sync (Yjs + Durable Objects)Presence indicators (cursors, selections)Custom node components per collectionAuto-layout with Dagre Use case: Workflow builders, decision trees, entity relationships, collaborative editing Note: Sync mode requires Cloudflare Durable Objects infrastructure.",{"id":8403,"title":8404,"titles":8405,"content":8406,"level":449},"/features#_9-data-export","9. Data Export",[188,3431],"File: 15.export.md | Status: Stable ✅ Export collection data to CSV and JSON formats: CSV and JSON export formatsCustom field selection and labelingField and row transformationsServer-side query exportsReady-to-use export button componenti18n support for labels Use case: Reporting, data backups, data migration, audit exports",{"id":8408,"title":8409,"titles":8410,"content":8411,"level":449},"/features#_10-admin-dashboard","10. Admin Dashboard",[188,3431],"File: 14.admin.md | Status: Stable ✅ Super admin dashboard for system-wide management: User management (list, create, ban, unban, delete)Team oversight (view all teams/members)User impersonation for debuggingSystem statistics overviewSuper admin middleware Use case: Platform administration, user moderation, system monitoring",{"id":8413,"title":8414,"titles":8415,"content":8416,"level":449},"/features#_11-collaboration","11. Collaboration",[188,3431],"File: 16.collaboration.md | Status: Beta 🔬 Real-time collaboration with presence awareness: Live cursor trackingUser presence indicatorsReal-time document syncYjs + Durable Objects integrationConflict resolution Use case: Collaborative editing, real-time dashboards, multiplayer features",{"id":8418,"title":8419,"titles":8420,"content":8421,"level":449},"/features#_12-email","12. Email",[188,3431],"File: 17.email.md | Status: Stable ✅ Email infrastructure with Vue Email templates: Verification codes and magic linksPassword reset and team invitationsVue Email templates with Resend deliveryClient flow componentsIntegration with auth system Use case: Authentication flows, transactional emails, team invitations",{"id":8423,"title":8424,"titles":8425,"content":8426,"level":449},"/features#_13-pages-cms","13. Pages (CMS)",[188,3431],"File: 18.pages.md | Status: Stable ✅ CMS-like page management system: Block-based page editorPage types from app packagesTree organization with drag-and-dropCustom domain supportPublic page routing Use case: Marketing sites, documentation, customer portals",{"id":8428,"title":8429,"titles":8430,"content":8431,"level":449},"/features#_14-bookings","14. Bookings",[188,3431],"File: 19.bookings.md | Status: Experimental ⚠️ Booking system for appointments and reservations: Slot-based bookings (courts, rooms)Inventory-based reservations (equipment)Customer booking wizardCart system for multiple bookingsEmail notifications (opt-in) Use case: Sports facilities, healthcare, rentals, services Note: API under active development.",{"id":8433,"title":8434,"titles":8435,"content":8436,"level":449},"/features#_15-sales-pos","15. Sales (POS)",[188,3431],"File: 20.sales.md | Status: Experimental ⚠️ Event-based Point of Sale system: Products and categoriesCart and checkout flowHelper authentication (PIN-based)Offline supportThermal receipt printing (opt-in) Use case: Markets, pop-up shops, events, food service Note: API under active development.",{"id":8438,"title":8439,"titles":8440,"content":528,"level":391},"/features#choosing-the-right-features","Choosing the Right Features",[188],{"id":8442,"title":8443,"titles":8444,"content":8445,"level":449},"/features#for-production-apps","For Production Apps",[188,8439],"Use Stable features with confidence. These are thoroughly tested and have stable APIs. Recommended: Internationalization for multi-language needsAI Integration for chat and content generationData Export for reporting and backupsAdmin Dashboard for user/team managementEmail for authentication and transactional emailsPages for CMS-like content managementAny stable features you need",{"id":8447,"title":8448,"titles":8449,"content":8450,"level":449},"/features#for-non-critical-apps","For Non-Critical Apps",[188,8439],"Beta features are safe to use but may have minor API changes in future releases. Consider: Rich Text Editor for content-heavy appsMaps Integration for location featuresFlow Visualization for workflows and decision treesCollaboration for real-time multiplayer featuresDevTools during development",{"id":8452,"title":8453,"titles":8454,"content":8455,"level":449},"/features#for-experimentation","For Experimentation",[188,8439],"Experimental features are under active development. Use in development/staging, but be prepared for API changes. Use with caution: Assets & File Management (storage patterns may evolve)Events System (hook signatures may change)Bookings (mini-app, API may change)Sales/POS (mini-app, API may change)",{"id":8457,"title":8458,"titles":8459,"content":8460,"level":391},"/features#feature-combinations","Feature Combinations",[188],"Many features work great together: i18n + Rich Text: Multilingual content with rich formattingAI + i18n: AI-powered content generation with translationsMaps + Assets: Location-based image galleriesEvents + External Connectors: Sync data to external systems on changesFlow + Events: Trigger actions on workflow step changesAI + Flow: AI-assisted workflow creation and optimizationDevTools + Any Feature: Debug and optimize during development",{"id":8462,"title":8463,"titles":8464,"content":8465,"level":391},"/features#migration-path","Migration Path",[188],"As features mature: Experimental features get refined based on feedbackBeta features stabilize with consistent APIsStable features guarantee backward compatibility We'll provide migration guides for any breaking changes in experimental/beta features.",{"id":8467,"title":2916,"titles":8468,"content":8469,"level":391},"/features#where-to-go-next",[188],"After exploring features: API Reference → Detailed API docs for feature composables and componentsGuides → Best practices and troubleshootingCustomization → Customize features to match your needs",{"id":8471,"title":426,"titles":8472,"content":8473,"level":391},"/features#prerequisites",[188],"Before using advanced features: Completed Getting StartedUnderstand FundamentalsFamiliarity with Generation workflow",{"id":8475,"title":2925,"titles":8476,"content":8477,"level":391},"/features#external-resources",[188],"Feature-specific external resources: i18n: Nuxt i18n ModuleRich Text: Tiptap EditorAI: Vercel AI SDK, OpenAI API, Anthropic APIAssets: Cloudflare R2, AWS S3Maps: Leaflet, MapboxFlow: Vue Flow, Yjs, Cloudflare Durable ObjectsEvents: Event-Driven Architecture",{"id":8479,"title":8480,"titles":8481,"content":8482,"level":391},"/features#feedback-contributions","Feedback & Contributions",[188],"Help us improve features: Report bugs for any stability levelSuggest API improvements for beta/experimental featuresShare use cases to help prioritize stabilizationContribute documentation improvements Features graduate to stable status based on: Production usage and feedbackAPI stability over timeComprehensive test coverageComplete documentation",{"id":261,"title":260,"titles":8484,"content":8485,"level":385},[],"Scope data and API calls to specific teams Team-based authentication is built into Nuxt Crouton, automatically scoping all data and API calls to the current team context.",{"id":8487,"title":1635,"titles":8488,"content":8489,"level":391},"/advanced/team-based-auth#how-it-works",[260],"All generated collections are team-scoped by default: Database fields are automatically added:teamId (text, required) - References the team/organizationowner (text, required) - References the user who owns the recordcreatedBy (text) - References the user who created the recordAPI endpoints automatically:Inject teamId and owner from the authenticated sessionInclude team membership validation via resolveTeamAndCheckMembershipScope all database queries to the current user's teamClient-side queries automatically:Include the current team ID in API callsFilter results by teamInvalidate when team changes Important: Do NOT manually define teamId or owner in your schema JSON files. The generator adds these automatically, and manual definitions will cause duplicate key errors.See Schema Format - Auto-Generated Fields for details.",{"id":8491,"title":4183,"titles":8492,"content":8493,"level":391},"/advanced/team-based-auth#requirements",[260],"Team authentication requires @fyit/crouton-auth: pnpm add @fyit/crouton-auth // nuxt.config.ts\nexport default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-auth'\n  ]\n})",{"id":8495,"title":8496,"titles":8497,"content":8498,"level":391},"/advanced/team-based-auth#client-side-usage","Client-Side Usage",[260],"Use useTeamContext() to access the current team: \u003Cscript setup lang=\"ts\">\nconst { teamId, teamSlug } = useTeamContext()\n\n// All API calls include team context\nconst { items } = await useCollectionQuery('shopProducts')\n// → Fetches /api/teams/[teamId]/shop-products\n\u003C/script> Query Examples: For complete useCollectionQuery patterns, see Querying Data.",{"id":8500,"title":8501,"titles":8502,"content":8503,"level":391},"/advanced/team-based-auth#server-side-usage","Server-Side Usage",[260],"Generated API endpoints use @fyit/crouton-auth/server/utils/team for team authentication: // Generated: server/api/teams/[team]/shop-products/index.get.ts\nimport { resolveTeamAndCheckMembership } from '@fyit/crouton-auth/server/utils/team'\n\nexport default defineEventHandler(async (event) => {\n  const { team, user } = await resolveTeamAndCheckMembership(event)\n\n  // Query scoped to team\n  const products = await db\n    .select()\n    .from(shopProducts)\n    .where(eq(shopProducts.teamId, team.id))\n\n  return products\n})",{"id":8505,"title":7586,"titles":8506,"content":8507,"level":391},"/advanced/team-based-auth#api-routes",[260],"Team-scoped API routes follow the pattern: /api/teams/[team]/{layer}-{collection}/ For example: /api/teams/abc123/shop-products/ - List products for team/api/teams/abc123/shop-products/xyz789 - Get/update/delete specific product",{"id":8509,"title":8510,"titles":8511,"content":8512,"level":391},"/advanced/team-based-auth#team-switching","Team Switching",[260],"Allow users to switch between teams with automatic query refetching: \u003Cscript setup lang=\"ts\">\nconst { currentTeam, teams, switchTeam } = useTeam()\n\nconst handleSwitchTeam = async (teamId: string) => {\n  await switchTeam(teamId)\n  // All queries auto-refetch for new team!\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUSelectMenu\n    v-model=\"currentTeam\"\n    :options=\"teams\"\n    @update:model-value=\"handleSwitchTeam\"\n  />\n\u003C/template>",{"id":8514,"title":8515,"titles":8516,"content":8517,"level":391},"/advanced/team-based-auth#how-team-context-flows","How Team Context Flows",[260],"User authenticates via @fyit/crouton-authTeam is resolved from route parameter [team]Membership is validated against the user's sessionteamId is injected into all queries and mutationsSwitching teams invalidates and refetches all queries",{"id":8519,"title":1958,"titles":8520,"content":8521,"level":391},"/advanced/team-based-auth#related-sections",[260],"Querying Data - Query patternsCaching - Cache invalidationData Operations - CRUD operationsMigration Guide - Migrating from older versions html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":265,"title":264,"titles":8523,"content":8524,"level":385},[],"Create dynamic forms with conditional logic Build dynamic forms that show/hide fields based on conditions and create cascading dropdown menus.",{"id":8526,"title":2450,"titles":8527,"content":8528,"level":391},"/advanced/conditional-fields#conditional-fields",[264],"Show or hide form fields based on the value of other fields: \u003Ctemplate>\n  \u003CUForm>\n    \u003CUFormField label=\"Product Type\" name=\"type\">\n      \u003CUSelectMenu\n        v-model=\"state.type\"\n        :options=\"['physical', 'digital']\"\n      />\n    \u003C/UFormField>\n\n    \u003C!-- Show only for physical products -->\n    \u003CUFormField v-if=\"state.type === 'physical'\" label=\"Weight\" name=\"weight\">\n      \u003CUInput v-model.number=\"state.weight\" type=\"number\" />\n    \u003C/UFormField>\n\n    \u003C!-- Show only for digital products -->\n    \u003CUFormField v-if=\"state.type === 'digital'\" label=\"Download URL\" name=\"downloadUrl\">\n      \u003CUInput v-model=\"state.downloadUrl\" />\n    \u003C/UFormField>\n  \u003C/UForm>\n\u003C/template>",{"id":8530,"title":8531,"titles":8532,"content":8533,"level":391},"/advanced/conditional-fields#dependent-dropdowns","Dependent Dropdowns",[264],"Create cascading dropdowns where options in one field depend on the selection in another: Query Examples: For complete useCollectionQuery patterns, see Querying Data. \u003Cscript setup lang=\"ts\">\nconst { items: categories } = await useCollectionQuery('shopCategories')\nconst selectedCategory = ref\u003Cstring | null>(null)\n\n// Fetch subcategories when category changes\nconst { items: subcategories } = await useCollectionQuery('shopSubcategories', {\n  query: computed(() => ({\n    categoryId: selectedCategory.value\n  }))\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUFormField label=\"Category\" name=\"categoryId\">\n    \u003CUSelectMenu\n      v-model=\"selectedCategory\"\n      :options=\"categories\"\n      option-attribute=\"name\"\n    />\n  \u003C/UFormField>\n\n  \u003CUFormField v-if=\"selectedCategory\" label=\"Subcategory\" name=\"subcategoryId\">\n    \u003CUSelectMenu\n      v-model=\"state.subcategoryId\"\n      :options=\"subcategories\"\n      option-attribute=\"name\"\n    />\n  \u003C/UFormField>\n\u003C/template>",{"id":8535,"title":1635,"titles":8536,"content":528,"level":391},"/advanced/conditional-fields#how-it-works",[264],{"id":8538,"title":2450,"titles":8539,"content":8540,"level":449},"/advanced/conditional-fields#conditional-fields-1",[264,1635],"Use Vue's v-if directive to conditionally render form fieldsReference reactive state values to determine visibilityFields are automatically added/removed from the form state",{"id":8542,"title":8531,"titles":8543,"content":8544,"level":449},"/advanced/conditional-fields#dependent-dropdowns-1",[264,1635],"The parent dropdown value is stored in a reactive refA computed query watches the parent valueWhen the parent changes, the child query automatically refetchesThe child dropdown only appears when the parent has a value",{"id":8546,"title":44,"titles":8547,"content":8548,"level":391},"/advanced/conditional-fields#best-practices",[264],"Always provide default values for conditional fieldsClear dependent field values when parent selection changesUse loading states while fetching dependent optionsValidate that required conditional fields are filled before submission",{"id":8550,"title":8551,"titles":8552,"content":8553,"level":391},"/advanced/conditional-fields#auto-generated-dependent-fields","Auto-Generated Dependent Fields",[264],"For common patterns like loading data from related collections, Nuxt Crouton can generate dependent field logic automatically through schema configuration.",{"id":8555,"title":8556,"titles":8557,"content":8558,"level":449},"/advanced/conditional-fields#when-to-use-auto-generated-vs-manual","When to Use Auto-Generated vs Manual",[264,8551],"Use Auto-Generated Dependent Fields when: Loading data from a referenced collection (e.g., slots from a location)The pattern is standard (fetch on change, reset on dependency change)You want regeneration-safe codeUsing supported display modes like slotButtonGroup Use Manual Conditional Fields when: Custom business logic beyond simple data loadingComplex validation or computed valuesUnsupported UI patternsOne-off scenarios",{"id":8560,"title":8561,"titles":8562,"content":8563,"level":449},"/advanced/conditional-fields#example-auto-generated-slot-selection","Example: Auto-Generated Slot Selection",[264,8551],"Instead of manually writing: \u003Cscript setup>\nconst { data: locationData } = await useFetch(() =>\n  state.value.location\n    ? `/api/teams/${teamId}/bookingsLocations/${state.value.location}`\n    : null\n, {\n  watch: [() => state.value.location],\n  immediate: false\n})\n\nwatch(() => state.value.location, () => {\n  state.value.slot = 0\n})\n\u003C/script> Simply configure your schema: {\n  \"slot\": {\n    \"type\": \"number\",\n    \"meta\": {\n      \"dependsOn\": \"location\",\n      \"dependsOnField\": \"slots\",\n      \"dependsOnCollection\": \"locations\",\n      \"displayAs\": \"slotButtonGroup\"\n    }\n  }\n} The generator creates all the fetch logic, watchers, and conditional UI automatically.",{"id":8565,"title":8566,"titles":8567,"content":8568,"level":449},"/advanced/conditional-fields#benefits-of-auto-generated-approach","Benefits of Auto-Generated Approach",[264,8551],"Regeneration-Safe - Persists through schema changes and regenerationConsistent - Same pattern across all collectionsMaintainable - Configuration in schema, not scattered in codeType-Safe - Automatically generates TypeScript typesDRY - No code duplication for common patterns See Schema Format - Dependent Fields for complete configuration details.",{"id":8570,"title":1958,"titles":8571,"content":8572,"level":391},"/advanced/conditional-fields#related-sections",[264],"Forms & Modals - Form basicsForm Patterns - Common patternsQuerying Data - Query patternsSchema Format - Schema configuration html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":269,"title":268,"titles":8574,"content":8575,"level":385},[],"Perform updates on multiple records at once Efficiently update or delete multiple records at once with bulk operations.",{"id":8577,"title":8578,"titles":8579,"content":8580,"level":391},"/advanced/bulk-operations#basic-bulk-update","Basic Bulk Update",[268],"Select multiple items and apply updates to all of them: \u003Cscript setup lang=\"ts\">\nconst selectedIds = ref\u003Cstring[]>([])\nconst { update } = useCollectionMutation('shopProducts')\n\nconst handleBulkUpdate = async (updates: any) => {\n  for (const id of selectedIds.value) {\n    await update(id, updates)\n  }\n  selectedIds.value = []\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003C!-- Selection UI -->\n    \u003CCroutonCollection\n      v-model:selected=\"selectedIds\"\n      selectable\n    />\n\n    \u003C!-- Bulk actions -->\n    \u003CUButton\n      v-if=\"selectedIds.length > 0\"\n      @click=\"handleBulkUpdate({ featured: true })\"\n    >\n      Mark {{ selectedIds.length }} as Featured\n    \u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":8582,"title":1635,"titles":8583,"content":8584,"level":391},"/advanced/bulk-operations#how-it-works",[268],"Selection: Use the selectable prop on CroutonCollection to enable row selectionTrack Selection: Bind selected IDs to a reactive ref with v-model:selectedApply Updates: Loop through selected IDs and apply mutationsClear Selection: Reset the selection array after completion",{"id":8586,"title":8587,"titles":8588,"content":8589,"level":391},"/advanced/bulk-operations#multiple-bulk-actions","Multiple Bulk Actions",[268],"Provide different bulk operations for the same selection: \u003Cscript setup lang=\"ts\">\nconst selectedIds = ref\u003Cstring[]>([])\nconst { update, deleteItems } = useCollectionMutation('shopProducts')\n\nconst bulkFeature = async () => {\n  for (const id of selectedIds.value) {\n    await update(id, { featured: true })\n  }\n  selectedIds.value = []\n}\n\nconst bulkUnpublish = async () => {\n  for (const id of selectedIds.value) {\n    await update(id, { published: false })\n  }\n  selectedIds.value = []\n}\n\nconst bulkDelete = async () => {\n  if (confirm(`Delete ${selectedIds.value.length} items?`)) {\n    for (const id of selectedIds.value) {\n      await deleteItems([id])\n    }\n    selectedIds.value = []\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"selectedIds.length > 0\" class=\"flex gap-2\">\n    \u003CUButton @click=\"bulkFeature\">\n      Mark as Featured\n    \u003C/UButton>\n    \u003CUButton @click=\"bulkUnpublish\" color=\"gray\">\n      Unpublish\n    \u003C/UButton>\n    \u003CUButton @click=\"bulkDelete\" color=\"red\">\n      Delete {{ selectedIds.length }}\n    \u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":8591,"title":44,"titles":8592,"content":8593,"level":391},"/advanced/bulk-operations#best-practices",[268],"Always confirm destructive bulk operations (like delete)Show the number of selected items in button labelsClear selection after successful operationsConsider adding loading states during bulk operationsUse optimistic updates for better UXHandle errors gracefully (some items might fail)",{"id":8595,"title":1958,"titles":8596,"content":8597,"level":391},"/advanced/bulk-operations#related-sections",[268],"Data Operations - CRUD operationsTable Patterns - Working with tablesOptimistic Updates - Instant feedback html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":273,"title":272,"titles":8599,"content":8600,"level":385},[],"Enhance UX with instant feedback and custom validation rules Provide instant user feedback with optimistic updates and implement custom validation logic.",{"id":8602,"title":2527,"titles":8603,"content":8604,"level":391},"/advanced/optimistic-updates#optimistic-updates",[272],"Mutations automatically invalidate cache and refetch data. However, you can add optimistic updates for instant feedback: \u003Cscript setup lang=\"ts\">\nconst { items } = await useCollectionQuery('shopProducts')\nconst { update } = useCollectionMutation('shopProducts')\n\nconst toggleFeatured = async (product: Product) => {\n  // Optimistic: Update UI immediately\n  const index = items.value.findIndex(p => p.id === product.id)\n  if (index !== -1) {\n    items.value[index].featured = !items.value[index].featured\n  }\n\n  try {\n    // API call\n    await update(product.id, {\n      featured: !product.featured\n    })\n    // Cache auto-refetches on success\n  } catch (error) {\n    // Rollback on error\n    if (index !== -1) {\n      items.value[index].featured = !items.value[index].featured\n    }\n  }\n}\n\u003C/script>",{"id":8606,"title":8607,"titles":8608,"content":8609,"level":391},"/advanced/optimistic-updates#how-optimistic-updates-work","How Optimistic Updates Work",[272],"Immediate Update: Modify local state before the API callAPI Call: Make the actual mutation requestAuto-Refetch: On success, cache automatically refreshesRollback: On error, revert the local state change",{"id":8611,"title":8612,"titles":8613,"content":8614,"level":391},"/advanced/optimistic-updates#custom-validation","Custom Validation",[272],"Implement custom validation rules using Zod schemas with cross-field and async validation: // composables/useProducts.ts\nconst schema = z.object({\n  name: z.string().min(1),\n  price: z.number().min(0),\n  discountPrice: z.number().optional()\n})\n.refine((data) => {\n  // Cross-field validation\n  if (data.discountPrice && data.discountPrice >= data.price) {\n    return false\n  }\n  return true\n}, {\n  message: 'Discount must be less than price',\n  path: ['discountPrice']\n})\n.refine(async (data) => {\n  // Async validation\n  const exists = await $fetch(`/api/products/check-name?name=${data.name}`)\n  return !exists\n}, {\n  message: 'Product name already exists'\n})",{"id":8616,"title":8617,"titles":8618,"content":528,"level":391},"/advanced/optimistic-updates#validation-types","Validation Types",[272],{"id":8620,"title":3940,"titles":8621,"content":8622,"level":449},"/advanced/optimistic-updates#cross-field-validation",[272,8617],"Validate relationships between multiple fields: .refine((data) => {\n  if (data.discountPrice && data.discountPrice >= data.price) {\n    return false\n  }\n  return true\n}, {\n  message: 'Discount must be less than price',\n  path: ['discountPrice']\n})",{"id":8624,"title":3945,"titles":8625,"content":8626,"level":449},"/advanced/optimistic-updates#async-validation",[272,8617],"Perform asynchronous checks like database queries: .refine(async (data) => {\n  const exists = await $fetch(`/api/products/check-name?name=${data.name}`)\n  return !exists\n}, {\n  message: 'Product name already exists'\n})",{"id":8628,"title":44,"titles":8629,"content":528,"level":391},"/advanced/optimistic-updates#best-practices",[272],{"id":8631,"title":2527,"titles":8632,"content":8633,"level":449},"/advanced/optimistic-updates#optimistic-updates-1",[272,44],"Always implement error rollbackUse for frequent, low-risk operations (toggles, favorites)Avoid for critical operations (payments, deletions)Provide visual feedback during the operation",{"id":8635,"title":8612,"titles":8636,"content":8637,"level":449},"/advanced/optimistic-updates#custom-validation-1",[272,44],"Keep validation logic close to your formsUse clear, user-friendly error messagesSpecify the path to attach errors to specific fieldsDebounce async validation to reduce API callsCache async validation results when possible",{"id":8639,"title":1958,"titles":8640,"content":8641,"level":391},"/advanced/optimistic-updates#related-sections",[272],"Data Operations - CRUD operationsCaching - Cache invalidationForm Patterns - Form best practices html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":277,"title":276,"titles":8643,"content":8644,"level":385},[],"Protect your authentication endpoints from abuse with rate limiting Protect your authentication and API endpoints from abuse using rate limiting with nuxthub-ratelimit.",{"id":8646,"title":8647,"titles":8648,"content":8649,"level":391},"/advanced/rate-limiting#why-rate-limit","Why Rate Limit?",[276],"Authentication endpoints are prime targets for: Brute force attacks - Trying many passwordsCredential stuffing - Using leaked credentialsAccount enumeration - Discovering valid emailsEmail abuse - Spamming password reset emails Rate limiting restricts how many requests a client can make in a given time window.",{"id":8651,"title":13,"titles":8652,"content":8653,"level":391},"/advanced/rate-limiting#installation",[276],"pnpm add nuxthub-ratelimit",{"id":8655,"title":8656,"titles":8657,"content":8658,"level":391},"/advanced/rate-limiting#basic-configuration","Basic Configuration",[276],"Add the module and configure routes in your nuxt.config.ts: // nuxt.config.ts\nexport default defineNuxtConfig({\n  extends: ['@fyit/crouton-auth'],\n  modules: ['nuxthub-ratelimit'],\n\n  rateLimit: {\n    routes: {\n      // Strict limits for auth endpoints\n      '/api/auth/*': {\n        maxRequests: 15,\n        intervalSeconds: 60\n      },\n      // More lenient for general API\n      '/api/*': {\n        maxRequests: 150,\n        intervalSeconds: 60\n      }\n    }\n  }\n})",{"id":8660,"title":8661,"titles":8662,"content":8663,"level":391},"/advanced/rate-limiting#recommended-configuration","Recommended Configuration",[276],"For production applications with @fyit/crouton-auth, we recommend these limits: // nuxt.config.ts\nexport default defineNuxtConfig({\n  extends: ['@fyit/crouton-auth'],\n  modules: ['nuxthub-ratelimit'],\n\n  rateLimit: {\n    routes: {\n      // Sign-in: Prevent brute force\n      '/api/auth/sign-in/*': {\n        maxRequests: 10,\n        intervalSeconds: 60\n      },\n      // Sign-up: Prevent account spam\n      '/api/auth/sign-up/*': {\n        maxRequests: 5,\n        intervalSeconds: 60\n      },\n      // Password reset: Prevent email abuse\n      '/api/auth/forgot-password': {\n        maxRequests: 3,\n        intervalSeconds: 60\n      },\n      // Email verification: Prevent verification spam\n      '/api/auth/verify-email': {\n        maxRequests: 5,\n        intervalSeconds: 60\n      },\n      // OAuth: Slightly higher for redirects\n      '/api/auth/callback/*': {\n        maxRequests: 20,\n        intervalSeconds: 60\n      },\n      // General auth fallback\n      '/api/auth/*': {\n        maxRequests: 15,\n        intervalSeconds: 60\n      },\n      // Team API endpoints\n      '/api/teams/*': {\n        maxRequests: 100,\n        intervalSeconds: 60\n      },\n      // General API\n      '/api/*': {\n        maxRequests: 150,\n        intervalSeconds: 60\n      }\n    }\n  }\n})",{"id":8665,"title":8666,"titles":8667,"content":8668,"level":391},"/advanced/rate-limiting#limits-reference","Limits Reference",[276],"Endpoint PatternRecommended LimitReason/api/auth/sign-in/*10/minBrute force protection/api/auth/sign-up/*5/minPrevent account spam/api/auth/forgot-password3/minPrevent email abuse/api/auth/verify-email5/minPrevent verification spam/api/auth/callback/*20/minOAuth redirect allowance/api/auth/*15/minGeneral auth fallback/api/teams/*100/minTeam operations/api/*150/minGeneral API",{"id":8670,"title":4183,"titles":8671,"content":8672,"level":391},"/advanced/rate-limiting#requirements",[276],"NuxtHub Required: Rate limiting uses NuxtHub KV for storage. Ensure you have KV enabled in your NuxtHub project.",{"id":8674,"title":8675,"titles":8676,"content":8677,"level":449},"/advanced/rate-limiting#nuxthub-configuration","NuxtHub Configuration",[276,4183],"// nuxt.config.ts\nexport default defineNuxtConfig({\n  hub: {\n    kv: true  // Enable KV storage\n  }\n})",{"id":8679,"title":6942,"titles":8680,"content":8681,"level":449},"/advanced/rate-limiting#limitations",[276,4183],"Minimum TTL: 60 seconds (NuxtHub KV limitation)Storage: Uses KV, not in-memory (persists across deployments)Edge: Works on Cloudflare Workers edge runtime",{"id":8683,"title":1635,"titles":8684,"content":8685,"level":391},"/advanced/rate-limiting#how-it-works",[276],"Each request is tracked by client IP + route patternCounter stored in NuxtHub KV with TTLWhen limit exceeded, returns 429 Too Many RequestsCounter resets after interval expires",{"id":8687,"title":8688,"titles":8689,"content":8690,"level":391},"/advanced/rate-limiting#handling-rate-limit-errors","Handling Rate Limit Errors",[276],"On the client, handle 429 errors gracefully: \u003Cscript setup lang=\"ts\">\nconst { signIn } = useAuth()\nconst error = ref\u003Cstring | null>(null)\n\nconst handleLogin = async () => {\n  try {\n    await signIn.email({ email, password })\n  } catch (e: any) {\n    if (e.statusCode === 429) {\n      error.value = 'Too many attempts. Please wait a minute and try again.'\n    } else {\n      error.value = e.message\n    }\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUAlert v-if=\"error\" color=\"red\" :title=\"error\" />\n\u003C/template>",{"id":8692,"title":8693,"titles":8694,"content":8695,"level":391},"/advanced/rate-limiting#testing-rate-limits","Testing Rate Limits",[276],"During development, you can test rate limits: # Quick test with curl\nfor i in {1..20}; do\n  curl -s -o /dev/null -w \"%{http_code}\\n\" \\\n    -X POST http://localhost:3000/api/auth/sign-in/email \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"email\":\"test@example.com\",\"password\":\"wrong\"}'\ndone You should see 200 responses turn to 429 after hitting the limit.",{"id":8697,"title":1958,"titles":8698,"content":8699,"level":391},"/advanced/rate-limiting#related-sections",[276],"Team-Based Auth - Multi-tenant authenticationAPI Reference - Server utilities html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":253,"title":257,"titles":8701,"content":8702,"level":385},[],"Advanced patterns and techniques for complex Nuxt Crouton implementations",{"id":8704,"title":257,"titles":8705,"content":8706,"level":385},"/advanced#advanced-topics",[],"This section covers advanced patterns and techniques for more complex use cases with Nuxt Crouton.",{"id":8708,"title":8709,"titles":8710,"content":8711,"level":391},"/advanced#whats-covered","What's Covered",[257],"Implement multi-tenant applications with team scoping on all queries and mutations.Show or hide form fields based on other field values or user permissions.Handle batch updates, deletes, and other operations on multiple records.Update the UI immediately while mutations are in progress for a snappier experience.Protect authentication endpoints from abuse with configurable rate limits.",{"id":289,"title":288,"titles":8713,"content":8714,"level":385},[],"Complete TypeScript reference for all interfaces, types, and configuration options in Nuxt Crouton Nuxt Crouton provides comprehensive TypeScript types for type-safe collection management, UI configuration, and data operations. This reference documents all core types, interfaces, and configuration patterns.",{"id":8716,"title":8717,"titles":8718,"content":528,"level":391},"/api-reference/types#core-configuration-types","Core Configuration Types",[288],{"id":8720,"title":8721,"titles":8722,"content":8723,"level":449},"/api-reference/types#collectionconfig","CollectionConfig",[288,8717],"The master configuration interface for registering collections in app.config.ts. type CollectionKind = 'data' | 'content' | 'media'\n\ninterface CollectionConfig {\n  // Basic Collection Info\n  name?: string\n  layer?: string\n  componentName?: string\n  /** Collection kind: determines behavior and available features. */\n  kind?: CollectionKind\n  /** Container type for create/update forms. Defaults to 'slideover'. */\n  container?: 'slideover' | 'modal' | 'dialog' | 'inline'\n  apiPath?: string\n  displayName?: string\n\n  // Pagination Settings\n  defaultPagination?: {\n    currentPage: number\n    pageSize: number\n    sortBy: string\n    sortDirection: 'asc' | 'desc'\n  }\n\n  // Relationship Declarations\n  references?: Record\u003Cstring, string>\n\n  // Custom Component Mapping\n  dependentFieldComponents?: Record\u003Cstring, string>\n\n  // Hierarchy (tree-enabled collections)\n  hierarchy?: {\n    enabled: boolean\n    parentField?: string\n    pathField?: string\n    depthField?: string\n    orderField?: string\n  }\n\n  // Sortable (flat collections with ordering, without hierarchy)\n  sortable?: {\n    enabled: boolean\n    orderField?: string\n  }\n\n  // Admin sidebar navigation\n  adminNav?: {\n    enabled?: boolean\n    icon?: string\n    label?: string\n    order?: number\n  }\n\n  // Display config: maps display roles to field names\n  display?: DisplayConfig\n\n  // Runtime field metadata for display components\n  fields?: RuntimeFieldMeta[]\n\n  // When true, collection auto-registers as a page type in crouton-pages\n  publishable?: boolean\n\n  // Extensible for custom properties\n  [key: string]: any\n}",{"id":8725,"title":8726,"titles":8727,"content":8728,"level":748},"/api-reference/types#property-details","Property Details",[288,8717,8721],"PropertyTypeDescriptionnamestringCollection name (usually auto-set by generator)layerstringNuxt layer this collection belongs tocomponentNamestringName of the form component (e.g., 'ShopProductsForm')kindCollectionKindCollection kind: 'data', 'content', or 'media'. Determines behavior and available featurescontainer'slideover' | 'modal' | 'dialog' | 'inline'Container type for create/update forms (default: 'slideover')apiPathstringAPI endpoint path (defaults to collection name)displayNamestringHuman-readable display name for the collectiondefaultPaginationobjectDefault pagination settings for this collectionreferencesRecord\u003Cstring, string>Field-to-collection mappings for automatic cache refreshdependentFieldComponentsRecord\u003Cstring, string>Custom component mappings for dependent fieldshierarchyobjectHierarchy config for tree-enabled collections (generated by CLI --hierarchy flag)sortableobjectSortable config for flat collections with drag-drop orderingadminNavobjectControls how the collection appears in the admin sidebardisplayDisplayConfigMaps display roles (title, subtitle, image, badge, description) to field namesfieldsRuntimeFieldMeta[]Lightweight runtime field metadata: name, type, label, area, displayAspublishablebooleanWhen true, auto-registers as a page type in crouton-pages",{"id":8730,"title":4173,"titles":8731,"content":8732,"level":748},"/api-reference/types#basic-usage",[288,8717,8721],"// app.config.ts\nexport default defineAppConfig({\n  croutonCollections: {\n    shopProducts: {\n      name: 'shopProducts',\n      layer: 'shop',\n      componentName: 'ShopProductsForm',\n      apiPath: 'products',\n      defaultPagination: {\n        currentPage: 1,\n        pageSize: 25,\n        sortBy: 'name',\n        sortDirection: 'asc'\n      }\n    }\n  }\n})",{"id":8734,"title":8735,"titles":8736,"content":8737,"level":748},"/api-reference/types#with-references-cache-invalidation","With References (Cache Invalidation)",[288,8717,8721],"When a collection has fields that reference other collections, declare them using references. This enables automatic cache refresh when related items are updated. // app.config.ts\nexport default defineAppConfig({\n  croutonCollections: {\n    bookingsEvents: {\n      name: 'bookingsEvents',\n      layer: 'bookings',\n      componentName: 'BookingsEventsForm',\n      references: {\n        location: 'bookingsLocations',  // 'location' field references 'bookingsLocations'\n        host: 'users',                   // 'host' field references 'users'\n        category: 'bookingsCategories'   // 'category' field references 'bookingsCategories'\n      }\n    }\n  }\n}) How references work: When you update an event with { location: 'location-123' }Crouton invalidates cache for both:\ncollection:bookingsEvents:* (the event collection)collection:bookingsLocations:* (the referenced location collection)Any UI displaying locations automatically refreshes to show updated dataThis keeps CardMini displays in sync across the application",{"id":8739,"title":8740,"titles":8741,"content":8742,"level":748},"/api-reference/types#with-custom-dependent-fields","With Custom Dependent Fields",[288,8717,8721],"For complex field relationships, map field names to custom components: // app.config.ts\nexport default defineAppConfig({\n  croutonCollections: {\n    bookingsEvents: {\n      name: 'bookingsEvents',\n      layer: 'bookings',\n      componentName: 'BookingsEventsForm',\n      dependentFieldComponents: {\n        slots: 'SlotSelect',           // Use SlotSelect component for 'slots' field\n        recurringPattern: 'RecurringPatternEditor'  // Custom editor for recurring events\n      }\n    }\n  }\n}) The FormDependentFieldLoader component uses this mapping to dynamically load the correct component for each field.",{"id":8744,"title":8745,"titles":8746,"content":8747,"level":748},"/api-reference/types#custom-properties","Custom Properties",[288,8717,8721],"You can extend CollectionConfig with any custom properties your application needs: export default defineAppConfig({\n  croutonCollections: {\n    shopProducts: {\n      name: 'shopProducts',\n      layer: 'shop',\n      componentName: 'ShopProductsForm',\n\n      // Custom properties\n      features: {\n        enableInventoryTracking: true,\n        enableVariants: true\n      },\n      permissions: {\n        create: 'product:create',\n        update: 'product:update',\n        delete: 'product:delete'\n      },\n      metadata: {\n        icon: 'i-lucide-package',\n        displayName: 'Products',\n        singularName: 'Product'\n      }\n    }\n  }\n}) Access custom properties via useCollections(): const collections = useCollections()\nconst config = collections.getConfig('shopProducts')\n\nif (config?.features?.enableInventoryTracking) {\n  // Show inventory fields\n}",{"id":8749,"title":8750,"titles":8751,"content":8752,"level":449},"/api-reference/types#externalcollectionconfig","ExternalCollectionConfig",[288,8717],"Configuration for external collections (e.g., users from auth system, third-party APIs). interface ExternalCollectionConfig {\n  name: string\n  schema: z.ZodSchema\n  apiPath?: string\n  fetchStrategy?: 'query' | 'restful'\n  readonly?: boolean\n  meta?: {\n    label?: string\n    description?: string\n  }\n  proxy?: {\n    enabled: boolean\n    sourceEndpoint: string\n    transform: (item: any) => { id: string; title: string; [key: string]: any }\n  }\n}",{"id":8754,"title":8726,"titles":8755,"content":8756,"level":748},"/api-reference/types#property-details-1",[288,8717,8750],"PropertyTypeDefaultDescriptionnamestringrequiredCollection name (must match app.config.ts key)schemaz.ZodSchemarequiredZod schema for validation and typesapiPathstringnameAPI endpoint pathfetchStrategy'query' | 'restful''query'Fetch method: ?ids= vs /{id}readonlybooleantrueHide edit/delete buttons in CardMinimetaobject{}Display metadataproxyobjectundefinedProxy configuration for external endpoints",{"id":8758,"title":8759,"titles":8760,"content":8761,"level":748},"/api-reference/types#basic-external-collection","Basic External Collection",[288,8717,8750],"// composables/useExternalCollections.ts\nimport { defineExternalCollection } from '@fyit/crouton'\nimport { z } from 'zod'\n\nconst userSchema = z.object({\n  id: z.string(),\n  title: z.string(),     // Required for CroutonReferenceSelect display\n  email: z.string().optional(),\n  avatarUrl: z.string().optional()\n})\n\nexport const usersConfig = defineExternalCollection({\n  name: 'users',\n  schema: userSchema,\n  apiPath: 'users',\n  readonly: true,        // Users managed by auth system\n  meta: {\n    label: 'Team Members',\n    description: 'Users from the authentication system'\n  }\n}) // app.config.ts\nimport { usersConfig } from '~/composables/useExternalCollections'\n\nexport default defineAppConfig({\n  croutonCollections: {\n    users: usersConfig,\n    // ... other collections\n  }\n})",{"id":8763,"title":8764,"titles":8765,"content":8766,"level":748},"/api-reference/types#with-proxy-transform","With Proxy Transform",[288,8717,8750],"Proxy external endpoints and transform data to Crouton format: import { defineExternalCollection } from '@fyit/crouton'\nimport { z } from 'zod'\n\nconst memberSchema = z.object({\n  id: z.string(),\n  title: z.string(),\n  role: z.string(),\n  joinedAt: z.string()\n})\n\nexport const membersConfig = defineExternalCollection({\n  name: 'members',\n  schema: memberSchema,\n  proxy: {\n    enabled: true,\n    sourceEndpoint: 'members',  // → /api/teams/[id]/members\n    transform: (item) => ({\n      id: item.userId,\n      title: `${item.firstName} ${item.lastName}`,  // Transform to required 'title' field\n      role: item.memberRole,\n      joinedAt: item.createdAt\n    })\n  }\n}) How proxy works: CroutonReferenceSelect fetches from /api/teams/[teamId]/membersRaw data from external system is transformed using the transform functionTransformed data has id and title fields (required by Crouton)Data displays correctly in CroutonItemCardMini and CroutonReferenceSelect",{"id":8768,"title":8769,"titles":8770,"content":8771,"level":748},"/api-reference/types#restful-fetch-strategy","RESTful Fetch Strategy",[288,8717,8750],"For APIs using /resource/{id} pattern instead of /resource?ids=: export const customersConfig = defineExternalCollection({\n  name: 'customers',\n  schema: customerSchema,\n  fetchStrategy: 'restful',  // Use /{id} instead of ?ids=\n  apiPath: 'customers'\n}) This changes how single items are fetched: 'query' (default): GET /api/teams/[id]/customers?ids=customer-123'restful': GET /api/teams/[id]/customers/customer-123",{"id":8773,"title":8774,"titles":8775,"content":528,"level":391},"/api-reference/types#layout-system-types","Layout System Types",[288],{"id":8777,"title":8778,"titles":8779,"content":8780,"level":449},"/api-reference/types#layouttype","LayoutType",[288,8774],"Basic layout modes for displaying collections. type LayoutType = 'table' | 'list' | 'grid' | 'tree' | 'kanban' | 'workspace' LayoutDescriptionUse CasetableTraditional data tableDesktop, detailed data with many columnslistCompact list viewMobile, simple data, quick scanninggridGrid of cards (2-3 columns)Mobile/tablet, visual contenttreeHierarchical tree viewParent-child relationships, nested datakanbanKanban board viewStatus-based workflows, task managementworkspaceWorkspace layoutComplex multi-panel interfaces",{"id":8782,"title":8783,"titles":8784,"content":8785,"level":449},"/api-reference/types#responsivelayout","ResponsiveLayout",[288,8774],"Responsive layout configuration with breakpoint support. interface ResponsiveLayout {\n  base: LayoutType\n  sm?: LayoutType   // 640px+\n  md?: LayoutType   // 768px+\n  lg?: LayoutType   // 1024px+\n  xl?: LayoutType   // 1280px+\n  '2xl'?: LayoutType // 1536px+\n}",{"id":8787,"title":8788,"titles":8789,"content":8790,"level":748},"/api-reference/types#basic-responsive-layout","Basic Responsive Layout",[288,8774,8783],"\u003Cscript setup lang=\"ts\">\nconst layout = {\n  base: 'list',   // Mobile: list\n  md: 'grid',     // Tablet: grid\n  lg: 'table'     // Desktop: table\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection\n    :layout=\"layout\"\n    :rows=\"products\"\n    :columns=\"columns\"\n    collection=\"shopProducts\"\n  />\n\u003C/template>",{"id":8792,"title":8793,"titles":8794,"content":8795,"level":748},"/api-reference/types#using-layout-presets","Using Layout Presets",[288,8774,8783],"// Built-in presets\nconst layoutPresets = {\n  'responsive': { base: 'list', md: 'grid', lg: 'table' },\n  'mobile-friendly': { base: 'list', lg: 'table' },\n  'compact': { base: 'list', xl: 'table' },\n  'tree-default': { base: 'tree' }\n} \u003Ctemplate>\n  \u003C!-- Use preset by name -->\n  \u003CCroutonCollection\n    layout=\"responsive\"\n    :rows=\"products\"\n    :columns=\"columns\"\n    collection=\"shopProducts\"\n  />\n\u003C/template>",{"id":8797,"title":8798,"titles":8799,"content":8800,"level":748},"/api-reference/types#complex-responsive-example","Complex Responsive Example",[288,8774,8783],"\u003Cscript setup lang=\"ts\">\nimport type { ResponsiveLayout } from '@fyit/crouton'\n\n// Fine-tuned for different screen sizes\nconst layout: ResponsiveLayout = {\n  base: 'list',      // Phone (\u003C 640px)\n  sm: 'list',        // Large phone (640px+)\n  md: 'grid',        // Tablet (768px+)\n  lg: 'grid',        // Small laptop (1024px+)\n  xl: 'table',       // Desktop (1280px+)\n  '2xl': 'table'     // Large desktop (1536px+)\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection\n    :layout=\"layout\"\n    :rows=\"products\"\n    :columns=\"columns\"\n    collection=\"shopProducts\"\n  />\n\u003C/template>",{"id":8802,"title":8803,"titles":8804,"content":528,"level":391},"/api-reference/types#table-and-column-types","Table and Column Types",[288],{"id":8806,"title":8807,"titles":8808,"content":8809,"level":449},"/api-reference/types#tablecolumn","TableColumn",[288,8803],"Column definition for table layouts. interface TableColumn {\n  id?: string\n  accessorKey?: string\n  header: string | ((props: any) => any)\n  cell?: (props: any) => any\n  sortable?: boolean\n  enableSorting?: boolean\n  enableHiding?: boolean\n}",{"id":8811,"title":8726,"titles":8812,"content":8813,"level":748},"/api-reference/types#property-details-2",[288,8803,8807],"PropertyTypeDefaultDescriptionidstringaccessorKeyUnique column identifieraccessorKeystring-Object property to access (dot notation supported)headerstring | functionrequiredColumn header text or render functioncellfunction-Custom cell renderer (receives { row, value })sortablebooleanfalseEnable sorting for this columnenableSortingbooleansortableTanStack Table sorting flagenableHidingbooleantrueAllow hiding this column",{"id":8815,"title":8816,"titles":8817,"content":8818,"level":748},"/api-reference/types#basic-columns","Basic Columns",[288,8803,8807],"const columns: TableColumn[] = [\n  {\n    accessorKey: 'name',\n    header: 'Product Name',\n    sortable: true\n  },\n  {\n    accessorKey: 'price',\n    header: 'Price',\n    sortable: true\n  },\n  {\n    accessorKey: 'inStock',\n    header: 'In Stock'\n  }\n]",{"id":8820,"title":8821,"titles":8822,"content":8823,"level":748},"/api-reference/types#custom-cell-renderers","Custom Cell Renderers",[288,8803,8807],"import type { TableColumn } from '@fyit/crouton'\n\nconst columns: TableColumn[] = [\n  {\n    accessorKey: 'price',\n    header: 'Price',\n    cell: ({ value }) => `$${value.toFixed(2)}`\n  },\n  {\n    accessorKey: 'status',\n    header: 'Status',\n    cell: ({ row }) => {\n      const status = row.original.status\n      const color = status === 'active' ? 'green' : 'gray'\n      return h('span', { class: `text-${color}-600` }, status)\n    }\n  }\n]",{"id":8825,"title":8826,"titles":8827,"content":8828,"level":748},"/api-reference/types#nested-data-access","Nested Data Access",[288,8803,8807],"const columns: TableColumn[] = [\n  {\n    accessorKey: 'user.name',    // Dot notation for nested properties\n    header: 'User Name'\n  },\n  {\n    accessorKey: 'location.city',\n    header: 'City'\n  }\n]",{"id":8830,"title":8831,"titles":8832,"content":8833,"level":748},"/api-reference/types#custom-header-renderers","Custom Header Renderers",[288,8803,8807],"const columns: TableColumn[] = [\n  {\n    accessorKey: 'price',\n    header: () => h('div', [\n      h('span', 'Price '),\n      h('span', { class: 'text-xs text-gray-500' }, '(USD)')\n    ])\n  }\n]",{"id":8835,"title":8836,"titles":8837,"content":8838,"level":449},"/api-reference/types#collectionprops","CollectionProps",[288,8803],"Props interface for CroutonCollection component. interface CollectionProps {\n  // Layout Configuration\n  layout?: LayoutType | ResponsiveLayout | keyof typeof layoutPresets\n  card?: 'Card' | 'CardMini' | 'CardSmall' | 'CardTree' | string  // Card variant\n  /** Direct card component (skips name resolution, for stateless mode) */\n  cardComponent?: any\n\n  // Data\n  rows?: any[]\n  columns?: TableColumn[]\n  collection: string\n\n  // Pagination\n  serverPagination?: boolean\n  paginationData?: PaginationData | null\n  refreshFn?: () => Promise\u003Cvoid> | null\n\n  // UI Options\n  create?: boolean\n  /** Hierarchy configuration for tree layouts */\n  hierarchy?: HierarchyConfig\n  /** Enable drag-and-drop row reordering (table layout only) */\n  sortable?: boolean | SortableOptions\n  /**\n   * Grid size for grid layout\n   * - compact: 4 columns, tight spacing\n   * - comfortable: 3 columns, medium spacing (default)\n   * - spacious: 2 columns, generous spacing\n   */\n  gridSize?: 'compact' | 'comfortable' | 'spacious'\n  hideDefaultColumns?: {\n    select?: boolean\n    createdAt?: boolean\n    updatedAt?: boolean\n    createdBy?: boolean\n    updatedBy?: boolean\n    presence?: boolean\n    actions?: boolean\n  }\n  /** Stateless mode: no config lookup, no mutations, just renders data */\n  stateless?: boolean\n  /** Show collaboration presence badges per row (requires @fyit/crouton-collab) */\n  showCollabPresence?: boolean | CollabPresenceConfig\n} The card prop allows specifying which card variant to use: card=\"CardSmall\" resolves to {Collection}CardSmall (e.g., BookingsCardSmall)card=\"CardTree\" resolves to {Collection}CardTreeNo card prop uses {Collection}Card with the layout prop passed to it",{"id":8840,"title":3195,"titles":8841,"content":8842,"level":748},"/api-reference/types#complete-example",[288,8803,8836],"Query Examples: For complete useCollectionQuery patterns, see Querying Data. \u003Cscript setup lang=\"ts\">\nimport type { CollectionProps, TableColumn } from '@fyit/crouton'\n\n// See /fundamentals/querying for query patterns\nconst { items, pending, refresh } = await useCollectionQuery('shopProducts')\n\nconst columns: TableColumn[] = [\n  { accessorKey: 'name', header: 'Name', sortable: true },\n  { accessorKey: 'price', header: 'Price', sortable: true }\n]\n\nconst paginationData = computed(() => ({\n  currentPage: page.value,\n  pageSize: 25,\n  totalItems: totalItems.value\n}))\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection\n    layout=\"responsive\"\n    :rows=\"items\"\n    :columns=\"columns\"\n    collection=\"shopProducts\"\n    :server-pagination=\"true\"\n    :pagination-data=\"paginationData\"\n    :refresh-fn=\"refresh\"\n    create\n    :hide-default-columns=\"{\n      createdBy: true,\n      updatedBy: true\n    }\"\n  />\n\u003C/template>",{"id":8844,"title":8845,"titles":8846,"content":528,"level":391},"/api-reference/types#pagination-types","Pagination Types",[288],{"id":8848,"title":8849,"titles":8850,"content":8851,"level":449},"/api-reference/types#paginationdata","PaginationData",[288,8845],"Pagination metadata for server-side pagination. interface PaginationData {\n  currentPage: number\n  pageSize: number\n  totalItems: number\n  totalPages?: number\n  sortBy?: string\n  sortDirection?: 'asc' | 'desc'\n}",{"id":8853,"title":8726,"titles":8854,"content":8855,"level":748},"/api-reference/types#property-details-3",[288,8845,8849],"PropertyTypeRequiredDescriptioncurrentPagenumberYesCurrent page number (1-indexed)pageSizenumberYesItems per pagetotalItemsnumberYesTotal number of items across all pagestotalPagesnumberNoTotal pages (auto-calculated if omitted)sortBystringNoCurrent sort columnsortDirection'asc' | 'desc'NoCurrent sort direction",{"id":8857,"title":8858,"titles":8859,"content":8860,"level":748},"/api-reference/types#pagination-usage","Pagination Usage",[288,8845,8849],"Pagination Examples: For complete pagination and sorting patterns with useCollectionQuery, see Querying Data. \u003Cscript setup lang=\"ts\">\n// See /fundamentals/querying for query patterns\nconst { items, data } = await useCollectionQuery('shopProducts')\n\nconst paginationData = computed(() => ({\n  currentPage: page.value,\n  pageSize: 25,\n  totalItems: data.value?.pagination?.totalItems || 0\n}))\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"items\"\n    :columns=\"columns\"\n    collection=\"shopProducts\"\n    :server-pagination=\"true\"\n    :pagination-data=\"paginationData\"\n  />\n\u003C/template>",{"id":8862,"title":8863,"titles":8864,"content":8865,"level":748},"/api-reference/types#server-response-format","Server Response Format",[288,8845,8849],"Your API should return data in this format: // GET /api/teams/[id]/products?page=1&pageSize=25\n{\n  items: [\n    { id: '1', name: 'Product 1', price: 29.99 },\n    // ... more items\n  ],\n  pagination: {\n    currentPage: 1,\n    pageSize: 25,\n    totalItems: 156,\n    totalPages: 7,\n    sortBy: 'name',\n    sortDirection: 'asc'\n  }\n}",{"id":8867,"title":8868,"titles":8869,"content":8870,"level":449},"/api-reference/types#paginationstate-internal","PaginationState (Internal)",[288,8845],"Internal pagination state (used within composables). interface PaginationState {\n  currentPage: number\n  pageSize: number\n  sortBy: string\n  sortDirection: 'asc' | 'desc'\n  totalItems?: number\n  totalPages?: number\n} Used internally by useCrouton() to manage pagination across multiple collections.",{"id":8872,"title":8873,"titles":8874,"content":528,"level":391},"/api-reference/types#composable-return-types","Composable Return Types",[288],{"id":8876,"title":8877,"titles":8878,"content":8879,"level":449},"/api-reference/types#collectionqueryreturn","CollectionQueryReturn",[288,8873],"Return type of useCollectionQuery(). interface CollectionQueryReturn\u003CT = any> {\n  items: ComputedRef\u003CT[]>\n  data: Ref\u003Cany>\n  refresh: () => Promise\u003Cvoid>\n  pending: Ref\u003Cboolean>\n  error: Ref\u003Cany>\n}",{"id":8881,"title":8882,"titles":8883,"content":8884,"level":748},"/api-reference/types#usage-with-types","Usage with Types",[288,8873,8877],"import type { ShopProduct } from '~/layers/shop/types/products'\n\nconst {\n  items,      // ComputedRef\u003CShopProduct[]>\n  pending,    // Ref\u003Cboolean>\n  error,      // Ref\u003Cany>\n  refresh     // () => Promise\u003Cvoid>\n} = await useCollectionQuery\u003CShopProduct>('shopProducts')",{"id":8886,"title":8887,"titles":8888,"content":8889,"level":449},"/api-reference/types#collectionqueryoptions","CollectionQueryOptions",[288,8873],"Options for useCollectionQuery(). interface CollectionQueryOptions {\n  query?: ComputedRef\u003CRecord\u003Cstring, any>> | Ref\u003CRecord\u003Cstring, any>>\n  watch?: boolean\n}",{"id":8891,"title":8726,"titles":8892,"content":8893,"level":748},"/api-reference/types#property-details-4",[288,8873,8887],"PropertyTypeDefaultDescriptionqueryComputedRef | Ref{}Reactive query parameterswatchbooleantrueAuto-refetch when query changes",{"id":8895,"title":8896,"titles":8897,"content":8898,"level":748},"/api-reference/types#basic-query-options","Basic Query Options",[288,8873,8887],"const page = ref(1)\nconst search = ref('')\n\nconst { items } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({\n    page: page.value,\n    search: search.value\n  })),\n  watch: true  // Auto-refetch when page or search changes\n})",{"id":8900,"title":8901,"titles":8902,"content":8903,"level":449},"/api-reference/types#collectionmutation","CollectionMutation",[288,8873],"Return type of useCollectionMutation(). interface CollectionMutation {\n  create: (data: any) => Promise\u003Cany>\n  update: (id: string, data: any) => Promise\u003Cany>\n  deleteItems: (ids: string[]) => Promise\u003Cvoid>\n}",{"id":8905,"title":1608,"titles":8906,"content":8907,"level":748},"/api-reference/types#usage",[288,8873,8901],"See Mutation Composables API for usage examples.",{"id":8909,"title":8910,"titles":8911,"content":528,"level":391},"/api-reference/types#component-prop-types","Component Prop Types",[288],{"id":8913,"title":8914,"titles":8915,"content":8916,"level":449},"/api-reference/types#cardprops","CardProps",[288,8910],"Props for custom card components. interface CardProps {\n  item: any\n  layout: 'list' | 'grid' | 'tree' | 'kanban' | 'workspace'\n  collection: string\n  pending?: boolean\n  error?: any\n}",{"id":8918,"title":8919,"titles":8920,"content":8921,"level":748},"/api-reference/types#custom-card-component","Custom Card Component",[288,8910,8914],"\u003Cscript setup lang=\"ts\">\nimport type { CardProps } from '@fyit/crouton'\n\nconst props = defineProps\u003CCardProps>()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv :class=\"layout === 'list' ? 'py-2' : 'p-4 border rounded'\">\n    \u003Ch3>{{ item.name }}\u003C/h3>\n    \u003Cp v-if=\"layout !== 'list'\">{{ item.description }}\u003C/p>\n  \u003C/div>\n\u003C/template>",{"id":8923,"title":8924,"titles":8925,"content":8926,"level":449},"/api-reference/types#tablesearchprops","TableSearchProps",[288,8910],"Props for CroutonTableSearch component. interface TableSearchProps {\n  modelValue: string\n  placeholder?: string\n  debounceMs?: number\n}",{"id":8928,"title":8929,"titles":8930,"content":8931,"level":449},"/api-reference/types#tablepaginationprops","TablePaginationProps",[288,8910],"Props for CroutonTablePagination component. interface TablePaginationProps {\n  page: number\n  pageCount: number\n  totalItems: number\n  loading?: boolean\n  pageSizes?: number[]\n}",{"id":8933,"title":8934,"titles":8935,"content":8936,"level":449},"/api-reference/types#tableactionsprops","TableActionsProps",[288,8910],"Props for CroutonTableActions component. interface TableActionsProps {\n  selectedRows: any[]\n  collection: string\n  table?: any\n  onDelete?: (ids: string[]) => void\n  onColumnVisibilityChange?: (column: string, visible: boolean) => void\n}",{"id":8938,"title":8939,"titles":8940,"content":528,"level":391},"/api-reference/types#hook-system-types","Hook System Types",[288],{"id":8942,"title":8943,"titles":8944,"content":8945,"level":449},"/api-reference/types#croutonmutation-hook","crouton:mutation Hook",[288,8939],"Nuxt hook emitted after successful mutations. Use for event tracking, analytics, or custom cache invalidation. // Hook payload type (defined as CroutonMutationEvent in crouton-hooks.d.ts)\ninterface CroutonMutationEvent {\n  operation: 'create' | 'update' | 'delete' | 'move' | 'reorder'\n  collection: string\n  itemId?: string\n  itemIds?: string[]\n  data?: Record\u003Cstring, unknown>\n  updates?: Record\u003Cstring, unknown>\n  /** The item data before the mutation (for update operations, enables change tracking) */\n  beforeData?: Record\u003Cstring, unknown>\n  result?: unknown\n  /** Correlation ID for linking related operations and events */\n  correlationId?: string\n  /** Timestamp when the mutation was initiated */\n  timestamp?: number\n}",{"id":8947,"title":8948,"titles":8949,"content":8950,"level":748},"/api-reference/types#registering-hook-listeners","Registering Hook Listeners",[288,8939,8943],"// plugins/crouton-events.ts\nexport default defineNuxtPlugin((nuxtApp) => {\n  nuxtApp.hooks.hook('crouton:mutation', async (payload) => {\n    const { operation, collection, itemId, data, result } = payload\n\n    // Track analytics\n    if (operation === 'create') {\n      console.log(`Created ${collection} item:`, itemId)\n      await $fetch('/api/analytics/track', {\n        method: 'POST',\n        body: {\n          event: `${collection}_created`,\n          properties: { itemId }\n        }\n      })\n    }\n\n    // Custom cache invalidation\n    if (collection === 'shopProducts' && operation === 'update') {\n      // Invalidate related caches\n      await clearNuxtData('product-analytics')\n      await clearNuxtData('trending-products')\n    }\n  })\n})",{"id":8952,"title":8953,"titles":8954,"content":8955,"level":748},"/api-reference/types#hook-payload-examples","Hook Payload Examples",[288,8939,8943],"Create operation: {\n  operation: 'create',\n  collection: 'shopProducts',\n  itemId: 'product-123',\n  data: {\n    name: 'New Product',\n    price: 49.99\n  },\n  result: {\n    id: 'product-123',\n    name: 'New Product',\n    price: 49.99,\n    createdAt: '2025-01-15T10:30:00Z'\n  },\n  correlationId: 'abc-123',\n  timestamp: 1705312200000\n} Update operation: {\n  operation: 'update',\n  collection: 'shopProducts',\n  itemId: 'product-123',\n  updates: {\n    price: 39.99\n  },\n  beforeData: {\n    id: 'product-123',\n    name: 'New Product',\n    price: 49.99\n  },\n  result: {\n    id: 'product-123',\n    name: 'New Product',\n    price: 39.99,\n    updatedAt: '2025-01-15T11:00:00Z'\n  },\n  correlationId: 'abc-456',\n  timestamp: 1705314000000\n} Delete operation: {\n  operation: 'delete',\n  collection: 'shopProducts',\n  itemIds: ['product-123', 'product-456'],\n  result: undefined,\n  correlationId: 'abc-789',\n  timestamp: 1705315800000\n} Move operation (tree/hierarchy): {\n  operation: 'move',\n  collection: 'shopCategories',\n  itemId: 'category-5',\n  data: { parentId: 'category-2', order: 3 },\n  correlationId: 'abc-012',\n  timestamp: 1705317600000\n} Reorder operation (sortable): {\n  operation: 'reorder',\n  collection: 'shopProducts',\n  itemIds: ['product-1', 'product-3', 'product-2'],\n  correlationId: 'abc-345',\n  timestamp: 1705319400000\n}",{"id":8957,"title":8958,"titles":8959,"content":8960,"level":748},"/api-reference/types#use-cases-for-hooks","Use Cases for Hooks",[288,8939,8943],"1. Event Tracking / Analytics nuxtApp.hooks.hook('crouton:mutation', async ({ operation, collection, itemId }) => {\n  await $fetch('/api/analytics/events', {\n    method: 'POST',\n    body: {\n      event: `${collection}.${operation}`,\n      userId: user.value?.id,\n      timestamp: new Date().toISOString(),\n      metadata: { itemId }\n    }\n  })\n}) 2. Audit Logging nuxtApp.hooks.hook('crouton:mutation', async (payload) => {\n  await $fetch('/api/audit-log', {\n    method: 'POST',\n    body: {\n      action: payload.operation,\n      resource: payload.collection,\n      resourceId: payload.itemId,\n      changes: payload.data,\n      performedBy: user.value?.id,\n      timestamp: new Date()\n    }\n  })\n}) 3. Custom Cache Invalidation nuxtApp.hooks.hook('crouton:mutation', async ({ collection, operation }) => {\n  // When products change, refresh dashboard stats\n  if (collection === 'shopProducts') {\n    await clearNuxtData('dashboard-stats')\n    await clearNuxtData('inventory-summary')\n  }\n\n  // When orders change, refresh customer data\n  if (collection === 'shopOrders') {\n    await clearNuxtData('customer-orders')\n    await clearNuxtData('revenue-stats')\n  }\n}) 4. Webhook Notifications nuxtApp.hooks.hook('crouton:mutation', async (payload) => {\n  // Notify external systems of changes\n  if (payload.operation === 'create' && payload.collection === 'shopOrders') {\n    await $fetch('/api/webhooks/order-created', {\n      method: 'POST',\n      body: {\n        orderId: payload.itemId,\n        order: payload.result\n      }\n    })\n  }\n}) 5. Real-time Updates (WebSocket) nuxtApp.hooks.hook('crouton:mutation', async (payload) => {\n  // Broadcast changes to connected clients\n  websocket.broadcast({\n    type: 'collection:mutation',\n    collection: payload.collection,\n    operation: payload.operation,\n    itemId: payload.itemId\n  })\n})",{"id":8962,"title":8963,"titles":8964,"content":528,"level":391},"/api-reference/types#state-management-types","State Management Types",[288],{"id":8966,"title":8967,"titles":8968,"content":8969,"level":449},"/api-reference/types#croutonstate-internal","CroutonState (Internal)",[288,8963],"Internal state for modal/form management (used by useCrouton()). interface CroutonState {\n  id: string\n  action: CroutonAction\n  collection: string | null\n  activeItem: any\n  items: any[]\n  loading: LoadingState\n  isOpen: boolean\n  containerType: 'slideover' | 'modal' | 'dialog' | 'inline'\n}\n\ntype CroutonAction = 'create' | 'update' | 'delete' | 'view' | undefined\n\ntype LoadingState =\n  | 'notLoading'\n  | 'create_send' | 'update_send' | 'delete_send' | 'view_send'\n  | 'create_open' | 'update_open' | 'delete_open' | 'view_open'",{"id":8971,"title":8972,"titles":8973,"content":528,"level":391},"/api-reference/types#utility-types","Utility Types",[288],{"id":8975,"title":8976,"titles":8977,"content":8978,"level":449},"/api-reference/types#proxyconfig-internal","ProxyConfig (Internal)",[288,8972],"Configuration for proxying external collections. interface ProxyConfig {\n  enabled: boolean\n  sourceEndpoint: string\n  transform: (item: any) => { id: string; title: string; [key: string]: any }\n}",{"id":8980,"title":8981,"titles":8982,"content":8983,"level":449},"/api-reference/types#configsmap-internal","ConfigsMap (Internal)",[288,8972],"Type for collection configuration registry. type ConfigsMap = {\n  [K in CollectionName]?: CollectionConfig\n}",{"id":8985,"title":8986,"titles":8987,"content":528,"level":391},"/api-reference/types#type-guards-and-helpers","Type Guards and Helpers",[288],{"id":8989,"title":8990,"titles":8991,"content":8992,"level":449},"/api-reference/types#checking-collection-config","Checking Collection Config",[288,8986],"const collections = useCollections()\nconst config = collections.getConfig('shopProducts')\n\nif (!config) {\n  throw new Error('Collection not found')\n}\n\n// Access config properties\nconst apiPath = config.apiPath || 'products'\nconst references = config.references || {}",{"id":8994,"title":8995,"titles":8996,"content":8997,"level":449},"/api-reference/types#type-safe-query-building","Type-Safe Query Building",[288,8986],"import type { PaginationData } from '@fyit/crouton'\n\nfunction buildPaginationData(\n  page: number,\n  pageSize: number,\n  total: number\n): PaginationData {\n  return {\n    currentPage: page,\n    pageSize,\n    totalItems: total,\n    totalPages: Math.ceil(total / pageSize)\n  }\n}",{"id":8999,"title":9000,"titles":9001,"content":9002,"level":449},"/api-reference/types#type-safe-column-definitions","Type-Safe Column Definitions",[288,8986],"import type { TableColumn } from '@fyit/crouton'\nimport type { ShopProduct } from '~/layers/shop/types/products'\n\nfunction defineProductColumns(): TableColumn[] {\n  return [\n    {\n      accessorKey: 'name',\n      header: 'Product Name',\n      sortable: true\n    },\n    {\n      accessorKey: 'price',\n      header: 'Price',\n      cell: ({ row }: { row: { original: ShopProduct } }) =>\n        `$${row.original.price.toFixed(2)}`\n    }\n  ]\n}",{"id":9004,"title":1650,"titles":9005,"content":528,"level":391},"/api-reference/types#common-patterns",[288],{"id":9007,"title":9008,"titles":9009,"content":9010,"level":449},"/api-reference/types#pattern-1-type-safe-collection-setup","Pattern 1: Type-Safe Collection Setup",[288,1650],"// 1. Define your data type\nimport type { ShopProduct } from '~/layers/shop/types/products'\n\n// 2. Register collection config\n// app.config.ts\nexport default defineAppConfig({\n  croutonCollections: {\n    shopProducts: {\n      name: 'shopProducts',\n      layer: 'shop',\n      componentName: 'ShopProductsForm'\n    }\n  }\n})\n\n// 3. Use with type parameter\nconst { items, pending } = await useCollectionQuery\u003CShopProduct>('shopProducts')\n// items is ComputedRef\u003CShopProduct[]>",{"id":9012,"title":9013,"titles":9014,"content":9015,"level":449},"/api-reference/types#pattern-2-external-collection-with-transform","Pattern 2: External Collection with Transform",[288,1650],"// 1. Define external collection\nimport { defineExternalCollection } from '@fyit/crouton'\n\nexport const membersConfig = defineExternalCollection({\n  name: 'members',\n  schema: z.object({\n    id: z.string(),\n    title: z.string()\n  }),\n  proxy: {\n    enabled: true,\n    sourceEndpoint: 'members',\n    transform: (item) => ({\n      id: item.userId,\n      title: `${item.firstName} ${item.lastName}`\n    })\n  }\n})\n\n// 2. Register in app.config.ts\nexport default defineAppConfig({\n  croutonCollections: {\n    members: membersConfig\n  }\n})\n\n// 3. Use in components\nconst { items } = await useCollectionQuery('members')",{"id":9017,"title":9018,"titles":9019,"content":9020,"level":449},"/api-reference/types#pattern-3-responsive-layout-with-type-safety","Pattern 3: Responsive Layout with Type Safety",[288,1650],"import type { ResponsiveLayout, TableColumn } from '@fyit/crouton'\n\nconst layout: ResponsiveLayout = {\n  base: 'list',\n  md: 'grid',\n  lg: 'table'\n}\n\nconst columns: TableColumn[] = [\n  { accessorKey: 'name', header: 'Name' }\n]",{"id":9022,"title":9023,"titles":9024,"content":9025,"level":449},"/api-reference/types#pattern-4-server-pagination-with-types","Pattern 4: Server Pagination with Types",[288,1650],"import type { PaginationData } from '@fyit/crouton'\n\nconst page = ref(1)\n\nconst { items, data } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({ page: page.value }))\n})\n\nconst paginationData = computed\u003CPaginationData>(() => ({\n  currentPage: page.value,\n  pageSize: 25,\n  totalItems: data.value?.pagination?.totalItems || 0\n}))",{"id":9027,"title":9028,"titles":9029,"content":9030,"level":449},"/api-reference/types#pattern-5-hook-integration","Pattern 5: Hook Integration",[288,1650],"// Plugin for mutation tracking\nexport default defineNuxtPlugin((nuxtApp) => {\n  nuxtApp.hooks.hook('crouton:mutation', async (payload) => {\n    // Type-safe payload access\n    const { operation, collection, itemId, data, result } = payload\n\n    console.log(`[${operation}] ${collection}:`, itemId)\n  })\n})",{"id":9032,"title":9033,"titles":9034,"content":528,"level":391},"/api-reference/types#typescript-configuration","TypeScript Configuration",[288],{"id":9036,"title":9037,"titles":9038,"content":9039,"level":449},"/api-reference/types#recommended-tsconfigjson","Recommended tsconfig.json",[288,9033],"{\n  \"extends\": \"./.nuxt/tsconfig.json\",\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"types\": [\"@nuxt/types\", \"@fyit/crouton\"]\n  }\n}",{"id":9041,"title":9042,"titles":9043,"content":9044,"level":449},"/api-reference/types#module-augmentation","Module Augmentation",[288,9033],"Extend Crouton types for your app: // types/crouton.d.ts\ndeclare module '@fyit/crouton' {\n  interface CollectionConfig {\n    // Add custom properties\n    permissions?: {\n      create?: string\n      update?: string\n      delete?: string\n    }\n    metadata?: {\n      icon?: string\n      displayName?: string\n    }\n  }\n}",{"id":9046,"title":9047,"titles":9048,"content":9049,"level":391},"/api-reference/types#type-checking","Type Checking",[288],"Always run type checking after making changes: # Type check your application\nnpx nuxt typecheck\n\n# Watch mode for development\nnpx nuxt typecheck --watch",{"id":9051,"title":1007,"titles":9052,"content":9053,"level":391},"/api-reference/types#related-resources",[288],"Components Reference - Component prop typesComposables Reference - Composable return typesCollection Generator - Generated types from schemasTypeScript in Nuxt - Official Nuxt TypeScript guide html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":293,"title":292,"titles":9055,"content":9056,"level":385},[],"Server-side helpers and utilities for Nuxt Crouton applications Server utilities in Nuxt Crouton provide powerful helpers for creating secure, multi-tenant API endpoints and managing team authorization. These utilities are designed to work seamlessly with Nuxt's server API routes and integrate with the client-side collection system. Auto-Imported: All server utilities are automatically imported by Nuxt. No import statements are needed in your server routes.",{"id":9058,"title":9059,"titles":9060,"content":9061,"level":391},"/api-reference/server#createexternalcollectionhandler","createExternalCollectionHandler",[292],"Creates API endpoints that transform and serve external data in Crouton's collection format. This utility bridges external data sources with Crouton's client-side collection system.",{"id":9063,"title":9064,"titles":9065,"content":9066,"level":449},"/api-reference/server#function-signature","Function Signature",[292,9059],"export function createExternalCollectionHandler\u003CT>(\n  fetchFn: ExternalCollectionFetchFn\u003CT>,\n  transform: ExternalCollectionTransform\u003CT>\n): (event: H3Event\u003CEventHandlerRequest>) => Promise\u003Cany[]>",{"id":9068,"title":9069,"titles":9070,"content":9071,"level":449},"/api-reference/server#type-definitions","Type Definitions",[292,9059],"/**\n * Transform function that converts external data to Crouton format\n * Must return at minimum: { id: string, title: string }\n * The 'title' field is used by CroutonReferenceSelect for display\n */\nexport type ExternalCollectionTransform\u003CT> = (item: T) => {\n  id: string\n  title: string\n  [key: string]: any\n}\n\n/**\n * Fetch function that retrieves data from your external system\n * Receives the H3Event for access to params, auth, etc.\n */\nexport type ExternalCollectionFetchFn\u003CT> = (\n  event: H3Event\u003CEventHandlerRequest>\n) => Promise\u003CT[]> | T[]",{"id":9073,"title":9074,"titles":9075,"content":9076,"level":449},"/api-reference/server#purpose-use-cases","Purpose & Use Cases",[292,9059],"External Data Integration: Connect external systems and custom APIs to CroutonReference Dropdowns: Power CroutonReferenceSelect with dynamic dataData Transformation: Convert external formats to Crouton's standardized structureMulti-Tenancy: Combine with team auth to create tenant-specific endpointsQuery Filtering: Support ?ids= parameter for selective data fetching",{"id":9078,"title":9079,"titles":9080,"content":9081,"level":449},"/api-reference/server#parameters","Parameters",[292,9059],"ParameterTypeDescriptionfetchFnExternalCollectionFetchFn\u003CT>Async function that retrieves items from your system. Receives H3Event for access to route params, auth, headers. Can throw errors to trigger proper error responses.transformExternalCollectionTransform\u003CT>Function that converts each item from your system to Crouton format. Must return object with at least { id, title }. Additional fields become available in the UI.",{"id":9083,"title":9084,"titles":9085,"content":9086,"level":449},"/api-reference/server#return-value","Return Value",[292,9059],"Returns an H3 event handler function ready for use in Nuxt server routes: (event: H3Event\u003CEventHandlerRequest>) => Promise\u003Cany[]> The handler returns an array of transformed items, each containing: id (string, required) - Unique identifier for the itemtitle (string, required) - Display name in UI components[key: string] (any) - Additional fields from your system",{"id":9088,"title":9089,"titles":9090,"content":9091,"level":449},"/api-reference/server#example-1-basic-external-collection","Example 1: Basic External Collection",[292,9059],"// server/api/teams/[id]/users/index.get.ts\n\nexport default createExternalCollectionHandler(\n  // Step 1: Fetch data from your system\n  async (event) => {\n    const teamId = getRouterParam(event, 'id')\n    \n    if (!teamId) {\n      throw createError({\n        status: 400,\n        statusText: 'Team ID is required'\n      })\n    }\n    \n    // Return raw data from your system\n    return await getActiveTeamMembers(teamId)\n  },\n\n  // Step 2: Transform to Crouton format\n  (member) => ({\n    id: member.userId,\n    title: member.name,              // Required for dropdown display\n    email: member.email,\n    avatarUrl: member.avatarUrl,\n    role: member.role\n  })\n) Usage in Components: \u003Cscript setup lang=\"ts\">\n// Reference select will fetch from /api/teams/[id]/users\nconst selectedUserId = ref('')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonReferenceSelect\n    v-model=\"selectedUserId\"\n    collection=\"teams\"\n    relationship=\"users\"\n    display-field=\"name\"\n  />\n\u003C/template>",{"id":9093,"title":9094,"titles":9095,"content":9096,"level":449},"/api-reference/server#example-2-authorization-with-team-context","Example 2: Authorization with Team Context",[292,9059],"// server/api/teams/[id]/projects/index.get.ts\n\nexport default createExternalCollectionHandler(\n  async (event) => {\n    const teamId = getRouterParam(event, 'id')\n    const { user } = await requireUserSession(event)\n\n    // Verify user is a team member\n    const isMember = await isTeamMember(teamId, user.id)\n    if (!isMember) {\n      throw createError({\n        status: 403,\n        statusText: 'You are not a member of this team'\n      })\n    }\n\n    // Fetch team-specific projects\n    return await db\n      .select()\n      .from(tables.projects)\n      .where(eq(tables.projects.teamId, teamId))\n      .all()\n  },\n\n  (project) => ({\n    id: project.id,\n    title: project.name,\n    description: project.description,\n    status: project.status,\n    teamId: project.teamId\n  })\n)",{"id":9098,"title":9099,"titles":9100,"content":9101,"level":449},"/api-reference/server#example-3-query-parameter-filtering","Example 3: Query Parameter Filtering",[292,9059],"// server/api/admin/users/index.get.ts\n\nexport default createExternalCollectionHandler(\n  async (event) => {\n    const currentUser = await requireAuth(event)\n    const query = getQuery(event)\n\n    // Check for admin access\n    if (currentUser.role !== 'admin') {\n      throw createError({\n        status: 403,\n        statusText: 'Admin access required'\n      })\n    }\n\n    // Get optional filters from query string\n    const roleFilter = query.role as string | undefined\n    const includeBanned = query.includeBanned === 'true'\n\n    // Build query with filters\n    const conditions = []\n    \n    if (!includeBanned) {\n      conditions.push(or(\n        eq(tables.users.banned, false),\n        eq(tables.users.banned, null)\n      ))\n    }\n\n    if (roleFilter) {\n      conditions.push(eq(tables.users.role, roleFilter))\n    }\n\n    return await db\n      .select()\n      .from(tables.users)\n      .where(conditions.length > 0 ? and(...conditions) : undefined)\n      .all()\n  },\n\n  (user) => ({\n    id: user.id,\n    title: user.name,\n    email: user.email,\n    role: user.role,\n    banned: user.banned,\n    image: user.image\n  })\n)\n\n// Usage:\n// GET /api/admin/users\n// GET /api/admin/users?role=admin\n// GET /api/admin/users?includeBanned=true\n// GET /api/admin/users?ids=uuid1,uuid2,uuid3",{"id":9103,"title":9104,"titles":9105,"content":9106,"level":449},"/api-reference/server#example-4-subscription-data-integration","Example 4: Subscription Data Integration",[292,9059],"// server/api/users/[id]/subscriptions/index.get.ts\n\nexport default createExternalCollectionHandler(\n  async (event) => {\n    const userId = getRouterParam(event, 'id')\n    const { user } = await requireUserSession(event)\n\n    // Only users can access their own subscription data\n    if (user.id !== userId) {\n      throw createError({\n        status: 403,\n        statusText: 'Unauthorized'\n      })\n    }\n\n    // Fetch with relations\n    return await db\n      .select({\n        subscription: tables.subscriptions,\n        plan: tables.subscriptionPlans\n      })\n      .from(tables.subscriptions)\n      .leftJoin(\n        tables.subscriptionPlans,\n        eq(tables.subscriptions.planId, tables.subscriptionPlans.id)\n      )\n      .where(eq(tables.subscriptions.userId, userId))\n      .all()\n  },\n\n  (subscription) => ({\n    id: subscription.subscription.id,\n    title: subscription.plan?.name || 'Unknown Plan',\n    status: subscription.subscription.status,\n    periodEnd: subscription.subscription.periodEnd,\n    cancelAtPeriodEnd: subscription.subscription.cancelAtPeriodEnd,\n    planId: subscription.subscription.planId,\n    features: subscription.plan?.features\n  })\n)",{"id":9108,"title":9109,"titles":9110,"content":9111,"level":449},"/api-reference/server#example-5-error-handling-validation","Example 5: Error Handling & Validation",[292,9059],"// server/api/teams/[id]/documents/index.get.ts\n\nexport default createExternalCollectionHandler(\n  async (event) => {\n    const teamId = getRouterParam(event, 'id')\n    \n    // Validate input\n    if (!teamId || typeof teamId !== 'string') {\n      throw createError({\n        status: 400,\n        statusText: 'Invalid or missing team ID'\n      })\n    }\n\n    // Validate team exists\n    const team = await db\n      .select()\n      .from(tables.teams)\n      .where(eq(tables.teams.id, teamId))\n      .get()\n\n    if (!team) {\n      throw createError({\n        status: 404,\n        statusText: 'Team not found'\n      })\n    }\n\n    // Validate user access\n    const { user } = await requireUserSession(event)\n    const access = await checkTeamAccess(user.id, teamId, 'read')\n\n    if (!access) {\n      throw createError({\n        status: 403,\n        statusText: 'You do not have permission to view this team\\'s documents'\n      })\n    }\n\n    try {\n      return await db\n        .select()\n        .from(tables.documents)\n        .where(eq(tables.documents.teamId, teamId))\n        .all()\n    } catch (error) {\n      console.error('[documents] Database error:', error)\n      throw createError({\n        status: 500,\n        statusText: 'Failed to fetch documents'\n      })\n    }\n  },\n\n  (doc) => {\n    // Validate transform output\n    if (!doc.id || !doc.title) {\n      console.warn('[documents] Invalid document structure:', doc)\n      return {\n        id: doc.id || 'unknown',\n        title: doc.title || 'Untitled'\n      }\n    }\n\n    return {\n      id: doc.id,\n      title: doc.title,\n      description: doc.description,\n      documentType: doc.type,\n      uploadedAt: doc.createdAt,\n      uploadedBy: doc.uploadedBy\n    }\n  }\n)",{"id":9113,"title":183,"titles":9114,"content":9115,"level":449},"/api-reference/server#features",[292,9059],"Automatic Error Handling: Wraps your code in try-catch with proper HTTP error responsesQuery Parameter Support: Built-in support for ?ids=id1,id2,id3 filteringType-Safe: Full TypeScript generics support for your data typeAuto-Import: No import needed - Nuxt auto-imports the utilityH3Event Access: Full access to route params, auth, headers, query params",{"id":9117,"title":9118,"titles":9119,"content":9120,"level":449},"/api-reference/server#integration-with-collection-proxy-utilities","Integration with Collection Proxy Utilities",[292,9059],"Client-side proxy utilities automatically integrate with external collection handlers: // composables/useProjects.ts\nimport { applyProxyTransform, getProxiedEndpoint } from '@fyit/crouton'\n\nexport function useTeamProjects(teamId: string) {\n  const config = useCollections().getConfig('projects')\n  const endpoint = getProxiedEndpoint(config, 'projects')\n  // Fetches from the proxied endpoint and transforms data\n  return useFetch(`/api/teams/${teamId}/${endpoint}`)\n}",{"id":9122,"title":9123,"titles":9124,"content":528,"level":391},"/api-reference/server#team-authorization-utilities","Team Authorization Utilities",[292],{"id":9126,"title":9127,"titles":9128,"content":9129,"level":449},"/api-reference/server#resolveteamandcheckmembership","resolveTeamAndCheckMembership",[292,9123],"Resolves a team by slug or ID and verifies the current user is a team member.",{"id":9131,"title":9064,"titles":9132,"content":9133,"level":748},"/api-reference/server#function-signature-1",[292,9123,9127],"export async function resolveTeamAndCheckMembership(\n  event: any\n): Promise\u003C{\n  team: Team\n  user: User\n  membership: Member\n}>",{"id":9135,"title":9074,"titles":9136,"content":9137,"level":748},"/api-reference/server#purpose-use-cases-1",[292,9123,9127],"Multi-Tenant Route Protection: Ensure user has access to team resourcesTeam Slug Resolution: Handle both slug and ID-based URLsMembership Verification: Single function call replaces multiple database queriesClean Error Handling: Returns 404 for missing teams, 403 for unauthorized access",{"id":9139,"title":9079,"titles":9140,"content":9141,"level":748},"/api-reference/server#parameters-1",[292,9123,9127],"ParameterTypeDescriptioneventH3EventThe Nuxt server event, automatically provides team slug/ID from route params",{"id":9143,"title":9084,"titles":9144,"content":9145,"level":748},"/api-reference/server#return-value-1",[292,9123,9127],"{\n  team: Team          // The resolved team object\n  user: User          // The authenticated user\n  membership: Member  // The member record proving user is in team\n}",{"id":9147,"title":9148,"titles":9149,"content":9150,"level":748},"/api-reference/server#example-1-basic-team-route","Example 1: Basic Team Route",[292,9123,9127],"// server/api/teams/[id]/settings.get.ts\n\nexport default defineEventHandler(async (event) => {\n  // Resolves team by slug or ID, verifies user member\n  const { team, user } = await resolveTeamAndCheckMembership(event)\n\n  return {\n    teamId: team.id,\n    teamName: team.name,\n    userId: user.id,\n    settings: team.settings\n  }\n})",{"id":9152,"title":9153,"titles":9154,"content":9155,"level":748},"/api-reference/server#example-2-with-team-data","Example 2: With Team Data",[292,9123,9127],"// server/api/teams/[id]/index.get.ts\n\nexport default defineEventHandler(async (event) => {\n  const { team, user, membership } = await resolveTeamAndCheckMembership(event)\n\n  // Fetch team statistics\n  const memberCount = await db\n    .select({ count: count() })\n    .from(tables.teamMembers)\n    .where(eq(tables.teamMembers.teamId, team.id))\n    .get()\n\n  const projectCount = await db\n    .select({ count: count() })\n    .from(tables.projects)\n    .where(eq(tables.projects.teamId, team.id))\n    .get()\n\n  return {\n    id: team.id,\n    name: team.name,\n    slug: team.slug,\n    description: team.description,\n    avatarUrl: team.avatarUrl,\n    memberCount: memberCount.count,\n    projectCount: projectCount.count,\n    userRole: membership.role,\n    currentUserId: user.id\n  }\n})",{"id":9157,"title":9158,"titles":9159,"content":9160,"level":748},"/api-reference/server#example-3-role-based-access-control","Example 3: Role-Based Access Control",[292,9123,9127],"// server/api/teams/[id]/members.delete.ts\n\nexport default defineEventHandler(async (event) => {\n  const { team, membership } = await resolveTeamAndCheckMembership(event)\n\n  // Check user has admin role\n  if (membership.role !== 'admin') {\n    throw createError({\n      status: 403,\n      statusText: 'Only team admins can manage members'\n    })\n  }\n\n  const { memberId } = await readBody(event)\n\n  // Delete the member\n  await db\n    .delete(tables.teamMembers)\n    .where(\n      and(\n        eq(tables.teamMembers.teamId, team.id),\n        eq(tables.teamMembers.id, memberId)\n      )\n    )\n\n  return { success: true }\n})",{"id":9162,"title":9163,"titles":9164,"content":9165,"level":748},"/api-reference/server#example-4-update-team-settings","Example 4: Update Team Settings",[292,9123,9127],"// server/api/teams/[id]/settings.patch.ts\n\nexport default defineEventHandler(async (event) => {\n  const { team, membership } = await resolveTeamAndCheckMembership(event)\n\n  // Verify owner permission\n  if (membership.role !== 'owner') {\n    throw createError({\n      status: 403,\n      statusText: 'Only team owner can update settings'\n    })\n  }\n\n  const body = await readBody(event)\n\n  // Validate input\n  const schema = z.object({\n    name: z.string().min(1).max(100).optional(),\n    description: z.string().max(500).optional(),\n    avatarUrl: z.string().url().optional()\n  })\n\n  const validated = schema.parse(body)\n\n  // Update team\n  const updated = await db\n    .update(tables.teams)\n    .set(validated)\n    .where(eq(tables.teams.id, team.id))\n    .returning()\n    .get()\n\n  return updated\n})",{"id":9167,"title":9168,"titles":9169,"content":9170,"level":449},"/api-reference/server#isteammemberwithevent","isTeamMemberWithEvent",[292,9123],"Checks if a specific user is a member of a team. Requires H3 event context for Better Auth API access.",{"id":9172,"title":9064,"titles":9173,"content":9174,"level":748},"/api-reference/server#function-signature-2",[292,9123,9168],"export async function isTeamMemberWithEvent(\n  event: H3Event,\n  teamId: string,\n  userId: string\n): Promise\u003Cboolean>",{"id":9176,"title":9074,"titles":9177,"content":9178,"level":748},"/api-reference/server#purpose-use-cases-2",[292,9123,9168],"Conditional Authorization: Skip full member fetch if you only need boolean resultAccess Verification: Check team member in API handlers",{"id":9180,"title":9079,"titles":9181,"content":9182,"level":748},"/api-reference/server#parameters-2",[292,9123,9168],"ParameterTypeDescriptioneventH3EventThe H3 event from the handlerteamIdstringThe team's ID (not slug)userIdstringThe user's ID to check",{"id":9184,"title":9084,"titles":9185,"content":9186,"level":748},"/api-reference/server#return-value-2",[292,9123,9168],"Returns a boolean: true if user is a team member, false otherwise.",{"id":9188,"title":9189,"titles":9190,"content":9191,"level":748},"/api-reference/server#example-conditional-logic","Example: Conditional Logic",[292,9123,9168],"// server/api/teams/[teamId]/invite.post.ts\nimport { isTeamMemberWithEvent } from '@fyit/crouton-auth/server'\n\nexport default defineEventHandler(async (event) => {\n  const { user } = await requireUserSession(event)\n  const { teamId } = getRouterParams(event)\n  const { inviteEmail } = await readBody(event)\n\n  // Only team members can invite others\n  const isInviter = await isTeamMemberWithEvent(event, teamId, user.id)\n\n  if (!isInviter) {\n    throw createError({\n      status: 403,\n      statusText: 'You are not a member of this team'\n    })\n  }\n\n  // Send invite\n  return await sendTeamInvite(teamId, inviteEmail)\n})",{"id":9193,"title":9194,"titles":9195,"content":528,"level":391},"/api-reference/server#best-practices-for-server-side-code","Best Practices for Server-Side Code",[292],{"id":9197,"title":9198,"titles":9199,"content":9200,"level":449},"/api-reference/server#_1-always-validate-and-authorize","1. Always Validate and Authorize",[292,9194],"// ✅ Good - comprehensive validation\nimport { isTeamMemberWithEvent } from '@fyit/crouton-auth/server'\n\nexport default defineEventHandler(async (event) => {\n  // Step 1: Authenticate\n  const { user } = await requireUserSession(event)\n\n  // Step 2: Get and validate team\n  const teamId = getRouterParam(event, 'id')\n  if (!teamId) {\n    throw createError({\n      status: 400,\n      statusText: 'Team ID is required'\n    })\n  }\n\n  // Step 3: Verify member\n  const isMember = await isTeamMemberWithEvent(event, teamId, user.id)\n  if (!isMember) {\n    throw createError({\n      status: 403,\n      statusText: 'Unauthorized'\n    })\n  }\n\n  // Now safe to proceed\n})\n\n// ❌ Bad - assumes access\nexport default defineEventHandler(async (event) => {\n  const { user } = await requireUserSession(event)\n  const teamId = getRouterParam(event, 'id')\n  // Missing validation!\n})",{"id":9202,"title":9203,"titles":9204,"content":9205,"level":449},"/api-reference/server#_2-use-external-collection-handler-for-lists","2. Use External Collection Handler for Lists",[292,9194],"// ✅ Good - use createExternalCollectionHandler\nexport default createExternalCollectionHandler(\n  async (event) => {\n    const { team } = await resolveTeamAndCheckMembership(event)\n    return await db.select().from(tables.items).where(...)\n  },\n  (item) => ({ id: item.id, title: item.name, ... })\n)\n\n// ❌ Bad - manual transformation\nexport default defineEventHandler(async (event) => {\n  const { team } = await resolveTeamAndCheckMembership(event)\n  const items = await db.select().from(tables.items).where(...)\n  return items.map(item => ({...}))  // Error handling is your problem\n})",{"id":9207,"title":9208,"titles":9209,"content":9210,"level":449},"/api-reference/server#_3-proper-error-handling","3. Proper Error Handling",[292,9194],"// ✅ Good - specific error messages and codes\ntry {\n  const data = await externalAPI.fetch(params)\n  return data\n} catch (error) {\n  if (error instanceof ValidationError) {\n    throw createError({\n      status: 400,\n      statusText: 'Invalid input: ' + error.message\n    })\n  }\n  \n  if (error instanceof NotFoundError) {\n    throw createError({\n      status: 404,\n      statusText: 'Resource not found'\n    })\n  }\n\n  console.error('Unexpected error:', error)\n  throw createError({\n    status: 500,\n    statusText: 'Internal server error'\n  })\n}\n\n// ❌ Bad - generic errors\ntry {\n  return await something()\n} catch (error) {\n  throw createError({\n    status: 500,\n    statusText: 'Something went wrong'\n  })\n}",{"id":9212,"title":9213,"titles":9214,"content":9215,"level":449},"/api-reference/server#_4-type-your-server-code","4. Type Your Server Code",[292,9194],"// ✅ Good - fully typed\ninterface TeamStats {\n  membersCount: number\n  projectsCount: number\n  filesSize: number\n}\n\nexport default defineEventHandler(async (event): Promise\u003CTeamStats> => {\n  const { team } = await resolveTeamAndCheckMembership(event)\n\n  const stats: TeamStats = {\n    membersCount: await countTeamMembers(team.id),\n    projectsCount: await countTeamProjects(team.id),\n    filesSize: await calculateTeamFilesSize(team.id)\n  }\n\n  return stats\n})\n\n// ❌ Bad - untyped\nexport default defineEventHandler(async (event) => {\n  const { team } = await resolveTeamAndCheckMembership(event)\n  return {\n    membersCount: await countTeamMembers(team.id),\n    projectsCount: await countTeamProjects(team.id),\n    filesSize: await calculateTeamFilesSize(team.id)\n  }\n})",{"id":9217,"title":9218,"titles":9219,"content":9220,"level":449},"/api-reference/server#_5-logging-and-debugging","5. Logging and Debugging",[292,9194],"// ✅ Good - contextual logging\nconst teamId = getRouterParam(event, 'id')\nconst { user } = await requireUserSession(event)\n\nconsole.log('[teams.get]', {\n  teamId,\n  userId: user.id,\n  timestamp: new Date().toISOString()\n})\n\ntry {\n  const data = await fetchTeamData(teamId)\n  return data\n} catch (error) {\n  console.error('[teams.get] Error:', {\n    teamId,\n    userId: user.id,\n    error: error instanceof Error ? error.message : String(error)\n  })\n  throw error\n}\n\n// ❌ Bad - no context\nconsole.log('Error:', error)",{"id":9222,"title":6823,"titles":9223,"content":528,"level":391},"/api-reference/server#security-considerations",[292],{"id":9225,"title":9226,"titles":9227,"content":9228,"level":449},"/api-reference/server#_1-team-isolation","1. Team Isolation",[292,6823],"Always verify team ownership before returning team-specific data: // ✅ Secure - check member on every request\nconst { team } = await resolveTeamAndCheckMembership(event)\n\n// ❌ Insecure - trusting client-provided teamId\nconst teamId = getQuery(event).teamId\nconst data = await db.select().from(tables.items)\n  .where(eq(tables.items.teamId, teamId))",{"id":9230,"title":9231,"titles":9232,"content":9233,"level":449},"/api-reference/server#_2-role-based-access-control","2. Role-Based Access Control",[292,6823],"Use member roles to gate sensitive operations: export default defineEventHandler(async (event) => {\n  const { membership } = await resolveTeamAndCheckMembership(event)\n\n  if (membership.role !== 'admin') {\n    throw createError({\n      status: 403,\n      statusText: 'Admin access required'\n    })\n  }\n\n  // Safe to proceed with admin operations\n})",{"id":9235,"title":9236,"titles":9237,"content":9238,"level":449},"/api-reference/server#_3-input-validation","3. Input Validation",[292,6823],"Always validate request body and query parameters: import { z } from 'zod'\n\nconst updateSchema = z.object({\n  name: z.string().min(1).max(100),\n  description: z.string().max(500).optional(),\n  isPublic: z.boolean().optional()\n})\n\nexport default defineEventHandler(async (event) => {\n  const { team } = await resolveTeamAndCheckMembership(event)\n  const body = await readBody(event)\n\n  // Validate and throw 400 on invalid data\n  const validated = updateSchema.parse(body)\n\n  return await updateTeam(team.id, validated)\n})",{"id":9240,"title":9241,"titles":9242,"content":9243,"level":449},"/api-reference/server#_4-rate-limiting","4. Rate Limiting",[292,6823],"Consider rate limiting for expensive operations: export default defineEventHandler(async (event) => {\n  const { user } = await requireUserSession(event)\n\n  // Check rate limit\n  const requests = await rateLimit.check(user.id, 'list-items', {\n    maxRequests: 100,\n    windowMs: 60000  // 1 minute\n  })\n\n  if (!requests.success) {\n    throw createError({\n      status: 429,\n      statusText: 'Too many requests. Please try again later.'\n    })\n  }\n\n  // Proceed\n})",{"id":9245,"title":36,"titles":9246,"content":528,"level":391},"/api-reference/server#troubleshooting",[292],{"id":9248,"title":9249,"titles":9250,"content":9251,"level":449},"/api-reference/server#handler-not-auto-imported","Handler Not Auto-Imported",[292,36],"If createExternalCollectionHandler is not found: Ensure Nuxt app is running (server needs to build imports)Check file is in server/ or app/server/ directoryRestart dev server: pnpm dev",{"id":9253,"title":9254,"titles":9255,"content":9256,"level":449},"/api-reference/server#_404-on-team-routes","404 on Team Routes",[292,36],"If team endpoints return 404: Verify team slug or ID matches URL paramCheck user is actually a team memberConfirm database has team record",{"id":9258,"title":9259,"titles":9260,"content":9261,"level":449},"/api-reference/server#membership-check-fails","Membership Check Fails",[292,36],"If resolveTeamAndCheckMembership throws 403: Log in with correct user accountVerify user was added to teamCheck teamMembers table in database",{"id":9263,"title":9264,"titles":9265,"content":9266,"level":449},"/api-reference/server#external-collection-returns-empty","External Collection Returns Empty",[292,36],"If CroutonReferenceSelect shows no options: Verify endpoint returns data: curl http://localhost:3000/api/pathCheck id and title fields are present in responseVerify fetch function isn't throwing error (check server console)",{"id":9268,"title":9269,"titles":9270,"content":9271,"level":449},"/api-reference/server#typescript-errors-with-h3event","TypeScript Errors with H3Event",[292,36],"If TypeScript complains about H3Event: // ✅ Correct import\nimport type { H3Event, EventHandlerRequest } from 'h3'\n\n// Or just use 'any' for quick development\nexport function resolveTeamAndCheckMembership(event: any) {",{"id":9273,"title":1007,"titles":9274,"content":9275,"level":391},"/api-reference/server#related-resources",[292],"useCollectionProxy - Client-side collection integrationCroutonReferenceSelect - UI component for external collectionsTeam-Based Auth - Team architecture and authorization patternsH3 Documentation - HTTP server framework html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":297,"title":296,"titles":9277,"content":9278,"level":385},[],"Advanced composables and utilities (use at your own risk) These composables and utilities are used internally by Nuxt Crouton components. You can use them for advanced customization, but they are not considered stable public API and may change between minor versions. Stability Warning: Internal APIs may change without notice. Use only if you need deep customization beyond what the public API provides.",{"id":9280,"title":9281,"titles":9282,"content":9283,"level":391},"/api-reference/internal-api#usetabledata","useTableData",[296],"Handles client-side data slicing, filtering, and pagination for tables. Use Case: Building a custom table component from scratch. import { useTableData } from '#imports'\n\nconst { slicedRows, pageTotalToShow } = useTableData({\n  rows: ref([...]),              // All data\n  search: ref(''),               // Search query\n  sort: ref({ column: 'name', direction: 'asc' }),\n  page: ref(1),\n  pageCount: ref(25),\n  serverPagination: false,\n  paginationData: null\n})",{"id":9285,"title":9286,"titles":9287,"content":9288,"level":391},"/api-reference/internal-api#usetablecolumns","useTableColumns",[296],"Manages column definitions including default columns (createdAt, updatedAt, createdBy, updatedBy, actions, select, presence). Use Case: Dynamically building column configurations. import { useTableColumns } from '#imports'\n\nconst { allColumns } = useTableColumns({\n  columns: [],\n  hideDefaultColumns: {\n    createdAt: false,\n    updatedAt: true,\n    createdBy: false,\n    updatedBy: false,\n    select: false,\n    presence: false,\n    actions: false\n  }\n})",{"id":9290,"title":9291,"titles":9292,"content":9293,"level":391},"/api-reference/internal-api#usetablesearch","useTableSearch",[296],"Provides debounced search functionality. Use Case: Custom search implementation with debouncing. import { useTableSearch } from '#imports'\n\nconst { search, isSearching, handleSearch, clearSearch } = useTableSearch({\n  debounceMs: 300\n})",{"id":9295,"title":9296,"titles":9297,"content":9298,"level":391},"/api-reference/internal-api#useexpandableslideover","useExpandableSlideover",[296],"Manages nested slideover state for multi-level forms. Use Case: Building custom nested form containers. import { useExpandableSlideover } from '#imports'\n\nconst { isOpen, isExpanded, toggleExpand, expand, collapse, open, close, slideoverUi, expandIcon, expandTooltip } = useExpandableSlideover() Note: For most use cases, stick to the public API documented in Composables and Components.",{"id":9300,"title":1007,"titles":9301,"content":9302,"level":391},"/api-reference/internal-api#related-resources",[296],"Composables Reference - Public APIComponents Reference - UI componentsAdvanced Table Configuration - Table features html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"id":301,"title":300,"titles":9304,"content":9305,"level":385},[],"Fetch a single collection item by ID with automatic caching and reactivity The useCollectionItem composable fetches individual collection items by ID. It's used internally by CardMini components and can be used anywhere you need to fetch a single item.",{"id":9307,"title":5176,"titles":9308,"content":9309,"level":391},"/api-reference/use-collection-item#type-signature",[300],"function useCollectionItem\u003CT = any>(\n  collection: string,\n  id: string | Ref\u003Cstring> | (() => string)\n): Promise\u003C{\n  item: ComputedRef\u003CT | null>\n  pending: Ref\u003Cboolean>\n  error: Ref\u003Cany>\n  refresh: () => Promise\u003Cvoid>\n}>",{"id":9311,"title":9079,"titles":9312,"content":9313,"level":391},"/api-reference/use-collection-item#parameters",[300],"collection (string) - The collection name (e.g., 'users', 'shopProducts')id (string | Ref | Function) - The item ID. Can be:\nStatic string: '123'Reactive ref: userId (a ref)Getter function: () => props.id",{"id":9315,"title":6217,"titles":9316,"content":9317,"level":391},"/api-reference/use-collection-item#returns",[300],"item - Computed reference to the fetched item (null if not found)pending - Boolean ref indicating loading stateerror - Error object if fetch failedrefresh - Function to manually refetch the item",{"id":9319,"title":4173,"titles":9320,"content":528,"level":391},"/api-reference/use-collection-item#basic-usage",[300],{"id":9322,"title":9323,"titles":9324,"content":9325,"level":449},"/api-reference/use-collection-item#static-id","Static ID",[300,4173],"\u003Cscript setup lang=\"ts\">\nconst { item, pending, error } = await useCollectionItem('users', '123')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"pending\">Loading...\u003C/div>\n  \u003Cdiv v-else-if=\"error\">Error: {{ error }}\u003C/div>\n  \u003Cdiv v-else-if=\"item\">\n    \u003Ch1>{{ item.name }}\u003C/h1>\n    \u003Cp>{{ item.email }}\u003C/p>\n  \u003C/div>\n\u003C/template>",{"id":9327,"title":9328,"titles":9329,"content":9330,"level":449},"/api-reference/use-collection-item#reactive-id-from-props","Reactive ID (from props)",[300,4173],"\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  userId: string\n}>()\n\n// Item refetches automatically when userId changes\nconst { item, pending } = await useCollectionItem('users', () => props.userId)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"item\">\n    \u003Ch2>{{ item.name }}\u003C/h2>\n    \u003Cp>{{ item.bio }}\u003C/p>\n  \u003C/div>\n\u003C/template>",{"id":9332,"title":9333,"titles":9334,"content":9335,"level":449},"/api-reference/use-collection-item#reactive-id-from-ref","Reactive ID (from ref)",[300,4173],"\u003Cscript setup lang=\"ts\">\nconst selectedId = ref('123')\n\nconst { item, pending } = await useCollectionItem('shopProducts', selectedId)\n\n// Change ID triggers automatic refetch\nconst changeProduct = (newId: string) => {\n  selectedId.value = newId  // Automatically refetches!\n}\n\u003C/script>",{"id":9337,"title":9338,"titles":9339,"content":528,"level":391},"/api-reference/use-collection-item#advanced-usage","Advanced Usage",[300],{"id":9341,"title":9342,"titles":9343,"content":9344,"level":449},"/api-reference/use-collection-item#with-typescript-types","With TypeScript Types",[300,9338],"\u003Cscript setup lang=\"ts\">\ninterface User {\n  id: string\n  name: string\n  email: string\n  avatar?: string\n}\n\nconst { item, pending } = await useCollectionItem\u003CUser>('users', '123')\n\n// item is typed as ComputedRef\u003CUser | null>\n\u003C/script>",{"id":9346,"title":9347,"titles":9348,"content":9349,"level":449},"/api-reference/use-collection-item#manual-refresh","Manual Refresh",[300,9338],"\u003Cscript setup lang=\"ts\">\nconst { item, pending, refresh } = await useCollectionItem('users', '123')\n\n// Force refetch\nconst reloadUser = async () => {\n  await refresh()\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>{{ item?.name }}\u003C/h1>\n    \u003CUButton @click=\"reloadUser\" :loading=\"pending\">\n      Reload\n    \u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":9351,"title":2522,"titles":9352,"content":9353,"level":449},"/api-reference/use-collection-item#error-handling",[300,9338],"\u003Cscript setup lang=\"ts\">\nconst { item, pending, error, refresh } = await useCollectionItem('users', '123')\n\n// Retry on error\nconst retry = () => {\n  if (error.value) {\n    refresh()\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"pending\">\n    \u003CUSkeleton class=\"h-20 w-full\" />\n  \u003C/div>\n\n  \u003Cdiv v-else-if=\"error\" class=\"border border-red-200 rounded p-4\">\n    \u003Cp class=\"text-red-600\">Failed to load user\u003C/p>\n    \u003CUButton @click=\"retry\" color=\"red\" variant=\"ghost\" size=\"sm\">\n      Try Again\n    \u003C/UButton>\n  \u003C/div>\n\n  \u003Cdiv v-else-if=\"item\">\n    \u003Ch1>{{ item.name }}\u003C/h1>\n  \u003C/div>\n\n  \u003Cdiv v-else class=\"text-gray-500\">\n    User not found\n  \u003C/div>\n\u003C/template>",{"id":9355,"title":9356,"titles":9357,"content":9358,"level":449},"/api-reference/use-collection-item#in-custom-cardmini-components","In Custom CardMini Components",[300,9338],"This is the primary use case - fetching items for reference field display: \u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  id: string\n  collection: string\n}>()\n\n// CardMini automatically uses this composable\nconst { item, pending, error } = await useCollectionItem(\n  props.collection,\n  () => props.id\n)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"group relative\">\n    \u003CUSkeleton v-if=\"pending\" class=\"h-12 w-full\" />\n\n    \u003Cdiv v-else-if=\"item\" class=\"border rounded-md p-2\">\n      \u003C!-- Custom layout here -->\n      \u003Cdiv>{{ item.title }}\u003C/div>\n    \u003C/div>\n\n    \u003Cdiv v-else-if=\"error\" class=\"text-red-500 text-xs\">\n      Error loading\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":9360,"title":9361,"titles":9362,"content":9363,"level":391},"/api-reference/use-collection-item#caching-behavior","Caching Behavior",[300],"Each item gets its own cache entry based on collection and ID: // Cache keys are generated as:\ncollection-item-users-123\ncollection-item-shopProducts-456\ncollection-item-locations-789\n\n// Items are cached using Nuxt's useAsyncData cache\n// Cache is shared across components\n// Updates automatically invalidate related caches Benefits: Multiple components showing the same item share one requestNavigating back to a page shows cached data instantlyMutations automatically invalidate item caches",{"id":9365,"title":2532,"titles":9366,"content":9367,"level":449},"/api-reference/use-collection-item#cache-invalidation",[300,9361],"When you update an item, its cache is automatically invalidated: \u003Cscript setup lang=\"ts\">\nconst { item } = await useCollectionItem('users', '123')\nconst { update } = useCollectionMutation('users')\n\nconst updateUser = async () => {\n  await update('123', { name: 'New Name' })\n  // Item cache for users:123 is invalidated\n  // useCollectionItem automatically refetches\n  // item.value now has updated data!\n}\n\u003C/script>",{"id":9369,"title":7586,"titles":9370,"content":9371,"level":391},"/api-reference/use-collection-item#api-routes",[300],"The composable determines the correct API path based on your current route:",{"id":9373,"title":9374,"titles":9375,"content":9376,"level":449},"/api-reference/use-collection-item#team-scoped-routes","Team-Scoped Routes",[300,7586],"// Current route: /teams/acme-corp/bookings\n// Fetches from: /api/teams/acme-corp/users/123\n\nconst { item } = await useCollectionItem('users', '123')",{"id":9378,"title":9379,"titles":9380,"content":9381,"level":449},"/api-reference/use-collection-item#super-admin-routes","Super Admin Routes",[300,7586],"// Current route: /super-admin/settings\n// Fetches from: /api/super-admin/users/123\n\nconst { item } = await useCollectionItem('users', '123')",{"id":9383,"title":9384,"titles":9385,"content":9386,"level":449},"/api-reference/use-collection-item#custom-api-paths","Custom API Paths",[300,7586],"If your collection uses a custom API path, configure it in the collection: // collections/locations/nuxt.config.ts\nexport default defineNuxtConfig({\n  crouton: {\n    apiPath: 'custom-locations'\n  }\n})\n\n// Fetches from: /api/teams/acme-corp/custom-locations/123\nconst { item } = await useCollectionItem('locations', '123')",{"id":9388,"title":9389,"titles":9390,"content":9391,"level":391},"/api-reference/use-collection-item#comparison-with-usecollectionquery","Comparison with useCollectionQuery",[300],"FeatureuseCollectionItemuseCollectionQueryPurposeFetch single itemFetch list of itemsInputItem IDOptional query paramsReturnsSingle objectArray of objectsCache Keycollection-item-{name}-{id}collection:{name}:{query}Use CaseCardMini, detail viewsTables, lists, forms When to use useCollectionItem: Fetching a single item by IDCardMini componentsDetail pagesReferenced entity display When to use useCollectionQuery: Fetching lists of itemsTables and listsSearch and filterPagination",{"id":9393,"title":1650,"titles":9394,"content":528,"level":391},"/api-reference/use-collection-item#common-patterns",[300],{"id":9396,"title":9397,"titles":9398,"content":9399,"level":449},"/api-reference/use-collection-item#loading-skeleton","Loading Skeleton",[300,1650],"Always show a skeleton while loading: \u003CUSkeleton v-if=\"pending\" class=\"h-12 w-full rounded\" />\n\u003Cdiv v-else-if=\"item\">\n  {{ item.name }}\n\u003C/div>",{"id":9401,"title":9402,"titles":9403,"content":9404,"level":449},"/api-reference/use-collection-item#conditional-rendering","Conditional Rendering",[300,1650],"Check if item exists before rendering: \u003Ctemplate>\n  \u003Cdiv v-if=\"item\">\n    \u003C!-- Safe to access item.* here -->\n    \u003Ch1>{{ item.title }}\u003C/h1>\n  \u003C/div>\n\u003C/template>",{"id":9406,"title":9407,"titles":9408,"content":9409,"level":449},"/api-reference/use-collection-item#optional-chaining","Optional Chaining",[300,1650],"Use optional chaining for deeply nested properties: \u003Cscript setup lang=\"ts\">\nconst { item } = await useCollectionItem('users', '123')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- Safe even if item is null or address is undefined -->\n  \u003Cdiv>{{ item?.address?.city }}\u003C/div>\n\u003C/template>",{"id":9411,"title":9412,"titles":9413,"content":9414,"level":449},"/api-reference/use-collection-item#watchers-on-items","Watchers on Items",[300,1650],"React to item changes: \u003Cscript setup lang=\"ts\">\nconst { item } = await useCollectionItem('users', () => props.userId)\n\n// Run side effects when item loads or changes\nwatch(item, (newItem) => {\n  if (newItem) {\n    console.log('Item loaded:', newItem)\n    // Update page title, analytics, etc.\n  }\n}, { immediate: true })\n\u003C/script>",{"id":9416,"title":9417,"titles":9418,"content":528,"level":391},"/api-reference/use-collection-item#error-scenarios","Error Scenarios",[300],{"id":9420,"title":9421,"titles":9422,"content":9423,"level":449},"/api-reference/use-collection-item#item-not-found","Item Not Found",[300,9417],"If the item doesn't exist, item will be null (not an error): \u003Cdiv v-if=\"item === null && !pending\">\n  Item not found\n\u003C/div>",{"id":9425,"title":9426,"titles":9427,"content":9428,"level":449},"/api-reference/use-collection-item#network-error","Network Error",[300,9417],"Network failures set error: \u003Cdiv v-if=\"error\">\n  Failed to load: {{ error.message }}\n  \u003CUButton @click=\"refresh\">Retry\u003C/UButton>\n\u003C/div>",{"id":9430,"title":9431,"titles":9432,"content":9433,"level":449},"/api-reference/use-collection-item#permission-denied","Permission Denied",[300,9417],"API returns 403, handled as error: \u003Cdiv v-if=\"error?.statusCode === 403\">\n  You don't have permission to view this item\n\u003C/div>",{"id":9435,"title":4872,"titles":9436,"content":528,"level":391},"/api-reference/use-collection-item#typescript-support",[300],{"id":9438,"title":9439,"titles":9440,"content":9441,"level":449},"/api-reference/use-collection-item#typed-item","Typed Item",[300,4872],"interface Product {\n  id: string\n  name: string\n  price: number\n  stock: number\n}\n\nconst { item } = await useCollectionItem\u003CProduct>('shopProducts', '123')\n\n// item is ComputedRef\u003CProduct | null>\n// TypeScript knows item.name, item.price, etc.",{"id":9443,"title":9444,"titles":9445,"content":9446,"level":449},"/api-reference/use-collection-item#type-guards","Type Guards",[300,4872],"const { item, pending } = await useCollectionItem\u003CProduct>('shopProducts', '123')\n\n// Type guard for template\nconst hasStock = computed(() => {\n  return item.value !== null && item.value.stock > 0\n})",{"id":9448,"title":44,"titles":9449,"content":9450,"level":391},"/api-reference/use-collection-item#best-practices",[300],"✅ DO: Use reactive ID parameters when ID can changeHandle all three states: pending, error, and loadedUse TypeScript types for better DXShow loading skeletons for better UXUse optional chaining for nested properties ❌ DON'T: Forget to handle error statesAccess item properties without checking if item existsFetch item data manually (let the composable handle it)Use this for lists (use useCollectionQuery instead)",{"id":9452,"title":9453,"titles":9454,"content":9455,"level":391},"/api-reference/use-collection-item#related-apis","Related APIs",[300],"useCollectionQuery - Fetch lists of itemsuseCollectionMutation - Create, update, delete itemsCardMini Components - Display components using this composableCustom CardMini Guide - Step-by-step guide to creating custom cards html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":311,"title":310,"titles":9457,"content":9458,"level":385},[],"Components for displaying rich text content, articles, and prose pages Content components provide beautiful layouts for displaying rich text content from any source - the Crouton editor, Nuxt Content, external CMSs, or plain HTML strings.",{"id":9460,"title":9461,"titles":9462,"content":9463,"level":391},"/api-reference/components/content-components#croutoncontentpreview","CroutonContentPreview",[310],"A compact preview component for displaying truncated content in table cells or cards. Automatically strips HTML tags and shows a tooltip with the full content on hover.",{"id":9465,"title":4987,"titles":9466,"content":9467,"level":449},"/api-reference/components/content-components#props",[310,9461],"interface ContentPreviewProps {\n  content?: string    // HTML content to preview\n  limit?: number      // Character limit (default: 100)\n} PropTypeDefaultDescriptioncontentstring''HTML content to previewlimitnumber100Maximum characters before truncation",{"id":9469,"title":4173,"titles":9470,"content":9471,"level":449},"/api-reference/components/content-components#basic-usage",[310,9461],"\u003Ctemplate>\n  \u003C!-- In a table cell -->\n  \u003CCroutonContentPreview :content=\"row.body\" />\n\n  \u003C!-- With custom limit -->\n  \u003CCroutonContentPreview :content=\"post.excerpt\" :limit=\"150\" />\n\u003C/template>",{"id":9473,"title":9474,"titles":9475,"content":9476,"level":449},"/api-reference/components/content-components#in-collection-tables","In Collection Tables",[310,9461],"Perfect for displaying content fields in your collection list views: \u003Ctemplate>\n  \u003CCroutonCollection\n    collection=\"posts\"\n    :columns=\"columns\"\n    :rows=\"posts\"\n  >\n    \u003Ctemplate #body-cell=\"{ row }\">\n      \u003CCroutonContentPreview :content=\"row.original.body\" :limit=\"80\" />\n    \u003C/template>\n  \u003C/CroutonCollection>\n\u003C/template>",{"id":9478,"title":9479,"titles":9480,"content":9481,"level":391},"/api-reference/components/content-components#croutoncontentpage","CroutonContentPage",[310],"A generic content page wrapper with Tailwind Typography (prose) styling, optional table of contents, and flexible slot-based layout.",{"id":9483,"title":4987,"titles":9484,"content":9485,"level":449},"/api-reference/components/content-components#props-1",[310,9479],"interface TocLink {\n  id: string\n  text: string\n  depth: number\n  children?: TocLink[]\n}\n\ninterface ContentPageProps {\n  content?: string                    // HTML content\n  title?: string                      // Page title\n  description?: string                // Page description/subtitle\n  toc?: boolean | TocLink[]           // Enable TOC or provide custom links\n  maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | 'full'\n} PropTypeDefaultDescriptioncontentstring''HTML content to rendertitlestringundefinedPage titledescriptionstringundefinedPage subtitle/descriptiontocboolean | TocLink[]falseAuto-generate TOC or provide custom linksmaxWidthstring'3xl'Maximum content width",{"id":9487,"title":5372,"titles":9488,"content":9489,"level":449},"/api-reference/components/content-components#slots",[310,9479],"SlotDescriptionheaderCustom header content (replaces title/description)defaultCustom content (replaces content prop rendering)sidebarSidebar content (replaces auto-generated TOC)footerFooter content below the main content",{"id":9491,"title":4173,"titles":9492,"content":9493,"level":449},"/api-reference/components/content-components#basic-usage-1",[310,9479],"\u003Ctemplate>\n  \u003CCroutonContentPage\n    title=\"About Us\"\n    description=\"Learn more about our company\"\n    :content=\"page.body\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { data: page } = await useFetch('/api/pages/about')\n\u003C/script>",{"id":9495,"title":9496,"titles":9497,"content":9498,"level":449},"/api-reference/components/content-components#with-table-of-contents","With Table of Contents",[310,9479],"Enable automatic TOC generation from headings in your content: \u003Ctemplate>\n  \u003CCroutonContentPage\n    title=\"Documentation\"\n    :content=\"docs.body\"\n    toc\n  />\n\u003C/template> Auto-generated TOC requires headings in your content to have id attributes. For example: \u003Ch2 id=\"getting-started\">Getting Started\u003C/h2>",{"id":9500,"title":9501,"titles":9502,"content":9503,"level":449},"/api-reference/components/content-components#with-custom-sidebar","With Custom Sidebar",[310,9479],"\u003Ctemplate>\n  \u003CCroutonContentPage :content=\"page.body\">\n    \u003Ctemplate #sidebar>\n      \u003Cnav class=\"space-y-2\">\n        \u003Ch3 class=\"font-semibold\">Related Pages\u003C/h3>\n        \u003CNuxtLink v-for=\"link in relatedPages\" :to=\"link.to\">\n          {{ link.title }}\n        \u003C/NuxtLink>\n      \u003C/nav>\n    \u003C/template>\n\n    \u003Ctemplate #footer>\n      \u003Cdiv class=\"flex justify-between\">\n        \u003CNuxtLink :to=\"prevPage\">Previous\u003C/NuxtLink>\n        \u003CNuxtLink :to=\"nextPage\">Next\u003C/NuxtLink>\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonContentPage>\n\u003C/template>",{"id":9505,"title":9506,"titles":9507,"content":9508,"level":449},"/api-reference/components/content-components#with-custom-content","With Custom Content",[310,9479],"Use the default slot to render content yourself: \u003Ctemplate>\n  \u003CCroutonContentPage title=\"My Page\">\n    \u003Cdiv class=\"prose dark:prose-invert\">\n      \u003Cp>Custom rendered content here...\u003C/p>\n      \u003CMyCustomComponent />\n    \u003C/div>\n  \u003C/CroutonContentPage>\n\u003C/template>",{"id":9510,"title":9511,"titles":9512,"content":9513,"level":391},"/api-reference/components/content-components#croutoncontentarticle","CroutonContentArticle",[310],"A full-featured article/blog post layout with title, author, date, featured image, tags, and reading time support.",{"id":9515,"title":4987,"titles":9516,"content":9517,"level":449},"/api-reference/components/content-components#props-2",[310,9511],"interface Author {\n  name: string\n  avatar?: string\n  description?: string\n}\n\ninterface ContentArticleProps {\n  title: string                       // Article title (required)\n  description?: string                // Excerpt/subtitle\n  author?: Author                     // Author information\n  date?: Date | string                // Publication date\n  image?: string                      // Featured image URL\n  imageAlt?: string                   // Featured image alt text\n  content?: string                    // HTML content\n  tags?: string[]                     // Article tags\n  readingTime?: string                // Reading time (e.g., \"5 min read\")\n  maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'\n} PropTypeDefaultDescriptiontitlestringrequiredArticle titledescriptionstringundefinedExcerpt or subtitleauthorAuthorundefinedAuthor name, avatar, and descriptiondateDate | stringundefinedPublication dateimagestringundefinedFeatured image URLimageAltstringtitleFeatured image alt textcontentstringundefinedHTML contenttagsstring[]undefinedArticle tagsreadingTimestringundefinedReading time displaymaxWidthstring'3xl'Maximum content width",{"id":9519,"title":5372,"titles":9520,"content":9521,"level":449},"/api-reference/components/content-components#slots-1",[310,9511],"SlotDescriptionheaderCustom header (replaces title, author, date, tags)defaultCustom content (replaces content prop rendering)sidebarSticky sidebar contentfooterFooter content (e.g., related posts, share buttons)",{"id":9523,"title":4173,"titles":9524,"content":9525,"level":449},"/api-reference/components/content-components#basic-usage-2",[310,9511],"\u003Ctemplate>\n  \u003CCroutonContentArticle\n    :title=\"post.title\"\n    :description=\"post.excerpt\"\n    :content=\"post.body\"\n    :date=\"post.publishedAt\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { data: post } = await useFetch('/api/posts/my-post')\n\u003C/script>",{"id":9527,"title":9528,"titles":9529,"content":9530,"level":449},"/api-reference/components/content-components#full-blog-post","Full Blog Post",[310,9511],"\u003Ctemplate>\n  \u003CCroutonContentArticle\n    :title=\"post.title\"\n    :description=\"post.excerpt\"\n    :author=\"{\n      name: post.author.name,\n      avatar: post.author.avatar,\n      description: post.author.role\n    }\"\n    :date=\"post.publishedAt\"\n    :image=\"post.featuredImage\"\n    :content=\"post.body\"\n    :tags=\"post.tags\"\n    reading-time=\"5 min read\"\n  >\n    \u003Ctemplate #sidebar>\n      \u003CTableOfContents :links=\"tocLinks\" />\n    \u003C/template>\n\n    \u003Ctemplate #footer>\n      \u003CShareButtons :url=\"post.url\" :title=\"post.title\" />\n      \u003CRelatedPosts :posts=\"relatedPosts\" />\n    \u003C/template>\n  \u003C/CroutonContentArticle>\n\u003C/template>",{"id":9532,"title":9533,"titles":9534,"content":9535,"level":449},"/api-reference/components/content-components#with-editor-content","With Editor Content",[310,9511],"Display content created with CroutonEditorSimple: \u003Ctemplate>\n  \u003CCroutonContentArticle\n    :title=\"article.title\"\n    :content=\"article.content\"\n    :date=\"article.createdAt\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\n// Content from CroutonEditorSimple is stored as HTML\nconst { data: article } = await useCollectionQuery('articles', {\n  query: { id: route.params.id }\n})\n\u003C/script>",{"id":9537,"title":9538,"titles":9539,"content":9540,"level":449},"/api-reference/components/content-components#with-nuxt-content","With Nuxt Content",[310,9511],"Works seamlessly with @nuxt/content: \u003Ctemplate>\n  \u003CCroutonContentArticle\n    :title=\"page.title\"\n    :description=\"page.description\"\n    :date=\"page.date\"\n    :tags=\"page.tags\"\n  >\n    \u003CContentRenderer :value=\"page\" />\n  \u003C/CroutonContentArticle>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { data: page } = await useAsyncData('post', () =>\n  queryContent('/blog/my-post').findOne()\n)\n\u003C/script>",{"id":9542,"title":6314,"titles":9543,"content":9544,"level":391},"/api-reference/components/content-components#styling",[310],"All content components use Tailwind Typography (prose) classes for beautiful default styling. The prose styles are automatically applied with dark mode support.",{"id":9546,"title":9547,"titles":9548,"content":9549,"level":449},"/api-reference/components/content-components#customizing-prose-styles","Customizing Prose Styles",[310,6314],"Override prose styles using Tailwind classes: \u003Ctemplate>\n  \u003CCroutonContentPage :content=\"content\">\n    \u003Ctemplate #default>\n      \u003Cdiv class=\"prose prose-lg prose-blue dark:prose-invert max-w-none\">\n        \u003Cdiv v-html=\"content\" />\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonContentPage>\n\u003C/template>",{"id":9551,"title":9552,"titles":9553,"content":9554,"level":449},"/api-reference/components/content-components#available-prose-modifiers","Available Prose Modifiers",[310,6314],"prose-sm / prose-lg / prose-xl - Size variantsprose-blue / prose-green / etc. - Color variants for linksprose-invert - Dark mode (applied automatically)max-w-none - Remove max-width constraint",{"id":9556,"title":9557,"titles":9558,"content":9559,"level":391},"/api-reference/components/content-components#content-sources","Content Sources",[310],"These components are source-agnostic. Use them with any content: SourceExampleCrouton Editor\u003CCroutonEditorSimple v-model=\"content\" />Nuxt ContentqueryContent('/blog').findOne()External CMSStrapi, Sanity, Contentful APIDatabaseDirect HTML from your DBMarkdownRendered to HTML",{"id":9561,"title":4341,"titles":9562,"content":9563,"level":391},"/api-reference/components/content-components#related",[310],"Rich Text Editor - CroutonEditorSimple for creating contentLayout Components - Other layout componentsNuxt Content - File-based content management html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}",{"id":315,"title":314,"titles":9565,"content":9566,"level":385},[],"Interactive form elements for data input with validation and dynamic behavior",{"id":9568,"title":9569,"titles":9570,"content":9571,"level":391},"/api-reference/components/form-components#croutonform","CroutonForm",[314],"The main orchestrator component that manages form instances in different container types (modals, dialogs, slideovers). This component is automatically registered globally and renders based on state managed by useCrouton().",{"id":9573,"title":2375,"titles":9574,"content":9575,"level":449},"/api-reference/components/form-components#container-types",[314,9569],"TypeUse CaseFeaturesModalStandard formsCentered dialog, backdrop, size: lgDialogSimple confirmationsLike modal but without body wrapper paddingSlideoverComplex forms, nested editingSide panel, expandable, supports 5-level nesting",{"id":9577,"title":2370,"titles":9578,"content":9579,"level":449},"/api-reference/components/form-components#opening-forms",[314,9569],"Forms are opened programmatically using the useCrouton() composable: \u003Cscript setup lang=\"ts\">\nconst { open } = useCrouton()\n\n// Create new item in slideover (default)\nconst handleCreate = () => {\n  open('create', 'users', [])\n}\n\n// Update item in modal\nconst handleEdit = (id: string) => {\n  open('update', 'users', [id], 'modal')\n}\n\n// Delete confirmation in dialog\nconst handleDelete = (ids: string[]) => {\n  open('delete', 'users', ids, 'dialog')\n}\n\n// View-only mode\nconst handleView = (id: string) => {\n  open('view', 'users', [id])\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUButton @click=\"handleCreate\">Create User\u003C/UButton>\n  \u003CUButton @click=\"handleEdit('user-123')\">Edit User\u003C/UButton>\n  \u003CUButton @click=\"handleDelete(['user-1', 'user-2'])\">Delete Users\u003C/UButton>\n\u003C/template>",{"id":9581,"title":9582,"titles":9583,"content":9584,"level":449},"/api-reference/components/form-components#slideover-nesting-expansion","Slideover Nesting & Expansion",[314,9569],"Slideovers support up to 5 levels of nesting for complex workflows like editing a product → adding a category → adding a tag.",{"id":9586,"title":9587,"titles":9588,"content":9589,"level":748},"/api-reference/components/form-components#nesting-example","Nesting Example",[314,9569,9582],"\u003Cscript setup lang=\"ts\">\nconst { open } = useCrouton()\n\n// Level 1: Edit product\nopen('update', 'products', ['product-123'])\n\n// Inside product form, user clicks \"Add Category\"\n// Level 2: Create category (nested)\nopen('create', 'categories', [])\n\n// Inside category form, user clicks \"Add Parent Category\"\n// Level 3: Create parent category (nested deeper)\nopen('create', 'categories', [])\n\u003C/script> Visual Stacking: Each nested slideover has a cascading offset for visual clarity: Level 1: No offsetLevel 2: Offset rightLevel 3: Offset more, etc. Breadcrumb Navigation: Nested slideovers show breadcrumbs indicating the parent context.",{"id":9591,"title":9592,"titles":9593,"content":9594,"level":748},"/api-reference/components/form-components#expandcollapse","Expand/Collapse",[314,9569,9582],"Slideovers can toggle between sidebar mode (max-w-2xl) and fullscreen: \u003C!-- Expand button appears in slideover header -->\n\u003C!-- Click to toggle between: -->\n\u003C!-- Sidebar: max-w-2xl with padding -->\n\u003C!-- Fullscreen: Full width, more workspace --> Transition: Smooth 400ms CSS transition with proper cleanup.",{"id":9596,"title":3965,"titles":9597,"content":9598,"level":449},"/api-reference/components/form-components#state-management",[314,9569],"Forms use the CroutonState interface for internal state: interface CroutonState {\n  id: string                          // Unique state ID\n  action: CroutonAction              // 'create' | 'update' | 'delete' | 'view'\n  collection: string | null           // Collection name\n  activeItem: any                     // Item being edited/viewed\n  items: any[]                        // Items for batch delete\n  loading: LoadingState              // Loading state per action\n  isOpen: boolean                     // Container open state\n  containerType: 'slideover' | 'modal' | 'dialog' | 'inline'\n  isExpanded?: boolean               // Slideover expand state\n}",{"id":9600,"title":5367,"titles":9601,"content":9602,"level":449},"/api-reference/components/form-components#events",[314,9569],"EventTriggerPurpose@update:openContainer closeHandles cleanup when form closes@after:leaveAnimation completeRemoves state after transition ends",{"id":9604,"title":3195,"titles":9605,"content":9606,"level":449},"/api-reference/components/form-components#complete-example",[314,9569],"\u003C!-- No manual template needed - Form renders globally -->\n\n\u003Cscript setup lang=\"ts\">\nconst { open, close } = useCrouton()\n\n// Open form\nconst createProduct = () => {\n  open('create', 'products', [], 'slideover')\n}\n\n// Forms close automatically after successful submission\n// Or close manually:\nconst cancelForm = () => {\n  close()\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003CUButton @click=\"createProduct\">\n      \u003CUIcon name=\"i-lucide-plus\" />\n      Create Product\n    \u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":9608,"title":9609,"titles":9610,"content":9611,"level":449},"/api-reference/components/form-components#keyboard-shortcuts","Keyboard Shortcuts",[314,9569],"ShortcutActionEscapeClose current form/slideover(Future) Expand/collapse shortcutNot yet implemented",{"id":9613,"title":36,"titles":9614,"content":528,"level":449},"/api-reference/components/form-components#troubleshooting",[314,9569],{"id":9616,"title":4035,"titles":9617,"content":9618,"level":748},"/api-reference/components/form-components#form-doesnt-open",[314,9569,36],"Check collection name: Must match configured collectionCheck component exists: Run generator to create form componentCheck console: Look for resolution errors",{"id":9620,"title":9621,"titles":9622,"content":9623,"level":748},"/api-reference/components/form-components#nested-slideovers-feel-confusing","Nested slideovers feel confusing",[314,9569,36],"Limit nesting: Consider alternative UX for >3 levelsUse breadcrumbs: Shows context pathUse modal for simple edits: Clearer separation",{"id":9625,"title":9626,"titles":9627,"content":9628,"level":748},"/api-reference/components/form-components#multiple-forms-open-at-once","Multiple forms open at once",[314,9569,36],"This is expected! You can have: 1 modal + 1 slideover5 nested slideovers (max depth)Multiple dialogs (avoid this) Each has independent state managed by useCrouton().",{"id":9630,"title":9631,"titles":9632,"content":9633,"level":391},"/api-reference/components/form-components#formdynamicloader","FormDynamicLoader",[314],"Dynamically resolves and loads the correct form component for a given collection. Used internally by CroutonForm to display collection-specific forms. Internal Component: You typically don't use this directly. It's used by CroutonForm to load generated form components.",{"id":9635,"title":4987,"titles":9636,"content":9637,"level":449},"/api-reference/components/form-components#props",[314,9631],"interface FormDynamicLoaderProps {\n  collection: string        // Collection name (e.g., 'users')\n  loading: string          // Loading state: 'create' | 'update' | 'delete' | ''\n  action: string           // Action type: 'create' | 'update' | 'delete' | 'view'\n  items: any[]             // Items for delete action\n  activeItem: any          // Item for update/view\n}",{"id":9639,"title":9640,"titles":9641,"content":9642,"level":449},"/api-reference/components/form-components#component-resolution","Component Resolution",[314,9631],"The loader uses useCollections().componentMap to find the correct component: // Resolution logic:\n// 1. Check componentMap for collection\nconst componentMap = useCollections().componentMap\n\n// 2. For 'view' action, try Detail component first\nif (action === 'view') {\n  // Try: UsersDetail, then fallback to UsersForm\n  const detailComponent = componentMap.get(`${collection}Detail`)\n  if (detailComponent) return detailComponent\n}\n\n// 3. Return standard form component\nreturn componentMap.get(`${collection}Form`)",{"id":9644,"title":9645,"titles":9646,"content":9647,"level":449},"/api-reference/components/form-components#convention-detail-vs-form-components","Convention: Detail vs Form Components",[314,9631],"ActionComponent LoadedConventioncreate[Collection]Form.vuee.g., UsersForm.vueupdate[Collection]Form.vuee.g., UsersForm.vuedelete[Collection]Form.vueConfirmation UIview[Collection]Detail.vue OR [Collection]Form.vueDetail first, fallback to Form Example: components/\n├── UsersForm.vue        # Create/Update/Delete\n└── UsersDetail.vue      # View-only (optional)",{"id":9649,"title":9650,"titles":9651,"content":9652,"level":449},"/api-reference/components/form-components#mode-detection","Mode Detection",[314,9631],"For special collections like translationsUi: // Detects mode based on route path\nconst mode = computed(() => {\n  const route = useRoute()\n  if (route.path.includes('/super-admin/')) {\n    return 'system'  // System-level translations\n  }\n  return 'team'      // Team-level translations\n})",{"id":9654,"title":9655,"titles":9656,"content":9657,"level":449},"/api-reference/components/form-components#usage-internal","Usage (Internal)",[314,9631],"\u003C!-- Used by _Form.vue -->\n\u003CFormDynamicLoader\n  :collection=\"state.collection\"\n  :loading=\"state.loading\"\n  :action=\"state.action\"\n  :items=\"state.items\"\n  :activeItem=\"state.activeItem\"\n/>",{"id":9659,"title":36,"titles":9660,"content":528,"level":449},"/api-reference/components/form-components#troubleshooting-1",[314,9631],{"id":9662,"title":9663,"titles":9664,"content":9665,"level":748},"/api-reference/components/form-components#component-not-found-error","\"Component not found\" error",[314,9631,36],"Run generator: npx crouton-generate config crouton.config.jsCheck naming: Must be [Collection]Form.vue (PascalCase)Check registration: Component must be in components/ directory",{"id":9667,"title":9668,"titles":9669,"content":9670,"level":748},"/api-reference/components/form-components#detail-component-not-loading-for-view-action","Detail component not loading for 'view' action",[314,9631,36],"Create Detail component: components/[Collection]Detail.vueRegister in componentMap: Generator handles this automaticallyFallback works: Form component shows if Detail is missing",{"id":9672,"title":9673,"titles":9674,"content":9675,"level":391},"/api-reference/components/form-components#formlayout","FormLayout",[314],"Responsive layout wrapper for forms with tabs, sidebar, and header/footer slots. Provides consistent structure for complex forms with validation error indicators.",{"id":9677,"title":4987,"titles":9678,"content":9679,"level":449},"/api-reference/components/form-components#props-1",[314,9673],"interface FormLayoutProps {\n  tabs?: boolean                      // Enable tab navigation (default: false)\n  navigationItems?: NavigationItem[]  // Tab definitions\n  tabErrors?: Record\u003Cstring, number>  // Error counts per tab\n  modelValue?: string                 // Active tab (v-model)\n}\n\ninterface NavigationItem {\n  label: string        // Tab label\n  value: string        // Tab value/ID\n  icon?: string        // Optional icon\n}",{"id":9681,"title":5372,"titles":9682,"content":9683,"level":449},"/api-reference/components/form-components#slots",[314,9673],"SlotScoped PropsPurposeheader-Form header content (title, actions)main{ activeSection }Primary form fields (receives active tab)sidebar-Meta fields, settings (responsive accordion on mobile)footer-Submit button, validation summary, action buttons",{"id":9685,"title":4173,"titles":9686,"content":9687,"level":449},"/api-reference/components/form-components#basic-usage",[314,9673],"\u003Ctemplate>\n  \u003CCroutonFormLayout>\n    \u003Ctemplate #header>\n      \u003Ch2 class=\"text-2xl font-bold\">Create Product\u003C/h2>\n    \u003C/template>\n\n    \u003Ctemplate #main>\n      \u003Cdiv class=\"space-y-6\">\n        \u003CUFormField label=\"Name\" name=\"name\">\n          \u003CUInput v-model=\"state.name\" />\n        \u003C/UFormField>\n\n        \u003CUFormField label=\"Price\" name=\"price\">\n          \u003CUInput v-model=\"state.price\" type=\"number\" />\n        \u003C/UFormField>\n      \u003C/div>\n    \u003C/template>\n\n    \u003Ctemplate #footer>\n      \u003CCroutonFormActionButton\n        :action=\"action\"\n        :collection=\"collection\"\n      />\n    \u003C/template>\n  \u003C/CroutonFormLayout>\n\u003C/template>",{"id":9689,"title":9690,"titles":9691,"content":9692,"level":449},"/api-reference/components/form-components#tab-navigation","Tab Navigation",[314,9673],"Enable tabs for organizing complex forms.",{"id":9694,"title":9695,"titles":9696,"content":9697,"level":748},"/api-reference/components/form-components#basic-tab-setup","Basic Tab Setup",[314,9673,9690],"\u003Cscript setup lang=\"ts\">\nconst activeSection = ref('general')\n\nconst navigationItems = [\n  { label: 'General', value: 'general', icon: 'i-lucide-info' },\n  { label: 'Pricing', value: 'pricing', icon: 'i-lucide-dollar-sign' },\n  { label: 'SEO', value: 'seo', icon: 'i-lucide-search' }\n]\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonFormLayout\n    :tabs=\"true\"\n    :navigation-items=\"navigationItems\"\n    v-model=\"activeSection\"\n  >\n    \u003Ctemplate #main=\"{ activeSection }\">\n      \u003Cdiv v-show=\"activeSection === 'general'\" class=\"space-y-6\">\n        \u003CUFormField label=\"Name\" name=\"name\">\n          \u003CUInput v-model=\"state.name\" />\n        \u003C/UFormField>\n      \u003C/div>\n\n      \u003Cdiv v-show=\"activeSection === 'pricing'\" class=\"space-y-6\">\n        \u003CUFormField label=\"Price\" name=\"price\">\n          \u003CUInput v-model=\"state.price\" type=\"number\" />\n        \u003C/UFormField>\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonFormLayout>\n\u003C/template>",{"id":9699,"title":9700,"titles":9701,"content":9702,"level":748},"/api-reference/components/form-components#tracking-validation-errors-per-tab","Tracking Validation Errors Per Tab",[314,9673,9690],"\u003Cscript setup lang=\"ts\">\nconst validationErrors = ref([])\n\n// Map fields to their tab groups\nconst fieldToGroup: Record\u003Cstring, string> = {\n  'name': 'general',\n  'description': 'general',\n  'price': 'pricing',\n  'compareAtPrice': 'pricing',\n  'metaTitle': 'seo',\n  'metaDescription': 'seo'\n}\n\n// Count errors per tab\nconst tabErrorCounts = computed(() => {\n  const counts: Record\u003Cstring, number> = {}\n  validationErrors.value.forEach((error: any) => {\n    const tabName = fieldToGroup[error.name] || 'general'\n    counts[tabName] = (counts[tabName] || 0) + 1\n  })\n  return counts\n})\n\nconst handleValidationError = (event: any) => {\n  if (event?.errors) {\n    validationErrors.value = event.errors\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm @error=\"handleValidationError\">\n    \u003CCroutonFormLayout\n      :tabs=\"true\"\n      :tab-errors=\"tabErrorCounts\"\n    >\n      \u003C!-- ... -->\n    \u003C/CroutonFormLayout>\n  \u003C/UForm>\n\u003C/template>",{"id":9704,"title":9705,"titles":9706,"content":9707,"level":748},"/api-reference/components/form-components#error-summary-with-tab-navigation","Error Summary with Tab Navigation",[314,9673,9690],"Combine tab navigation with error summary in the footer: \u003Ctemplate>\n  \u003CCroutonFormLayout :tabs=\"true\">\n    \u003Ctemplate #footer>\n      \u003CCroutonValidationErrorSummary\n        :tab-errors=\"tabErrorCounts\"\n        :navigation-items=\"navigationItems\"\n        @switch-tab=\"activeSection = $event\"\n      />\n\n      \u003CCroutonFormActionButton\n        :action=\"action\"\n        :collection=\"collection\"\n      />\n    \u003C/template>\n  \u003C/CroutonFormLayout>\n\u003C/template>",{"id":9709,"title":9710,"titles":9711,"content":9712,"level":449},"/api-reference/components/form-components#error-indicators","Error Indicators",[314,9673],"Tabs automatically show error badges when validation fails: \u003C!-- Error badge appears as red dot with count -->\n\u003CUTabs :items=\"enhancedNavigationItems\">\n  \u003C!-- General (2) ← Shows 2 errors -->\n  \u003C!-- Pricing ← No errors -->\n  \u003C!-- SEO (1) ← Shows 1 error -->\n\u003C/UTabs>",{"id":9714,"title":9715,"titles":9716,"content":9717,"level":449},"/api-reference/components/form-components#responsive-sidebar","Responsive Sidebar",[314,9673],"The sidebar adapts to screen size: BreakpointBehaviorMobile (\u003C lg)Accordion at top of formDesktop (>= lg)Fixed sidebar column on right \u003Ctemplate>\n  \u003CCroutonFormLayout>\n    \u003Ctemplate #main>\n      \u003C!-- Primary fields -->\n    \u003C/template>\n\n    \u003Ctemplate #sidebar>\n      \u003C!-- Meta settings, status, timestamps, etc. -->\n      \u003Cdiv class=\"space-y-4\">\n        \u003CUFormField label=\"Status\" name=\"status\">\n          \u003CUSelectMenu v-model=\"state.status\" :items=\"statusOptions\" />\n        \u003C/UFormField>\n\n        \u003CUFormField label=\"Published At\" name=\"publishedAt\">\n          \u003CCroutonDate v-model=\"state.publishedAt\" />\n        \u003C/UFormField>\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonFormLayout>\n\u003C/template>",{"id":9719,"title":9720,"titles":9721,"content":9722,"level":449},"/api-reference/components/form-components#complete-example-product-form-with-all-features","Complete Example: Product Form with All Features",[314,9673],"For a complete working example demonstrating a multi-tab product form with validation tracking, error summary, and all CroutonFormLayout features, see this interactive demo: View Full Interactive Demo →Fork the demo to experiment with different configurations. The complete example includes:Multi-tab layout (General, Pricing, Organization, SEO)Per-tab validation error trackingReference selects for categories and tagsDate picker integrationFull CRUD operations (create/update)Validation error summary component",{"id":9724,"title":9725,"titles":9726,"content":9727,"level":748},"/api-reference/components/form-components#focused-example-tab-error-tracking","Focused Example: Tab Error Tracking",[314,9673,9720],"This snippet shows the key pattern for tracking validation errors per tab: \u003Cscript setup lang=\"ts\">\nconst { create, update } = useCollectionMutation('products')\nconst activeSection = ref('general')\n\nconst navigationItems = [\n  { label: 'General', value: 'general', icon: 'i-lucide-info' },\n  { label: 'Pricing', value: 'pricing', icon: 'i-lucide-dollar-sign' },\n  { label: 'Organization', value: 'organization', icon: 'i-lucide-folder' },\n  { label: 'SEO', value: 'seo', icon: 'i-lucide-search' }\n]\n\n// Map fields to their tab groups\nconst fieldToGroup: Record\u003Cstring, string> = {\n  'name': 'general',\n  'description': 'general',\n  'price': 'pricing',\n  'compareAtPrice': 'pricing',\n  'categoryId': 'organization',\n  'tags': 'organization',\n  'metaTitle': 'seo',\n  'metaDescription': 'seo'\n}\n\n// Count errors per tab\nconst tabErrorCounts = computed(() => {\n  const counts: Record\u003Cstring, number> = {}\n  validationErrors.value.forEach((error: any) => {\n    const tabName = fieldToGroup[error.name] || 'general'\n    counts[tabName] = (counts[tabName] || 0) + 1\n  })\n  return counts\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm :state=\"state\" :schema=\"schema\" @submit=\"handleSubmit\">\n    \u003CCroutonFormLayout\n      :tabs=\"true\"\n      :navigation-items=\"navigationItems\"\n      :tab-errors=\"tabErrorCounts\"\n      v-model=\"activeSection\"\n    >\n      \u003C!-- Tab content sections... -->\n      \u003Ctemplate #footer>\n        \u003CCroutonValidationErrorSummary\n          :tab-errors=\"tabErrorCounts\"\n          :navigation-items=\"navigationItems\"\n          @switch-tab=\"activeSection = $event\"\n        />\n      \u003C/template>\n    \u003C/CroutonFormLayout>\n  \u003C/UForm>\n\u003C/template>",{"id":9729,"title":1383,"titles":9730,"content":528,"level":449},"/api-reference/components/form-components#customization",[314,9673],{"id":9732,"title":9733,"titles":9734,"content":9735,"level":748},"/api-reference/components/form-components#custom-max-width","Custom Max Width",[314,9673,1383],"\u003C!-- Default: max-w-7xl -->\n\u003CCroutonFormLayout class=\"max-w-5xl\">\n  \u003C!-- ... -->\n\u003C/CroutonFormLayout>",{"id":9737,"title":9738,"titles":9739,"content":9740,"level":748},"/api-reference/components/form-components#custom-sidebar-label-mobile","Custom Sidebar Label (Mobile)",[314,9673,1383],"Currently hardcoded as \"Meta settings\". To customize: \u003C!-- Feature request: Make configurable -->\n\u003C!-- Workaround: Hide sidebar on mobile, use tabs instead -->",{"id":9742,"title":36,"titles":9743,"content":528,"level":449},"/api-reference/components/form-components#troubleshooting-2",[314,9673],{"id":9745,"title":9746,"titles":9747,"content":9748,"level":748},"/api-reference/components/form-components#sidebar-not-showing","Sidebar not showing",[314,9673,36],"Check slot usage: Must use #sidebar slotCheck content: Sidebar must have contentCheck breakpoint: Only visible on lg and up",{"id":9750,"title":9751,"titles":9752,"content":9753,"level":748},"/api-reference/components/form-components#tabs-not-working","Tabs not working",[314,9673,36],"Check tabs prop: Must be trueCheck navigation-items: Must have 2+ itemsCheck v-show: Use v-show not v-if for tab content",{"id":9755,"title":9756,"titles":9757,"content":9758,"level":748},"/api-reference/components/form-components#error-badges-not-appearing","Error badges not appearing",[314,9673,36],"Check tab-errors prop: Must be reactive objectCheck field mapping: Ensure fieldToGroup maps all fieldsCheck validation: Errors must be captured via @error event",{"id":9760,"title":9761,"titles":9762,"content":9763,"level":391},"/api-reference/components/form-components#formreferenceselect","FormReferenceSelect",[314],"A smart dropdown component for selecting related entities from other collections. Supports both single and multi-select modes, inline creation, and comprehensive error handling. Component Name: The actual component is CroutonFormReferenceSelect but is typically referenced as just FormReferenceSelect or auto-aliased as ReferenceSelect in generated forms.",{"id":9765,"title":4987,"titles":9766,"content":9767,"level":449},"/api-reference/components/form-components#props-2",[314,9761],"interface FormReferenceSelectProps {\n  modelValue: string | string[] | null  // Selected ID(s)\n  collection: string                     // Collection to fetch from\n  label?: string                         // Display label\n  labelKey?: string                      // Field to use as label (default: 'title')\n  filterFields?: string[]                // Fields to search (default: ['title', 'name'])\n  hideCreate?: boolean                   // Hide \"Create new\" button (default: false)\n  multiple?: boolean                     // Multi-select mode (default: false)\n} PropTypeDefaultDescriptionmodelValuestring | string[] | nullrequiredSelected item ID(s) - array for multiple modecollectionstringrequiredCollection name to fetch options fromlabelstringcollectionDisplay label for the fieldlabelKeystring'title'Field to display as option labelfilterFieldsstring[]['title', 'name']Fields to search acrosshideCreatebooleanfalseHide inline create buttonmultiplebooleanfalseEnable multi-select mode",{"id":9769,"title":5367,"titles":9770,"content":9771,"level":449},"/api-reference/components/form-components#events-1",[314,9761],"@update:modelValue=\"handleUpdate\"  // Emits: string | string[] | null",{"id":9773,"title":9774,"titles":9775,"content":9776,"level":449},"/api-reference/components/form-components#basic-usage-single-select","Basic Usage (Single Select)",[314,9761],"Automatically generated for fields with refTarget in the schema: \u003Ctemplate>\n  \u003CUFormField label=\"Author\" name=\"authorId\">\n    \u003CCroutonFormReferenceSelect\n      v-model=\"state.authorId\"\n      collection=\"authors\"\n      label=\"Author\"\n    />\n  \u003C/UFormField>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst state = ref({\n  authorId: null,  // Will contain selected author ID\n  title: '',\n  content: ''\n})\n\u003C/script>",{"id":9778,"title":9779,"titles":9780,"content":9781,"level":449},"/api-reference/components/form-components#multi-select-mode","Multi-Select Mode",[314,9761],"Enable multi-select for many-to-many relationships: \u003Ctemplate>\n  \u003CUFormField label=\"Tags\" name=\"tags\">\n    \u003CCroutonFormReferenceSelect\n      v-model=\"state.tags\"\n      collection=\"tags\"\n      label=\"Tags\"\n      :multiple=\"true\"\n    />\n  \u003C/UFormField>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst state = ref({\n  title: '',\n  tags: []  // Will contain array of tag IDs: ['tag-1', 'tag-2', 'tag-3']\n})\n\u003C/script>",{"id":9783,"title":9784,"titles":9785,"content":9786,"level":449},"/api-reference/components/form-components#custom-label-and-filter-fields","Custom Label and Filter Fields",[314,9761],"Customize which field displays and which fields are searchable: \u003Ctemplate>\n  \u003CUFormField label=\"Category\" name=\"categoryId\">\n    \u003CCroutonFormReferenceSelect\n      v-model=\"state.categoryId\"\n      collection=\"categories\"\n      label=\"Category\"\n      label-key=\"name\"\n      :filter-fields=\"['name', 'description', 'slug']\"\n    />\n  \u003C/UFormField>\n\u003C/template>",{"id":9788,"title":9789,"titles":9790,"content":9791,"level":449},"/api-reference/components/form-components#inline-creation","Inline Creation",[314,9761],"Click the \"+\" button in the dropdown to create a new item inline. The newly created item is automatically selected. \u003C!-- \"Create new\" button appears in dropdown by default -->\n\u003CCroutonFormReferenceSelect\n  v-model=\"state.categoryId\"\n  collection=\"categories\"\n  label=\"Category\"\n/>\n\n\u003C!-- Hide create button if you don't want inline creation -->\n\u003CCroutonFormReferenceSelect\n  v-model=\"state.categoryId\"\n  collection=\"categories\"\n  label=\"Category\"\n  :hide-create=\"true\"\n/> User Flow: User clicks dropdown → sees available itemsUser types to search → results filtered in real-timeUser clicks \"+ Create new\" button → nested slideover opensUser fills form and saves → slideover closesNewly created item is auto-selected → form continues",{"id":9793,"title":2522,"titles":9794,"content":9795,"level":449},"/api-reference/components/form-components#error-handling",[314,9761],"FormReferenceSelect provides user-friendly error messages for common scenarios: \u003C!-- Component handles these errors automatically --> Status CodeError MessageCause404\"The collection collection could not be found\"Collection doesn't exist403\"You don't have permission to view collection\"Authorization failed500+\"There was a problem loading the data. Please try again.\"Server error Visual Feedback: Error alert appears above dropdownDropdown is disabled when error occursAlert includes icon and descriptive message",{"id":9797,"title":5825,"titles":9798,"content":528,"level":449},"/api-reference/components/form-components#usage-examples",[314,9761],{"id":9800,"title":9801,"titles":9802,"content":9803,"level":748},"/api-reference/components/form-components#single-select","Single Select",[314,9761,5825],"\u003Cscript setup lang=\"ts\">\nconst state = ref({ authorId: null })\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUFormField label=\"Author\" name=\"authorId\">\n    \u003CCroutonFormReferenceSelect\n      v-model=\"state.authorId\"\n      collection=\"users\"\n      label=\"Author\"\n      label-key=\"fullName\"\n      :filter-fields=\"['fullName', 'email']\"\n    />\n  \u003C/UFormField>\n\u003C/template>",{"id":9805,"title":9806,"titles":9807,"content":9808,"level":748},"/api-reference/components/form-components#multi-select","Multi-Select",[314,9761,5825],"\u003Cscript setup lang=\"ts\">\nconst state = ref({ tags: [] })\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUFormField label=\"Tags\" name=\"tags\">\n    \u003CCroutonFormReferenceSelect\n      v-model=\"state.tags\"\n      collection=\"tags\"\n      label=\"Tags\"\n      :multiple=\"true\"\n    />\n  \u003C/UFormField>\n\u003C/template>",{"id":9810,"title":9811,"titles":9812,"content":9813,"level":748},"/api-reference/components/form-components#hide-create-button","Hide Create Button",[314,9761,5825],"Prevent users from creating new items: \u003Ctemplate>\n  \u003CCroutonFormReferenceSelect\n    v-model=\"state.categoryId\"\n    collection=\"categories\"\n    label=\"Category\"\n    :hide-create=\"true\"\n  />\n\u003C/template>",{"id":9815,"title":9816,"titles":9817,"content":9818,"level":748},"/api-reference/components/form-components#custom-filter-fields","Custom Filter Fields",[314,9761,5825],"Search across multiple fields: \u003Ctemplate>\n  \u003CCroutonFormReferenceSelect\n    v-model=\"state.relatedPosts\"\n    collection=\"posts\"\n    label=\"Related Posts\"\n    :multiple=\"true\"\n    :filter-fields=\"['title', 'excerpt']\"\n  />\n\u003C/template>",{"id":9820,"title":183,"titles":9821,"content":9822,"level":449},"/api-reference/components/form-components#features",[314,9761],"✅ Searchable dropdown - Full-text search across specified fields✅ Loading states - Skeleton UI while fetching items✅ Inline creation - \"+\" button opens nested form for quick creation✅ Auto-selection - Newly created items automatically selected✅ Multi-select - Support for many-to-many relationships✅ Error handling - User-friendly messages for 404/403/500 errors✅ Lazy loading - Items fetched on demand via useCollectionQuery✅ Instance isolation - Local state prevents cross-contamination",{"id":9824,"title":36,"titles":9825,"content":528,"level":449},"/api-reference/components/form-components#troubleshooting-3",[314,9761],{"id":9827,"title":9828,"titles":9829,"content":9830,"level":748},"/api-reference/components/form-components#dropdown-shows-undefined-for-labels","Dropdown shows \"undefined\" for labels",[314,9761,36],"Check labelKey prop: Must match a field in the collectionCheck data: Ensure items have the specified label fieldFallback chain: Component tries labelKey → title → name → id",{"id":9832,"title":9833,"titles":9834,"content":9835,"level":748},"/api-reference/components/form-components#create-button-not-working","Create button not working",[314,9761,36],"Check permissions: User must have create permission for collectionCheck form component: Collection must have a generated form componentCheck generator: Run npx crouton-generate config crouton.config.js if forms are missing",{"id":9837,"title":9838,"titles":9839,"content":9840,"level":748},"/api-reference/components/form-components#newly-created-item-not-auto-selected","Newly created item not auto-selected",[314,9761,36],"This is handled automatically. If not working: Check return value: API must return created item with idCheck array length: Component watches items.length for changesCheck console: Look for errors during creation",{"id":9842,"title":9843,"titles":9844,"content":9845,"level":748},"/api-reference/components/form-components#multi-select-not-working","Multi-select not working",[314,9761,36],"Check multiple prop: Must be trueCheck modelValue type: Must be array, not stringInitialize as array: tags: [] not tags: null",{"id":9847,"title":7818,"titles":9848,"content":9849,"level":391},"/api-reference/components/form-components#croutonassetspicker",[314],"Browse and select from a centralized asset library. Part of the @fyit/crouton-assets package.",{"id":9851,"title":4987,"titles":9852,"content":9853,"level":449},"/api-reference/components/form-components#props-3",[314,7818],"interface CroutonAssetsPickerProps {\n  modelValue?: string              // Selected asset ID (v-model)\n  collection?: string              // Assets collection name (default: 'assets')\n}",{"id":9855,"title":4173,"titles":9856,"content":9857,"level":449},"/api-reference/components/form-components#basic-usage-1",[314,7818],"\u003Ctemplate>\n  \u003CUFormField label=\"Featured Image\" name=\"imageId\">\n    \u003CCroutonAssetsPicker v-model=\"state.imageId\" />\n  \u003C/UFormField>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst state = ref({\n  imageId: ''\n})\n\u003C/script>",{"id":9859,"title":9860,"titles":9861,"content":9862,"level":449},"/api-reference/components/form-components#in-schema-definition","In Schema Definition",[314,7818],"Use in your collection schema with refTarget - the component is auto-detected: {\n  \"imageId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"assets\",\n    \"meta\": {\n      \"label\": \"Featured Image\"\n      // Component automatically detected as CroutonAssetsPicker!\n    }\n  }\n}",{"id":9864,"title":183,"titles":9865,"content":9866,"level":449},"/api-reference/components/form-components#features-1",[314,7818],"Grid view - Visual thumbnail grid of all assetsSearch - Real-time search by filename or alt textUpload new - Quick upload button opens uploader modalAuto-refresh - Automatically updates after new uploadsTeam-scoped - Shows only assets for current teamLoading states - Skeleton UI while fetching assets",{"id":9868,"title":3195,"titles":9869,"content":9870,"level":449},"/api-reference/components/form-components#complete-example-1",[314,7818],"\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003C!-- Asset picker with preview -->\n    \u003CUFormField label=\"Header Image\" name=\"headerId\">\n      \u003CCroutonAssetsPicker v-model=\"state.headerId\" />\n    \u003C/UFormField>\n\n    \u003C!-- Display selected asset URL -->\n    \u003Cdiv v-if=\"state.headerId\">\n      \u003Cimg :src=\"selectedAssetUrl\" alt=\"Selected header\" class=\"w-full rounded-lg\" />\n    \u003C/div>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst state = ref({\n  headerId: ''\n})\n\n// Fetch selected asset details\nconst { data: selectedAsset } = await useFetch(\n  `/api/teams/${useRoute().params.team}/assets/${state.headerId}`,\n  { watch: [() => state.headerId] }\n)\n\nconst selectedAssetUrl = computed(() =>\n  selectedAsset.value?.pathname ? `/images/${selectedAsset.value.pathname}` : ''\n)\n\u003C/script>",{"id":9872,"title":9873,"titles":9874,"content":9875,"level":391},"/api-reference/components/form-components#croutonassetuploader","CroutonAssetUploader",[314],"Upload files with metadata to the centralized asset library. Part of the @fyit/crouton-assets package.",{"id":9877,"title":4987,"titles":9878,"content":9879,"level":449},"/api-reference/components/form-components#props-4",[314,9873],"interface CroutonAssetUploaderProps {\n  collection?: string              // Assets collection name (default: 'assets')\n}",{"id":9881,"title":5367,"titles":9882,"content":9883,"level":449},"/api-reference/components/form-components#events-2",[314,9873],"@uploaded=\"handleUploaded\"        // Emits: string (new asset ID)",{"id":9885,"title":4173,"titles":9886,"content":9887,"level":449},"/api-reference/components/form-components#basic-usage-2",[314,9873],"\u003Ctemplate>\n  \u003CUModal v-model=\"showUploader\">\n    \u003Ctemplate #content=\"{ close }\">\n      \u003Cdiv class=\"p-6\">\n        \u003Ch3 class=\"text-lg font-semibold mb-4\">Upload New Asset\u003C/h3>\n        \u003CCroutonAssetUploader @uploaded=\"handleUploaded(close)\" />\n      \u003C/div>\n    \u003C/template>\n  \u003C/UModal>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst showUploader = ref(false)\n\nconst handleUploaded = async (close: () => void, assetId: string) => {\n  console.log('New asset created:', assetId)\n  close()\n}\n\u003C/script>",{"id":9889,"title":183,"titles":9890,"content":9891,"level":449},"/api-reference/components/form-components#features-2",[314,9873],"File preview - Shows preview after file selectionAlt text input - Accessibility and SEO metadataFile metadata - Displays filename, size, and MIME typeTwo-step upload - Uploads to blob storage then creates database recordLoading states - Shows progress during uploadAuto-emit - Emits new asset ID on successful upload",{"id":9893,"title":9894,"titles":9895,"content":9896,"level":449},"/api-reference/components/form-components#metadata-collected","Metadata Collected",[314,9873],"filename - Original filenamepathname - Blob storage pathcontentType - MIME typesize - File size in bytesalt - Alt text for accessibilityuploadedAt - Upload timestampteamId - Team/organization ownershipuserId - User who uploaded",{"id":9898,"title":9899,"titles":9900,"content":9901,"level":391},"/api-reference/components/form-components#calendar","Calendar",[314],"Interactive date picker component for selecting single dates or date ranges. Wraps Nuxt UI's \u003CUCalendar> with timezone-aware date handling. Part of the base @fyit/crouton package. Timezone Handling: Calendar uses @internationalized/date library to handle timezone conversions transparently. You can pass JavaScript Date objects or timestamps (numbers), and the component handles the rest.",{"id":9903,"title":4987,"titles":9904,"content":9905,"level":449},"/api-reference/components/form-components#props-5",[314,9899],"interface CalendarProps {\n  // Single Date Mode\n  date?: Date | number | null       // Date or timestamp for single date\n\n  // Range Mode\n  range?: boolean                   // Enable range selection\n  startDate?: Date | number | null  // Start date or timestamp for range\n  endDate?: Date | number | null    // End date or timestamp for range\n\n  // Constraints\n  minDate?: Date | number | null    // Min selectable date or timestamp\n  maxDate?: Date | number | null    // Max selectable date or timestamp\n  isDateDisabled?: (date: Date) => boolean  // Function to disable specific dates\n\n  // UI Customization\n  color?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'\n  variant?: 'solid' | 'outline' | 'soft' | 'subtle'\n  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n  ui?: Record\u003Cstring, unknown>      // Passthrough UI customization to UCalendar\n\n  // Controls\n  disabled?: boolean                // Disable selection\n  monthControls?: boolean           // Show month navigation (default: true)\n  yearControls?: boolean            // Show year navigation (default: true)\n  numberOfMonths?: number           // Number of months to display\n}",{"id":9907,"title":9908,"titles":9909,"content":9910,"level":748},"/api-reference/components/form-components#prop-details","Prop Details",[314,9899,4987],"PropTypeDefaultDescriptiondateDate | number | nullnullCurrent date value (single mode)rangebooleanfalseEnable date range selectionstartDateDate | number | nullnullRange start dateendDateDate | number | nullnullRange end dateminDateDate | number | nullnullMinimum selectable datemaxDateDate | number | nullnullMaximum selectable dateisDateDisabled(date: Date) => booleanundefinedFunction to disable specific datescolorstring'primary'Color themevariantstring'solid'Visual style variantsizestring'md'Component sizeuiRecord\u003Cstring, unknown>undefinedPassthrough UI customization to UCalendardisabledbooleanfalseDisable date selectionmonthControlsbooleantrueShow month navigation arrowsyearControlsbooleantrueShow year dropdownnumberOfMonthsnumberAutoMonths to display (auto: 1 for single, 2 for range)",{"id":9912,"title":5372,"titles":9913,"content":9914,"level":449},"/api-reference/components/form-components#slots-1",[314,9899],"SlotPropsDescriptionday{ day: DateValue, date: Date }Custom rendering for each day cell The #day slot receives both the raw DateValue from @internationalized/date and a converted JavaScript Date object for convenience.",{"id":9916,"title":5367,"titles":9917,"content":9918,"level":449},"/api-reference/components/form-components#events-3",[314,9899],"{\n  'update:date': [value: Date | null]        // Single date changed\n  'update:startDate': [value: Date | null]   // Range start changed\n  'update:endDate': [value: Date | null]     // Range end changed\n}",{"id":9920,"title":5825,"titles":9921,"content":528,"level":449},"/api-reference/components/form-components#usage-examples-1",[314,9899],{"id":9923,"title":9924,"titles":9925,"content":9926,"level":748},"/api-reference/components/form-components#single-date-picker","Single Date Picker",[314,9899,5825],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch3 class=\"font-semibold mb-2\">Select Event Date\u003C/h3>\n    \u003CCalendar\n      v-model:date=\"eventDate\"\n      :min-date=\"new Date()\"\n      color=\"primary\"\n    />\n    \u003Cp v-if=\"eventDate\" class=\"mt-2 text-sm\">\n      Selected: {{ eventDate.toLocaleDateString() }}\n    \u003C/p>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst eventDate = ref\u003CDate | null>(null)\n\u003C/script>",{"id":9928,"title":9929,"titles":9930,"content":9931,"level":748},"/api-reference/components/form-components#date-range-picker","Date Range Picker",[314,9899,5825],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch3 class=\"font-semibold mb-2\">Select Booking Period\u003C/h3>\n    \u003CCalendar\n      range\n      v-model:start-date=\"startDate\"\n      v-model:end-date=\"endDate\"\n      :min-date=\"new Date()\"\n      :number-of-months=\"2\"\n      color=\"success\"\n    />\n    \u003Cdiv v-if=\"startDate && endDate\" class=\"mt-2 text-sm\">\n      \u003Cp>Check-in: {{ startDate.toLocaleDateString() }}\u003C/p>\n      \u003Cp>Check-out: {{ endDate.toLocaleDateString() }}\u003C/p>\n      \u003Cp>Nights: {{ calculateNights(startDate, endDate) }}\u003C/p>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst startDate = ref\u003CDate | null>(null)\nconst endDate = ref\u003CDate | null>(null)\n\nconst calculateNights = (start: Date, end: Date) => {\n  return Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))\n}\n\u003C/script>",{"id":9933,"title":9934,"titles":9935,"content":9936,"level":748},"/api-reference/components/form-components#with-date-constraints","With Date Constraints",[314,9899,5825],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch3 class=\"font-semibold mb-2\">Select Appointment Date\u003C/h3>\n    \u003CCalendar\n      v-model:date=\"appointmentDate\"\n      :min-date=\"minDate\"\n      :max-date=\"maxDate\"\n      :disabled=\"isLoading\"\n      variant=\"outline\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst appointmentDate = ref\u003CDate | null>(null)\nconst isLoading = ref(false)\n\n// Only allow dates within next 30 days\nconst minDate = computed(() => new Date())\nconst maxDate = computed(() => {\n  const date = new Date()\n  date.setDate(date.getDate() + 30)\n  return date\n})\n\u003C/script>",{"id":9938,"title":9939,"titles":9940,"content":9941,"level":748},"/api-reference/components/form-components#using-timestamps","Using Timestamps",[314,9899,5825],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003CCalendar\n      v-model:date=\"publishTimestamp\"\n      :min-date=\"Date.now()\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\n// Can work with timestamps instead of Date objects\nconst publishTimestamp = ref\u003Cnumber | null>(null)\n\nwatchEffect(() => {\n  if (publishTimestamp.value) {\n    console.log('Publish at:', new Date(publishTimestamp.value))\n  }\n})\n\u003C/script>",{"id":9943,"title":9944,"titles":9945,"content":9946,"level":748},"/api-reference/components/form-components#disabling-specific-dates","Disabling Specific Dates",[314,9899,5825],"Use the isDateDisabled prop to disable specific dates based on custom logic. The function receives a JavaScript Date object. \u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch3 class=\"font-semibold mb-2\">Select Appointment (No Weekends)\u003C/h3>\n    \u003CCalendar\n      v-model:date=\"appointmentDate\"\n      :min-date=\"new Date()\"\n      :is-date-disabled=\"isWeekend\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst appointmentDate = ref\u003CDate | null>(null)\n\n// Disable weekends\nfunction isWeekend(date: Date): boolean {\n  const day = date.getDay()\n  return day === 0 || day === 6 // Sunday = 0, Saturday = 6\n}\n\u003C/script> You can also combine multiple conditions: \u003Cscript setup lang=\"ts\">\n// Disable weekends and specific dates (e.g., holidays)\nconst holidays = [\n  new Date('2024-12-25'),\n  new Date('2024-12-26'),\n  new Date('2025-01-01')\n]\n\nfunction isDateDisabled(date: Date): boolean {\n  // Check weekends\n  const day = date.getDay()\n  if (day === 0 || day === 6) return true\n\n  // Check holidays\n  return holidays.some(h =>\n    h.getFullYear() === date.getFullYear() &&\n    h.getMonth() === date.getMonth() &&\n    h.getDate() === date.getDate()\n  )\n}\n\u003C/script>",{"id":9948,"title":9949,"titles":9950,"content":9951,"level":748},"/api-reference/components/form-components#custom-day-rendering","Custom Day Rendering",[314,9899,5825],"Use the #day slot to customize how each day cell is rendered. The slot provides both the raw DateValue and a converted JavaScript Date. \u003Ctemplate>\n  \u003CCalendar v-model:date=\"selectedDate\" :min-date=\"new Date()\">\n    \u003Ctemplate #day=\"{ day, date }\">\n      \u003Cdiv class=\"relative w-full h-full flex items-center justify-center\">\n        \u003C!-- Day number -->\n        \u003Cspan>{{ day.day }}\u003C/span>\n\n        \u003C!-- Show indicator for dates with bookings -->\n        \u003Cspan\n          v-if=\"hasBooking(date)\"\n          class=\"absolute bottom-0.5 w-1.5 h-1.5 bg-primary rounded-full\"\n        />\n      \u003C/div>\n    \u003C/template>\n  \u003C/Calendar>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedDate = ref\u003CDate | null>(null)\n\n// Example: dates with existing bookings\nconst bookedDates = [\n  new Date('2024-12-15'),\n  new Date('2024-12-20'),\n  new Date('2024-12-25')\n]\n\nfunction hasBooking(date: Date): boolean {\n  return bookedDates.some(b =>\n    b.getFullYear() === date.getFullYear() &&\n    b.getMonth() === date.getMonth() &&\n    b.getDate() === date.getDate()\n  )\n}\n\u003C/script>",{"id":9953,"title":9954,"titles":9955,"content":9956,"level":748},"/api-reference/components/form-components#ui-customization","UI Customization",[314,9899,5825],"Pass custom styling to the underlying UCalendar using the ui prop: \u003Ctemplate>\n  \u003CCalendar\n    v-model:date=\"selectedDate\"\n    :ui=\"{\n      body: 'p-4',\n      day: 'rounded-full',\n      daySelected: 'bg-primary text-white'\n    }\"\n  />\n\u003C/template>",{"id":9958,"title":9959,"titles":9960,"content":9961,"level":748},"/api-reference/components/form-components#in-forms","In Forms",[314,9899,5825],"\u003Ctemplate>\n  \u003CUForm :state=\"state\" :schema=\"schema\" @submit=\"onSubmit\">\n    \u003CUFormField label=\"Event Date\" name=\"eventDate\" required>\n      \u003CCalendar\n        v-model:date=\"state.eventDate\"\n        :min-date=\"new Date()\"\n      />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Booking Period\" name=\"dateRange\" required>\n      \u003CCalendar\n        range\n        v-model:start-date=\"state.startDate\"\n        v-model:end-date=\"state.endDate\"\n        :min-date=\"new Date()\"\n      />\n    \u003C/UFormField>\n\n    \u003CUButton type=\"submit\">Submit\u003C/UButton>\n  \u003C/UForm>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nimport { z } from 'zod'\n\nconst schema = z.object({\n  eventDate: z.date().min(new Date(), 'Date must be in the future'),\n  startDate: z.date(),\n  endDate: z.date()\n}).refine(\n  data => data.endDate > data.startDate,\n  { message: 'End date must be after start date', path: ['endDate'] }\n)\n\nconst state = ref({\n  eventDate: null as Date | null,\n  startDate: null as Date | null,\n  endDate: null as Date | null\n})\n\nconst onSubmit = (data: any) => {\n  console.log('Form submitted:', data)\n}\n\u003C/script>",{"id":9963,"title":183,"titles":9964,"content":9965,"level":449},"/api-reference/components/form-components#features-3",[314,9899],"Dual Mode Support: Single date or date range selectionFlexible Input: Accepts both Date objects and timestamps (numbers)Timezone Aware: Automatic timezone conversion using @internationalized/dateDate Constraints: Min/max date validation with minDate/maxDateCustom Date Disabling: Use isDateDisabled function to disable specific dates (weekends, holidays, etc.)Custom Day Rendering: Use #day slot for custom day cell content (indicators, badges, etc.)UI Passthrough: Full customization via ui prop passed to underlying UCalendarAuto-detection: Displays 2 months for range mode, 1 for single mode (configurable)Full Theming: Complete Nuxt UI 4 theme support (color, variant, size)Navigation Controls: Optional month and year navigationDisabled State: Prevent date selection when needed",{"id":9967,"title":9968,"titles":9969,"content":9970,"level":449},"/api-reference/components/form-components#timezone-handling","Timezone Handling",[314,9899],"The component uses @internationalized/date to handle timezones correctly: // Internal conversion helpers\nfunction toCalendarDateValue(value: Date | number | null): DateValue {\n  const date = value instanceof Date ? value : new Date(value)\n  const zonedDateTime = fromDate(date, getLocalTimeZone())\n  return toCalendarDate(zonedDateTime)\n}\n\nfunction calendarDateToDate(date: DateValue | null): Date | null {\n  if (!date) return null\n  return date.toDate(getLocalTimeZone())\n} What this means: You always work with JavaScript Date objects or timestampsThe component handles timezone conversions transparentlyDates are stored in the user's local timezoneNo need to worry about UTC vs local time",{"id":9972,"title":9973,"titles":9974,"content":9975,"level":449},"/api-reference/components/form-components#auto-month-detection","Auto Month Detection",[314,9899],"\u003C!-- Single mode: Shows 1 month -->\n\u003CCalendar v-model:date=\"date\" />\n\n\u003C!-- Range mode: Shows 2 months -->\n\u003CCalendar range v-model:start-date=\"start\" v-model:end-date=\"end\" />\n\n\u003C!-- Override: Show 3 months -->\n\u003CCalendar range :number-of-months=\"3\" />",{"id":9977,"title":36,"titles":9978,"content":528,"level":449},"/api-reference/components/form-components#troubleshooting-4",[314,9899],{"id":9980,"title":9981,"titles":9982,"content":9983,"level":748},"/api-reference/components/form-components#issue-date-not-updating-in-form","Issue: Date not updating in form",[314,9899,36],"Problem: Calendar shows selected date but form state doesn't update \u003C!-- ❌ WRONG: Missing v-model:date binding -->\n\u003CCalendar :date=\"state.date\" />\n\n\u003C!-- ✅ CORRECT: Use v-model:date for two-way binding -->\n\u003CCalendar v-model:date=\"state.date\" />",{"id":9985,"title":9986,"titles":9987,"content":9988,"level":748},"/api-reference/components/form-components#issue-range-selection-not-working","Issue: Range selection not working",[314,9899,36],"Problem: Only one date is selected in range mode \u003C!-- ❌ WRONG: Missing range prop -->\n\u003CCalendar\n  v-model:start-date=\"start\"\n  v-model:end-date=\"end\"\n/>\n\n\u003C!-- ✅ CORRECT: Add range prop -->\n\u003CCalendar\n  range\n  v-model:start-date=\"start\"\n  v-model:end-date=\"end\"\n/>",{"id":9990,"title":9991,"titles":9992,"content":9993,"level":748},"/api-reference/components/form-components#issue-constraints-not-working","Issue: Constraints not working",[314,9899,36],"Problem: Users can select dates outside allowed range \u003C!-- ❌ WRONG: Using strings for dates -->\n\u003CCalendar\n  v-model:date=\"date\"\n  min-date=\"2024-01-01\"\n  max-date=\"2024-12-31\"\n/>\n\n\u003C!-- ✅ CORRECT: Use Date objects or timestamps -->\n\u003CCalendar\n  v-model:date=\"date\"\n  :min-date=\"new Date('2024-01-01')\"\n  :max-date=\"new Date('2024-12-31')\"\n/>",{"id":9995,"title":9996,"titles":9997,"content":9998,"level":748},"/api-reference/components/form-components#issue-typescript-errors-with-timestamps","Issue: TypeScript errors with timestamps",[314,9899,36],"Problem: Type mismatch when using timestamps // ❌ WRONG: Type doesn't match\nconst date = ref\u003CDate>(Date.now())\n\n// ✅ CORRECT: Use union type\nconst date = ref\u003CDate | number | null>(Date.now())\n\n// ✅ ALSO CORRECT: Convert to Date\nconst date = ref\u003CDate | null>(new Date())",{"id":10000,"title":10001,"titles":10002,"content":10003,"level":391},"/api-reference/components/form-components#calendaryear","CalendarYear",[314],"Year calendar view component that displays all 12 months in a responsive grid. Ideal for year-at-a-glance scheduling, availability views, or selecting dates across the entire year. Part of the base @fyit/crouton package. Year View: Unlike the standard Calendar component which shows 1-2 months, CalendarYear displays all 12 months simultaneously in a responsive grid layout.",{"id":10005,"title":4987,"titles":10006,"content":10007,"level":449},"/api-reference/components/form-components#props-6",[314,10001],"interface CalendarYearProps {\n  modelValue?: Date | number | null  // Selected date (v-model)\n  year?: number                      // Year to display (default: current year)\n  color?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'\n  size?: 'xs' | 'sm' | 'md'         // Calendar size (default: 'xs')\n} PropTypeDefaultDescriptionmodelValueDate | number | nullnullSelected date (supports v-model)yearnumberCurrent yearYear to displaycolorstring'primary'Color theme for selected datessize'xs' | 'sm' | 'md''xs'Size of each month calendar",{"id":10009,"title":5367,"titles":10010,"content":10011,"level":449},"/api-reference/components/form-components#events-4",[314,10001],"EventPayloadDescriptionupdate:modelValueDate | nullEmitted when a date is selected",{"id":10013,"title":5825,"titles":10014,"content":528,"level":449},"/api-reference/components/form-components#usage-examples-2",[314,10001],{"id":10016,"title":10017,"titles":10018,"content":10019,"level":748},"/api-reference/components/form-components#basic-year-view","Basic Year View",[314,10001,5825],"\u003Ctemplate>\n  \u003CCroutonCalendarYear />\n\u003C/template>",{"id":10021,"title":10022,"titles":10023,"content":10024,"level":748},"/api-reference/components/form-components#with-date-selection","With Date Selection",[314,10001,5825],"\u003Cscript setup lang=\"ts\">\nconst selectedDate = ref\u003CDate | null>(null)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCalendarYear v-model=\"selectedDate\" />\n\n  \u003Cp v-if=\"selectedDate\">\n    Selected: {{ selectedDate.toLocaleDateString() }}\n  \u003C/p>\n\u003C/template>",{"id":10026,"title":10027,"titles":10028,"content":10029,"level":748},"/api-reference/components/form-components#specific-year","Specific Year",[314,10001,5825],"\u003Cscript setup lang=\"ts\">\nconst selectedDate = ref\u003CDate | null>(null)\nconst displayYear = ref(2024)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"flex gap-2 mb-4\">\n    \u003CUButton @click=\"displayYear--\">Previous Year\u003C/UButton>\n    \u003Cspan class=\"font-bold\">{{ displayYear }}\u003C/span>\n    \u003CUButton @click=\"displayYear++\">Next Year\u003C/UButton>\n  \u003C/div>\n\n  \u003CCroutonCalendarYear\n    v-model=\"selectedDate\"\n    :year=\"displayYear\"\n  />\n\u003C/template>",{"id":10031,"title":10032,"titles":10033,"content":10034,"level":748},"/api-reference/components/form-components#with-styling-options","With Styling Options",[314,10001,5825],"\u003Ctemplate>\n  \u003CCroutonCalendarYear\n    v-model=\"selectedDate\"\n    :year=\"2025\"\n    color=\"neutral\"\n    size=\"sm\"\n  />\n\u003C/template>",{"id":10036,"title":10037,"titles":10038,"content":10039,"level":449},"/api-reference/components/form-components#layout","Layout",[314,10001],"The component uses a responsive CSS grid: Desktop (lg+): 4 columns (3 rows)Tablet (md): 3 columns (4 rows)Mobile: 2 columns (6 rows) Each month is displayed in its own card with the month name header.",{"id":10041,"title":10042,"titles":10043,"content":10044,"level":449},"/api-reference/components/form-components#notes","Notes",[314,10001],"Days from adjacent months are automatically hidden to prevent confusionAll 12 months share the same selected date stateThe component handles timezone conversions internally using @internationalized/date",{"id":10046,"title":10047,"titles":10048,"content":10049,"level":391},"/api-reference/components/form-components#croutondate","CroutonDate",[314],"Read-only date display component that shows both absolute and relative timestamps. Commonly used in tables, cards, and detail views. Part of the base @fyit/crouton package. Display Only: This is NOT an input component. For date selection, use \u003CCalendar>. This component is for displaying dates in a user-friendly format.",{"id":10051,"title":4987,"titles":10052,"content":10053,"level":449},"/api-reference/components/form-components#props-7",[314,10047],"interface CroutonDateProps {\n  date?: string | Date   // Date to display\n} PropTypeDefaultDescriptiondatestring | DateundefinedDate to display (ISO string or Date object)",{"id":10055,"title":10056,"titles":10057,"content":10058,"level":449},"/api-reference/components/form-components#display-format","Display Format",[314,10047],"The component shows two date representations stacked vertically: Absolute Date (top): Full formatted dateExample: \"November 17, 2025, 3:45 PM\"Uses Nuxt's \u003CNuxtTime> with style=\"long\"Relative Date (bottom): Time ago formatExample: \"2 hours ago\"Smaller, italicized, semi-transparent (opacity-50)Updates automatically as time passes",{"id":10060,"title":5825,"titles":10061,"content":528,"level":449},"/api-reference/components/form-components#usage-examples-3",[314,10047],{"id":10063,"title":10064,"titles":10065,"content":10066,"level":748},"/api-reference/components/form-components#in-table-cells","In Table Cells",[314,10047,5825],"Most common use case - displaying timestamps in data tables: \u003Ctemplate>\n  \u003CCroutonTable\n    :rows=\"bookings\"\n    :columns=\"columns\"\n    collection=\"bookings\"\n  >\n    \u003C!-- Custom date display for createdAt -->\n    \u003Ctemplate #createdAt-cell=\"{ row }\">\n      \u003CCroutonDate :date=\"row.original.createdAt\" />\n    \u003C/template>\n\n    \u003C!-- Custom date display for updatedAt -->\n    \u003Ctemplate #updatedAt-cell=\"{ row }\">\n      \u003CCroutonDate :date=\"row.original.updatedAt\" />\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst columns = [\n  { accessorKey: 'name', header: 'Name' },\n  { accessorKey: 'createdAt', header: 'Created' },\n  { accessorKey: 'updatedAt', header: 'Last Updated' }\n]\n\nconst { data: bookings } = await useFetch('/api/bookings')\n\u003C/script>",{"id":10068,"title":10069,"titles":10070,"content":10071,"level":748},"/api-reference/components/form-components#in-cards","In Cards",[314,10047,5825],"\u003Ctemplate>\n  \u003CUCard>\n    \u003Ctemplate #header>\n      \u003Ch3 class=\"font-semibold\">{{ booking.title }}\u003C/h3>\n    \u003C/template>\n\n    \u003Cdiv class=\"space-y-2\">\n      \u003Cdiv>\n        \u003Cspan class=\"text-sm text-gray-500\">Booking Date:\u003C/span>\n        \u003CCroutonDate :date=\"booking.date\" />\n      \u003C/div>\n\n      \u003Cdiv>\n        \u003Cspan class=\"text-sm text-gray-500\">Created:\u003C/span>\n        \u003CCroutonDate :date=\"booking.createdAt\" />\n      \u003C/div>\n    \u003C/div>\n  \u003C/UCard>\n\u003C/template>",{"id":10073,"title":10074,"titles":10075,"content":10076,"level":748},"/api-reference/components/form-components#in-detail-layouts","In Detail Layouts",[314,10047,5825],"\u003Ctemplate>\n  \u003CCroutonDetailLayout collection=\"projects\" :item-id=\"projectId\">\n    \u003Ctemplate #default=\"{ item }\">\n      \u003Cdiv class=\"space-y-4\">\n        \u003Cdiv>\n          \u003Ch2 class=\"text-2xl font-bold\">{{ item.name }}\u003C/h2>\n          \u003Cdiv class=\"flex gap-4 mt-2 text-sm text-gray-600\">\n            \u003Cdiv>\n              \u003Cspan class=\"font-medium\">Created:\u003C/span>\n              \u003CCroutonDate :date=\"item.createdAt\" />\n            \u003C/div>\n            \u003Cdiv>\n              \u003Cspan class=\"font-medium\">Last Updated:\u003C/span>\n              \u003CCroutonDate :date=\"item.updatedAt\" />\n            \u003C/div>\n          \u003C/div>\n        \u003C/div>\n\n        \u003C!-- Project details -->\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonDetailLayout>\n\u003C/template>",{"id":10078,"title":10079,"titles":10080,"content":10081,"level":748},"/api-reference/components/form-components#with-iso-strings","With ISO Strings",[314,10047,5825],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003C!-- Works with ISO 8601 strings from APIs -->\n    \u003CCroutonDate :date=\"isoTimestamp\" />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\n// Common API response format\nconst isoTimestamp = ref('2025-01-17T15:30:00.000Z')\n\u003C/script>",{"id":10083,"title":10084,"titles":10085,"content":10086,"level":748},"/api-reference/components/form-components#with-date-objects","With Date Objects",[314,10047,5825],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003C!-- Works with JavaScript Date objects -->\n    \u003CCroutonDate :date=\"dateObject\" />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst dateObject = ref(new Date())\n\u003C/script>",{"id":10088,"title":10089,"titles":10090,"content":10091,"level":449},"/api-reference/components/form-components#visual-example","Visual Example",[314,10047],"Here's what the component renders: ┌─────────────────────────────────┐\n│ November 17, 2025, 3:45 PM     │  ← Absolute (normal size, full opacity)\n│ 2 hours ago                    │  ← Relative (xs, italic, 50% opacity)\n└─────────────────────────────────┘",{"id":10093,"title":183,"titles":10094,"content":10095,"level":449},"/api-reference/components/form-components#features-4",[314,10047],"Dual Display: Shows both absolute and relative timeAuto-updates: Relative time updates automatically (e.g., \"2 hours ago\" → \"3 hours ago\")Flexible Input: Accepts ISO strings or Date objectsInternationalized: Uses \u003CNuxtTime> for locale-aware formattingResponsive: Stacks vertically for clean layoutRead-only: No user interaction, pure display component",{"id":10097,"title":10098,"titles":10099,"content":10100,"level":449},"/api-reference/components/form-components#integration-with-croutontable","Integration with CroutonTable",[314,10047],"The component is commonly used with CroutonTable for timestamp columns: \u003Ctemplate>\n  \u003CCroutonTable\n    :rows=\"items\"\n    :columns=\"columns\"\n    collection=\"items\"\n  >\n    \u003C!-- Default columns (createdAt, updatedAt) automatically use CroutonDate -->\n    \u003C!-- Override with custom slot if needed -->\n    \u003Ctemplate #createdAt-cell=\"{ row }\">\n      \u003Cdiv class=\"flex items-center gap-2\">\n        \u003CUIcon name=\"i-lucide-calendar\" />\n        \u003CCroutonDate :date=\"row.original.createdAt\" />\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template>",{"id":10102,"title":36,"titles":10103,"content":528,"level":449},"/api-reference/components/form-components#troubleshooting-5",[314,10047],{"id":10105,"title":10106,"titles":10107,"content":10108,"level":748},"/api-reference/components/form-components#issue-date-not-displaying","Issue: Date not displaying",[314,10047,36],"Problem: Component shows empty space \u003C!-- ❌ WRONG: Date is undefined or null -->\n\u003CCroutonDate :date=\"undefined\" />\n\n\u003C!-- ✅ CORRECT: Check for date before rendering -->\n\u003CCroutonDate v-if=\"item.createdAt\" :date=\"item.createdAt\" />\n\n\u003C!-- ✅ ALSO CORRECT: Use optional chaining -->\n\u003CCroutonDate :date=\"item?.createdAt\" />",{"id":10110,"title":10111,"titles":10112,"content":10113,"level":748},"/api-reference/components/form-components#issue-wrong-time-displayed","Issue: Wrong time displayed",[314,10047,36],"Problem: Time shows incorrectly (timezone issues) // ❌ WRONG: Storing dates as local strings\nconst date = '11/17/2025'  // Ambiguous format\n\n// ✅ CORRECT: Use ISO 8601 format\nconst date = '2025-11-17T15:30:00.000Z'  // UTC timestamp\n\n// ✅ ALSO CORRECT: Use Date objects\nconst date = new Date()",{"id":10115,"title":10116,"titles":10117,"content":10118,"level":748},"/api-reference/components/form-components#issue-need-different-format","Issue: Need different format",[314,10047,36],"Problem: Default \"long\" format is too verbose \u003C!-- ❌ LIMITATION: CroutonDate only supports \"long\" style -->\n\u003CCroutonDate :date=\"date\" />\n\n\u003C!-- ✅ WORKAROUND: Use NuxtTime directly for custom formatting -->\n\u003CNuxtTime\n  :datetime=\"date\"\n  numeric=\"auto\"\n  style=\"medium\"\n/>",{"id":10120,"title":10121,"titles":10122,"content":10123,"level":748},"/api-reference/components/form-components#issue-relative-time-not-updating","Issue: Relative time not updating",[314,10047,36],"Problem: \"2 hours ago\" never changes Solution: This is handled automatically by \u003CNuxtTime>. If not updating, check: Date value is reactive (wrapped in ref() or computed())Component is actually mounted in DOMBrowser tab is active (may throttle when inactive)",{"id":10125,"title":10126,"titles":10127,"content":10128,"level":449},"/api-reference/components/form-components#comparison-calendar-vs-croutondate","Comparison: Calendar vs CroutonDate",[314,10047],"FeatureCalendarCroutonDatePurposeDate selection inputDate display formatterUser Interaction✅ Interactive picker❌ Read-onlyv-model✅ Yes❌ NoUse in Forms✅ Yes❌ No (display only)Use in Tables❌ No✅ YesModesSingle / RangeDisplay onlyEvents3 update eventsNoneProps13 configuration props1 prop (date)",{"id":10130,"title":10131,"titles":10132,"content":10133,"level":391},"/api-reference/components/form-components#croutonimageupload","CroutonImageUpload",[314],"Simple file picker with preview for direct image uploads. Part of the base @fyit/crouton package.",{"id":10135,"title":4987,"titles":10136,"content":10137,"level":449},"/api-reference/components/form-components#props-8",[314,10131],"interface CroutonImageUploadProps {\n  modelValue?: string              // Preview URL (v-model)\n  accept?: string                  // Accepted file types (default: 'image/*')\n  maxSize?: number                 // Max file size in bytes\n}",{"id":10139,"title":5367,"titles":10140,"content":10141,"level":449},"/api-reference/components/form-components#events-5",[314,10131],"@file-selected=\"handleFile\"       // Emits: File | null",{"id":10143,"title":4173,"titles":10144,"content":10145,"level":449},"/api-reference/components/form-components#basic-usage-3",[314,10131],"For simple uploads without the asset library: \u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonImageUpload\n      v-model=\"imageUrl\"\n      @file-selected=\"handleUpload\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst imageUrl = ref('')\n\nconst handleUpload = async (file: File | null) => {\n  if (!file) return\n\n  const formData = new FormData()\n  formData.append('image', file)\n\n  const pathname = await $fetch('/api/upload-image', {\n    method: 'POST',\n    body: formData\n  })\n\n  imageUrl.value = `/images/${pathname}`\n}\n\u003C/script>",{"id":10147,"title":183,"titles":10148,"content":10149,"level":449},"/api-reference/components/form-components#features-5",[314,10131],"File picker - Click to select or drag-and-dropPreview - Shows image preview after selectionValidation - File type and size validationClear button - Remove selected file",{"id":10151,"title":3170,"titles":10152,"content":10153,"level":449},"/api-reference/components/form-components#use-cases",[314,10131],"Simple approach - Store URL directly: // Schema\n{\n  \"imageUrl\": { \"type\": \"string\" }\n} Full asset management - Use with CroutonAssetPicker: // Schema\n{\n  \"imageId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"assets\",\n    \"meta\": { \"component\": \"CroutonAssetPicker\" }\n  }\n}",{"id":10155,"title":10156,"titles":10157,"content":10158,"level":391},"/api-reference/components/form-components#croutonavatarupload","CroutonAvatarUpload",[314],"Specialized variant of CroutonImageUpload for avatar/profile images. Part of the base @fyit/crouton package.",{"id":10160,"title":4987,"titles":10161,"content":10162,"level":449},"/api-reference/components/form-components#props-9",[314,10156],"Same as CroutonImageUpload but with avatar-optimized defaults: interface CroutonAvatarUploadProps {\n  modelValue?: string              // Preview URL (v-model)\n  accept?: string                  // Default: 'image/png,image/jpeg,image/webp'\n  maxSize?: number                 // Default: 2MB\n}",{"id":10164,"title":5367,"titles":10165,"content":10141,"level":449},"/api-reference/components/form-components#events-6",[314,10156],{"id":10167,"title":4173,"titles":10168,"content":10169,"level":449},"/api-reference/components/form-components#basic-usage-4",[314,10156],"\u003Ctemplate>\n  \u003Cdiv class=\"flex items-center gap-4\">\n    \u003CCroutonAvatarUpload\n      v-model=\"avatarUrl\"\n      @file-selected=\"handleAvatarUpload\"\n    />\n    \u003Cdiv>\n      \u003Ch3>{{ user.name }}\u003C/h3>\n      \u003Cp class=\"text-sm text-gray-500\">Click avatar to change\u003C/p>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst avatarUrl = ref('/default-avatar.png')\n\nconst handleAvatarUpload = async (file: File | null) => {\n  if (!file) return\n\n  const formData = new FormData()\n  formData.append('image', file)\n\n  const pathname = await $fetch('/api/upload-image', {\n    method: 'POST',\n    body: formData\n  })\n\n  avatarUrl.value = `/images/${pathname}`\n\n  // Update user profile\n  await $fetch(`/api/users/${user.id}`, {\n    method: 'PATCH',\n    body: { avatar: pathname }\n  })\n}\n\u003C/script>",{"id":10171,"title":183,"titles":10172,"content":10173,"level":449},"/api-reference/components/form-components#features-6",[314,10156],"Circular preview - Avatar-style circular crop previewSize optimization - Automatically enforces reasonable size limitsFormat validation - Accepts common web image formatsResponsive - Works well in profile forms and settings",{"id":10175,"title":10176,"titles":10177,"content":10178,"level":391},"/api-reference/components/form-components#croutonrepeater","CroutonRepeater",[314],"Manage arrays of structured data with add/remove/reorder functionality. Perfect for time slots, contact information, price tiers, and other repeating data patterns.",{"id":10180,"title":4987,"titles":10181,"content":10182,"level":449},"/api-reference/components/form-components#props-10",[314,10176],"interface CroutonRepeaterProps {\n  modelValue: any[]                // Array of items\n  componentName: string            // Name of component to render per item\n  addLabel?: string               // Button text (default: \"Add Item\")\n  sortable?: boolean              // Enable drag-to-reorder (default: true)\n}",{"id":10184,"title":5367,"titles":10185,"content":10186,"level":449},"/api-reference/components/form-components#events-7",[314,10176],"@update:modelValue=\"handleUpdate\"  // Emits: any[]",{"id":10188,"title":4173,"titles":10189,"content":10190,"level":449},"/api-reference/components/form-components#basic-usage-5",[314,10176],"Automatically generated for fields with type: \"repeater\" in the schema: \u003Ctemplate>\n  \u003CUFormField label=\"Available Time Slots\" name=\"slots\">\n    \u003CCroutonRepeater\n      v-model=\"state.slots\"\n      component-name=\"Slot\"\n      add-label=\"Add Time Slot\"\n      :sortable=\"true\"\n    />\n  \u003C/UFormField>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst state = ref({\n  slots: []\n})\n\u003C/script>",{"id":10192,"title":10193,"titles":10194,"content":10195,"level":449},"/api-reference/components/form-components#creating-item-components","Creating Item Components",[314,10176],"You must create a component for each repeater field that defines the structure of a single item: \u003C!-- components/Slot.vue -->\n\u003Cscript setup lang=\"ts\">\nimport { nanoid } from 'nanoid'\n\ninterface TimeSlot {\n  id: string\n  label: string\n  startTime: string\n  endTime: string\n}\n\nconst props = defineProps\u003C{\n  modelValue: TimeSlot\n}>()\n\nconst emit = defineEmits\u003C{\n  'update:modelValue': [value: TimeSlot]\n}>()\n\n// Initialize with defaults\nconst localValue = computed({\n  get: () => props.modelValue || {\n    id: nanoid(),\n    label: '',\n    startTime: '09:00',\n    endTime: '17:00'\n  },\n  set: (val) => emit('update:modelValue', val)\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"grid grid-cols-4 gap-4\">\n    \u003CUFormField label=\"ID\" name=\"id\">\n      \u003CUInput v-model=\"localValue.id\" disabled class=\"bg-gray-50\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Label\" name=\"label\">\n      \u003CUInput v-model=\"localValue.label\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Start Time\" name=\"startTime\">\n      \u003CUInput v-model=\"localValue.startTime\" type=\"time\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"End Time\" name=\"endTime\">\n      \u003CUInput v-model=\"localValue.endTime\" type=\"time\" />\n    \u003C/UFormField>\n  \u003C/div>\n\u003C/template>",{"id":10197,"title":183,"titles":10198,"content":10199,"level":449},"/api-reference/components/form-components#features-7",[314,10176],"Add items - Click button to create new items with auto-generated IDsRemove items - Click × to delete an itemReorder items - Drag handle (⋮⋮) to reorder when sortable: trueEmpty state - Helpful message when no items existAuto-IDs - Each item gets unique ID using nanoid()",{"id":10201,"title":9860,"titles":10202,"content":10203,"level":449},"/api-reference/components/form-components#in-schema-definition-1",[314,10176],"Define repeater fields in your collection schema: {\n  \"slots\": {\n    \"type\": \"repeater\",\n    \"meta\": {\n      \"label\": \"Available Time Slots\",\n      \"repeaterComponent\": \"Slot\",\n      \"addLabel\": \"Add Time Slot\",\n      \"sortable\": true,\n      \"area\": \"main\"\n    }\n  }\n} This generates: JSON/JSONB database columnForm field with CroutonRepeaterZod validation z.array(z.any()).optional()Default value []",{"id":10205,"title":10206,"titles":10207,"content":10208,"level":449},"/api-reference/components/form-components#item-component-requirements","Item Component Requirements",[314,10176],"Your item component must: Accept modelValue prop with the item dataEmit update:modelValue when data changesProvide default values in computed getterUse two-way binding with v-model",{"id":10210,"title":3122,"titles":10211,"content":10212,"level":449},"/api-reference/components/form-components#common-use-cases",[314,10176],"Contact Persons \u003CCroutonRepeater\n  v-model=\"state.contacts\"\n  component-name=\"ContactPerson\"\n  add-label=\"Add Contact\"\n/> Price Tiers \u003CCroutonRepeater\n  v-model=\"state.priceTiers\"\n  component-name=\"PriceTier\"\n  add-label=\"Add Tier\"\n  :sortable=\"false\"\n/> Social Media Links \u003CCroutonRepeater\n  v-model=\"state.socialLinks\"\n  component-name=\"SocialLink\"\n  add-label=\"Add Link\"\n/>",{"id":10214,"title":3127,"titles":10215,"content":10216,"level":449},"/api-reference/components/form-components#data-storage",[314,10176],"Data is stored as JSON arrays in the database: {\n  \"slots\": [\n    {\n      \"id\": 1697123456789,\n      \"label\": \"Morning\",\n      \"startTime\": \"09:00\",\n      \"endTime\": \"12:00\"\n    },\n    {\n      \"id\": 1697123456790,\n      \"label\": \"Afternoon\",\n      \"startTime\": \"13:00\",\n      \"endTime\": \"17:00\"\n    }\n  ]\n} Drag-to-reorder: The repeater uses useSortable from @vueuse/core for smooth drag-and-drop reordering. This is enabled by default but can be disabled with :sortable=\"false\". When to use: Use repeater fields when items are tightly coupled to their parent and don't need to be queried independently. For complex relationships or items that need their own table, use reference fields instead. No delete confirmation: Clicking the remove button (×) immediately deletes the item without confirmation. This is intentional for streamlined UX but means deletions can be accidental. Consider implementing undo functionality or confirmation dialogs for critical data.",{"id":10218,"title":10219,"titles":10220,"content":10221,"level":449},"/api-reference/components/form-components#drag-to-reorder-implementation","Drag-to-Reorder Implementation",[314,10176],"The repeater uses VueUse's useSortable integration with SortableJS: \u003C!-- Drag handle automatically appears when sortable={true} -->\n\u003CCroutonRepeater\n  v-model=\"state.items\"\n  component-name=\"ItemInput\"\n  :sortable=\"true\"\n/> Features: Drag handle: i-lucide-grip-vertical icon (⋮⋮)Ghost class: Visual feedback during dragSmooth animations: CSS transitions for reorderingTouch support: Works on mobile devices Disable sorting: \u003CCroutonRepeater\n  v-model=\"state.items\"\n  component-name=\"ItemInput\"\n  :sortable=\"false\"\n/>",{"id":10223,"title":10224,"titles":10225,"content":10226,"level":449},"/api-reference/components/form-components#component-name-resolution","Component Name Resolution",[314,10176],"CroutonRepeater dynamically resolves the item component by name: // Resolution process:\n// 1. Uses resolveComponent() from Vue\n// 2. Looks for component in components/ directory\n// 3. Warns if component not found\n\n// Example: component-name=\"ContactPerson\"\n// Resolves to: components/ContactPerson.vue Naming Requirements: Must be PascalCase: ContactPerson not contact-personMust be in components/ directoryComponent must be auto-registered by Nuxt",{"id":10228,"title":10229,"titles":10230,"content":10231,"level":449},"/api-reference/components/form-components#complete-example-with-generated-form","Complete Example with Generated Form",[314,10176],"\u003C!-- Automatically generated by crouton generator -->\n\u003Cscript setup lang=\"ts\">\nimport { z } from 'zod'\n\nconst props = defineProps\u003C{\n  action: 'create' | 'update'\n  loading: string\n  activeItem?: any\n}>()\n\nconst state = ref({\n  id: props.activeItem?.id || null,\n  name: props.activeItem?.name || '',\n  contacts: props.activeItem?.contacts || []\n})\n\nconst schema = z.object({\n  name: z.string().min(1),\n  contacts: z.array(z.any()).optional()\n})\n\nconst handleSubmit = async () => {\n  // state.contacts is an array of objects:\n  // [\n  //   { id: 'abc123', name: 'John', email: 'john@example.com' },\n  //   { id: 'def456', name: 'Jane', email: 'jane@example.com' }\n  // ]\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm :state=\"state\" :schema=\"schema\" @submit=\"handleSubmit\">\n    \u003CUFormField label=\"Name\" name=\"name\">\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Contacts\" name=\"contacts\">\n      \u003CCroutonRepeater\n        v-model=\"state.contacts\"\n        component-name=\"ContactPersonInput\"\n        add-label=\"Add Contact\"\n        :sortable=\"true\"\n      />\n    \u003C/UFormField>\n\n    \u003CUButton type=\"submit\">Save\u003C/UButton>\n  \u003C/UForm>\n\u003C/template> \u003C!-- components/ContactPersonInput.vue -->\n\u003Cscript setup lang=\"ts\">\nimport { nanoid } from 'nanoid'\n\ninterface ContactPerson {\n  id: string\n  name: string\n  email: string\n  phone?: string\n}\n\nconst model = defineModel\u003CContactPerson>()\n\n// Ensure defaults\nif (!model.value) {\n  model.value = {\n    id: nanoid(),\n    name: '',\n    email: '',\n    phone: ''\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"grid grid-cols-3 gap-4\">\n    \u003CUFormField label=\"Name\" name=\"name\">\n      \u003CUInput v-model=\"model.name\" placeholder=\"Full name\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Email\" name=\"email\">\n      \u003CUInput v-model=\"model.email\" type=\"email\" placeholder=\"email@example.com\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Phone\" name=\"phone\">\n      \u003CUInput v-model=\"model.phone\" type=\"tel\" placeholder=\"(555) 123-4567\" />\n    \u003C/UFormField>\n  \u003C/div>\n\u003C/template>",{"id":10233,"title":4027,"titles":10234,"content":10235,"level":449},"/api-reference/components/form-components#performance-considerations",[314,10176],"For large arrays (100+ items), consider these optimizations: \u003C!-- ⚠️ Deep watch can be expensive for large arrays -->\n\u003CCroutonRepeater\n  v-model=\"state.largeArray\"\n  component-name=\"ItemInput\"\n/> Recommendations: Limit items: Use pagination or virtualization for >50 itemsDisable sorting: Set :sortable=\"false\" for very large listsOptimize child components: Avoid heavy computations in item componentsUse production builds: Dev mode is slower due to reactivity tracking",{"id":10237,"title":36,"titles":10238,"content":528,"level":449},"/api-reference/components/form-components#troubleshooting-6",[314,10176],{"id":10240,"title":10241,"titles":10242,"content":10243,"level":748},"/api-reference/components/form-components#component-not-found-error-1","Component not found error",[314,10176,36],"[Vue warn]: Failed to resolve component: ContactPerson Solutions: Check component name: Must match exactly (PascalCase)Check file location: Must be in components/ directoryCheck file name: ContactPerson.vue not contact-person.vueRestart dev server: Nuxt may not have detected the new component",{"id":10245,"title":10246,"titles":10247,"content":10248,"level":748},"/api-reference/components/form-components#items-not-updating","Items not updating",[314,10176,36],"Problem: Changes to items don't reflect in the UI Solutions: Check v-model: Child component must use defineModel() or emit update:modelValueCheck reactivity: Ensure you're mutating the array correctlyAvoid direct assignment: Use reactive methods like push(), splice()",{"id":10250,"title":10251,"titles":10252,"content":10253,"level":748},"/api-reference/components/form-components#drag-and-drop-not-working","Drag-and-drop not working",[314,10176,36],"Problem: Can't reorder items Solutions: Check sortable prop: Must be true (default)Check item key: Each item needs unique idCheck browser support: Some browsers may not support drag-and-dropCheck CSS conflicts: Z-index or overflow issues",{"id":10255,"title":10256,"titles":10257,"content":10258,"level":748},"/api-reference/components/form-components#empty-state-not-showing","Empty state not showing",[314,10176,36],"Problem: No message when array is empty Solutions: Initialize as empty array: contacts: [] not contacts: nullCheck UCard rendering: Empty state is inside a UCardCheck styling: May be hidden by CSS",{"id":10260,"title":10261,"titles":10262,"content":10263,"level":748},"/api-reference/components/form-components#delete-removes-wrong-item","Delete removes wrong item",[314,10176,36],"Problem: Clicking × removes different item than expected Solutions: Check unique IDs: Each item must have unique id fieldUse nanoid(): Generates collision-resistant IDsDon't use array index: Index changes when items are reordered",{"id":10265,"title":10266,"titles":10267,"content":10268,"level":449},"/api-reference/components/form-components#when-not-to-use-repeater","When Not to Use Repeater",[314,10176],"Use Reference Fields Instead When: Items need their own database tableItems are shared across multiple parent recordsItems need complex querying or filteringItems have their own lifecycle (created/updated independently)You need referential integrity Example: Don't use repeater for \"tags\" - use a proper tags table with references.",{"id":10270,"title":10271,"titles":10272,"content":10273,"level":391},"/api-reference/components/form-components#croutonformactionbutton","CroutonFormActionButton",[314],"A styled submit button for form actions that shows loading states and validation warnings.",{"id":10275,"title":4987,"titles":10276,"content":10277,"level":449},"/api-reference/components/form-components#props-11",[314,10271],"PropTypeDefaultDescriptionactionstringrequiredAction type (e.g., 'create', 'update', 'delete')collectionstringrequiredCollection name for button labelitemsArray[]Items being acted uponloadingstring''Loading state identifierhasValidationErrorsbooleanfalseWhether form has validation errors",{"id":10279,"title":183,"titles":10280,"content":10281,"level":449},"/api-reference/components/form-components#features-8",[314,10271],"Action-Based Styling: Different colors and labels based on action typeLoading States: Shows loading spinner when loading !== 'notLoading'Validation Warning: Shows alert icon and outline variant when errors existAuto-Labeling: Generates button text from action and collection nameDelete Confirmation: Uses error color for destructive actions",{"id":10283,"title":1608,"titles":10284,"content":528,"level":449},"/api-reference/components/form-components#usage",[314,10271],{"id":10286,"title":4173,"titles":10287,"content":10288,"level":748},"/api-reference/components/form-components#basic-usage-6",[314,10271,1608],"\u003Ctemplate>\n  \u003CCroutonForm\n    collection=\"products\"\n    action=\"create\"\n    v-slot=\"{ loading, hasValidationErrors }\"\n  >\n    \u003C!-- Form fields -->\n    \n    \u003CCroutonFormActionButton\n      action=\"create\"\n      collection=\"products\"\n      :loading=\"loading\"\n      :has-validation-errors=\"hasValidationErrors\"\n    />\n  \u003C/CroutonForm>\n\u003C/template>",{"id":10290,"title":10291,"titles":10292,"content":10293,"level":748},"/api-reference/components/form-components#different-actions","Different Actions",[314,10271,1608],"\u003C!-- Create action (primary color) -->\n\u003CCroutonFormActionButton\n  action=\"create\"\n  collection=\"products\"\n  :loading=\"loading\"\n/>\n\u003C!-- Button text: \"Create Product\" -->\n\n\u003C!-- Update action (primary color) -->\n\u003CCroutonFormActionButton\n  action=\"update\"\n  collection=\"products\"\n  :loading=\"loading\"\n/>\n\u003C!-- Button text: \"Update Product\" -->\n\n\u003C!-- Delete action (error color) -->\n\u003CCroutonFormActionButton\n  action=\"delete\"\n  collection=\"products\"\n  :loading=\"loading\"\n/>\n\u003C!-- Button text: \"Delete Product\" -->",{"id":10295,"title":10296,"titles":10297,"content":10298,"level":449},"/api-reference/components/form-components#visual-states","Visual States",[314,10271],"StateAppearanceNormalSolid button with action colorLoadingDisabled with spinnerHas ErrorsOutline variant with warning iconDelete ActionError color (red) Validation Behavior: When hasValidationErrors is true, the button shows an alert icon and uses outline variant, but remains clickable to allow form validation feedback.",{"id":10300,"title":10301,"titles":10302,"content":10303,"level":391},"/api-reference/components/form-components#croutonformdependentbuttongroup","CroutonFormDependentButtonGroup",[314],"A button group component that renders selectable cards for dependent field options. Used within forms to select from options stored in a related item.",{"id":10305,"title":4987,"titles":10306,"content":10307,"level":449},"/api-reference/components/form-components#props-12",[314,10301],"PropTypeDefaultDescriptionmodelValuestring[] | nullnullSelected option ID(s)optionsOption[][]Available options to select frommultiplebooleanfalseAllow multiple selectionsdependentCollectionstringrequiredCollection name for card resolutiondependentFieldstringrequiredField name for card resolutioncardVariantstring'Mini'Card component variant suffix",{"id":10309,"title":183,"titles":10310,"content":10311,"level":449},"/api-reference/components/form-components#features-9",[314,10301],"Single/Multiple Selection: Supports both single and multi-select modesVisual Selection State: Selected items show a primary ringCustom Card Components: Dynamically resolves custom card componentsFallback Rendering: Uses badges if no custom component existsKeyboard Accessible: Clickable cards with proper focus states",{"id":10313,"title":1608,"titles":10314,"content":528,"level":449},"/api-reference/components/form-components#usage-1",[314,10301],{"id":10316,"title":10317,"titles":10318,"content":10319,"level":748},"/api-reference/components/form-components#single-selection","Single Selection",[314,10301,1608],"\u003Ctemplate>\n  \u003CCroutonFormDependentButtonGroup\n    v-model=\"selectedSlot\"\n    :options=\"availableSlots\"\n    dependent-collection=\"locations\"\n    dependent-field=\"slots\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedSlot = ref\u003Cstring[] | null>(null)\n\nconst availableSlots = [\n  { id: 'slot-1', label: 'Morning', time: '9:00 AM' },\n  { id: 'slot-2', label: 'Afternoon', time: '2:00 PM' },\n  { id: 'slot-3', label: 'Evening', time: '7:00 PM' }\n]\n\u003C/script>",{"id":10321,"title":10322,"titles":10323,"content":10324,"level":748},"/api-reference/components/form-components#multiple-selection","Multiple Selection",[314,10301,1608],"\u003Ctemplate>\n  \u003CCroutonFormDependentButtonGroup\n    v-model=\"selectedFeatures\"\n    :options=\"availableFeatures\"\n    :multiple=\"true\"\n    dependent-collection=\"products\"\n    dependent-field=\"features\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedFeatures = ref\u003Cstring[] | null>(null)\n\u003C/script>",{"id":10326,"title":8919,"titles":10327,"content":10328,"level":449},"/api-reference/components/form-components#custom-card-component",[314,10301],"Create a custom card component for rich option display: \u003C!-- components/LocationsSlotCardMini.vue -->\n\u003Ctemplate>\n  \u003Cdiv class=\"p-4 border rounded-lg hover:border-primary transition\">\n    \u003Cdiv class=\"flex items-center gap-3\">\n      \u003CUIcon :name=\"value.icon\" class=\"w-6 h-6\" />\n      \u003Cdiv>\n        \u003Cp class=\"font-semibold\">{{ value.label }}\u003C/p>\n        \u003Cp class=\"text-sm text-gray-500\">{{ value.time }}\u003C/p>\n      \u003C/div>\n    \u003C/div>\n    \u003CUBadge\n      v-if=\"value.available\"\n      color=\"success\"\n      variant=\"soft\"\n      size=\"sm\"\n      class=\"mt-2\"\n    >\n      Available\n    \u003C/UBadge>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{\n  value: {\n    id: string\n    label: string\n    time: string\n    icon: string\n    available: boolean\n  }\n}>()\n\u003C/script>",{"id":10330,"title":10331,"titles":10332,"content":528,"level":449},"/api-reference/components/form-components#selection-behavior","Selection Behavior",[314,10301],{"id":10334,"title":10335,"titles":10336,"content":10337,"level":748},"/api-reference/components/form-components#single-selection-mode-multiple-false","Single Selection Mode (multiple: false)",[314,10301,10331],"Click an option: Selects it (stores as [id] array)Click selected option: Deselects it (sets to null)Click different option: Replaces current selection",{"id":10339,"title":10340,"titles":10341,"content":10342,"level":748},"/api-reference/components/form-components#multiple-selection-mode-multiple-true","Multiple Selection Mode (multiple: true)",[314,10301,10331],"Click an option: Adds to selectionClick selected option: Removes from selectionModel value is always string[] or null",{"id":10344,"title":10345,"titles":10346,"content":10347,"level":449},"/api-reference/components/form-components#visual-feedback","Visual Feedback",[314,10301],"// Selected state\n'ring-2 ring-primary-500 rounded-lg'\n\n// Unselected state\n'opacity-70 hover:opacity-100'",{"id":10349,"title":10350,"titles":10351,"content":10352,"level":391},"/api-reference/components/form-components#croutonformdependentfieldloader","CroutonFormDependentFieldLoader",[314],"A loader component that fetches options from a parent item's field and delegates rendering to the appropriate component. This is the main entry point for dependent field handling in forms.",{"id":10354,"title":4987,"titles":10355,"content":10356,"level":449},"/api-reference/components/form-components#props-13",[314,10350],"PropTypeDefaultDescriptionmodelValuestring[] | nullnullSelected option ID(s)dependentValuestring | nullnullParent item ID to fetchdependentCollectionstringrequiredParent collection namedependentFieldstringrequiredField in parent containing optionsdependentLabelstring'Selection'Label for empty state messagemultiplebooleanfalseAllow multiple selectionscardVariantstring'Mini'Card component variantidKeystring'id'Key for option IDslabelKeystring'label'Key for option labelsvalueKeystring'value'Key for option values",{"id":10358,"title":183,"titles":10359,"content":10360,"level":449},"/api-reference/components/form-components#features-10",[314,10350],"Automatic Data Fetching: Fetches parent item using useCollectionItemCustom Component Resolution: Looks for custom selection componentsSmart Fallbacks: Falls back to CroutonFormDependentButtonGroup if no custom componentLoading & Error States: Built-in pending and error handlingAuto-Reset: Clears selection when dependent value changesValidation: Clears invalid selections when options change",{"id":10362,"title":1608,"titles":10363,"content":528,"level":449},"/api-reference/components/form-components#usage-2",[314,10350],{"id":10365,"title":10366,"titles":10367,"content":10368,"level":748},"/api-reference/components/form-components#in-a-form","In a Form",[314,10350,1608],"\u003Ctemplate>\n  \u003CCroutonForm collection=\"bookings\" action=\"create\">\n    \u003CUFormField label=\"Location\" name=\"locationId\">\n      \u003CCroutonFormReferenceSelect\n        v-model=\"formData.locationId\"\n        collection=\"locations\"\n      />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Time Slot\" name=\"slotId\">\n      \u003CCroutonFormDependentFieldLoader\n        v-model=\"formData.slotId\"\n        :dependent-value=\"formData.locationId\"\n        dependent-collection=\"locations\"\n        dependent-field=\"slots\"\n        dependent-label=\"Location\"\n      />\n    \u003C/UFormField>\n  \u003C/CroutonForm>\n\u003C/template>",{"id":10370,"title":10371,"titles":10372,"content":10373,"level":449},"/api-reference/components/form-components#states","States",[314,10350],"The component handles several states automatically: StateDisplayLoading\"Loading options...\" with spinnerError\"Failed to load options\" in warning colorNo Parent Selected\"{dependentLabel} required\" messageNo Options\"No options available\" messageSuccessRenders selection component with options",{"id":10375,"title":10376,"titles":10377,"content":10378,"level":449},"/api-reference/components/form-components#dependent-value-watching","Dependent Value Watching",[314,10350],"The component watches for changes to dependentValue and automatically: Clears current selection when parent changesRe-fetches options from new parentValidates selection against new options // Example: User changes location\nformData.locationId = 'loc-2'\n\n// Component automatically:\n// 1. Clears formData.slotId\n// 2. Fetches new slots from 'loc-2'\n// 3. Updates available options",{"id":10380,"title":10381,"titles":10382,"content":10383,"level":449},"/api-reference/components/form-components#custom-component-registration","Custom Component Registration",[314,10350],"Register custom selection components using the dependentFieldComponentMap: // In your layer's `useCollections.ts`\nexport const dependentFieldComponentMap = {\n  'locations': {\n    'slots': 'LocationsSlotsSelector'  // Custom component\n  },\n  'events': {\n    'categories': 'EventsCategoriesSelector'\n  }\n} If no custom component is registered, falls back to CroutonFormDependentButtonGroup.",{"id":10385,"title":10386,"titles":10387,"content":10388,"level":391},"/api-reference/components/form-components#croutonformdependentselectoption","CroutonFormDependentSelectOption",[314],"An intermediate component that wraps CroutonFormDependentButtonGroup and handles loading/error states for dependent field options. Note: This component is typically used internally by CroutonFormDependentFieldLoader. You rarely need to use it directly.",{"id":10390,"title":4987,"titles":10391,"content":10392,"level":449},"/api-reference/components/form-components#props-14",[314,10386],"PropTypeDefaultDescriptionmodelValuestring[] | nullnullSelected option ID(s)optionsOption[][]Available optionspendingbooleanfalseLoading stateerroranynullError objectdependentValuestring | nullnullParent item IDdependentLabelstring'Selection'Label for empty statemultiplebooleanfalseAllow multiple selectionsdependentCollectionstringrequiredCollection namedependentFieldstringrequiredField namecardVariantstring'Mini'Card variant",{"id":10394,"title":1608,"titles":10395,"content":10396,"level":449},"/api-reference/components/form-components#usage-3",[314,10386],"\u003Ctemplate>\n  \u003CCroutonFormDependentSelectOption\n    v-model=\"selectedOptions\"\n    :options=\"fetchedOptions\"\n    :pending=\"isLoading\"\n    :error=\"fetchError\"\n    :dependent-value=\"parentId\"\n    dependent-label=\"Parent Item\"\n    dependent-collection=\"items\"\n    dependent-field=\"options\"\n  />\n\u003C/template>",{"id":10398,"title":10399,"titles":10400,"content":10401,"level":449},"/api-reference/components/form-components#state-handling","State Handling",[314,10386],"The component renders different UI based on the current state: \u003C!-- Loading -->\n\u003Cdiv v-if=\"pending\">\n  \u003CUIcon name=\"i-lucide-refresh-cw\" class=\"animate-spin\" />\n  Loading options...\n\u003C/div>\n\n\u003C!-- Error -->\n\u003Cdiv v-else-if=\"error\" class=\"text-warning\">\n  Failed to load options\n\u003C/div>\n\n\u003C!-- No parent selected -->\n\u003Cdiv v-else-if=\"!dependentValue\" class=\"text-neutral\">\n  {{ dependentLabel }} required\n\u003C/div>\n\n\u003C!-- No options available -->\n\u003Cdiv v-else-if=\"!options || options.length === 0\" class=\"text-neutral\">\n  No options available\n\u003C/div>\n\n\u003C!-- Success: render button group -->\n\u003CCroutonFormDependentButtonGroup\n  v-else\n  v-model=\"localValue\"\n  :options=\"options\"\n  :multiple=\"multiple\"\n  :dependent-collection=\"dependentCollection\"\n  :dependent-field=\"dependentField\"\n  :card-variant=\"cardVariant\"\n/>",{"id":10403,"title":10404,"titles":10405,"content":10406,"level":391},"/api-reference/components/form-components#croutonformexpandableslideover","CroutonFormExpandableSlideOver",[314],"An advanced slideover component with expand/collapse functionality for transitioning between sidebar and fullscreen modes.",{"id":10408,"title":4987,"titles":10409,"content":10410,"level":449},"/api-reference/components/form-components#props-15",[314,10404],"PropTypeDefaultDescriptionopenbooleanfalseSlideover visibility (v-model)expandedbooleanfalseExpanded state (v-model)titlestringrequiredSlideover titleiconstringundefinedTitle iconbadgestringundefinedBadge textbadgeColorstring'primary'Badge colorbadgeVariantstring'soft'Badge variantloadingbooleanfalseLoading stateerrorobjectundefinedError objectdismissiblebooleantrueAllow closingportalboolean | string | HTMLElementtruePortal targettransitionbooleantrueEnable transitionscloseOnExpandbooleanfalseClose when expandingcontentClassstring''Content wrapper classfooterClassstring''Footer wrapper classmaxWidthstring'xl'Max width when collapsed",{"id":10412,"title":183,"titles":10413,"content":10414,"level":449},"/api-reference/components/form-components#features-11",[314,10404],"Dual-Mode Display: Switches between sidebar and fullscreenSmooth Transitions: 500ms ease-in-out animations with hardware accelerationLoading State: Built-in skeleton loading UIError Handling: Error display with retry functionalityAction Buttons: Expand/collapse and close buttonsCustomizable Content: Slots for header actions, body, and footerResponsive Sizing: Multiple max-width options",{"id":10416,"title":1608,"titles":10417,"content":528,"level":449},"/api-reference/components/form-components#usage-4",[314,10404],{"id":10419,"title":4173,"titles":10420,"content":10421,"level":748},"/api-reference/components/form-components#basic-usage-7",[314,10404,1608],"\u003Ctemplate>\n  \u003CUButton @click=\"isOpen = true\">\n    Open Slideover\n  \u003C/UButton>\n\n  \u003CCroutonFormExpandableSlideOver\n    v-model:open=\"isOpen\"\n    v-model:expanded=\"isExpanded\"\n    title=\"Edit Product\"\n    icon=\"i-lucide-package\"\n    badge=\"Draft\"\n  >\n    \u003C!-- Content here -->\n    \u003CCroutonForm\n      collection=\"products\"\n      :item-id=\"productId\"\n      action=\"update\"\n    />\n  \u003C/CroutonFormExpandableSlideOver>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst isOpen = ref(false)\nconst isExpanded = ref(false)\nconst productId = ref('prod-123')\n\u003C/script>",{"id":10423,"title":10424,"titles":10425,"content":10426,"level":748},"/api-reference/components/form-components#with-loading-and-error-states","With Loading and Error States",[314,10404,1608],"\u003Ctemplate>\n  \u003CCroutonFormExpandableSlideOver\n    v-model:open=\"isOpen\"\n    v-model:expanded=\"isExpanded\"\n    title=\"Product Details\"\n    :loading=\"isLoading\"\n    :error=\"loadError\"\n  >\n    \u003C!-- Content only shown when not loading and no error -->\n    \u003CProductDetails :product=\"product\" />\n  \u003C/CroutonFormExpandableSlideOver>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst isOpen = ref(false)\nconst isExpanded = ref(false)\nconst isLoading = ref(true)\nconst loadError = ref(null)\n\nconst { data: product } = await useFetch('/api/products/123', {\n  onResponse() { isLoading.value = false },\n  onError(err) {\n    loadError.value = {\n      title: 'Failed to Load',\n      description: err.message,\n      retry: () => refresh()\n    }\n  }\n})\n\u003C/script>",{"id":10428,"title":5372,"titles":10429,"content":528,"level":449},"/api-reference/components/form-components#slots-2",[314,10404],{"id":10431,"title":10432,"titles":10433,"content":10434,"level":748},"/api-reference/components/form-components#actions-scoped","actions (Scoped)",[314,10404,5372],"Custom action buttons in the header: \u003Ctemplate #actions=\"{ expanded }\">\n  \u003CUButton\n    icon=\"i-lucide-share\"\n    variant=\"ghost\"\n    size=\"sm\"\n    @click=\"shareItem\"\n  />\n  \u003CUButton\n    icon=\"i-lucide-star\"\n    variant=\"ghost\"\n    size=\"sm\"\n    :color=\"isFavorite ? 'primary' : 'neutral'\"\n    @click=\"toggleFavorite\"\n  />\n\u003C/template>",{"id":10436,"title":10437,"titles":10438,"content":10439,"level":748},"/api-reference/components/form-components#default-slot-scoped","Default Slot (Scoped)",[314,10404,5372],"Main content area: \u003Ctemplate #default=\"{ expanded, toggleExpand }\">\n  \u003Cdiv :class=\"{ 'max-w-4xl mx-auto': expanded }\">\n    \u003Ch3>Content adjusts based on expanded state\u003C/h3>\n    \u003CUButton @click=\"toggleExpand\">\n      {{ expanded ? 'Collapse' : 'Expand' }}\n    \u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":10441,"title":10442,"titles":10443,"content":10444,"level":748},"/api-reference/components/form-components#footer-scoped","footer (Scoped)",[314,10404,5372],"Optional footer content: \u003Ctemplate #footer=\"{ expanded }\">\n  \u003Cdiv class=\"flex justify-between items-center\">\n    \u003CUButton variant=\"outline\" @click=\"cancel\">\n      Cancel\n    \u003C/UButton>\n    \u003CUButton color=\"primary\" @click=\"save\">\n      Save Changes\n    \u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":10446,"title":5367,"titles":10447,"content":10448,"level":449},"/api-reference/components/form-components#events-8",[314,10404],"EventPayloadDescriptionupdate:openbooleanEmitted when open state changesupdate:expandedbooleanEmitted when expanded state changesexpand-Emitted when slideover expandscollapse-Emitted when slideover collapsestoggle-Emitted on any expand/collapse toggle",{"id":10450,"title":10451,"titles":10452,"content":10453,"level":449},"/api-reference/components/form-components#max-width-options","Max Width Options",[314,10404],"Available maxWidth values when in sidebar mode: ValueWidth'sm'max-w-sm (24rem)'md'max-w-md (28rem)'lg'max-w-lg (32rem)'xl'max-w-xl (36rem)'2xl'max-w-2xl (42rem)'4xl'max-w-4xl (56rem)'7xl'max-w-7xl (80rem)'full'w-full (100%) When expanded: true, the slideover always uses full screen width.",{"id":10455,"title":10456,"titles":10457,"content":10458,"level":449},"/api-reference/components/form-components#advanced-example-immersive-mode","Advanced Example: Immersive Mode",[314,10404],"\u003Ctemplate>\n  \u003CCroutonFormExpandableSlideOver\n    v-model:open=\"isOpen\"\n    v-model:expanded=\"isExpanded\"\n    title=\"Detailed Editor\"\n    :close-on-expand=\"true\"\n    max-width=\"2xl\"\n  >\n    \u003Ctemplate #actions=\"{ expanded }\">\n      \u003CUBadge v-if=\"expanded\" color=\"primary\" variant=\"soft\">\n        Fullscreen Mode\n      \u003C/UBadge>\n    \u003C/template>\n\n    \u003CRichTextEditor\n      v-model=\"content\"\n      :fullscreen=\"isExpanded\"\n    />\n\n    \u003Ctemplate #footer=\"{ expanded, toggleExpand }\">\n      \u003Cdiv class=\"flex justify-between\">\n        \u003CUButton\n          v-if=\"!expanded\"\n          variant=\"outline\"\n          icon=\"i-lucide-maximize-2\"\n          @click=\"toggleExpand\"\n        >\n          Expand Editor\n        \u003C/UButton>\n        \u003CUButton color=\"primary\" @click=\"save\">\n          Save\n        \u003C/UButton>\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonFormExpandableSlideOver>\n\u003C/template> Immersive Mode: Set closeOnExpand: true to automatically close the overlay when expanding, creating a seamless transition to fullscreen.",{"id":10460,"title":10461,"titles":10462,"content":10463,"level":391},"/api-reference/components/form-components#croutoncalendar","CroutonCalendar",[314],"A flexible calendar component that supports both single date and date range selection with native Date/timestamp support.",{"id":10465,"title":4987,"titles":10466,"content":10467,"level":449},"/api-reference/components/form-components#props-16",[314,10461],"PropTypeDefaultDescriptiondateDate | number | nullnullDate or timestamp for single date moderangebooleanfalseEnable range selection modestartDateDate | number | nullnullStart date or timestamp for range modeendDateDate | number | nullnullEnd date or timestamp for range modecolor'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral''primary'Calendar accent colorvariant'solid' | 'outline' | 'soft' | 'subtle''solid'Visual style variantsize'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Calendar sizedisabledbooleanfalseDisable all interactionsminDateDate | number | nullnullMinimum selectable date or timestampmaxDateDate | number | nullnullMaximum selectable date or timestampmonthControlsbooleantrueShow month navigation controlsyearControlsbooleantrueShow year navigation controlsnumberOfMonthsnumberundefinedNumber of months to display (default: 1 for single, 2 for range)",{"id":10469,"title":5367,"titles":10470,"content":10471,"level":449},"/api-reference/components/form-components#events-9",[314,10461],"EventPayloadDescriptionupdate:dateDate | nullEmitted when single date changesupdate:startDateDate | nullEmitted when range start date changesupdate:endDateDate | nullEmitted when range end date changes",{"id":10473,"title":183,"titles":10474,"content":10475,"level":449},"/api-reference/components/form-components#features-12",[314,10461],"Dual Mode: Single date or range selectionFlexible Input: Accepts Date objects or timestampsDate Constraints: Min/max date restrictionsTimezone Aware: Uses local timezone for conversionsAuto Months: Automatically shows 1 month for single, 2 for rangeUCalendar Wrapper: Built on Nuxt UI 4 calendar component",{"id":10477,"title":1608,"titles":10478,"content":528,"level":449},"/api-reference/components/form-components#usage-5",[314,10461],{"id":10480,"title":10481,"titles":10482,"content":10483,"level":748},"/api-reference/components/form-components#single-date-selection","Single Date Selection",[314,10461,1608],"\u003Ctemplate>\n  \u003CCroutonCalendar\n    v-model:date=\"selectedDate\"\n    :min-date=\"minDate\"\n    :max-date=\"maxDate\"\n    color=\"primary\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedDate = ref\u003CDate | null>(new Date())\nconst minDate = new Date('2024-01-01')\nconst maxDate = new Date('2024-12-31')\n\u003C/script>",{"id":10485,"title":10486,"titles":10487,"content":10488,"level":748},"/api-reference/components/form-components#date-range-selection","Date Range Selection",[314,10461,1608],"\u003Ctemplate>\n  \u003CCroutonCalendar\n    v-model:start-date=\"startDate\"\n    v-model:end-date=\"endDate\"\n    range\n    :number-of-months=\"2\"\n    color=\"primary\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst startDate = ref\u003CDate | null>(null)\nconst endDate = ref\u003CDate | null>(null)\n\nwatch([startDate, endDate], ([start, end]) => {\n  if (start && end) {\n    console.log('Range selected:', { start, end })\n  }\n})\n\u003C/script>",{"id":10490,"title":10491,"titles":10492,"content":10493,"level":748},"/api-reference/components/form-components#with-timestamp-support","With Timestamp Support",[314,10461,1608],"\u003Ctemplate>\n  \u003CCroutonCalendar\n    v-model:date=\"timestamp\"\n    :min-date=\"minTimestamp\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\n// Works with timestamps directly\nconst timestamp = ref\u003Cnumber>(Date.now())\nconst minTimestamp = Date.now() - (30 * 24 * 60 * 60 * 1000) // 30 days ago\n\u003C/script>",{"id":10495,"title":10496,"titles":10497,"content":10498,"level":748},"/api-reference/components/form-components#constrained-range-selection","Constrained Range Selection",[314,10461,1608],"\u003Ctemplate>\n  \u003CCroutonCalendar\n    v-model:start-date=\"checkIn\"\n    v-model:end-date=\"checkOut\"\n    range\n    :min-date=\"today\"\n    :max-date=\"maxBookingDate\"\n    :number-of-months=\"3\"\n    color=\"success\"\n    variant=\"soft\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst checkIn = ref\u003CDate | null>(null)\nconst checkOut = ref\u003CDate | null>(null)\nconst today = new Date()\nconst maxBookingDate = new Date()\nmaxBookingDate.setFullYear(today.getFullYear() + 1)\n\u003C/script>",{"id":10500,"title":10501,"titles":10502,"content":10503,"level":748},"/api-reference/components/form-components#custom-styling-and-size","Custom Styling and Size",[314,10461,1608],"\u003Ctemplate>\n  \u003CCroutonCalendar\n    v-model:date=\"eventDate\"\n    color=\"secondary\"\n    variant=\"outline\"\n    size=\"lg\"\n    :month-controls=\"false\"\n    :year-controls=\"true\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst eventDate = ref\u003CDate | null>(null)\n\u003C/script>",{"id":10505,"title":10506,"titles":10507,"content":10508,"level":449},"/api-reference/components/form-components#date-conversion","Date Conversion",[314,10461],"The component automatically handles conversion between native Date/timestamp and Nuxt UI's CalendarDate format: // Input: Date or number (timestamp)\nconst myDate = new Date('2024-03-15') // or Date.now()\n\n// Component converts to @internationalized/date format internally\n// Output: Date object emitted via events Timezone Handling: The component uses getLocalTimeZone() for all conversions, ensuring dates are interpreted in the user's local timezone.",{"id":10510,"title":10511,"titles":10512,"content":10513,"level":391},"/api-reference/components/form-components#croutonformdynamicloader","CroutonFormDynamicLoader",[314],"Dynamically loads collection-specific form/detail components based on collection name and action.",{"id":10515,"title":4987,"titles":10516,"content":10517,"level":449},"/api-reference/components/form-components#props-17",[314,10511],"PropTypeDefaultDescriptioncollectionstring-Collection name to load component forloadingstring-Loading state indicatoractionstring-Action type ('view', 'create', 'update', 'delete')itemsArray[]Array of items to pass to componentactiveItemObject{}Currently active item object",{"id":10519,"title":183,"titles":10520,"content":10521,"level":449},"/api-reference/components/form-components#features-13",[314,10511],"Dynamic Component Resolution: Automatically resolves components from useCollections().componentMapConvention-Based Detail Views: Replaces 'Form' with 'Detail' suffix for view actionsFallback Mechanism: Falls back to Form component if Detail doesn't existSpecial Mode Handling: Supports system/team modes for translationsUi collectionAttribute Passthrough: Forwards all attrs to dynamic component",{"id":10523,"title":1608,"titles":10524,"content":528,"level":449},"/api-reference/components/form-components#usage-6",[314,10511],{"id":10526,"title":10527,"titles":10528,"content":10529,"level":748},"/api-reference/components/form-components#basic-dynamic-loading","Basic Dynamic Loading",[314,10511,1608],"\u003Ctemplate>\n  \u003CCroutonFormDynamicLoader\n    :collection=\"currentCollection\"\n    :action=\"currentAction\"\n    :items=\"items\"\n    :active-item=\"selectedItem\"\n    :loading=\"loadingState\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst currentCollection = ref('users')\nconst currentAction = ref('update')\nconst selectedItem = ref({ id: '123', name: 'John' })\nconst items = ref([])\nconst loadingState = ref('')\n\u003C/script>",{"id":10531,"title":10532,"titles":10533,"content":10534,"level":748},"/api-reference/components/form-components#with-viewdetail-convention","With View/Detail Convention",[314,10511,1608],"\u003Ctemplate>\n  \u003C!-- When action='view', tries to load UserDetail component -->\n  \u003C!-- Falls back to UserForm if UserDetail doesn't exist -->\n  \u003CCroutonFormDynamicLoader\n    collection=\"users\"\n    action=\"view\"\n    :active-item=\"user\"\n  />\n\u003C/template>",{"id":10536,"title":10537,"titles":10538,"content":10539,"level":748},"/api-reference/components/form-components#collection-component-map-setup","Collection Component Map Setup",[314,10511,1608],"// In useCollections composable\nexport const useCollections = () => {\n  const componentMap = {\n    'users': 'UserForm',\n    'products': 'ProductForm',\n    'orders': 'OrderForm'\n    // For action='view', loader will try:\n    // - UserDetail (if exists)\n    // - UserForm (fallback)\n  }\n  \n  return { componentMap }\n}",{"id":10541,"title":10542,"titles":10543,"content":10544,"level":748},"/api-reference/components/form-components#translationsui-mode-support","TranslationsUi Mode Support",[314,10511,1608],"\u003Ctemplate>\n  \u003C!-- Automatically detects system vs team mode from route -->\n  \u003CCroutonFormDynamicLoader\n    collection=\"translationsUi\"\n    action=\"update\"\n    :active-item=\"translation\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\n// Route: /super-admin/translations → mode: 'system'\n// Route: /team/translations → mode: 'team'\n// Mode is automatically passed to the loaded component\n\u003C/script>",{"id":10546,"title":10547,"titles":10548,"content":10549,"level":449},"/api-reference/components/form-components#component-not-found-handling","Component Not Found Handling",[314,10511],"\u003Ctemplate>\n  \u003CCroutonFormDynamicLoader\n    collection=\"unknownCollection\"\n    action=\"create\"\n  />\n  \u003C!-- Shows: \"Component not found for collection: unknownCollection\" -->\n\u003C/template> Component Registration: Ensure all components referenced in componentMap are properly registered and auto-imported.",{"id":10551,"title":10552,"titles":10553,"content":10554,"level":391},"/api-reference/components/form-components#croutonformlayout","CroutonFormLayout",[314],"A responsive form layout with optional tabs, sidebar, and error indicators.",{"id":10556,"title":4987,"titles":10557,"content":10558,"level":449},"/api-reference/components/form-components#props-18",[314,10552],"PropTypeDefaultDescriptiontabsbooleanfalseEnable tab navigationnavigationItemsNavigationItem[][]Array of navigation/tab itemstabErrorsRecord\u003Cstring, number>{}Error counts per tab (key: tab value, value: error count)modelValuestring''Active section/tab value (v-model)",{"id":10560,"title":6551,"titles":10561,"content":10562,"level":449},"/api-reference/components/form-components#types",[314,10552],"interface NavigationItem {\n  label: string\n  value: string\n  icon?: string\n}",{"id":10564,"title":5367,"titles":10565,"content":10566,"level":449},"/api-reference/components/form-components#events-10",[314,10552],"EventPayloadDescriptionupdate:modelValuestringEmitted when active section changes",{"id":10568,"title":5372,"titles":10569,"content":10570,"level":449},"/api-reference/components/form-components#slots-3",[314,10552],"SlotPropsDescriptionheader-Header content above main areamain{ activeSection: string }Main content areasidebar-Sidebar content (desktop column, mobile accordion)footer-Footer content below main area",{"id":10572,"title":183,"titles":10573,"content":10574,"level":449},"/api-reference/components/form-components#features-14",[314,10552],"Responsive Grid: 1 column mobile, 3 columns desktop when sidebar existsTab Navigation: Optional UTabs with error badgesError Indicators: Red badges on tabs with validation errorsMobile Accordion: Sidebar converts to accordion on mobileContainer Queries: Uses @container for responsive breakpointsAuto-Detection: Detects sidebar slot usage automatically",{"id":10576,"title":1608,"titles":10577,"content":528,"level":449},"/api-reference/components/form-components#usage-7",[314,10552],{"id":10579,"title":10580,"titles":10581,"content":10582,"level":748},"/api-reference/components/form-components#basic-layout-with-tabs","Basic Layout with Tabs",[314,10552,1608],"\u003Ctemplate>\n  \u003CCroutonFormLayout\n    v-model=\"activeSection\"\n    tabs\n    :navigation-items=\"sections\"\n    :tab-errors=\"validationErrors\"\n  >\n    \u003Ctemplate #header>\n      \u003Ch1>Edit Profile\u003C/h1>\n    \u003C/template>\n\n    \u003Ctemplate #main=\"{ activeSection }\">\n      \u003Cdiv v-show=\"activeSection === 'general'\">\n        \u003C!-- General fields -->\n      \u003C/div>\n      \u003Cdiv v-show=\"activeSection === 'security'\">\n        \u003C!-- Security fields -->\n      \u003C/div>\n    \u003C/template>\n\n    \u003Ctemplate #footer>\n      \u003Cdiv class=\"flex justify-end gap-2\">\n        \u003CUButton variant=\"outline\">Cancel\u003C/UButton>\n        \u003CUButton color=\"primary\">Save\u003C/UButton>\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonFormLayout>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst activeSection = ref('general')\n\nconst sections = [\n  { label: 'General', value: 'general', icon: 'i-lucide-user' },\n  { label: 'Security', value: 'security', icon: 'i-lucide-lock' }\n]\n\nconst validationErrors = ref({\n  'general': 2, // 2 errors in general tab\n  'security': 0\n})\n\u003C/script>",{"id":10584,"title":10585,"titles":10586,"content":10587,"level":748},"/api-reference/components/form-components#with-sidebar","With Sidebar",[314,10552,1608],"\u003Ctemplate>\n  \u003CCroutonFormLayout\n    v-model=\"activeTab\"\n    tabs\n    :navigation-items=\"tabs\"\n  >\n    \u003Ctemplate #main=\"{ activeSection }\">\n      \u003C!-- Main form content -->\n      \u003CUFormGroup label=\"Title\">\n        \u003CUInput v-model=\"title\" />\n      \u003C/UFormGroup>\n    \u003C/template>\n\n    \u003Ctemplate #sidebar>\n      \u003C!-- Sidebar metadata (desktop: right column, mobile: accordion) -->\n      \u003CUCard>\n        \u003Ch3 class=\"text-sm font-semibold mb-2\">Metadata\u003C/h3>\n        \u003CUFormGroup label=\"Status\">\n          \u003CUSelect v-model=\"status\" :items=\"statusOptions\" />\n        \u003C/UFormGroup>\n        \u003CUFormGroup label=\"Category\">\n          \u003CUSelect v-model=\"category\" :items=\"categories\" />\n        \u003C/UFormGroup>\n      \u003C/UCard>\n    \u003C/template>\n\n    \u003Ctemplate #footer>\n      \u003CUButton color=\"primary\" @click=\"save\">Publish\u003C/UButton>\n    \u003C/template>\n  \u003C/CroutonFormLayout>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst activeTab = ref('content')\nconst tabs = [\n  { label: 'Content', value: 'content' },\n  { label: 'SEO', value: 'seo' }\n]\n\nconst title = ref('')\nconst status = ref('draft')\nconst category = ref('')\n\u003C/script>",{"id":10589,"title":10590,"titles":10591,"content":10592,"level":748},"/api-reference/components/form-components#error-badge-visualization","Error Badge Visualization",[314,10552,1608],"\u003Ctemplate>\n  \u003CCroutonFormLayout\n    v-model=\"section\"\n    tabs\n    :navigation-items=\"formSections\"\n    :tab-errors=\"errorsBySection\"\n  >\n    \u003C!-- Tabs with errors show red dot badges -->\n  \u003C/CroutonFormLayout>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst formSections = [\n  { label: 'Profile', value: 'profile' },\n  { label: 'Address', value: 'address' },\n  { label: 'Payment', value: 'payment' }\n]\n\n// Tabs with >0 errors show red badge with \"●\"\nconst errorsBySection = ref({\n  'profile': 0,\n  'address': 3, // Shows red dot on Address tab\n  'payment': 1  // Shows red dot on Payment tab\n})\n\u003C/script>",{"id":10594,"title":10595,"titles":10596,"content":10597,"level":748},"/api-reference/components/form-components#simple-form-no-tabs-or-sidebar","Simple Form (No Tabs or Sidebar)",[314,10552,1608],"\u003Ctemplate>\n  \u003CCroutonFormLayout>\n    \u003Ctemplate #header>\n      \u003Ch2>Create Account\u003C/h2>\n    \u003C/template>\n\n    \u003Ctemplate #main>\n      \u003Cdiv class=\"space-y-4\">\n        \u003CUFormGroup label=\"Email\">\n          \u003CUInput v-model=\"email\" type=\"email\" />\n        \u003C/UFormGroup>\n        \u003CUFormGroup label=\"Password\">\n          \u003CUInput v-model=\"password\" type=\"password\" />\n        \u003C/UFormGroup>\n      \u003C/div>\n    \u003C/template>\n\n    \u003Ctemplate #footer>\n      \u003CUButton color=\"primary\" block>Sign Up\u003C/UButton>\n    \u003C/template>\n  \u003C/CroutonFormLayout>\n\u003C/template>",{"id":10599,"title":10600,"titles":10601,"content":10602,"level":449},"/api-reference/components/form-components#responsive-behavior","Responsive Behavior",[314,10552],"Screen SizeMain AreaSidebarMobile (\u003C @lg)Full widthAccordion above mainDesktop (@lg+)2/3 width1/3 right column Container Queries: Uses @container instead of traditional media queries for more flexible responsive behavior within any parent.",{"id":10604,"title":10605,"titles":10606,"content":10607,"level":391},"/api-reference/components/form-components#croutonformreferenceselect","CroutonFormReferenceSelect",[314],"A select menu for referencing items from another collection with create-on-the-fly support.",{"id":10609,"title":4987,"titles":10610,"content":10611,"level":449},"/api-reference/components/form-components#props-19",[314,10605],"PropTypeDefaultDescriptionmodelValuestring | string[] | null-Selected item ID(s)collectionstring-Collection to fetch items fromlabelstring-Display label for the selectlabelKeystring'title'Object key to use as display labelfilterFieldsstring[]['title', 'name']Fields to search when filteringhideCreatebooleanfalseHide the \"Create new\" buttonmultiplebooleanfalseEnable multiple selection",{"id":10613,"title":5367,"titles":10614,"content":10615,"level":449},"/api-reference/components/form-components#events-11",[314,10605],"EventPayloadDescriptionupdate:modelValuestring | string[] | nullEmitted when selection changes",{"id":10617,"title":183,"titles":10618,"content":10619,"level":449},"/api-reference/components/form-components#features-15",[314,10605],"Auto-Create: Create new items directly from the dropdownSearchable: Built-in search across filter fieldsMultiple Selection: Support for selecting multiple itemsError Handling: User-friendly error messages (404, 403, 500)Auto-Select: Automatically selects newly created itemsLoading States: Shows loading indicator while fetchingNull Handling: Returns null when selection is cleared",{"id":10621,"title":1608,"titles":10622,"content":528,"level":449},"/api-reference/components/form-components#usage-8",[314,10605],{"id":10624,"title":10625,"titles":10626,"content":10627,"level":748},"/api-reference/components/form-components#basic-reference-select","Basic Reference Select",[314,10605,1608],"\u003Ctemplate>\n  \u003CUFormGroup label=\"Author\">\n    \u003CCroutonFormReferenceSelect\n      v-model=\"authorId\"\n      collection=\"users\"\n      label=\"author\"\n      label-key=\"name\"\n      :filter-fields=\"['name', 'email']\"\n    />\n  \u003C/UFormGroup>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst authorId = ref\u003Cstring | null>(null)\n\nwatch(authorId, (newId) => {\n  console.log('Selected author:', newId)\n})\n\u003C/script>",{"id":10629,"title":10322,"titles":10630,"content":10631,"level":748},"/api-reference/components/form-components#multiple-selection-1",[314,10605,1608],"\u003Ctemplate>\n  \u003CUFormGroup label=\"Tags\">\n    \u003CCroutonFormReferenceSelect\n      v-model=\"tagIds\"\n      collection=\"tags\"\n      label=\"tags\"\n      label-key=\"name\"\n      multiple\n    />\n  \u003C/UFormGroup>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst tagIds = ref\u003Cstring[]>([])\n\nwatch(tagIds, (newIds) => {\n  console.log('Selected tags:', newIds)\n})\n\u003C/script>",{"id":10633,"title":10634,"titles":10635,"content":10636,"level":748},"/api-reference/components/form-components#with-create-disabled","With Create Disabled",[314,10605,1608],"\u003Ctemplate>\n  \u003CCroutonFormReferenceSelect\n    v-model=\"categoryId\"\n    collection=\"categories\"\n    label=\"category\"\n    hide-create\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst categoryId = ref\u003Cstring | null>(null)\n\u003C/script>",{"id":10638,"title":10639,"titles":10640,"content":10641,"level":748},"/api-reference/components/form-components#auto-create-workflow","Auto-Create Workflow",[314,10605,1608],"\u003Ctemplate>\n  \u003CCroutonFormReferenceSelect\n    v-model=\"projectId\"\n    collection=\"projects\"\n    label=\"project\"\n  />\n  \u003C!-- User clicks \"Create new project\" → Modal opens\n       User saves new project → Item auto-selected -->\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst projectId = ref\u003Cstring | null>(null)\n\n// After creating a new project, projectId automatically updates\nwatch(projectId, (id) => {\n  if (id) {\n    console.log('Project selected or created:', id)\n  }\n})\n\u003C/script>",{"id":10643,"title":9816,"titles":10644,"content":10645,"level":748},"/api-reference/components/form-components#custom-filter-fields-1",[314,10605,1608],"\u003Ctemplate>\n  \u003CCroutonFormReferenceSelect\n    v-model=\"productId\"\n    collection=\"products\"\n    label=\"product\"\n    label-key=\"title\"\n    :filter-fields=\"['title', 'sku', 'description']\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst productId = ref\u003Cstring | null>(null)\n// Search will match against title, SKU, or description\n\u003C/script>",{"id":10647,"title":10648,"titles":10649,"content":10650,"level":449},"/api-reference/components/form-components#error-states","Error States",[314,10605],"The component displays user-friendly error messages: Status CodeError Message404\"The data endpoint could not be found. Please check your team settings or contact support.\"403\"You do not have permission to view this data.\"500+\"A server error occurred. Please try again later.\"OtherDisplays statusMessage or generic error \u003Ctemplate>\n  \u003C!-- If collection endpoint returns 404 -->\n  \u003CCroutonFormReferenceSelect\n    v-model=\"itemId\"\n    collection=\"missing-collection\"\n  />\n  \u003C!-- Shows red alert with error message above select -->\n\u003C/template>",{"id":10652,"title":10653,"titles":10654,"content":10655,"level":449},"/api-reference/components/form-components#label-key-fallback","Label Key Fallback",[314,10605],"If the specified labelKey isn't found, the component falls back through: labelKey prop valuetitle fieldname fieldid field // Display order: option[labelKey] || option.title || option.name || option.id Collection Query: Ensure the collection endpoint is available at /api/collections/{collection} and returns an array of items with id fields.",{"id":10657,"title":10658,"titles":10659,"content":10660,"level":391},"/api-reference/components/form-components#croutonformrepeater","CroutonFormRepeater",[314],"A repeater component for managing dynamic lists of sub-forms with drag-to-reorder support.",{"id":10662,"title":4987,"titles":10663,"content":10664,"level":449},"/api-reference/components/form-components#props-20",[314,10658],"PropTypeDefaultDescriptionmodelValueany[] | null-Array of repeater itemscomponentNamestring-Name of component to render for each itemaddLabelstring'Add Item'Label for add buttonsortablebooleantrueEnable drag-to-reorder",{"id":10666,"title":5367,"titles":10667,"content":10668,"level":449},"/api-reference/components/form-components#events-12",[314,10658],"EventPayloadDescriptionupdate:modelValueany[]Emitted when items array changes",{"id":10670,"title":183,"titles":10671,"content":10672,"level":449},"/api-reference/components/form-components#features-16",[314,10658],"Drag-to-Reorder: Uses VueUse useSortable for drag-drop reorderingUnique IDs: Auto-generates nanoid for new itemsEmpty State: Shows helpful empty state when no itemsItem Removal: Delete button for each itemAnimation: 200ms animation on reorderComponent Resolution: Automatically resolves registered componentsDebug Logging: Console logs for add/remove/update operations",{"id":10674,"title":1608,"titles":10675,"content":528,"level":449},"/api-reference/components/form-components#usage-9",[314,10658],{"id":10677,"title":10678,"titles":10679,"content":10680,"level":748},"/api-reference/components/form-components#basic-repeater","Basic Repeater",[314,10658,1608],"\u003Ctemplate>\n  \u003CUFormGroup label=\"Contact Methods\">\n    \u003CCroutonFormRepeater\n      v-model=\"contacts\"\n      component-name=\"ContactMethodInput\"\n      add-label=\"Add Contact Method\"\n    />\n  \u003C/UFormGroup>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst contacts = ref([\n  { id: '1', type: 'email', value: 'john@example.com' },\n  { id: '2', type: 'phone', value: '+1234567890' }\n])\n\u003C/script>\n\n\u003C!-- ContactMethodInput.vue -->\n\u003Ctemplate>\n  \u003Cdiv class=\"flex gap-2\">\n    \u003CUSelect v-model=\"localValue.type\" :items=\"types\" />\n    \u003CUInput v-model=\"localValue.value\" />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{ modelValue: any }>()\nconst emit = defineEmits\u003C{ 'update:modelValue': [value: any] }>()\n\nconst localValue = computed({\n  get: () => props.modelValue,\n  set: (val) => emit('update:modelValue', val)\n})\n\nconst types = ['email', 'phone', 'address']\n\u003C/script>",{"id":10682,"title":10683,"titles":10684,"content":10685,"level":748},"/api-reference/components/form-components#with-sortable-disabled","With Sortable Disabled",[314,10658,1608],"\u003Ctemplate>\n  \u003CCroutonFormRepeater\n    v-model=\"items\"\n    component-name=\"SimpleInput\"\n    add-label=\"Add Item\"\n    :sortable=\"false\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst items = ref([])\n// No drag handle shown when sortable=false\n\u003C/script>",{"id":10687,"title":10688,"titles":10689,"content":10690,"level":748},"/api-reference/components/form-components#complex-repeater-items","Complex Repeater Items",[314,10658,1608],"\u003Ctemplate>\n  \u003CCroutonFormRepeater\n    v-model=\"addresses\"\n    component-name=\"AddressForm\"\n    add-label=\"Add Address\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst addresses = ref([\n  {\n    id: '1',\n    street: '123 Main St',\n    city: 'New York',\n    state: 'NY',\n    zip: '10001'\n  }\n])\n\u003C/script>\n\n\u003C!-- AddressForm.vue -->\n\u003Ctemplate>\n  \u003Cdiv class=\"space-y-2\">\n    \u003CUInput v-model=\"localValue.street\" placeholder=\"Street\" />\n    \u003Cdiv class=\"grid grid-cols-3 gap-2\">\n      \u003CUInput v-model=\"localValue.city\" placeholder=\"City\" />\n      \u003CUInput v-model=\"localValue.state\" placeholder=\"State\" />\n      \u003CUInput v-model=\"localValue.zip\" placeholder=\"ZIP\" />\n    \u003C/div>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{ modelValue: any }>()\nconst emit = defineEmits\u003C{ 'update:modelValue': [value: any] }>()\n\nconst localValue = computed({\n  get: () => props.modelValue,\n  set: (val) => emit('update:modelValue', val)\n})\n\u003C/script>",{"id":10692,"title":10693,"titles":10694,"content":10695,"level":748},"/api-reference/components/form-components#empty-state","Empty State",[314,10658,1608],"\u003Ctemplate>\n  \u003CCroutonFormRepeater\n    v-model=\"emptyList\"\n    component-name=\"ItemForm\"\n    add-label=\"Add First Item\"\n  />\n  \u003C!-- Shows: \"No items yet. Click 'Add First Item' to get started.\" -->\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst emptyList = ref([])\n\u003C/script>",{"id":10697,"title":10698,"titles":10699,"content":10700,"level":449},"/api-reference/components/form-components#drag-handle","Drag Handle",[314,10658],"When sortable: true, each item card shows a drag handle button: Icon: i-lucide-grip-verticalClass: drag-handle (required for sortable)Cursor: cursor-move",{"id":10702,"title":10703,"titles":10704,"content":10705,"level":449},"/api-reference/components/form-components#item-card-structure","Item Card Structure",[314,10658],"Each repeater item is wrapped in a UCard with: Drag handle (if sortable)Remove button (always shown)Component slot for item content \u003C!-- Internal structure -->\n\u003CUCard>\n  \u003Cdiv class=\"flex gap-2 justify-between\">\n    \u003CUButton v-if=\"sortable\" class=\"drag-handle\" />\n    \u003CUButton color=\"error\" @click=\"removeItem\" />\n  \u003C/div>\n  \u003Ccomponent :is=\"componentName\" v-model=\"item\" />\n\u003C/UCard>",{"id":10707,"title":3113,"titles":10708,"content":10709,"level":449},"/api-reference/components/form-components#component-requirements",[314,10658],"The component specified in componentName must: Accept modelValue propEmit update:modelValue eventBe globally registered or auto-imported \u003C!-- ✅ Correct -->\n\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{ modelValue: any }>()\ndefineEmits\u003C{ 'update:modelValue': [value: any] }>()\n\u003C/script>\n\n\u003C!-- ❌ Wrong -->\n\u003Cscript setup lang=\"ts\">\n// Missing modelValue/emit - won't work with repeater\n\u003C/script> Auto-Import: Components must be registered globally or available via Nuxt auto-imports. Check console for warnings if component resolution fails.",{"id":10711,"title":10712,"titles":10713,"content":10714,"level":391},"/api-reference/components/form-components#croutonusersavatarupload","CroutonUsersAvatarUpload",[314],"Specialized avatar upload component with file selection and removal.",{"id":10716,"title":4987,"titles":10717,"content":10718,"level":449},"/api-reference/components/form-components#props-21",[314,10712],"PropTypeDefaultDescriptionavatarSize'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl''3xl'Avatar display size",{"id":10720,"title":10721,"titles":10722,"content":10723,"level":449},"/api-reference/components/form-components#model","Model",[314,10712],"v-model: string | undefined  // Avatar URL (object URL or uploaded URL)",{"id":10725,"title":5367,"titles":10726,"content":10727,"level":449},"/api-reference/components/form-components#events-13",[314,10712],"EventPayloadDescriptionfile-selectedFile | nullEmitted when file is selected or removed",{"id":10729,"title":183,"titles":10730,"content":10731,"level":449},"/api-reference/components/form-components#features-17",[314,10712],"File Dialog: Uses VueUse useFileDialog for native file pickerImage Preview: Shows selected image immediatelyObject URLs: Creates object URLs for local previewUpload Icon: Shows upload icon when no imageChange/Remove: Buttons to change or remove avatarRing Border: Styled with ring borderFile Validation: Accepts image/* only",{"id":10733,"title":1608,"titles":10734,"content":528,"level":449},"/api-reference/components/form-components#usage-10",[314,10712],{"id":10736,"title":4173,"titles":10737,"content":10738,"level":748},"/api-reference/components/form-components#basic-usage-8",[314,10712,1608],"\u003Ctemplate>\n  \u003CUFormGroup label=\"Profile Picture\">\n    \u003CCroutonUsersAvatarUpload\n      v-model=\"avatarUrl\"\n      @file-selected=\"handleFileSelected\"\n    />\n  \u003C/UFormGroup>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst avatarUrl = ref\u003Cstring | undefined>(undefined)\n\nconst handleFileSelected = (file: File | null) => {\n  if (file) {\n    console.log('Selected file:', file.name, file.size)\n    // Upload file to server\n    uploadAvatar(file)\n  } else {\n    console.log('Avatar removed')\n  }\n}\n\u003C/script>",{"id":10740,"title":10741,"titles":10742,"content":10743,"level":748},"/api-reference/components/form-components#different-sizes","Different Sizes",[314,10712,1608],"\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003CCroutonUsersAvatarUpload v-model=\"small\" avatar-size=\"sm\" />\n    \u003CCroutonUsersAvatarUpload v-model=\"medium\" avatar-size=\"md\" />\n    \u003CCroutonUsersAvatarUpload v-model=\"large\" avatar-size=\"xl\" />\n    \u003CCroutonUsersAvatarUpload v-model=\"huge\" avatar-size=\"3xl\" />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst small = ref\u003Cstring>()\nconst medium = ref\u003Cstring>()\nconst large = ref\u003Cstring>()\nconst huge = ref\u003Cstring>()\n\u003C/script>",{"id":10745,"title":10746,"titles":10747,"content":10748,"level":748},"/api-reference/components/form-components#with-upload-to-server","With Upload to Server",[314,10712,1608],"\u003Ctemplate>\n  \u003CCroutonUsersAvatarUpload\n    v-model=\"avatarUrl\"\n    @file-selected=\"uploadToServer\"\n  />\n  \u003Cp v-if=\"uploading\">Uploading...\u003C/p>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst avatarUrl = ref\u003Cstring>()\nconst uploading = ref(false)\n\nconst uploadToServer = async (file: File | null) => {\n  if (!file) {\n    // Handle removal\n    await deleteAvatar()\n    avatarUrl.value = undefined\n    return\n  }\n\n  uploading.value = true\n  \n  const formData = new FormData()\n  formData.append('avatar', file)\n  \n  const response = await $fetch('/api/upload/avatar', {\n    method: 'POST',\n    body: formData\n  })\n  \n  avatarUrl.value = response.url\n  uploading.value = false\n}\n\u003C/script>",{"id":10750,"title":10751,"titles":10752,"content":10753,"level":748},"/api-reference/components/form-components#in-user-profile-form","In User Profile Form",[314,10712,1608],"\u003Ctemplate>\n  \u003CUForm :state=\"userForm\" @submit=\"saveProfile\">\n    \u003CCroutonUsersAvatarUpload\n      v-model=\"userForm.avatarUrl\"\n      avatar-size=\"2xl\"\n      @file-selected=\"handleAvatarChange\"\n    />\n    \n    \u003CUFormGroup label=\"Name\">\n      \u003CUInput v-model=\"userForm.name\" />\n    \u003C/UFormGroup>\n    \n    \u003CUFormGroup label=\"Email\">\n      \u003CUInput v-model=\"userForm.email\" type=\"email\" />\n    \u003C/UFormGroup>\n    \n    \u003CUButton type=\"submit\" color=\"primary\">\n      Save Profile\n    \u003C/UButton>\n  \u003C/UForm>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst userForm = ref({\n  name: '',\n  email: '',\n  avatarUrl: undefined,\n  avatarFile: null\n})\n\nconst handleAvatarChange = (file: File | null) => {\n  userForm.value.avatarFile = file\n}\n\nconst saveProfile = async () => {\n  // Upload avatar first if file selected\n  if (userForm.value.avatarFile) {\n    const uploadedUrl = await uploadAvatar(userForm.value.avatarFile)\n    userForm.value.avatarUrl = uploadedUrl\n  }\n  \n  // Save user profile\n  await $fetch('/api/users/profile', {\n    method: 'PUT',\n    body: userForm.value\n  })\n}\n\u003C/script>",{"id":10755,"title":10756,"titles":10757,"content":10758,"level":449},"/api-reference/components/form-components#button-states","Button States",[314,10712],"StateButtons ShownNo image\"Upload\" button onlyImage selected\"Change\" and \"Remove\" buttons",{"id":10760,"title":10761,"titles":10762,"content":10763,"level":449},"/api-reference/components/form-components#avatar-styling","Avatar Styling",[314,10712],"Icon: i-lucide-upload (when no image)Icon size: text-lgRing: ring-1 ring-neutral-200 dark:ring-neutral-800",{"id":10765,"title":10766,"titles":10767,"content":10768,"level":449},"/api-reference/components/form-components#file-acceptance","File Acceptance",[314,10712],"Only image files are accepted: accept: 'image/*'\n// Accepts: .jpg, .jpeg, .png, .gif, .webp, etc. Object URLs: The component creates blob URLs for preview. Remember to upload the actual File object to your server, not the object URL.",{"id":10770,"title":1007,"titles":10771,"content":10772,"level":391},"/api-reference/components/form-components#related-resources",[314],"Form Composables - Form state managementNuxt UI Form - Base form component documentationValidation Guide - Form validation patterns html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":305,"title":304,"titles":10774,"content":10775,"level":385},[],"Complete reference for all Nuxt Crouton UI components Nuxt Crouton provides a comprehensive set of UI components built on top of Nuxt UI. These components are designed for rapid CRUD interface development with built-in accessibility, responsiveness, and customization options.",{"id":10777,"title":603,"titles":10778,"content":528,"level":391},"/api-reference/components#quick-reference",[304],{"id":10780,"title":314,"titles":10781,"content":10782,"level":449},"/api-reference/components#form-components",[304,603],"Interactive form elements for data input with validation and dynamic behavior. See Form Components for complete CroutonForm documentation with all props, slots, and examples. ComponentPurposeCategoryCroutonFormMain form component with validationFormFormDynamicLoaderDynamically load form fieldsFormFormLayoutForm layout wrapperFormFormReferenceSelectSelect with reference dataFormCroutonDateDate picker fieldFormCroutonImageUploadImage upload fieldFormCroutonAvatarUploadAvatar upload fieldFormCroutonRepeaterRepeatable form fieldsFormCroutonAssetsPickerMulti-asset pickerFormCroutonAssetUploaderAsset upload componentFormCalendarCalendar pickerFormCroutonCalendarEnhanced calendarFormCroutonCalendarYearYear calendar viewFormCroutonFormActionButtonForm action buttonsFormCroutonFormDependentButtonGroupConditional button groupsFormCroutonFormDependentFieldLoaderLoad fields based on dependenciesFormCroutonFormDependentSelectOptionConditional select optionsFormCroutonFormExpandableSlideOverExpandable slideover formsFormCroutonFormDynamicLoaderDynamic form loaderFormCroutonFormLayoutForm layout componentFormCroutonFormReferenceSelectReference select fieldFormCroutonFormRepeaterForm repeater componentFormCroutonUsersAvatarUploadUser avatar uploadForm",{"id":10784,"title":325,"titles":10785,"content":10786,"level":449},"/api-reference/components#table-components",[304,603],"Data table components with sorting, filtering, and pagination. ComponentPurposeCategoryCroutonTableMain data table componentTableCroutonTableActionsTable action buttonsTableCroutonTableCheckboxTable row selectionTableCroutonTableHeaderTable header componentTableCroutonTablePaginationTable pagination controlsTableCroutonTableSearchTable search functionalityTable",{"id":10788,"title":179,"titles":10789,"content":10790,"level":449},"/api-reference/components#layout-components",[304,603],"Container and card components for organizing content. ComponentPurposeCategoryCroutonCollectionCollection list viewLayoutCroutonItemCardMiniCompact item cardLayoutCroutonDetailLayoutDetail page layoutLayoutCroutonListList layout componentLayoutCardMiniMini card componentLayoutCroutonDependentFieldCardMiniConditional mini cardsLayoutCroutonItemButtonsMiniMini card action buttons[Layout](/api-reference/components/layout-components#croutonitembu ttonsmini)CroutonItemDependentFieldConditional item fieldsLayoutCroutonUsersCardMiniUser mini cardLayout",{"id":10792,"title":321,"titles":10793,"content":10794,"level":449},"/api-reference/components#modal-components",[304,603],"Modal, slideover, and dialog components for overlay interfaces. ComponentPurposeCategoryCroutonButtonInteractive button with actionsModal",{"id":10796,"title":310,"titles":10797,"content":10798,"level":449},"/api-reference/components#content-components",[304,603],"Components for displaying rich text content, articles, and prose pages. ComponentPurposeCategoryCroutonContentPreviewTruncated content preview for tablesContentCroutonContentPageGeneric content page with prose stylingContentCroutonContentArticleBlog post / article layoutContent",{"id":10800,"title":329,"titles":10801,"content":10802,"level":449},"/api-reference/components#utility-components",[304,603],"Helper components for loading states, errors, and special behaviors. ComponentPurposeCategoryLoadingLoading state indicatorUtilityValidationErrorSummaryForm validation errorsUtilityCroutonCollectionViewerCollection data viewerUtilityCroutonLoadingLoading componentUtilityCroutonValidationErrorSummaryValidation error displayUtility",{"id":10804,"title":10805,"titles":10806,"content":10807,"level":391},"/api-reference/components#detailed-documentation","Detailed Documentation",[304],"Click any category below to view complete documentation with props, slots, events, examples, and customization options: Interactive form elements with validation and dynamic behaviorData tables with sorting, filtering, and paginationContainer and card components for organizing contentModal, slideover, and dialog overlay interfacesRich text content, articles, and prose page layoutsLoading states, errors, and helper components",{"id":10809,"title":1007,"titles":10810,"content":10811,"level":391},"/api-reference/components#related-resources",[304],"Nuxt UI Components - Base component library documentationNuxt UI Pro - Advanced UI components and templatesComposables Reference - Composables for component logicTypeScript Types - Component prop types and interfacesCustomization Guide - Customize component appearance and behavior",{"id":318,"title":179,"titles":10813,"content":10814,"level":385},[],"Container and card components for organizing content Query Examples: For complete useCollectionQuery patterns (basic, filtering, pagination, sorting, relations), see Querying Data.",{"id":10816,"title":10817,"titles":10818,"content":10819,"level":391},"/api-reference/components/layout-components#croutoncollection","CroutonCollection",[179],"The unified collection display component that supports multiple layout modes (table, list, grid, cards) with responsive breakpoint support. This is the recommended component for displaying collection data. CroutonCollection is the primary component for rendering collections, with responsive layouts, custom card components, and grid/cards support.",{"id":10821,"title":4987,"titles":10822,"content":10823,"level":449},"/api-reference/components/layout-components#props",[179,10817],"interface CollectionProps {\n  // Layout Configuration\n  layout?: LayoutType | ResponsiveLayout | keyof typeof layoutPresets\n\n  // Data\n  rows?: any[]\n  columns?: TableColumn[]\n  collection: string\n\n  // Pagination\n  serverPagination?: boolean\n  paginationData?: PaginationData | null\n  refreshFn?: () => Promise\u003Cvoid> | null\n\n  // UI Options\n  create?: boolean\n  hideDefaultColumns?: {\n    createdAt?: boolean\n    updatedAt?: boolean\n    createdBy?: boolean\n    updatedBy?: boolean\n    select?: boolean\n    presence?: boolean\n    actions?: boolean\n  }\n}\n\n// Layout Types\ntype LayoutType = 'table' | 'list' | 'grid' | 'tree' | 'kanban' | 'workspace'\n\ninterface ResponsiveLayout {\n  base: LayoutType\n  sm?: LayoutType\n  md?: LayoutType\n  lg?: LayoutType\n  xl?: LayoutType\n  '2xl'?: LayoutType\n}\n\n// Layout Presets\nconst layoutPresets = {\n  'responsive': { base: 'list', md: 'grid', lg: 'table' },\n  'mobile-friendly': { base: 'list', lg: 'table' },\n  'compact': { base: 'list', xl: 'table' }\n}",{"id":10825,"title":9908,"titles":10826,"content":10827,"level":748},"/api-reference/components/layout-components#prop-details",[179,10817,4987],"PropTypeDefaultDescriptionlayoutLayoutType | ResponsiveLayout | string'table'Layout mode, responsive object, or preset namerowsany[][]Data rows to displaycolumnsTableColumn[][]Column definitionscollectionstringrequiredCollection name for card resolution and actionsserverPaginationbooleanfalseEnable server-side paginationpaginationDataPaginationData | nullnullPagination metadatarefreshFn() => Promise\u003Cvoid>undefinedFunction to refresh datacreatebooleanfalseShow create button in headerhideDefaultColumnsobject{}Hide specific default columns",{"id":10829,"title":5372,"titles":10830,"content":528,"level":449},"/api-reference/components/layout-components#slots",[179,10817],{"id":10832,"title":10833,"titles":10834,"content":10835,"level":748},"/api-reference/components/layout-components#header-scoped","header (Scoped)",[179,10817,5372],"Customize the header content. By default, shows CroutonTableHeader with optional create button. \u003Ctemplate #header>\n  \u003Cdiv class=\"flex items-center justify-between p-4\">\n    \u003Ch2 class=\"text-xl font-bold\">My Custom Header\u003C/h2>\n    \u003CUButton @click=\"exportData\">Export\u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":10837,"title":10838,"titles":10839,"content":10840,"level":748},"/api-reference/components/layout-components#dynamic-data-slots-pass-through","Dynamic Data Slots (Pass-through)",[179,10817,5372],"All other slots are passed through to child components for custom cell rendering: \u003Ctemplate #location-cell=\"{ row }\">\n  \u003CCroutonItemCardMini\n    :id=\"row.original.location\"\n    collection=\"locations\"\n  />\n\u003C/template>\n\n\u003Ctemplate #date-cell=\"{ row }\">\n  \u003CCroutonDate :date=\"row.original.date\" />\n\u003C/template>",{"id":10842,"title":4173,"titles":10843,"content":10844,"level":449},"/api-reference/components/layout-components#basic-usage",[179,10817],"\u003Ctemplate>\n  \u003CCroutonCollection\n    layout=\"table\"\n    collection=\"bookings\"\n    :columns=\"columns\"\n    :rows=\"bookings\"\n    create\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { items: bookings, pending } = await useCollectionQuery('bookings')\n\nconst columns = [\n  { accessorKey: 'name', header: 'Name' },\n  { accessorKey: 'email', header: 'Email' },\n  { accessorKey: 'date', header: 'Date' }\n]\n\u003C/script>",{"id":10846,"title":10847,"titles":10848,"content":528,"level":449},"/api-reference/components/layout-components#layout-modes","Layout Modes",[179,10817],{"id":10850,"title":10851,"titles":10852,"content":10853,"level":748},"/api-reference/components/layout-components#table-layout","Table Layout",[179,10817,10847],"Ideal for data-dense views with sorting, filtering, and pagination: \u003CCroutonCollection\n  layout=\"table\"\n  collection=\"users\"\n  :columns=\"columns\"\n  :rows=\"users\"\n/>",{"id":10855,"title":157,"titles":10856,"content":10857,"level":748},"/api-reference/components/layout-components#list-layout",[179,10817,10847],"Optimized for mobile devices with automatic field detection: \u003CCroutonCollection\n  layout=\"list\"\n  collection=\"bookings\"\n  :columns=\"columns\"\n  :rows=\"bookings\"\n/> Auto-detected fields (priority order): Title: name, title, label, email, username, idSubtitle: description, email, username, role, createdAtAvatar: avatar, image, avatarUrl, profileImage",{"id":10859,"title":10860,"titles":10861,"content":10862,"level":748},"/api-reference/components/layout-components#grid-layout","Grid Layout",[179,10817,10847],"CSS grid with 2-4 columns depending on screen size: \u003CCroutonCollection\n  layout=\"grid\"\n  collection=\"products\"\n  :columns=\"columns\"\n  :rows=\"products\"\n/> Renders: grid-cols-2 md:grid-cols-3 lg:grid-cols-4",{"id":10864,"title":10865,"titles":10866,"content":10867,"level":748},"/api-reference/components/layout-components#cards-layout","Cards Layout",[179,10817,10847],"Card-based layout with 1-3 columns: \u003CCroutonCollection\n  layout=\"cards\"\n  collection=\"projects\"\n  :columns=\"columns\"\n  :rows=\"projects\"\n/> Renders: grid-cols-1 md:grid-cols-2 lg:grid-cols-3",{"id":10869,"title":4303,"titles":10870,"content":528,"level":449},"/api-reference/components/layout-components#responsive-layouts",[179,10817],{"id":10872,"title":10873,"titles":10874,"content":10875,"level":748},"/api-reference/components/layout-components#using-responsive-object","Using Responsive Object",[179,10817,4303],"Define different layouts for different screen sizes: \u003CCroutonCollection\n  :layout=\"{\n    base: 'list',    // Mobile\n    md: 'grid',      // Tablet\n    lg: 'table'      // Desktop\n  }\"\n  collection=\"locations\"\n  :columns=\"columns\"\n  :rows=\"locations\"\n/>",{"id":10877,"title":8793,"titles":10878,"content":10879,"level":748},"/api-reference/components/layout-components#using-layout-presets",[179,10817,4303],"Choose from predefined responsive patterns: \u003C!-- Preset: 'responsive' (list → grid → table) -->\n\u003CCroutonCollection\n  layout=\"responsive\"\n  collection=\"bookings\"\n  :columns=\"columns\"\n  :rows=\"bookings\"\n/>\n\n\u003C!-- Preset: 'mobile-friendly' (list → table) -->\n\u003CCroutonCollection\n  layout=\"mobile-friendly\"\n  collection=\"users\"\n  :columns=\"columns\"\n  :rows=\"users\"\n/>\n\n\u003C!-- Preset: 'compact' (list → table at xl) -->\n\u003CCroutonCollection\n  layout=\"compact\"\n  collection=\"activities\"\n  :columns=\"columns\"\n  :rows=\"activities\"\n/>",{"id":10881,"title":4308,"titles":10882,"content":10883,"level":449},"/api-reference/components/layout-components#custom-card-components",[179,10817],"For list, grid, and cards layouts, CroutonCollection automatically looks for custom card components matching your collection name: Expected file location: layers/{layer}/collections/{collection}/app/components/Card.vue Example Card Component: \u003C!-- layers/bookings/collections/bookings/app/components/Card.vue -->\n\u003Cscript setup lang=\"ts\">\ninterface Props {\n  item: any\n  layout: 'list' | 'grid' | 'kanban'\n  collection: string\n}\n\nconst props = defineProps\u003CProps>()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- List Layout (compact) -->\n  \u003Cdiv v-if=\"layout === 'list'\" class=\"flex items-center gap-3 p-3\">\n    \u003CUAvatar :src=\"item.avatar\" :alt=\"item.name\" />\n    \u003Cdiv>\n      \u003Cdiv class=\"font-semibold\">{{ item.name }}\u003C/div>\n      \u003Cdiv class=\"text-sm text-gray-500\">{{ item.email }}\u003C/div>\n    \u003C/div>\n  \u003C/div>\n\n  \u003C!-- Grid Layout (medium cards) -->\n  \u003CUCard v-else-if=\"layout === 'grid'\">\n    \u003Cdiv class=\"space-y-2\">\n      \u003Ch3 class=\"font-semibold\">{{ item.name }}\u003C/h3>\n      \u003Cp class=\"text-sm text-gray-600\">{{ item.description }}\u003C/p>\n    \u003C/div>\n  \u003C/UCard>\n\n  \u003C!-- Cards Layout (large cards) -->\n  \u003CUCard v-else-if=\"layout === 'kanban'\">\n    \u003Cdiv class=\"space-y-3\">\n      \u003CUAvatar :src=\"item.avatar\" :alt=\"item.name\" size=\"lg\" />\n      \u003Ch3 class=\"text-lg font-bold\">{{ item.name }}\u003C/h3>\n      \u003Cp>{{ item.description }}\u003C/p>\n      \u003Cdiv class=\"flex gap-2\">\n        \u003CUBadge>{{ item.status }}\u003C/UBadge>\n        \u003CUBadge color=\"gray\">{{ item.role }}\u003C/UBadge>\n      \u003C/div>\n    \u003C/div>\n  \u003C/UCard>\n\u003C/template> Component Resolution: // Collection: \"bookingsBookings\"\n// Resolves to: \"BookingsBookingsCard\"\n// Expected: layers/bookings/collections/bookings/app/components/Card.vue If no custom card component is found, CroutonCollection displays helpful developer guidance with the expected file path and example code structure.",{"id":10885,"title":10886,"titles":10887,"content":10888,"level":449},"/api-reference/components/layout-components#server-pagination","Server Pagination",[179,10817],"Enable server-side pagination for large datasets: \u003Ctemplate>\n  \u003CCroutonCollection\n    layout=\"table\"\n    collection=\"users\"\n    :columns=\"columns\"\n    :rows=\"users\"\n    server-pagination\n    :pagination-data=\"paginationData\"\n    :refresh-fn=\"refreshUsers\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst pageSize = ref(10)\n\nconst { data: response, pending, refresh: refreshUsers } = await useFetch('/api/users', {\n  query: { page, pageSize }\n})\n\nconst users = computed(() => response.value?.items || [])\nconst paginationData = computed(() => ({\n  currentPage: response.value?.page || 1,\n  pageSize: response.value?.pageSize || 10,\n  totalItems: response.value?.total || 0,\n  totalPages: Math.ceil((response.value?.total || 0) / pageSize.value)\n}))\n\nconst columns = [\n  { accessorKey: 'name', header: 'Name' },\n  { accessorKey: 'email', header: 'Email' }\n]\n\u003C/script>",{"id":10890,"title":10891,"titles":10892,"content":10893,"level":449},"/api-reference/components/layout-components#custom-cell-rendering","Custom Cell Rendering",[179,10817],"Use scoped slots for custom cell content: \u003CCroutonCollection\n  layout=\"table\"\n  collection=\"bookings\"\n  :columns=\"columns\"\n  :rows=\"bookings\"\n>\n  \u003C!-- Related entity display -->\n  \u003Ctemplate #location-cell=\"{ row }\">\n    \u003CCroutonItemCardMini\n      :id=\"row.original.location\"\n      collection=\"locations\"\n    />\n  \u003C/template>\n\n  \u003C!-- Date formatting -->\n  \u003Ctemplate #date-cell=\"{ row }\">\n    \u003CCroutonDate :date=\"row.original.date\" format=\"long\" />\n  \u003C/template>\n\n  \u003C!-- Status badge -->\n  \u003Ctemplate #status-cell=\"{ row }\">\n    \u003CUBadge\n      :color=\"row.original.status === 'active' ? 'green' : 'gray'\"\n    >\n      {{ row.original.status }}\n    \u003C/UBadge>\n  \u003C/template>\n\u003C/CroutonCollection>",{"id":10895,"title":10896,"titles":10897,"content":10898,"level":449},"/api-reference/components/layout-components#multi-collection-dashboard","Multi-Collection Dashboard",[179,10817],"Display multiple collections side by side.",{"id":10900,"title":10901,"titles":10902,"content":10903,"level":748},"/api-reference/components/layout-components#single-collection-dashboard","Single Collection Dashboard",[179,10817,10896],"\u003Ctemplate>\n  \u003CCroutonCollection\n    layout=\"list\"\n    collection=\"bookings\"\n    :columns=\"columns\"\n    :rows=\"recentBookings\"\n    create\n  >\n    \u003Ctemplate #header>\n      \u003Cdiv class=\"p-4\">\n        \u003Ch2 class=\"font-bold\">Recent Bookings\u003C/h2>\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonCollection>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { items: bookings } = await useCollectionQuery('bookings')\nconst recentBookings = computed(() => bookings.value.slice(0, 8))\n\nconst columns = [\n  { accessorKey: 'name', header: 'Name' },\n  { accessorKey: 'date', header: 'Date' }\n]\n\u003C/script>",{"id":10905,"title":10896,"titles":10906,"content":10907,"level":748},"/api-reference/components/layout-components#multi-collection-dashboard-1",[179,10817,10896],"\u003Ctemplate>\n  \u003Cdiv class=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n    \u003CCroutonCollection\n      layout=\"list\"\n      collection=\"bookings\"\n      :columns=\"bookingsColumns\"\n      :rows=\"recentBookings\"\n      create\n    >\n      \u003Ctemplate #header>\n        \u003Cdiv class=\"p-4\">\u003Ch2 class=\"font-bold\">Recent Bookings\u003C/h2>\u003C/div>\n      \u003C/template>\n    \u003C/CroutonCollection>\n\n    \u003CCroutonCollection\n      layout=\"list\"\n      collection=\"locations\"\n      :columns=\"locationsColumns\"\n      :rows=\"activeLocations\"\n    >\n      \u003Ctemplate #header>\n        \u003Cdiv class=\"p-4\">\u003Ch2 class=\"font-bold\">Active Locations\u003C/h2>\u003C/div>\n      \u003C/template>\n    \u003C/CroutonCollection>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { items: bookings } = await useCollectionQuery('bookings')\nconst { items: locations } = await useCollectionQuery('locations')\n\nconst recentBookings = computed(() => bookings.value.slice(0, 8))\nconst activeLocations = computed(() => locations.value.slice(0, 8))\n\nconst bookingsColumns = [{ accessorKey: 'name', header: 'Name' }]\nconst locationsColumns = [{ accessorKey: 'name', header: 'Location' }]\n\u003C/script>",{"id":10909,"title":10910,"titles":10911,"content":10912,"level":449},"/api-reference/components/layout-components#hide-default-columns","Hide Default Columns",[179,10817],"Control visibility of automatically-added columns: \u003CCroutonCollection\n  layout=\"table\"\n  collection=\"products\"\n  :columns=\"columns\"\n  :rows=\"products\"\n  :hide-default-columns=\"{\n    createdAt: true,\n    updatedAt: true,\n    createdBy: true,\n    updatedBy: true,\n    actions: false  // Keep actions column\n  }\"\n/>",{"id":10914,"title":10915,"titles":10916,"content":10917,"level":449},"/api-reference/components/layout-components#create-button-integration","Create Button Integration",[179,10817],"The create prop adds a button that automatically opens the appropriate form modal: \u003CCroutonCollection\n  layout=\"table\"\n  collection=\"bookings\"\n  :columns=\"columns\"\n  :rows=\"bookings\"\n  create\n/> This internally calls: const { open } = useCrouton()\nopen('create', 'bookings')",{"id":10919,"title":3195,"titles":10920,"content":10921,"level":449},"/api-reference/components/layout-components#complete-example",[179,10817],"For a complete working example demonstrating layout switching, search/filters, custom cell renderers, and avatar groups, see this interactive demo: View Full Interactive Demo →Fork the demo to experiment with different configurations. The complete example includes:Layout switching (table/grid/list)Search and filter integrationCustom cell renderers (status badges, date formatting)CroutonItemCardMini for related itemsAvatar groups for attendeesClient-side filtering with server pagination",{"id":10923,"title":10924,"titles":10925,"content":10926,"level":748},"/api-reference/components/layout-components#focused-example-layout-switching-and-filters","Focused Example: Layout Switching and Filters",[179,10817,3195],"This snippet shows the key pattern for combining layout switching with client-side filtering: \u003Cscript setup lang=\"ts\">\nconst currentLayout = ref\u003CLayoutType>('table')\nconst searchQuery = ref('')\nconst statusFilter = ref('all')\n\nconst { items: bookings, refresh } = await useCollectionQuery('bookings')\n\nconst filteredBookings = computed(() => {\n  let result = bookings.value\n  if (searchQuery.value) {\n    result = result.filter(b => b.name.toLowerCase().includes(searchQuery.value))\n  }\n  if (statusFilter.value !== 'all') {\n    result = result.filter(b => b.status === statusFilter.value)\n  }\n  return result\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection\n    :layout=\"currentLayout\"\n    collection=\"bookings\"\n    :rows=\"filteredBookings\"\n    server-pagination\n  >\n    \u003Ctemplate #header>\n      \u003Cdiv class=\"flex items-center justify-between p-4\">\n        \u003CUInput v-model=\"searchQuery\" placeholder=\"Search...\" />\n        \u003CUButtonGroup>\n          \u003CUButton\n            :variant=\"currentLayout === 'table' ? 'solid' : 'outline'\"\n            @click=\"currentLayout = 'table'\"\n            icon=\"i-lucide-table\"\n          />\n          \u003C!-- Grid and list buttons... -->\n        \u003C/UButtonGroup>\n      \u003C/div>\n    \u003C/template>\n    \u003C!-- See interactive demo for custom cell renderers -->\n  \u003C/CroutonCollection>\n\u003C/template>",{"id":10928,"title":10929,"titles":10930,"content":10931,"level":449},"/api-reference/components/layout-components#integration-with-collection-system","Integration with Collection System",[179,10817],"CroutonCollection integrates seamlessly with the collection architecture: \u003Cscript setup lang=\"ts\">\n// 1. Fetch data with useCollectionQuery\nconst { items, pending, refresh } = await useCollectionQuery('bookings')\n\n// 2. Get collection configuration\nconst collections = useCollections()\nconst config = collections.getConfig('bookings')\n\n// 3. Use configured columns or define custom ones\nconst columns = config.columns || [\n  { accessorKey: 'name', header: 'Name' },\n  { accessorKey: 'date', header: 'Date' }\n]\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- 4. Render with CroutonCollection -->\n  \u003CCroutonCollection\n    layout=\"table\"\n    collection=\"bookings\"\n    :columns=\"columns\"\n    :rows=\"items\"\n    :refresh-fn=\"refresh\"\n    create\n  />\n\u003C/template>",{"id":10933,"title":36,"titles":10934,"content":528,"level":449},"/api-reference/components/layout-components#troubleshooting",[179,10817],{"id":10936,"title":10937,"titles":10938,"content":10939,"level":748},"/api-reference/components/layout-components#custom-card-not-showing","Custom Card Not Showing",[179,10817,36],"If your custom card component isn't being used: Check file location: Must be layers/{layer}/collections/{collection}/app/components/Card.vueCheck component name: Must export as {PascalCaseCollection}CardCheck layout prop: Custom cards only work for list, grid, and cards layoutsCheck console: CroutonCollection logs which component it's trying to resolve",{"id":10941,"title":10942,"titles":10943,"content":10944,"level":748},"/api-reference/components/layout-components#responsive-layout-not-working","Responsive Layout Not Working",[179,10817,36],"If responsive layouts aren't switching: Check Tailwind config: Ensure breakpoints are configured correctlyTest breakpoints: Use useBreakpoints(breakpointsTailwind) to debugVerify layout object: Must follow ResponsiveLayout interfaceCheck base layout: The base property is required for responsive layouts",{"id":10946,"title":10947,"titles":10948,"content":10949,"level":748},"/api-reference/components/layout-components#pagination-not-updating","Pagination Not Updating",[179,10817,36],"If pagination doesn't trigger data refresh: Provide refreshFn: Required for pagination to workEnable server pagination: Set server-pagination prop to trueProvide pagination data: Must include currentPage, pageSize, totalItemsCheck API response: Ensure it returns proper pagination metadata",{"id":10951,"title":10952,"titles":10953,"content":10954,"level":391},"/api-reference/components/layout-components#croutonitemcardmini","CroutonItemCardMini",[179],"A smart component that fetches and displays a referenced collection item with quick-edit functionality. Supports custom card components via naming convention. Auto-registration: This component is globally available as CroutonItemCardMini and automatically used in table cells for reference fields. Data Fetching: This component uses useCollectionItem internally. See the useCollectionItem API Reference for details on caching, reactivity, and error handling.",{"id":10956,"title":4987,"titles":10957,"content":10958,"level":449},"/api-reference/components/layout-components#props-1",[179,10952],"interface ItemCardMiniProps {\n  id: string                       // Item ID to fetch and display (required)\n  collection: string               // Collection name (required)\n}",{"id":10960,"title":10961,"titles":10962,"content":10963,"level":449},"/api-reference/components/layout-components#props-details","Props Details",[179,10952],"PropTypeDefaultDescriptionidstringrequiredUnique identifier of the item to fetchcollectionstringrequiredCollection name (e.g., 'users', 'locations')",{"id":10965,"title":183,"titles":10966,"content":528,"level":449},"/api-reference/components/layout-components#features",[179,10952],{"id":10968,"title":10969,"titles":10970,"content":10971,"level":748},"/api-reference/components/layout-components#dynamic-component-resolution","Dynamic Component Resolution",[179,10952,183],"CroutonItemCardMini uses a plugin architecture to allow collection-specific customization: Naming convention: Collection: \"bookingsLocations\"\nResolves to: \"CroutonBookingsLocationsCardMini\"\nExpected file: layers/{layer}/collections/bookingsLocations/app/components/CardMini.vue Resolution flow: Converts collection name to PascalCase with 'Crouton' prefixChecks Vue component registry for custom componentFalls back to default badge display if not found",{"id":10973,"title":10974,"titles":10975,"content":10976,"level":748},"/api-reference/components/layout-components#default-display-fallback","Default Display (Fallback)",[179,10952,183],"When no custom component exists: Badge: Shows item.title in a neutral badgeLoading: Skeleton loader while fetchingError: Red \"Error loading\" messageHover interaction: Edit button appears on hover",{"id":10978,"title":10979,"titles":10980,"content":10981,"level":748},"/api-reference/components/layout-components#data-fetching","Data Fetching",[179,10952,183],"Uses useCollectionItem() composableReactive ID parameter (prevents SSR hydration mismatches)Auto-refresh when ID changesCaching via Nuxt's built-in system",{"id":10983,"title":4173,"titles":10984,"content":10985,"level":449},"/api-reference/components/layout-components#basic-usage-1",[179,10952],"In table cell: \u003Ctemplate #location-cell=\"{ row }\">\n  \u003CCroutonItemCardMini\n    v-if=\"row.original.location\"\n    :id=\"row.original.location\"\n    collection=\"bookingsLocations\"\n  />\n\u003C/template> Multiple references (array): \u003Ctemplate #tags-cell=\"{ row }\">\n  \u003Cdiv v-if=\"row.original.tags && row.original.tags.length > 0\" class=\"flex flex-wrap gap-1\">\n    \u003CCroutonItemCardMini\n      v-for=\"itemId in row.original.tags\"\n      :key=\"itemId\"\n      :id=\"itemId\"\n      collection=\"tags\"\n    />\n  \u003C/div>\n  \u003Cspan v-else class=\"text-gray-400\">—\u003C/span>\n\u003C/template> Read-only form field: \u003Ctemplate>\n  \u003CUFormField label=\"Location\" name=\"location\">\n    \u003CCroutonItemCardMini\n      v-if=\"state.location\"\n      :id=\"state.location\"\n      collection=\"bookingsLocations\"\n    />\n    \u003Cspan v-else class=\"text-gray-400 text-sm\">Not set\u003C/span>\n  \u003C/UFormField>\n\u003C/template>",{"id":10987,"title":64,"titles":10988,"content":10989,"level":449},"/api-reference/components/layout-components#creating-custom-cardmini-components",[179,10952],"Create collection-specific card components for richer displays: File location: layers/{layer}/collections/{collection}/app/components/CardMini.vue Example: UsersCardMini.vue \u003Cscript setup lang=\"ts\">\ninterface UserItem {\n  title?: string\n  name?: string\n  avatarUrl?: string\n}\n\ninterface Props {\n  item?: UserItem\n  name?: boolean\n}\n\nconst props = withDefaults(defineProps\u003CProps>(), {\n  name: false\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"item\" class=\"w-full flex gap-2\">\n    \u003CUTooltip\n      :text=\"item.title || item.name\"\n      :delay-duration=\"0\"\n    >\n      \u003CUAvatar\n        :src=\"item.avatarUrl || ''\"\n        :alt=\"item.title || item.name\"\n        size=\"xs\"\n        class=\"ring-2 ring-neutral-200 dark:ring-white/10\"\n      />\n    \u003C/UTooltip>\n    \u003Cspan v-if=\"name\" class=\"\">{{ item.name }}\u003C/span>\n  \u003C/div>\n\u003C/template> Example: LocationCardMini.vue \u003Cscript setup lang=\"ts\">\ninterface Props {\n  item?: {\n    title?: string\n    address?: string\n    city?: string\n  }\n}\n\nconst props = defineProps\u003CProps>()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"item\" class=\"flex items-center gap-2\">\n    \u003CUIcon name=\"i-lucide-map-pin\" class=\"text-gray-400\" />\n    \u003Cdiv>\n      \u003Cdiv class=\"font-medium text-sm\">{{ item.title }}\u003C/div>\n      \u003Cdiv class=\"text-xs text-gray-500\">{{ item.city }}\u003C/div>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":10991,"title":10992,"titles":10993,"content":10994,"level":449},"/api-reference/components/layout-components#custom-component-props","Custom Component Props",[179,10952],"Custom CardMini components receive these props automatically: interface CustomCardMiniProps {\n  item: any                           // Fetched item data\n  pending: Ref\u003Cboolean>               // Loading state\n  error: Ref\u003Cany>                     // Error state\n  id: string                          // Item ID\n  collection: string                  // Collection name\n  refresh: () => Promise\u003Cvoid>        // Refresh function\n}",{"id":10996,"title":10997,"titles":10998,"content":10999,"level":449},"/api-reference/components/layout-components#hover-interactions","Hover Interactions",[179,10952],"The default display includes smooth hover effects: \u003C!-- Normal state -->\n\u003CUBadge>{{ item.title }}\u003C/UBadge>\n\n\u003C!-- Hover state -->\n\u003Cdiv class=\"group\">\n  \u003CUBadge>{{ item.title }}\u003C/UBadge>\n  \u003Cdiv class=\"absolute -top-6 transition-all group-hover:scale-110\">\n    \u003CUButton\n      icon=\"i-lucide-pencil\"\n      @click=\"open('update', collection, [id])\"\n    />\n  \u003C/div>\n\u003C/div> Transition: Button slides from -top-1 to -top-6 on hoverButton scales to 110%150ms delay, 300ms durationEasing: ease-in-out",{"id":11001,"title":3195,"titles":11002,"content":11003,"level":449},"/api-reference/components/layout-components#complete-example-1",[179,10952],"Full implementation with custom components and error handling: \u003Ctemplate>\n  \u003CCroutonTable\n    collection=\"bookings\"\n    :columns=\"columns\"\n    :rows=\"bookings\"\n  >\n    \u003C!-- Single reference -->\n    \u003Ctemplate #location-cell=\"{ row }\">\n      \u003CCroutonItemCardMini\n        v-if=\"row.original.location\"\n        :id=\"row.original.location\"\n        collection=\"bookingsLocations\"\n      />\n      \u003Cspan v-else class=\"text-gray-400 text-sm\">No location\u003C/span>\n    \u003C/template>\n\n    \u003C!-- User reference (uses custom UsersCardMini) -->\n    \u003Ctemplate #assignedTo-cell=\"{ row }\">\n      \u003CCroutonItemCardMini\n        v-if=\"row.original.assignedTo\"\n        :id=\"row.original.assignedTo\"\n        collection=\"users\"\n      />\n    \u003C/template>\n\n    \u003C!-- Array of references -->\n    \u003Ctemplate #tags-cell=\"{ row }\">\n      \u003Cdiv class=\"flex flex-wrap gap-1\">\n        \u003CCroutonItemCardMini\n          v-for=\"tagId in row.original.tags\"\n          :key=\"tagId\"\n          :id=\"tagId\"\n          collection=\"tags\"\n        />\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { items: bookings } = await useCollectionQuery('bookings')\n\nconst columns = [\n  { accessorKey: 'name', header: 'Name' },\n  { accessorKey: 'location', header: 'Location' },\n  { accessorKey: 'assignedTo', header: 'Assigned To' },\n  { accessorKey: 'tags', header: 'Tags' }\n]\n\u003C/script>",{"id":11005,"title":10929,"titles":11006,"content":11007,"level":449},"/api-reference/components/layout-components#integration-with-collection-system-1",[179,10952],"Automatic generation: When you define a reference field in your schema: {\n  \"location\": {\n    \"type\": \"string\",\n    \"refTarget\": \"bookingsLocations\",\n    \"meta\": {\n      \"label\": \"Location\"\n    }\n  }\n} The generator automatically creates table cell slots with CroutonItemCardMini: \u003Ctemplate #location-cell=\"{ row }\">\n  \u003CCroutonItemCardMini\n    v-if=\"row.original.location\"\n    :id=\"row.original.location\"\n    collection=\"bookingsLocations\"\n  />\n\u003C/template>",{"id":11009,"title":11010,"titles":11011,"content":11012,"level":449},"/api-reference/components/layout-components#composables-used","Composables Used",[179,10952],"useCollectionItem() - Fetch individual item by IDuseCrouton() - Open edit modal on button clickuseNuxtApp() - Check component registry for custom components",{"id":11014,"title":36,"titles":11015,"content":528,"level":449},"/api-reference/components/layout-components#troubleshooting-1",[179,10952],{"id":11017,"title":11018,"titles":11019,"content":11020,"level":748},"/api-reference/components/layout-components#custom-cardmini-not-loading","Custom CardMini Not Loading",[179,10952,36],"If your custom component isn't being used: Check file location: Must be layers/{layer}/collections/{collection}/app/components/CardMini.vueCheck component name: Must match pattern Crouton{PascalCaseCollection}CardMiniCheck exports: Component must be properly exportedCheck console: Look for component resolution logs",{"id":11022,"title":11023,"titles":11024,"content":11025,"level":748},"/api-reference/components/layout-components#shows-error-loading","Shows \"Error loading\"",[179,10952,36],"If the component displays an error: Check ID: Verify the item ID existsCheck collection: Ensure collection name is correctCheck API: Verify API endpoint returns proper dataCheck permissions: Ensure user has read access",{"id":11027,"title":11028,"titles":11029,"content":11030,"level":748},"/api-reference/components/layout-components#hover-button-not-appearing","Hover Button Not Appearing",[179,10952,36],"If edit button doesn't show on hover: Check CSS: Ensure Tailwind classes are compiledCheck parent: Parent must not have overflow: hiddenCheck z-index: Button may be behind other elements",{"id":11032,"title":11033,"titles":11034,"content":11035,"level":391},"/api-reference/components/layout-components#croutondetaillayout","CroutonDetailLayout",[179],"A view-only layout component for displaying item details with optional edit functionality. New in v1.5.3. New in v1.5.3: DetailLayout provides a standardized structure for read-only detail pages with built-in loading and error states.",{"id":11037,"title":4987,"titles":11038,"content":11039,"level":449},"/api-reference/components/layout-components#props-2",[179,11033],"interface DetailLayoutProps {\n  item?: any                          // Item data to display\n  pending?: boolean                   // Loading state (default: false)\n  error?: string | null               // Error message (default: null)\n  title?: string                      // Header title (default: 'Details')\n  subtitle?: string                   // Header subtitle (default: '')\n  canEdit?: boolean                   // Show edit button (default: true)\n}",{"id":11041,"title":10961,"titles":11042,"content":11043,"level":449},"/api-reference/components/layout-components#props-details-1",[179,11033],"PropTypeDefaultDescriptionitemanynullThe data item to displaypendingbooleanfalseShows skeleton loaders when trueerrorstring | nullnullError message to displaytitlestring'Details'Main heading textsubtitlestring''Subheading text below titlecanEditbooleantrueWhether to show Edit button",{"id":11045,"title":5372,"titles":11046,"content":528,"level":449},"/api-reference/components/layout-components#slots-1",[179,11033],{"id":11048,"title":11049,"titles":11050,"content":11051,"level":748},"/api-reference/components/layout-components#header-title","header-title",[179,11033,5372],"Custom title content: \u003Ctemplate #header-title>\n  \u003Ch2 class=\"text-2xl font-bold\">{{ item.name }}\u003C/h2>\n\u003C/template>",{"id":11053,"title":11054,"titles":11055,"content":11056,"level":748},"/api-reference/components/layout-components#header-subtitle","header-subtitle",[179,11033,5372],"Custom subtitle content: \u003Ctemplate #header-subtitle>\n  \u003Cp class=\"text-sm text-gray-600\">\n    Created {{ formatDate(item.createdAt) }}\n  \u003C/p>\n\u003C/template>",{"id":11058,"title":11059,"titles":11060,"content":11061,"level":748},"/api-reference/components/layout-components#header-actions","header-actions",[179,11033,5372],"Custom header action buttons: \u003Ctemplate #header-actions>\n  \u003Cdiv class=\"flex gap-2\">\n    \u003CUButton\n      icon=\"i-lucide-share\"\n      variant=\"soft\"\n      @click=\"handleShare\"\n    >\n      Share\n    \u003C/UButton>\n    \u003CUButton\n      icon=\"i-lucide-pencil\"\n      color=\"primary\"\n      @click=\"handleEdit\"\n    >\n      Edit\n    \u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":11063,"title":11064,"titles":11065,"content":11066,"level":748},"/api-reference/components/layout-components#content-scoped","content (Scoped)",[179,11033,5372],"Main content area - receives the item as a scoped slot prop: \u003Ctemplate #content=\"{ item }\">\n  \u003Cdiv class=\"space-y-6\">\n    \u003Cdiv>\n      \u003Ch3 class=\"font-semibold mb-2\">Description\u003C/h3>\n      \u003Cp class=\"text-gray-600 dark:text-gray-400\">{{ item.description }}\u003C/p>\n    \u003C/div>\n\n    \u003Cdiv>\n      \u003Ch3 class=\"font-semibold mb-2\">Status\u003C/h3>\n      \u003CUBadge :color=\"getStatusColor(item.status)\">\n        {{ item.status }}\n      \u003C/UBadge>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":11068,"title":10442,"titles":11069,"content":11070,"level":748},"/api-reference/components/layout-components#footer-scoped",[179,11033,5372],"Optional footer content: \u003Ctemplate #footer=\"{ item }\">\n  \u003Cdiv class=\"text-xs text-gray-500\">\n    Last updated: {{ formatDate(item.updatedAt) }} by {{ item.updatedBy }}\n  \u003C/div>\n\u003C/template>",{"id":11072,"title":5367,"titles":11073,"content":11074,"level":449},"/api-reference/components/layout-components#events",[179,11033],"@edit=\"handleEdit\"  // Emitted when default Edit button clicked",{"id":11076,"title":4173,"titles":11077,"content":11078,"level":449},"/api-reference/components/layout-components#basic-usage-2",[179,11033],"Standalone detail page with basic setup: \u003Ctemplate>\n  \u003CCroutonDetailLayout\n    :item=\"booking\"\n    :pending=\"pending\"\n    :error=\"error\"\n    :title=\"booking?.name || 'Booking Details'\"\n    @edit=\"handleEdit\"\n  >\n    \u003Ctemplate #content=\"{ item }\">\n      \u003Cdiv class=\"space-y-6\">\n        \u003Cdiv>\n          \u003Ch3 class=\"font-semibold text-sm text-gray-500 mb-1\">Location\u003C/h3>\n          \u003CCroutonItemCardMini\n            :id=\"item.location\"\n            collection=\"locations\"\n          />\n        \u003C/div>\n\n        \u003Cdiv>\n          \u003Ch3 class=\"font-semibold text-sm text-gray-500 mb-1\">Date & Time\u003C/h3>\n          \u003CCroutonDate :date=\"item.date\" format=\"long\" />\n        \u003C/div>\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonDetailLayout>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\nconst { open } = useCrouton()\n\nconst { item: booking, pending, error } = await useCollectionItem(\n  'bookings',\n  computed(() => route.params.id as string)\n)\n\nconst handleEdit = () => {\n  open('update', 'bookings', [booking.value?.id])\n}\n\u003C/script> With custom formatting and status badges: \u003Ctemplate>\n  \u003CCroutonDetailLayout\n    :title=\"booking?.name\"\n    :subtitle=\"`Created ${formatDate(booking?.createdAt)}`\"\n  >\n    \u003Ctemplate #content=\"{ item }\">\n      \u003Cdiv>\n        \u003Ch3 class=\"font-semibold text-sm text-gray-500 mb-1\">Status\u003C/h3>\n        \u003CUBadge :color=\"getStatusColor(item.status)\">\n          {{ item.status }}\n        \u003C/UBadge>\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonDetailLayout>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst getStatusColor = (status: string) => {\n  const colors = {\n    confirmed: 'green',\n    pending: 'yellow',\n    cancelled: 'red'\n  }\n  return colors[status] || 'gray'\n}\n\nconst formatDate = (date: string) => {\n  return new Date(date).toLocaleDateString('en-US', {\n    month: 'long',\n    day: 'numeric',\n    year: 'numeric'\n  })\n}\n\u003C/script>",{"id":11080,"title":2973,"titles":11081,"content":11082,"level":449},"/api-reference/components/layout-components#loading-states",[179,11033],"Automatic skeleton loaders when pending={true}: \u003Ctemplate>\n  \u003CCroutonDetailLayout\n    :item=\"item\"\n    :pending=\"pending\"\n    :error=\"error\"\n    title=\"User Details\"\n  >\n    \u003C!-- Content slot -->\n  \u003C/CroutonDetailLayout>\n\u003C/template> Loading display: Title skeleton (w-48)Two subtitle skeletons (w-32, w-24)Large content skeleton (h-96)",{"id":11084,"title":10648,"titles":11085,"content":11086,"level":449},"/api-reference/components/layout-components#error-states",[179,11033],"Displays error panel when error prop is provided: \u003Ctemplate>\n  \u003CCroutonDetailLayout\n    :item=\"item\"\n    :pending=\"false\"\n    :error=\"errorMessage\"\n  >\n    \u003C!-- Content slot -->\n  \u003C/CroutonDetailLayout>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst errorMessage = ref\u003Cstring | null>(null)\n\ntry {\n  // Fetch item\n} catch (err) {\n  errorMessage.value = err.message\n}\n\u003C/script> Error display: Alert icon (i-lucide-octagon-alert)Error message in red panelDark mode support",{"id":11088,"title":11089,"titles":11090,"content":11091,"level":449},"/api-reference/components/layout-components#convention-based-loading","Convention-Based Loading",[179,11033],"DetailLayout integrates with FormDynamicLoader's naming convention: Pattern: Form component: \"BookingsForm.vue\"\nDetail component: \"BookingsDetail.vue\" FormDynamicLoader resolution (when action='view'): Looks for BookingsDetail.vueFalls back to BookingsForm.vue if not foundPasses activeItem and view-specific props Example Detail component: \u003C!-- layers/bookings/collections/bookings/app/components/Detail.vue -->\n\u003Ctemplate>\n  \u003CCroutonDetailLayout\n    :item=\"activeItem\"\n    :title=\"activeItem?.name || 'Booking Details'\"\n    @edit=\"handleEdit\"\n  >\n    \u003Ctemplate #content=\"{ item }\">\n      \u003C!-- Custom detail view -->\n    \u003C/template>\n  \u003C/CroutonDetailLayout>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\n// Receives props from FormDynamicLoader\nconst props = defineProps\u003C{\n  activeItem: any\n  loading: string\n  action: 'view' | 'create' | 'update'\n  items: any[]\n  collection: string\n}>()\n\nconst { open } = useCrouton()\n\nconst handleEdit = () => {\n  open('update', props.collection, [props.activeItem.id])\n}\n\u003C/script>",{"id":11093,"title":11094,"titles":11095,"content":11096,"level":449},"/api-reference/components/layout-components#view-edit-workflow","View → Edit Workflow",[179,11033],"Complete flow from view to edit mode: \u003Ctemplate>\n  \u003C!-- View button in table -->\n  \u003CCroutonItemButtonsMini\n    :view=\"true\"\n    :update=\"true\"\n    @view=\"handleView(item)\"\n    @update=\"handleUpdate(item)\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { open } = useCrouton()\n\n// 1. User clicks eye icon\nconst handleView = (item: any) => {\n  open('view', 'bookings', [item.id])\n  // Opens DetailLayout in slideover\n}\n\n// 2. User clicks Edit button in DetailLayout\n// DetailLayout emits @edit event\n\n// 3. Transition to Form component\nconst handleUpdate = (item: any) => {\n  open('update', 'bookings', [item.id])\n  // Opens Form in slideover for editing\n}\n\u003C/script>",{"id":11098,"title":3195,"titles":11099,"content":11100,"level":449},"/api-reference/components/layout-components#complete-example-2",[179,11033],"For a complete working example showing a full-featured detail page with custom header actions, multiple content sections, related item displays, and metadata footer, see this interactive demo: View Full Interactive Demo →Fork the demo to explore all CroutonDetailLayout customizations. The complete example includes:Custom header actions (Refresh, Edit, Delete)Multiple content sections (Basic Info, Description, Status, Related Items)Related item display with CroutonItemCardMiniStatus badges and progress indicatorsFooter metadata displayPermission-based editing",{"id":11102,"title":11103,"titles":11104,"content":11105,"level":748},"/api-reference/components/layout-components#focused-example-custom-header-actions","Focused Example: Custom Header Actions",[179,11033,3195],"This snippet shows the key pattern for adding custom action buttons to the detail page header: \u003Cscript setup lang=\"ts\">\nconst route = useRoute()\nconst { open } = useCrouton()\n\nconst { item: job, pending, refresh } = await useCollectionItem(\n  'discubotJobs',\n  computed(() => route.params.id as string)\n)\n\nconst canEdit = computed(() => job.value?.status !== 'completed')\n\nconst handleEdit = () => {\n  open('update', 'discubotJobs', [job.value?.id])\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonDetailLayout\n    :item=\"job\"\n    :pending=\"pending\"\n    :title=\"job?.name || 'Job Details'\"\n    :subtitle=\"`Status: ${job?.status}`\"\n    :can-edit=\"canEdit\"\n    @edit=\"handleEdit\"\n  >\n    \u003Ctemplate #header-actions>\n      \u003Cdiv class=\"flex gap-2\">\n        \u003CUButton icon=\"i-lucide-refresh-cw\" variant=\"soft\" @click=\"refresh\">\n          Refresh\n        \u003C/UButton>\n        \u003CUButton icon=\"i-lucide-pencil\" color=\"primary\" @click=\"handleEdit\">\n          Edit\n        \u003C/UButton>\n      \u003C/div>\n    \u003C/template>\n\n    \u003C!-- See interactive demo for complete content sections -->\n  \u003C/CroutonDetailLayout>\n\u003C/template>",{"id":11107,"title":11108,"titles":11109,"content":11110,"level":449},"/api-reference/components/layout-components#layout-structure","Layout Structure",[179,11033],"DetailLayout provides a three-section layout: ┌──────────────────────────────────────┐\n│ Header (sticky)                      │\n│  - Title                             │\n│  - Subtitle                          │\n│  - Actions (Edit button)             │\n├──────────────────────────────────────┤\n│                                      │\n│ Content (scrollable)                 │\n│                                      │\n│  \u003CYour custom content here>          │\n│                                      │\n│                                      │\n├──────────────────────────────────────┤\n│ Footer (optional)                    │\n│  - Metadata, timestamps, etc.        │\n└──────────────────────────────────────┘ CSS: Full height flex containerSticky header with borderScrollable content areaOptional footer (only renders if slot used)",{"id":11112,"title":36,"titles":11113,"content":528,"level":449},"/api-reference/components/layout-components#troubleshooting-2",[179,11033],{"id":11115,"title":11116,"titles":11117,"content":11118,"level":748},"/api-reference/components/layout-components#skeleton-loaders-wrong-size","Skeleton Loaders Wrong Size",[179,11033,36],"If skeleton doesn't match content: Customize skeletons: Override loading state with custom slotUse pending prop: Ensure pending is reactive and updates properlyProvide skeleton slot: Create custom loading template",{"id":11120,"title":11121,"titles":11122,"content":11123,"level":748},"/api-reference/components/layout-components#edit-button-not-working","Edit Button Not Working",[179,11033,36],"If edit button doesn't open form: Check @edit handler: Must call useCrouton().open()Check canEdit prop: May be set to falseCheck permissions: User may not have edit access",{"id":11125,"title":11126,"titles":11127,"content":11128,"level":748},"/api-reference/components/layout-components#content-not-scrolling","Content Not Scrolling",[179,11033,36],"If content area doesn't scroll: Check parent container: Must not have fixed heightCheck CSS: Ensure no overflow: hidden on parentsCheck content height: Content must exceed viewport",{"id":11130,"title":11131,"titles":11132,"content":11133,"level":385},"/api-reference/components/layout-components#form-system-components","Form System Components",[],"Nuxt Crouton provides a sophisticated form system that handles CRUD operations with multiple container types, dynamic component loading, validation, and complex field types. Form Architecture: Forms in Nuxt Crouton are managed globally via useCrouton() and rendered automatically in modals, slideovers, or dialogs. You don't manually place Form components in your templates.",{"id":11135,"title":10817,"titles":11136,"content":11137,"level":391},"/api-reference/components/layout-components#croutoncollection-1",[11131],"Display collection items in table or list layouts. Grid and cards layouts are planned for future releases.",{"id":11139,"title":4987,"titles":11140,"content":11141,"level":449},"/api-reference/components/layout-components#props-3",[11131,10817],"interface CroutonCollectionProps {\n  rows: any[]                    // Array of items to display\n  columns: Column[]              // Column definitions\n  loading?: boolean              // Loading state\n  layout?: 'table' | 'grid' | 'list' | 'kanban'  // Display layout\n  collection?: string            // Collection name (for actions)\n  selectable?: boolean           // Enable row selection\n  selected?: string[]            // Selected row IDs (v-model)\n}",{"id":11143,"title":11144,"titles":11145,"content":11146,"level":449},"/api-reference/components/layout-components#column-definition","Column Definition",[11131,10817],"interface Column {\n  key: string                    // Property key or unique identifier\n  label: string                  // Display label\n  sortable?: boolean             // Enable sorting\n  render?: (row: any) => string  // Custom render function\n  component?: string             // Custom component name\n}",{"id":11148,"title":4173,"titles":11149,"content":11150,"level":449},"/api-reference/components/layout-components#basic-usage-3",[11131,10817],"\u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"items\"\n    :columns=\"columns\"\n    :loading=\"pending\"\n    layout=\"table\"\n    collection=\"shopProducts\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { items, pending } = await useCollectionQuery('shopProducts')\n\nconst columns = [\n  { key: 'name', label: 'Product Name', sortable: true },\n  { key: 'price', label: 'Price', sortable: true },\n  { key: 'inStock', label: 'In Stock' }\n]\n\u003C/script>",{"id":11152,"title":11153,"titles":11154,"content":528,"level":391},"/api-reference/components/layout-components#available-layouts","Available Layouts",[11131],{"id":10850,"title":11156,"titles":11157,"content":11158,"level":449},"✅ Table Layout",[11131,11153],"The table layout is ideal for data-dense views with full sorting, filtering, and pagination support: \u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"products\"\n    :columns=\"columns\"\n    layout=\"table\"\n  />\n\u003C/template>",{"id":10855,"title":11160,"titles":11161,"content":11162,"level":449},"✅ List Layout",[11131,11153],"List layout optimized for mobile devices with automatic field detection and avatar support. Automatic Field Mapping: The list layout intelligently detects common fields in your data without configuration: Title fields (priority order): name, title, label, email, username, idSubtitle fields: description, email, username, role, createdAtAvatar fields: avatar, image, avatarUrl, profileImage \u003Cscript setup lang=\"ts\">\n// Data with standard field names - works automatically!\nconst users = [\n  {\n    id: 1,\n    name: 'John Doe',              // → Title\n    email: 'john@example.com',     // → Subtitle\n    avatar: { src: '/john.jpg' }   // → Avatar image\n  }\n]\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- Zero configuration needed -->\n  \u003CCroutonCollection\n    :rows=\"users\"\n    layout=\"list\"\n  />\n\u003C/template> For detailed information about list layout features and customization, see the List Layout Guide.",{"id":11164,"title":11165,"titles":11166,"content":11167,"level":449},"/api-reference/components/layout-components#grid-layout-coming-soon","🚧 Grid Layout (Coming Soon)",[11131,11153],"Grid layout for image-heavy content is planned for a future release.",{"id":11169,"title":11170,"titles":11171,"content":11172,"level":449},"/api-reference/components/layout-components#cards-layout-coming-soon","🚧 Cards Layout (Coming Soon)",[11131,11153],"Card-based layout for rich content is planned for a future release.",{"id":11174,"title":11175,"titles":11176,"content":11177,"level":449},"/api-reference/components/layout-components#custom-render-functions","Custom Render Functions",[11131,11153],"Add computed columns with custom rendering: \u003Cscript setup lang=\"ts\">\nconst columns = [\n  { key: 'name', label: 'Name' },\n  { key: 'price', label: 'Price' },\n  {\n    key: 'status',\n    label: 'Status',\n    render: (row) => row.inStock ? 'Available' : 'Out of Stock'\n  },\n  {\n    key: 'profit',\n    label: 'Profit',\n    render: (row) => `$${(row.price - row.cost).toFixed(2)}`\n  }\n]\n\u003C/script>",{"id":11179,"title":171,"titles":11180,"content":11181,"level":449},"/api-reference/components/layout-components#custom-components",[11131,11153],"Use custom components for specific columns: \u003Cscript setup lang=\"ts\">\nconst columns = [\n  { key: 'name', label: 'Product' },\n  { key: 'price', label: 'Price' },\n  {\n    key: 'actions',\n    label: '',\n    component: 'ProductActions'  // Your custom component\n  }\n]\n\u003C/script>",{"id":11183,"title":11184,"titles":11185,"content":11186,"level":449},"/api-reference/components/layout-components#selectable-rows","Selectable Rows",[11131,11153],"Enable row selection for bulk operations: \u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonCollection\n      v-model:selected=\"selectedIds\"\n      :rows=\"products\"\n      :columns=\"columns\"\n      selectable\n    />\n\n    \u003CUButton\n      v-if=\"selectedIds.length > 0\"\n      @click=\"handleBulkAction\"\n    >\n      Process {{ selectedIds.length }} items\n    \u003C/UButton>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedIds = ref\u003Cstring[]>([])\n\nconst handleBulkAction = async () => {\n  // Perform bulk operation\n  console.log('Selected:', selectedIds.value)\n}\n\u003C/script>",{"id":11188,"title":11189,"titles":11190,"content":11191,"level":449},"/api-reference/components/layout-components#with-related-data","With Related Data",[11131,11153],"Display related data using render functions: \u003Cscript setup lang=\"ts\">\nconst { items: products } = await useCollectionQuery('shopProducts')\nconst { items: categories } = await useCollectionQuery('shopCategories')\n\n// Map categories by ID for quick lookup\nconst categoryMap = computed(() =>\n  Object.fromEntries(categories.value.map(c => [c.id, c]))\n)\n\nconst columns = [\n  { key: 'name', label: 'Product' },\n  { key: 'price', label: 'Price' },\n  {\n    key: 'category',\n    label: 'Category',\n    render: (row) => categoryMap.value[row.categoryId]?.name || 'N/A'\n  }\n]\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonCollection :rows=\"products\" :columns=\"columns\" />\n\u003C/template>",{"id":11193,"title":11194,"titles":11195,"content":11196,"level":391},"/api-reference/components/layout-components#cardmini","CardMini",[11131],"Display a referenced item with its title and a quick-edit button. Used in table cells to show relationships.",{"id":11198,"title":4987,"titles":11199,"content":11200,"level":449},"/api-reference/components/layout-components#props-4",[11131,11194],"interface CardMiniProps {\n  id: string                       // ID of the referenced item\n  collection: string               // Collection name\n}",{"id":11202,"title":4173,"titles":11203,"content":11204,"level":449},"/api-reference/components/layout-components#basic-usage-4",[11131,11194],"Automatically generated in List views for reference fields: \u003Ctemplate #authorId-cell=\"{ row }\">\n  \u003CCardMini\n    v-if=\"row.original.authorId\"\n    :id=\"row.original.authorId\"\n    collection=\"authors\"\n  />\n\u003C/template>",{"id":11206,"title":183,"titles":11207,"content":11208,"level":449},"/api-reference/components/layout-components#features-1",[11131,11194],"Item preview - Shows referenced item's titleQuick edit - Hover reveals edit buttonLoading state - Skeleton while fetching itemNull handling - Gracefully handles missing references",{"id":11210,"title":11211,"titles":11212,"content":11213,"level":449},"/api-reference/components/layout-components#in-list-component","In List Component",[11131,11194],"The generator automatically creates CardMini slots for reference fields: \u003Ctemplate>\n  \u003CCroutonCollection\n    :rows=\"posts\"\n    :columns=\"columns\"\n    collection=\"blogPosts\"\n  >\n    \u003C!-- Auto-generated for refTarget fields -->\n    \u003Ctemplate #authorId-cell=\"{ row }\">\n      \u003CCardMini\n        v-if=\"row.original.authorId\"\n        :id=\"row.original.authorId\"\n        collection=\"authors\"\n      />\n    \u003C/template>\n\n    \u003Ctemplate #categoryId-cell=\"{ row }\">\n      \u003CCardMini\n        v-if=\"row.original.categoryId\"\n        :id=\"row.original.categoryId\"\n        collection=\"categories\"\n      />\n    \u003C/template>\n  \u003C/CroutonCollection>\n\u003C/template>",{"id":11215,"title":11216,"titles":11217,"content":11218,"level":449},"/api-reference/components/layout-components#visual-design","Visual Design",[11131,11194],"┌─────────────────────────────────┐\n│  John Doe                    ✏️ │  ← Hover state\n└─────────────────────────────────┘\n\n┌─────────────────────────────────┐\n│  John Doe                       │  ← Normal state\n└─────────────────────────────────┘\n\n┌─────────────────────────────────┐\n│  ▬▬▬▬▬▬▬▬                       │  ← Loading state\n└─────────────────────────────────┘",{"id":11220,"title":1383,"titles":11221,"content":11222,"level":449},"/api-reference/components/layout-components#customization",[11131,11194],"Override the display field by modifying the component: \u003C!-- Custom CardMini wrapper -->\n\u003Ctemplate #authorId-cell=\"{ row }\">\n  \u003CCardMini\n    v-if=\"row.original.authorId\"\n    :id=\"row.original.authorId\"\n    collection=\"authors\"\n  >\n    \u003Ctemplate #default=\"{ item }\">\n      {{ item.firstName }} {{ item.lastName }}\n    \u003C/template>\n  \u003C/CardMini>\n\u003C/template>",{"id":11224,"title":11225,"titles":11226,"content":11227,"level":391},"/api-reference/components/layout-components#croutondependentfieldcardmini","CroutonDependentFieldCardMini",[11131],"Displays dependent field values by resolving ID references to full objects from a parent item's JSON array field. Use Case: When you have a field that references options stored in another item. For example, a booking that references time slots stored in a location object.",{"id":11229,"title":4987,"titles":11230,"content":11231,"level":449},"/api-reference/components/layout-components#props-5",[11131,11225],"PropTypeDefaultDescriptionvaluestring | string[] | nullrequiredID(s) of selected option(s)dependentValuestringrequiredParent item ID (e.g., locationId)dependentCollectionstringrequiredParent collection namedependentFieldstringrequiredField in parent containing options array",{"id":11233,"title":183,"titles":11234,"content":11235,"level":449},"/api-reference/components/layout-components#features-2",[11131,11225],"Automatic Resolution: Fetches parent item and resolves IDs to full objectsCustom Component Support: Looks for custom card components for rich displayFallback Rendering: Uses badges if no custom component existsLoading & Error States: Built-in skeleton and error handlingAutomatic Caching: Uses useCollectionItem for efficient data fetching",{"id":11237,"title":1608,"titles":11238,"content":528,"level":449},"/api-reference/components/layout-components#usage",[11131,11225],{"id":11240,"title":4776,"titles":11241,"content":11242,"level":748},"/api-reference/components/layout-components#basic-example",[11131,11225,1608],"\u003Ctemplate>\n  \u003CCroutonDependentFieldCardMini\n    :value=\"booking.slotIds\"\n    :dependent-value=\"booking.locationId\"\n    dependent-collection=\"locations\"\n    dependent-field=\"slots\"\n  />\n\u003C/template> This will: Fetch the location item using booking.locationIdGet the slots array from the locationResolve booking.slotIds to full slot objectsDisplay them using custom component or badges",{"id":11244,"title":11245,"titles":11246,"content":11247,"level":748},"/api-reference/components/layout-components#with-custom-card-component","With Custom Card Component",[11131,11225,1608],"Create a custom card component for rich display: \u003C!-- components/LocationsSlotCardMini.vue -->\n\u003Ctemplate>\n  \u003Cdiv class=\"flex flex-wrap gap-2\">\n    \u003CUBadge\n      v-for=\"slot in value\"\n      :key=\"slot.id\"\n      :color=\"slot.available ? 'success' : 'neutral'\"\n      variant=\"soft\"\n      size=\"lg\"\n    >\n      \u003CUIcon :name=\"slot.icon\" class=\"mr-1\" />\n      {{ slot.label }}\n      \u003Cspan class=\"text-xs ml-1\">{{ slot.time }}\u003C/span>\n    \u003C/UBadge>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{\n  value: Array\u003C{ id: string; label: string; time: string; icon: string; available: boolean }>\n}>()\n\u003C/script>",{"id":11249,"title":11250,"titles":11251,"content":11252,"level":449},"/api-reference/components/layout-components#component-naming-convention","Component Naming Convention",[11131,11225],"The component looks for custom cards using this pattern: {Collection}{Field}CardMini Examples: dependentCollection: \"locations\", dependentField: \"slots\" → LocationsSlotCardMinidependentCollection: \"events\", dependentField: \"categories\" → EventsCategoryCardMini Singularization: The field name is automatically singularized (e.g., \"slots\" → \"slot\")",{"id":11254,"title":10371,"titles":11255,"content":11256,"level":449},"/api-reference/components/layout-components#states",[11131,11225],"StateDisplayLoadingSkeleton placeholderError\"Error loading\" in redEmptyEm dash (—)SuccessCustom component or badge list",{"id":11258,"title":11259,"titles":11260,"content":11261,"level":449},"/api-reference/components/layout-components#data-structure-example","Data Structure Example",[11131,11225],"// Location object structure\n{\n  id: \"loc-1\",\n  name: \"Downtown Studio\",\n  slots: [\n    { id: \"slot-1\", label: \"Morning\", time: \"9:00 AM\", icon: \"i-lucide-sun\" },\n    { id: \"slot-2\", label: \"Afternoon\", time: \"2:00 PM\", icon: \"i-lucide-sunset\" },\n    { id: \"slot-3\", label: \"Evening\", time: \"7:00 PM\", icon: \"i-lucide-moon\" }\n  ]\n}\n\n// Booking object structure\n{\n  id: \"book-1\",\n  locationId: \"loc-1\",\n  slotIds: [\"slot-1\", \"slot-3\"]  // References to slots\n}",{"id":11263,"title":11264,"titles":11265,"content":11266,"level":391},"/api-reference/components/layout-components#croutonitembuttonsmini","CroutonItemButtonsMini",[11131],"Compact action buttons for view, edit, and delete operations on individual items.",{"id":11268,"title":4987,"titles":11269,"content":11270,"level":449},"/api-reference/components/layout-components#props-6",[11131,11264],"PropTypeDefaultDescriptionviewbooleanfalseShow view buttondeletebooleanfalseShow delete buttonupdatebooleanfalseShow update/edit buttonbuttonClassesstring''Additional classes for buttonscontainerClassesstring'flex flex-row gap-2'Container wrapper classesviewTooltipstring''Tooltip text for view buttonupdateTooltipstring''Tooltip text for update buttondeleteTooltipstring''Tooltip text for delete buttonviewLoadingbooleanfalseLoading state for view buttonupdateLoadingbooleanfalseLoading state for update buttondeleteLoadingbooleanfalseLoading state for delete button",{"id":11272,"title":5367,"titles":11273,"content":11274,"level":449},"/api-reference/components/layout-components#events-1",[11131,11264],"EventDescriptionviewEmitted when view button is clickedupdateEmitted when update button is clickeddeleteEmitted when delete button is clicked",{"id":11276,"title":183,"titles":11277,"content":11278,"level":449},"/api-reference/components/layout-components#features-3",[11131,11264],"Conditional Display: Only shows buttons you enable via propsLoading States: Individual loading states for each buttonTooltips: Optional tooltips for each actionConsistent Styling: Pre-configured colors and iconsCompact Size: Uses xs size for tight spaces",{"id":11280,"title":1608,"titles":11281,"content":528,"level":449},"/api-reference/components/layout-components#usage-1",[11131,11264],{"id":11283,"title":4173,"titles":11284,"content":11285,"level":748},"/api-reference/components/layout-components#basic-usage-5",[11131,11264,1608],"\u003Ctemplate>\n  \u003CCroutonItemButtonsMini\n    view\n    update\n    delete\n    @view=\"handleView\"\n    @update=\"handleUpdate\"\n    @delete=\"handleDelete\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst handleView = () => {\n  console.log('View clicked')\n}\n\nconst handleUpdate = () => {\n  console.log('Update clicked')\n}\n\nconst handleDelete = () => {\n  console.log('Delete clicked')\n}\n\u003C/script>",{"id":11287,"title":11288,"titles":11289,"content":11290,"level":748},"/api-reference/components/layout-components#with-tooltips-and-loading","With Tooltips and Loading",[11131,11264,1608],"\u003Ctemplate>\n  \u003CCroutonItemButtonsMini\n    view\n    update\n    delete\n    view-tooltip=\"Preview item\"\n    update-tooltip=\"Edit details\"\n    delete-tooltip=\"Remove item\"\n    :update-loading=\"isSaving\"\n    :delete-loading=\"isDeleting\"\n    @view=\"openPreview\"\n    @update=\"openEditor\"\n    @delete=\"confirmDelete\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst isSaving = ref(false)\nconst isDeleting = ref(false)\n\nconst openPreview = () => {\n  // Open preview modal\n}\n\nconst openEditor = async () => {\n  isSaving.value = true\n  // Open editor\n  isSaving.value = false\n}\n\nconst confirmDelete = async () => {\n  isDeleting.value = true\n  // Perform deletion\n  isDeleting.value = false\n}\n\u003C/script>",{"id":11292,"title":11293,"titles":11294,"content":11295,"level":748},"/api-reference/components/layout-components#in-a-table-cell","In a Table Cell",[11131,11264,1608],"\u003Ctemplate>\n  \u003CCroutonTable :rows=\"products\" :columns=\"columns\">\n    \u003Ctemplate #actions-cell=\"{ row }\">\n      \u003CCroutonItemButtonsMini\n        view\n        update\n        delete\n        @view=\"viewProduct(row.id)\"\n        @update=\"editProduct(row.id)\"\n        @delete=\"deleteProduct(row.id)\"\n      />\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template>",{"id":11297,"title":11298,"titles":11299,"content":11300,"level":748},"/api-reference/components/layout-components#custom-styling","Custom Styling",[11131,11264,1608],"\u003Ctemplate>\n  \u003CCroutonItemButtonsMini\n    update\n    delete\n    button-classes=\"rounded-full\"\n    container-classes=\"flex flex-col gap-1\"\n    @update=\"handleUpdate\"\n    @delete=\"handleDelete\"\n  />\n\u003C/template>",{"id":11302,"title":11303,"titles":11304,"content":11305,"level":449},"/api-reference/components/layout-components#button-styling","Button Styling",[11131,11264],"Each button has pre-configured styling: ButtonIconColorVariantViewi-lucide-eyeneutralsoftUpdatei-ph-pencilprimarysoftDeletei-ph-trash-duotoneerrorsoft",{"id":11307,"title":11308,"titles":11309,"content":11310,"level":449},"/api-reference/components/layout-components#conditional-rendering-example","Conditional Rendering Example",[11131,11264],"\u003Ctemplate>\n  \u003CCroutonItemButtonsMini\n    :view=\"hasViewPermission\"\n    :update=\"hasUpdatePermission\"\n    :delete=\"hasDeletePermission\"\n    @view=\"handleView\"\n    @update=\"handleUpdate\"\n    @delete=\"handleDelete\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { user } = useAuth()\n\nconst hasViewPermission = computed(() => user.value?.role !== 'guest')\nconst hasUpdatePermission = computed(() => ['admin', 'editor'].includes(user.value?.role))\nconst hasDeletePermission = computed(() => user.value?.role === 'admin')\n\u003C/script>",{"id":11312,"title":11313,"titles":11314,"content":11315,"level":391},"/api-reference/components/layout-components#croutonitemdependentfield","CroutonItemDependentField",[11131],"Displays a resolved dependent field value with loading and error states.",{"id":11317,"title":4987,"titles":11318,"content":11319,"level":449},"/api-reference/components/layout-components#props-7",[11131,11313],"PropTypeDefaultDescriptionvalueIdstring-ID of the dependent field valueparentIdstring-ID of the parent itemparentCollectionstring-Parent collection nameparentFieldstring-Parent field namedisplayFieldstring-Which field to display (default: label → value → id)",{"id":11321,"title":183,"titles":11322,"content":11323,"level":449},"/api-reference/components/layout-components#features-4",[11131,11313],"Auto Resolution: Uses useDependentFieldResolver to fetch valueSmart Display: Falls back through label → value → id → JSONLoading State: Shows skeleton while resolvingError Handling: Displays error message if resolution failsBadge Display: Shows resolved value in UBadgeNot Found State: Graceful handling when value doesn't exist",{"id":11325,"title":1608,"titles":11326,"content":528,"level":449},"/api-reference/components/layout-components#usage-2",[11131,11313],{"id":11328,"title":4173,"titles":11329,"content":11330,"level":748},"/api-reference/components/layout-components#basic-usage-6",[11131,11313,1608],"\u003Ctemplate>\n  \u003CCroutonItemDependentField\n    :value-id=\"selectedOptionId\"\n    :parent-id=\"formData.id\"\n    parent-collection=\"products\"\n    parent-field=\"category\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedOptionId = ref('opt-123')\nconst formData = ref({ id: 'prod-456' })\n\u003C/script>",{"id":11332,"title":11333,"titles":11334,"content":11335,"level":748},"/api-reference/components/layout-components#custom-display-field","Custom Display Field",[11131,11313,1608],"\u003Ctemplate>\n  \u003CCroutonItemDependentField\n    :value-id=\"statusId\"\n    :parent-id=\"taskId\"\n    parent-collection=\"tasks\"\n    parent-field=\"status\"\n    display-field=\"name\"\n  />\n  \u003C!-- Will show resolvedValue.name instead of default fallback -->\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst statusId = ref('status-1')\nconst taskId = ref('task-99')\n\u003C/script>",{"id":11337,"title":11293,"titles":11338,"content":11339,"level":748},"/api-reference/components/layout-components#in-a-table-cell-1",[11131,11313,1608],"\u003Ctemplate>\n  \u003CCroutonTable :rows=\"orders\" :columns=\"columns\">\n    \u003Ctemplate #status-cell=\"{ row }\">\n      \u003CCroutonItemDependentField\n        :value-id=\"row.statusId\"\n        :parent-id=\"row.id\"\n        parent-collection=\"orders\"\n        parent-field=\"status\"\n      />\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template>",{"id":11341,"title":11342,"titles":11343,"content":11344,"level":748},"/api-reference/components/layout-components#display-fallback-order","Display Fallback Order",[11131,11313,1608],"\u003Ctemplate>\n  \u003CCroutonItemDependentField\n    :value-id=\"optionId\"\n    :parent-id=\"parentId\"\n    parent-collection=\"forms\"\n    parent-field=\"dropdown\"\n  />\n  \u003C!-- Displays first available:\n       1. displayField prop value (if provided and exists)\n       2. resolvedValue.label\n       3. resolvedValue.value\n       4. resolvedValue.id\n       5. JSON.stringify(resolvedValue)\n  -->\n\u003C/template>",{"id":11346,"title":10371,"titles":11347,"content":11348,"level":449},"/api-reference/components/layout-components#states-1",[11131,11313],"StateDisplayLoadingSkeleton (h-4 w-24)Error\"Error loading\" (red text)Not Found\"Not found\" (gray italic)SuccessUBadge with resolved value",{"id":11350,"title":11351,"titles":11352,"content":11353,"level":449},"/api-reference/components/layout-components#badge-styling","Badge Styling",[11131,11313],"Resolved values are displayed in: Color: neutralVariant: subtleSize: mdFont: font-medium \u003Ctemplate>\n  \u003C!-- Renders as: -->\n  \u003CUBadge color=\"neutral\" variant=\"subtle\" size=\"md\">\n    Resolved Value\n  \u003C/UBadge>\n\u003C/template> Dependent Field Resolver: This component relies on useDependentFieldResolver composable to fetch and resolve dependent field values from the API.",{"id":11355,"title":11356,"titles":11357,"content":11358,"level":391},"/api-reference/components/layout-components#croutonuserscardmini","CroutonUsersCardMini",[11131],"Compact user card with avatar and optional name display.",{"id":11360,"title":4987,"titles":11361,"content":11362,"level":449},"/api-reference/components/layout-components#props-8",[11131,11356],"interface UserItem {\n  title?: string\n  name?: string\n  avatarUrl?: string\n}\n\ninterface Props {\n  item?: UserItem\n  name?: boolean  // Show name text beside avatar\n} PropTypeDefaultDescriptionitemUserItem-User object with title/name/avatarUrlnamebooleanfalseDisplay name text beside avatar",{"id":11364,"title":183,"titles":11365,"content":11366,"level":449},"/api-reference/components/layout-components#features-5",[11131,11356],"Tooltip: Hover shows user's title or nameAvatar: UAvatar with fallback to initialsResponsive: Compact size perfect for tables/listsRing Border: Styled with ring borderName Display: Optional name text beside avatarNull Safe: Gracefully handles missing user data",{"id":11368,"title":1608,"titles":11369,"content":528,"level":449},"/api-reference/components/layout-components#usage-3",[11131,11356],{"id":11371,"title":11372,"titles":11373,"content":11374,"level":748},"/api-reference/components/layout-components#basic-usage-avatar-only","Basic Usage (Avatar Only)",[11131,11356,1608],"\u003Ctemplate>\n  \u003CCroutonUsersCardMini :item=\"user\" />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst user = ref({\n  title: 'John Doe',\n  name: 'John Doe',\n  avatarUrl: 'https://example.com/avatar.jpg'\n})\n\u003C/script>",{"id":11376,"title":11377,"titles":11378,"content":11379,"level":748},"/api-reference/components/layout-components#with-name-display","With Name Display",[11131,11356,1608],"\u003Ctemplate>\n  \u003CCroutonUsersCardMini :item=\"user\" name />\n  \u003C!-- Shows: [Avatar] John Doe -->\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst user = ref({\n  title: 'Jane Smith',\n  name: 'Jane Smith',\n  avatarUrl: 'https://example.com/jane.jpg'\n})\n\u003C/script>",{"id":11381,"title":11382,"titles":11383,"content":11384,"level":748},"/api-reference/components/layout-components#in-table-cell","In Table Cell",[11131,11356,1608],"\u003Ctemplate>\n  \u003CCroutonTable :rows=\"tasks\" :columns=\"columns\">\n    \u003Ctemplate #assignee-cell=\"{ row }\">\n      \u003CCroutonUsersCardMini :item=\"row.assignee\" />\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst tasks = ref([\n  {\n    id: '1',\n    title: 'Fix bug',\n    assignee: {\n      title: 'John Doe',\n      avatarUrl: 'https://...'\n    }\n  }\n])\n\u003C/script>",{"id":11386,"title":11387,"titles":11388,"content":11389,"level":748},"/api-reference/components/layout-components#multiple-users-in-row","Multiple Users in Row",[11131,11356,1608],"\u003Ctemplate>\n  \u003Cdiv class=\"flex -space-x-2\">\n    \u003CCroutonUsersCardMini\n      v-for=\"user in teamMembers\"\n      :key=\"user.id\"\n      :item=\"user\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst teamMembers = ref([\n  { title: 'Alice', avatarUrl: '...' },\n  { title: 'Bob', avatarUrl: '...' },\n  { title: 'Carol', avatarUrl: '...' }\n])\n\u003C/script>",{"id":11391,"title":11392,"titles":11393,"content":11394,"level":748},"/api-reference/components/layout-components#with-missing-avatar","With Missing Avatar",[11131,11356,1608],"\u003Ctemplate>\n  \u003CCroutonUsersCardMini :item=\"userWithoutAvatar\" name />\n  \u003C!-- Shows initials instead of image -->\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst userWithoutAvatar = ref({\n  title: 'John Doe',\n  name: 'John Doe',\n  avatarUrl: '' // Empty - UAvatar shows initials \"JD\"\n})\n\u003C/script>",{"id":11396,"title":11397,"titles":11398,"content":11399,"level":748},"/api-reference/components/layout-components#list-of-users","List of Users",[11131,11356,1608],"\u003Ctemplate>\n  \u003Cdiv class=\"space-y-2\">\n    \u003Cdiv\n      v-for=\"member in members\"\n      :key=\"member.id\"\n      class=\"flex items-center gap-2\"\n    >\n      \u003CCroutonUsersCardMini :item=\"member\" name />\n      \u003Cspan class=\"text-sm text-gray-500\">{{ member.role }}\u003C/span>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst members = ref([\n  { id: '1', name: 'Alice', title: 'Alice', avatarUrl: '...', role: 'Admin' },\n  { id: '2', name: 'Bob', title: 'Bob', avatarUrl: '...', role: 'Editor' }\n])\n\u003C/script>",{"id":11401,"title":10761,"titles":11402,"content":11403,"level":449},"/api-reference/components/layout-components#avatar-styling",[11131,11356],"Size: xs (smallest size)Ring: ring-2 ring-neutral-200 dark:ring-white/10Tooltip delay: 0ms (instant)Tooltip position: Top center with arrow",{"id":11405,"title":11406,"titles":11407,"content":11408,"level":449},"/api-reference/components/layout-components#tooltip-content","Tooltip Content",[11131,11356],"Shows item.title or item.name (in that order): \u003C!-- If both exist, title is used -->\n\u003CUTooltip :text=\"item.title || item.name\">\n  \u003CUAvatar ... />\n\u003C/UTooltip>",{"id":11410,"title":11411,"titles":11412,"content":11413,"level":449},"/api-reference/components/layout-components#nullempty-handling","Null/Empty Handling",[11131,11356],"\u003Ctemplate>\n  \u003CCroutonUsersCardMini :item=\"undefined\" />\n  \u003C!-- Renders nothing (v-if=\"item\") -->\n\u003C/template> User Object: The component expects title OR name field. If both are missing, the tooltip will be empty but the avatar will still show.",{"id":11415,"title":1007,"titles":11416,"content":11417,"level":391},"/api-reference/components/layout-components#related-resources",[11131],"Data Composables - Data fetching for layoutsNuxt UI Card - Base card componentLayout Patterns - Layout customization html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":322,"title":321,"titles":11419,"content":11420,"level":385},[],"Modal, slideover, and dialog components for overlay interfaces",{"id":11422,"title":10271,"titles":11423,"content":11424,"level":391},"/api-reference/components/modal-components#croutonformactionbutton",[321],"Submit button for CRUD forms with built-in loading states and labels.",{"id":11426,"title":4987,"titles":11427,"content":11428,"level":449},"/api-reference/components/modal-components#props",[321,10271],"interface CroutonFormActionButtonProps {\n  action: 'create' | 'update' | 'delete'  // Action type\n  collection: string                       // Collection name\n  loading?: string                         // Loading state\n  hasValidationErrors?: boolean            // Disable button when form has validation errors\n}",{"id":11430,"title":4173,"titles":11431,"content":11432,"level":449},"/api-reference/components/modal-components#basic-usage",[321,10271],"\u003Ctemplate>\n  \u003CUForm @submit=\"handleSubmit\">\n    \u003C!-- Form fields -->\n\n    \u003CCroutonFormActionButton\n      :action=\"action\"\n      :collection=\"collection\"\n      :loading=\"loading\"\n    />\n  \u003C/UForm>\n\u003C/template>",{"id":11434,"title":11435,"titles":11436,"content":11437,"level":449},"/api-reference/components/modal-components#in-generated-forms","In Generated Forms",[321,10271],"The button is automatically included in generated forms: \u003Ctemplate>\n  \u003CUForm :state=\"state\" :schema=\"schema\" @submit=\"handleSubmit\">\n    \u003CUFormField label=\"Name\" name=\"name\">\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Price\" name=\"price\">\n      \u003CUInput v-model.number=\"state.price\" type=\"number\" />\n    \u003C/UFormField>\n\n    \u003C!-- Button automatically shows correct label and loading state -->\n    \u003CCroutonFormActionButton\n      :action=\"action\"\n      :collection=\"collection\"\n      :loading=\"loading\"\n    />\n  \u003C/UForm>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  action: 'create' | 'update' | 'delete'\n  collection: string\n  loading: string\n}>()\n\nconst { create, update, deleteItems } = useCollectionMutation(props.collection)\n\nconst handleSubmit = async () => {\n  if (props.action === 'create') {\n    await create(state.value)\n  } else if (props.action === 'update') {\n    await update(state.value.id, state.value)\n  }\n  close()\n}\n\u003C/script>",{"id":11439,"title":11440,"titles":11441,"content":11442,"level":449},"/api-reference/components/modal-components#button-labels","Button Labels",[321,10271],"The button automatically displays the appropriate label based on the action: create → \"Create\"update → \"Update\"delete → \"Delete\" Labels are translation-ready when using the i18n extension.",{"id":11444,"title":2973,"titles":11445,"content":11446,"level":449},"/api-reference/components/modal-components#loading-states",[321,10271],"The button shows a loading spinner when loading prop matches the action: \u003Cscript setup lang=\"ts\">\nconst { loading } = useCrouton()\n\n// loading.value will be 'create', 'update', or 'delete' during operations\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonFormActionButton\n    action=\"create\"\n    collection=\"shopProducts\"\n    :loading=\"loading\"\n  />\n  \u003C!-- Shows spinner when loading === 'create' -->\n\u003C/template>",{"id":11448,"title":11298,"titles":11449,"content":11450,"level":449},"/api-reference/components/modal-components#custom-styling",[321,10271],"Override default styles using Nuxt UI's class props: \u003Ctemplate>\n  \u003CCroutonFormActionButton\n    :action=\"action\"\n    :collection=\"collection\"\n    color=\"primary\"\n    size=\"lg\"\n    variant=\"solid\"\n  />\n\u003C/template>",{"id":11452,"title":11453,"titles":11454,"content":528,"level":391},"/api-reference/components/modal-components#advanced-props","Advanced Props",[321],{"id":11456,"title":11457,"titles":11458,"content":11459,"level":449},"/api-reference/components/modal-components#croutoncollection-complete-api","CroutonCollection Complete API",[321,11453],"interface CroutonCollectionProps {\n  // Data\n  rows?: any[]                   // Array of items to display\n  columns?: Column[]             // Column definitions\n  collection: string             // Collection name for actions\n\n  // Layout\n  layout?: LayoutType | ResponsiveLayout | keyof typeof layoutPresets\n  // Layout options: 'table' | 'list' | 'grid' | 'tree' | 'kanban'\n\n  // Pagination\n  serverPagination?: boolean                 // Enable server-side pagination\n  paginationData?: PaginationData | null    // Server pagination metadata\n  refreshFn?: () => Promise\u003Cvoid>           // Custom refresh function\n\n  // Features\n  create?: boolean                           // Show create button\n  sortable?: boolean | SortableOptions       // Enable drag-and-drop reordering\n  hierarchy?: HierarchyConfig                // Tree layout hierarchy config\n  stateless?: boolean                        // No config lookup or mutations, just renders data\n  showCollabPresence?: boolean | CollabPresenceConfig  // Show collab presence badges per row\n\n  // Cards\n  card?: string                              // Card variant name (e.g. 'Card', 'CardMini')\n  cardComponent?: any                        // Direct card component (skips name resolution)\n  gridSize?: 'compact' | 'comfortable' | 'spacious'  // Grid layout size (default: 'comfortable')\n\n  // Customization\n  hideDefaultColumns?: {\n    select?: boolean\n    createdAt?: boolean\n    updatedAt?: boolean\n    createdBy?: boolean\n    updatedBy?: boolean\n    presence?: boolean\n    actions?: boolean\n  }\n}",{"id":11461,"title":11144,"titles":11462,"content":11463,"level":449},"/api-reference/components/modal-components#column-definition",[321,11453],"interface Column {\n  accessorKey: string              // Property key or unique identifier\n  header: string                   // Display label\n  sortable?: boolean               // Enable sorting (default: false)\n  cell?: (props: any) => any       // Custom cell renderer\n  enableSorting?: boolean          // TanStack Table sorting flag\n  enableHiding?: boolean           // Allow hiding column\n}",{"id":11465,"title":11466,"titles":11467,"content":11468,"level":449},"/api-reference/components/modal-components#pagination-data","Pagination Data",[321,11453],"interface PaginationData {\n  currentPage: number\n  pageSize: number\n  totalItems: number\n  totalPages?: number\n  sortBy?: string\n  sortDirection?: 'asc' | 'desc'\n}",{"id":11470,"title":11471,"titles":11472,"content":11473,"level":449},"/api-reference/components/modal-components#responsive-layout","Responsive Layout",[321,11453],"type LayoutType = 'table' | 'list' | 'grid' | 'tree' | 'kanban' | 'workspace'\n\ninterface ResponsiveLayout {\n  base: LayoutType               // Default/mobile layout\n  sm?: LayoutType                // ≥ 640px\n  md?: LayoutType                // ≥ 768px\n  lg?: LayoutType                // ≥ 1024px\n  xl?: LayoutType                // ≥ 1280px\n  '2xl'?: LayoutType             // ≥ 1536px\n}\n\n// Built-in presets\ntype LayoutPreset = 'responsive' | 'mobile-friendly' | 'compact'",{"id":11475,"title":11476,"titles":11477,"content":11478,"level":449},"/api-reference/components/modal-components#croutonformactionbutton-complete-api","CroutonFormActionButton Complete API",[321,11453],"interface CroutonFormActionButtonProps {\n  action: 'create' | 'update' | 'delete'  // Action type\n  collection: string                       // Collection name\n  loading?: string                         // Loading state identifier\n  hasValidationErrors?: boolean            // Disable button when form has validation errors\n  items?: any[]                           // Items for bulk operations\n}",{"id":11480,"title":11481,"titles":11482,"content":528,"level":391},"/api-reference/components/modal-components#component-events","Component Events",[321],{"id":11484,"title":11485,"titles":11486,"content":11487,"level":449},"/api-reference/components/modal-components#croutoncollection-events","CroutonCollection Events",[321,11481],"// Row selection\n@update:selected=\"handleSelection\"  // Emits: string[] (row IDs)\n\n// Column visibility\n@update:column-visibility=\"handleVisibility\"  // Emits: Record\u003Cstring, boolean>\n\n// Pagination (when server-pagination enabled)\n@update:page=\"handlePageChange\"     // Emits: number\n@update:page-size=\"handleSizeChange\" // Emits: number",{"id":11489,"title":11490,"titles":11491,"content":11492,"level":449},"/api-reference/components/modal-components#croutonformactionbutton-events","CroutonFormActionButton Events",[321,11481],"No custom events - uses native form submission.",{"id":11494,"title":11495,"titles":11496,"content":528,"level":391},"/api-reference/components/modal-components#component-slots","Component Slots",[321],{"id":11498,"title":11499,"titles":11500,"content":11501,"level":449},"/api-reference/components/modal-components#croutoncollection-slots","CroutonCollection Slots",[321,11495],"\u003Ctemplate>\n  \u003CCroutonCollection :rows=\"items\" :columns=\"columns\">\n    \u003C!-- Header slot -->\n    \u003Ctemplate #header>\n      \u003Ch2>Custom Header\u003C/h2>\n    \u003C/template>\n\n    \u003C!-- Custom cell template (per column) -->\n    \u003Ctemplate #[columnKey]-cell=\"{ row }\">\n      {{ row[columnKey] }}\n    \u003C/template>\n\n    \u003C!-- List item actions (list layout only) -->\n    \u003Ctemplate #list-item-actions=\"{ row }\">\n      \u003C!-- Customize actions shown on the right side of each list item -->\n      \u003CUSelect\n        :model-value=\"row.role\"\n        :items=\"['admin', 'member', 'viewer']\"\n        size=\"sm\"\n        @update:model-value=\"updateRole(row.id, $event)\"\n      />\n      \u003CUButton\n        icon=\"i-lucide-trash\"\n        color=\"red\"\n        variant=\"ghost\"\n        size=\"sm\"\n        @click=\"handleDelete(row.id)\"\n      />\n    \u003C/template>\n  \u003C/CroutonCollection>\n\u003C/template>",{"id":11503,"title":1007,"titles":11504,"content":11505,"level":391},"/api-reference/components/modal-components#related-resources",[321],"Form Composables - Modal state managementNuxt UI Modal - Base modal componentNuxt UI Slideover - Base slideover component html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":326,"title":325,"titles":11507,"content":11508,"level":385},[],"Data table components with sorting, filtering, and pagination",{"id":11510,"title":11511,"titles":11512,"content":11513,"level":391},"/api-reference/components/table-components#croutontable","CroutonTable",[325],"A powerful data table component with sorting, filtering, pagination, and row selection. Used internally by CroutonCollection for table layout mode. Note: CroutonTable is typically used through CroutonCollection's layout=\"table\" prop. Use this component directly only when you need fine-grained control over table behavior.",{"id":11515,"title":4987,"titles":11516,"content":11517,"level":449},"/api-reference/components/table-components#props",[325,11511],"interface TableProps {\n  // Data props\n  columns?: TableColumn[]                    // Column definitions (default: [])\n  rows?: any[]                              // Data rows to display (default: [])\n  collection: string                        // Collection name for CRUD operations\n\n  // Optional props\n  serverPagination?: boolean                // Enable server-side pagination (default: false)\n  paginationData?: PaginationData | null    // Pagination metadata (default: null)\n  refreshFn?: () => Promise\u003Cvoid>           // Refresh function for server pagination\n  sortable?: boolean | SortableOptions      // Enable drag-and-drop row reordering (default: false)\n  hideDefaultColumns?: {                    // Hide automatically-added columns\n    createdAt?: boolean\n    updatedAt?: boolean\n    createdBy?: boolean\n    updatedBy?: boolean\n    select?: boolean\n    presence?: boolean\n    actions?: boolean\n  }\n}\n\n// Sortable Options\ninterface SortableOptions {\n  handle?: boolean     // Show drag handle column (default: true)\n  animation?: number   // SortableJS animation duration in ms (default: 150)\n  disabled?: boolean   // Temporarily disable drag-and-drop (default: false)\n}\n\n// Column Definition\ninterface TableColumn {\n  id?: string\n  accessorKey?: string                      // Field key in data\n  header: string | ((props: any) => any)   // Column header text or component\n  cell?: (props: any) => any               // Custom cell renderer\n  sortable?: boolean                        // Enable sorting (default: false)\n  enableSorting?: boolean                   // Alternative sorting flag\n  enableHiding?: boolean                    // Allow hiding column\n}\n\n// Pagination Data\ninterface PaginationData {\n  currentPage: number\n  pageSize: number\n  totalItems: number\n  totalPages?: number\n  sortBy?: string\n  sortDirection?: 'asc' | 'desc'\n}",{"id":11519,"title":10961,"titles":11520,"content":11521,"level":449},"/api-reference/components/table-components#props-details",[325,11511],"PropTypeDefaultDescriptioncolumnsTableColumn[]requiredColumn definitions for table headers and cellsrowsany[]requiredArray of data objects to displaycollectionstringrequiredCollection name (used for CRUD modal actions)serverPaginationbooleanfalseUse server-side pagination instead of client-sidepaginationDataPaginationData | nullnullPagination metadata (required for server pagination)refreshFn() => Promise\u003Cvoid>undefinedFunction to refresh data (required for server pagination)hideDefaultColumnsobject{}Control visibility of auto-generated columns",{"id":11523,"title":5372,"titles":11524,"content":528,"level":449},"/api-reference/components/table-components#slots",[325,11511],{"id":11526,"title":11527,"titles":11528,"content":11529,"level":748},"/api-reference/components/table-components#header-pass-through","header (Pass-through)",[325,11511,5372],"Customize the entire header section: \u003Ctemplate #header>\n  \u003CCroutonTableHeader\n    :collection=\"collection\"\n    :create-button=\"true\"\n  />\n\u003C/template>",{"id":11531,"title":11532,"titles":11533,"content":11534,"level":748},"/api-reference/components/table-components#dynamic-cell-slots","Dynamic Cell Slots",[325,11511,5372],"All slots are passed through to Nuxt UI's UTable for custom cell rendering: \u003Ctemplate #location-cell=\"{ row }\">\n  \u003CCroutonItemCardMini\n    :id=\"row.original.location\"\n    collection=\"locations\"\n  />\n\u003C/template>\n\n\u003Ctemplate #status-cell=\"{ row }\">\n  \u003CUBadge\n    :color=\"row.original.status === 'active' ? 'green' : 'gray'\"\n  >\n    {{ row.original.status }}\n  \u003C/UBadge>\n\u003C/template>",{"id":11536,"title":11537,"titles":11538,"content":11539,"level":748},"/api-reference/components/table-components#pre-defined-column-slots","Pre-defined Column Slots",[325,11511,5372],"CroutonTable provides default renderers for common columns (can be hidden via hideDefaultColumns): createdBy-cell - Shows user avatar and name (via CroutonUsersCardMini)createdAt-cell - Formatted date/timeupdatedBy-cell - Shows user avatar and nameupdatedAt-cell - Formatted date/timeactions-cell - Edit and delete buttons (via CroutonItemButtonsMini)",{"id":11541,"title":4173,"titles":11542,"content":11543,"level":449},"/api-reference/components/table-components#basic-usage",[325,11511],"Query Examples: For complete useCollectionQuery patterns, see Querying Data. \u003Ctemplate>\n  \u003CCroutonTable\n    :collection=\"collection\"\n    :columns=\"columns\"\n    :rows=\"users\"\n  >\n    \u003Ctemplate #header>\n      \u003CCroutonTableHeader\n        :collection=\"collection\"\n        :create-button=\"true\"\n      />\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { items: users } = await useCollectionQuery('users')\n\nconst columns = [\n  { accessorKey: 'name', header: 'Name', sortable: true },\n  { accessorKey: 'email', header: 'Email', sortable: true },\n  { accessorKey: 'role', header: 'Role' }\n]\n\u003C/script>",{"id":11545,"title":183,"titles":11546,"content":528,"level":449},"/api-reference/components/table-components#features",[325,11511],{"id":11548,"title":4163,"titles":11549,"content":11550,"level":748},"/api-reference/components/table-components#sorting",[325,11511,183],"Client-side and server-side sorting on sortable columns: \u003Cscript setup lang=\"ts\">\nconst columns = [\n  { accessorKey: 'name', header: 'Name', sortable: true },\n  { accessorKey: 'createdAt', header: 'Created', sortable: true },\n  { accessorKey: 'email', header: 'Email' }  // Not sortable\n]\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonTable\n    collection=\"users\"\n    :columns=\"columns\"\n    :rows=\"users\"\n  />\n\u003C/template> Behavior: Click column header to sortClick again to reverse directionDefault sort: createdAt descendingServer pagination: triggers API call with sortBy and sortDirection",{"id":11552,"title":11553,"titles":11554,"content":11555,"level":748},"/api-reference/components/table-components#search-and-filtering","Search and Filtering",[325,11511,183],"Built-in search bar with debounced input (300ms): \u003Ctemplate>\n  \u003CCroutonTable\n    collection=\"bookings\"\n    :columns=\"columns\"\n    :rows=\"bookings\"\n  >\n    \u003Ctemplate #header>\n      \u003CCroutonTableHeader\n        :collection=\"collection\"\n        :create-button=\"true\"\n      />\n      \u003C!-- Search component automatically included -->\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template> Search behavior: Case-insensitive string matchingSearches across all visible columnsFilters rows in real-timeResets to page 1 on new search",{"id":11557,"title":11558,"titles":11559,"content":11560,"level":748},"/api-reference/components/table-components#pagination","Pagination",[325,11511,183],"Client-side pagination (default): \u003Ctemplate>\n  \u003CCroutonTable\n    collection=\"products\"\n    :columns=\"columns\"\n    :rows=\"allProducts\"\n  />\n\u003C/template> Server-side pagination: \u003Ctemplate>\n  \u003CCroutonTable\n    collection=\"users\"\n    :columns=\"columns\"\n    :rows=\"users\"\n    server-pagination\n    :pagination-data=\"paginationData\"\n    :refresh-fn=\"refresh\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst pageSize = ref(10)\n\nconst { data: response, refresh } = await useFetch('/api/users', {\n  query: { page, pageSize }\n})\n\nconst users = computed(() => response.value?.items || [])\nconst paginationData = computed(() => ({\n  currentPage: response.value?.page || 1,\n  pageSize: response.value?.pageSize || 10,\n  totalItems: response.value?.total || 0,\n  totalPages: Math.ceil((response.value?.total || 0) / pageSize.value)\n}))\n\u003C/script> Page sizes: 5, 10, 20, 30, 40",{"id":11562,"title":11563,"titles":11564,"content":11565,"level":748},"/api-reference/components/table-components#row-selection","Row Selection",[325,11511,183],"Select rows with checkboxes for bulk operations: \u003Ctemplate>\n  \u003CCroutonTable\n    collection=\"users\"\n    :columns=\"columns\"\n    :rows=\"users\"\n  >\n    \u003Ctemplate #header>\n      \u003Cdiv class=\"flex items-center justify-between p-4\">\n        \u003CCroutonTableHeader :collection=\"collection\" />\n        \u003CUButton\n          v-if=\"selectedRows.length > 0\"\n          color=\"red\"\n          @click=\"handleBulkDelete\"\n        >\n          Delete {{ selectedRows.length }} items\n        \u003C/UButton>\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedRows = ref([])\n\nconst handleBulkDelete = async () => {\n  const ids = selectedRows.value.map(row => row.id)\n  // Perform bulk delete\n}\n\u003C/script> Features: Select individual rowsSelect all rows (header checkbox)Selected count badgeBulk delete button in TableActions",{"id":11567,"title":11568,"titles":11569,"content":11570,"level":748},"/api-reference/components/table-components#column-visibility","Column Visibility",[325,11511,183],"Toggle column visibility via dropdown menu: \u003Ctemplate>\n  \u003CCroutonTable\n    collection=\"bookings\"\n    :columns=\"columns\"\n    :rows=\"bookings\"\n  >\n    \u003Ctemplate #header>\n      \u003CCroutonTableHeader :collection=\"collection\" />\n      \u003C!-- Column visibility dropdown in TableActions -->\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template> Default hidden: id column Toggle location: TableActions component (eye icon dropdown)",{"id":11572,"title":10910,"titles":11573,"content":11574,"level":449},"/api-reference/components/table-components#hide-default-columns",[325,11511],"Control which auto-generated columns appear: \u003Ctemplate>\n  \u003CCroutonTable\n    collection=\"products\"\n    :columns=\"columns\"\n    :rows=\"products\"\n    :hide-default-columns=\"{\n      createdAt: true,\n      updatedAt: true,\n      createdBy: true,\n      updatedBy: true,\n      actions: false  // Keep actions column\n    }\"\n  />\n\u003C/template>",{"id":11576,"title":10891,"titles":11577,"content":11578,"level":449},"/api-reference/components/table-components#custom-cell-rendering",[325,11511],"Use slots for rich cell content: \u003Ctemplate>\n  \u003CCroutonTable\n    collection=\"bookings\"\n    :columns=\"columns\"\n    :rows=\"bookings\"\n  >\n    \u003C!-- Related entity -->\n    \u003Ctemplate #location-cell=\"{ row }\">\n      \u003CCroutonItemCardMini\n        :id=\"row.original.location\"\n        collection=\"locations\"\n      />\n    \u003C/template>\n\n    \u003C!-- Date formatting -->\n    \u003Ctemplate #date-cell=\"{ row }\">\n      \u003CCroutonDate :date=\"row.original.date\" format=\"long\" />\n    \u003C/template>\n\n    \u003C!-- Status badge -->\n    \u003Ctemplate #status-cell=\"{ row }\">\n      \u003CUBadge\n        :color=\"row.original.status === 'confirmed' ? 'green' : 'yellow'\"\n      >\n        {{ row.original.status }}\n      \u003C/UBadge>\n    \u003C/template>\n\n    \u003C!-- Avatar group -->\n    \u003Ctemplate #attendees-cell=\"{ row }\">\n      \u003CUAvatarGroup :max=\"3\">\n        \u003CUAvatar\n          v-for=\"attendee in row.original.attendees\"\n          :key=\"attendee.id\"\n          :alt=\"attendee.name\"\n          :src=\"attendee.avatar\"\n          size=\"xs\"\n        />\n      \u003C/UAvatarGroup>\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template>",{"id":11580,"title":2973,"titles":11581,"content":11582,"level":449},"/api-reference/components/table-components#loading-states",[325,11511],"Automatic loading overlay during server pagination: \u003Ctemplate>\n  \u003CCroutonTable\n    collection=\"users\"\n    :columns=\"columns\"\n    :rows=\"users\"\n    server-pagination\n    :pagination-data=\"paginationData\"\n    :refresh-fn=\"refresh\"\n  />\n  \u003C!-- Loading overlay appears automatically during refresh -->\n\u003C/template> Loading behavior: Semi-transparent overlaySpinner iconTable fades to 50% opacityTriggered on page change, sort, or refresh",{"id":11584,"title":5825,"titles":11585,"content":528,"level":449},"/api-reference/components/table-components#usage-examples",[325,11511],{"id":11587,"title":11588,"titles":11589,"content":11590,"level":748},"/api-reference/components/table-components#basic-table-with-pagination","Basic Table with Pagination",[325,11511,5825],"\u003Ctemplate>\n  \u003CCroutonTable\n    collection=\"bookings\"\n    :columns=\"columns\"\n    :rows=\"bookings\"\n    server-pagination\n    :pagination-data=\"paginationData\"\n    :refresh-fn=\"refresh\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { items: bookings, refresh } = await useCollectionQuery('bookings')\n\nconst paginationData = computed(() => ({\n  currentPage: 1,\n  pageSize: 20,\n  totalItems: bookings.value.length,\n  totalPages: Math.ceil(bookings.value.length / 20)\n}))\n\nconst columns = [\n  { accessorKey: 'name', header: 'Name', sortable: true },\n  { accessorKey: 'date', header: 'Date', sortable: true },\n  { accessorKey: 'status', header: 'Status' }\n]\n\u003C/script>",{"id":11592,"title":11593,"titles":11594,"content":11595,"level":748},"/api-reference/components/table-components#custom-header-with-actions","Custom Header with Actions",[325,11511,5825],"\u003Ctemplate>\n  \u003CCroutonTable collection=\"bookings\">\n    \u003Ctemplate #header>\n      \u003Cdiv class=\"flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-900\">\n        \u003Ch2 class=\"text-lg font-semibold\">Bookings\u003C/h2>\n        \u003Cdiv class=\"flex items-center gap-4\">\n          \u003CUButton\n            @click=\"open('create', 'bookings')\"\n            icon=\"i-lucide-plus\"\n            color=\"primary\"\n          >\n            New Booking\n          \u003C/UButton>\n          \u003CCroutonTableSearch />\n        \u003C/div>\n      \u003C/div>\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { open } = useCrouton()\n\u003C/script>",{"id":11597,"title":8821,"titles":11598,"content":11599,"level":748},"/api-reference/components/table-components#custom-cell-renderers",[325,11511,5825],"\u003Ctemplate>\n  \u003CCroutonTable collection=\"bookings\">\n    \u003C!-- Reference item with CardMini -->\n    \u003Ctemplate #location-cell=\"{ row }\">\n      \u003CCroutonItemCardMini\n        :id=\"row.original.location\"\n        collection=\"locations\"\n      />\n    \u003C/template>\n\n    \u003C!-- Formatted date -->\n    \u003Ctemplate #date-cell=\"{ row }\">\n      \u003CCroutonDate :date=\"row.original.date\" format=\"medium\" />\n    \u003C/template>\n\n    \u003C!-- Status badge with color -->\n    \u003Ctemplate #status-cell=\"{ row }\">\n      \u003CUBadge\n        :color=\"getStatusColor(row.original.status)\"\n        variant=\"subtle\"\n      >\n        {{ row.original.status }}\n      \u003C/UBadge>\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst getStatusColor = (status: string) => {\n  const colors = {\n    confirmed: 'green',\n    pending: 'yellow',\n    cancelled: 'red'\n  }\n  return colors[status] || 'gray'\n}\n\u003C/script>",{"id":11601,"title":11602,"titles":11603,"content":11604,"level":449},"/api-reference/components/table-components#sub-components","Sub-Components",[325,11511],"CroutonTable uses several sub-components that can be customized: CroutonTableHeader - Header with title and create buttonCroutonTableSearch - Search input with debouncingCroutonTablePagination - Pagination controlsCroutonTableActions - Bulk actions and column visibilityCroutonItemButtonsMini - Edit/delete buttons for rows See Table Components for detailed documentation.",{"id":11606,"title":11010,"titles":11607,"content":11608,"level":449},"/api-reference/components/table-components#composables-used",[325,11511],"CroutonTable leverages these composables internally: useTableData - Data filtering, searching, pagination logicuseTableColumns - Column management with default columnsuseCrouton - Modal/form state managementuseT - Translation/i18n support See Composables Reference for details.",{"id":11610,"title":36,"titles":11611,"content":528,"level":449},"/api-reference/components/table-components#troubleshooting",[325,11511],{"id":11613,"title":11614,"titles":11615,"content":11616,"level":748},"/api-reference/components/table-components#sorting-not-working","Sorting Not Working",[325,11511,36],"If column sorting doesn't respond: Check sortable prop: Must be true on column definitionServer pagination: Ensure refreshFn triggers API call with sort paramsColumn key: Verify accessorKey matches data field name",{"id":11618,"title":11619,"titles":11620,"content":11621,"level":748},"/api-reference/components/table-components#search-not-filtering","Search Not Filtering",[325,11511,36],"If search doesn't filter results: Check data structure: Search works on string fields onlyServer pagination: Implement search on backend, not client-sideCase sensitivity: Search is case-insensitive by default",{"id":11623,"title":10947,"titles":11624,"content":11625,"level":748},"/api-reference/components/table-components#pagination-not-updating",[325,11511,36],"If pagination controls don't work: Server pagination: Must provide paginationData and refreshFnTotal items: Ensure totalItems in paginationData is correctPage change: Verify refreshFn is called on page change",{"id":11627,"title":325,"titles":11628,"content":11629,"level":391},"/api-reference/components/table-components#table-components",[325],"These four components work together to provide a complete table interface experience. They are designed to be used within CroutonTable or custom table layouts.",{"id":11631,"title":11632,"titles":11633,"content":11634,"level":449},"/api-reference/components/table-components#tableheader","TableHeader",[325,325],"Dashboard navbar header with optional create button functionality for collection tables. Displays the collection title and allows users to trigger the creation of new items.",{"id":11636,"title":4987,"titles":11637,"content":11638,"level":748},"/api-reference/components/table-components#props-1",[325,325,11632],"interface TableHeaderProps {\n  title?: string          // Display title for the header (default: '')\n  collection?: string     // Collection name for routing/actions (default: '')\n  createButton?: boolean  // Show/hide create button (default: false)\n} PropTypeDefaultDescriptiontitlestring''Title text displayed in the left section of the navbarcollectionstring''Collection identifier used for formatting and routing (e.g., 'users', 'articles')createButtonbooleanfalseControls visibility of the \"Create\" button in the right section",{"id":11640,"title":5372,"titles":11641,"content":11642,"level":748},"/api-reference/components/table-components#slots-1",[325,325,11632],"#extraButtons Located in the right section, before the create button. Allows injection of additional action buttons. \u003CTableHeader collection=\"users\" :create-button=\"true\">\n  \u003Ctemplate #extraButtons>\n    \u003CUButton icon=\"i-lucide-filter\">Filter\u003C/UButton>\n    \u003CUButton icon=\"i-lucide-download\">Export\u003C/UButton>\n  \u003C/template>\n\u003C/TableHeader>",{"id":11644,"title":183,"titles":11645,"content":11646,"level":748},"/api-reference/components/table-components#features-1",[325,325,11632],"Automatic Collection Name Formatting\nUses useFormatCollections().collectionWithCapitalSingular() to convert collection names (e.g., \"articles\" → \"Article\") Responsive Create Button Label\nShows full label on medium+ screens, abbreviated on mobile: Mobile: \"Create\"Desktop: \"Create Collection Name\" (e.g., \"Create Article\") Integrated Modal Triggering\nCalls useCrouton().open('create', collection) to open create modal",{"id":11648,"title":4173,"titles":11649,"content":11650,"level":748},"/api-reference/components/table-components#basic-usage-1",[325,325,11632],"\u003Ctemplate>\n  \u003CUDashboardPanel>\n    \u003Ctemplate #header>\n      \u003CTableHeader\n        :collection=\"collection\"\n        :create-button=\"true\"\n        title=\"User Management\"\n      />\n    \u003C/template>\n  \u003C/UDashboardPanel>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst collection = 'users'\n\u003C/script>",{"id":11652,"title":11653,"titles":11654,"content":11655,"level":748},"/api-reference/components/table-components#with-extra-buttons","With Extra Buttons",[325,325,11632],"\u003Ctemplate>\n  \u003CTableHeader\n    collection=\"articles\"\n    :create-button=\"true\"\n  >\n    \u003Ctemplate #extraButtons>\n      \u003CUButton\n        icon=\"i-lucide-filter\"\n        color=\"gray\"\n        variant=\"ghost\"\n        @click=\"openFilters\"\n      >\n        Filter\n      \u003C/UButton>\n      \u003CUButton\n        icon=\"i-lucide-download\"\n        color=\"gray\"\n        variant=\"ghost\"\n        @click=\"exportData\"\n      >\n        Export\n      \u003C/UButton>\n    \u003C/template>\n  \u003C/TableHeader>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst openFilters = () => {\n  // Open filter modal\n}\n\nconst exportData = () => {\n  // Export table data\n}\n\u003C/script>",{"id":11657,"title":10098,"titles":11658,"content":11659,"level":748},"/api-reference/components/table-components#integration-with-croutontable",[325,325,11632],"TableHeader is automatically used when the create prop is set on CroutonTable: \u003Ctemplate>\n  \u003CCroutonTable\n    :collection=\"collection\"\n    :rows=\"rows\"\n    :columns=\"columns\"\n    :create=\"true\"  \u003C!-- TableHeader with create button -->\n  />\n\u003C/template>",{"id":11661,"title":11662,"titles":11663,"content":11664,"level":449},"/api-reference/components/table-components#tablesearch","TableSearch",[325,325],"Debounced search input component for filtering table data. Implements best practices for search UX by preventing excessive API calls during typing.",{"id":11666,"title":4987,"titles":11667,"content":11668,"level":748},"/api-reference/components/table-components#props-2",[325,325,11662],"interface TableSearchProps {\n  modelValue: string      // Current search value (required)\n  placeholder?: string    // Input placeholder text (default: 'Search...')\n  debounceMs?: number    // Debounce delay in milliseconds (default: 300)\n} PropTypeDefaultDescriptionmodelValuestringrequiredTwo-way bound search value (v-model)placeholderstring'Search...'Placeholder text shown in empty inputdebounceMsnumber300Milliseconds to wait before emitting search value",{"id":11670,"title":5367,"titles":11671,"content":11672,"level":748},"/api-reference/components/table-components#events",[325,325,11662],"emit('update:modelValue', value: string)  // Emitted after debounce period EventPayloadDescriptionupdate:modelValuestringEmitted when user stops typing (after debounce delay). Enables v-model usage.",{"id":11674,"title":183,"titles":11675,"content":11676,"level":748},"/api-reference/components/table-components#features-2",[325,325,11662],"Automatic Debouncing\nUses VueUse's useDebounceFn to prevent excessive updates during typing v-model Compatible\nImplements Vue 3's v-model pattern with modelValue prop and update:modelValue emit Icon Integration\nIncludes a search icon (i-lucide-search) for better UX Configurable Debounce\nAllows customization of debounce timing per use case (default 300ms is optimal for most searches) Responsive Width\nUses max-w-sm class for consistent sizing",{"id":11678,"title":4173,"titles":11679,"content":11680,"level":748},"/api-reference/components/table-components#basic-usage-2",[325,325,11662],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003CTableSearch\n      v-model=\"search\"\n      placeholder=\"Search users...\"\n    />\n    \u003CCroutonTable\n      :rows=\"filteredRows\"\n      :columns=\"columns\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst search = ref('')\nconst { items } = await useCollectionQuery('users')\n\nconst filteredRows = computed(() =>\n  search.value\n    ? items.value.filter(u => u.name.includes(search.value))\n    : items.value\n)\n\u003C/script>",{"id":11682,"title":11683,"titles":11684,"content":11685,"level":748},"/api-reference/components/table-components#custom-debounce-timing","Custom Debounce Timing",[325,325,11662],"\u003Ctemplate>\n  \u003CTableSearch\n    v-model=\"search\"\n    placeholder=\"Search products...\"\n    :debounce-ms=\"500\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst search = ref('')\n\n// With longer debounce for expensive searches\nwatch(search, async (value) => {\n  await $fetch('/api/expensive-search', {\n    query: { q: value }\n  })\n})\n\u003C/script>",{"id":11687,"title":10098,"titles":11688,"content":11689,"level":748},"/api-reference/components/table-components#integration-with-croutontable-1",[325,325,11662],"TableSearch is automatically included in CroutonTable when search functionality is enabled: \u003Ctemplate>\n  \u003CCroutonTable\n    :collection=\"collection\"\n    :rows=\"rows\"\n    :columns=\"columns\"\n    searchable  \u003C!-- Includes TableSearch -->\n  />\n\u003C/template>",{"id":11691,"title":36,"titles":11692,"content":11693,"level":748},"/api-reference/components/table-components#troubleshooting-1",[325,325,11662],"Search not triggering Problem: Search updates aren't being detectedSolution: Ensure you're using v-model with a reactive ref, not a plain variable Too many API calls Problem: Search is making too many requestsSolution: Increase debounceMs to 500-1000ms for expensive operations Search resets on page change Problem: Search value is lost when navigatingSolution: Store search in URL query params or global state",{"id":11695,"title":11696,"titles":11697,"content":11698,"level":449},"/api-reference/components/table-components#tablepagination","TablePagination",[325,325],"Comprehensive pagination controls including page size selector, current page indicator, and page navigation. Displays contextual information about the current data view (e.g., \"Showing 1 to 10 of 100 results\").",{"id":11700,"title":4987,"titles":11701,"content":11702,"level":748},"/api-reference/components/table-components#props-3",[325,325,11696],"interface TablePaginationProps {\n  page: number            // Current page number (1-indexed) (required)\n  pageCount: number       // Items per page (required)\n  totalItems: number      // Total number of items across all pages (required)\n  loading?: boolean       // Disables controls during loading (default: false)\n  pageSizes?: number[]   // Available page size options (default: [5, 10, 20, 30, 40])\n} PropTypeDefaultDescriptionpagenumberrequiredCurrent active page (starts at 1)pageCountnumberrequiredNumber of items shown per pagetotalItemsnumberrequiredTotal count of items in the datasetloadingbooleanfalseWhen true, disables pagination controlspageSizesnumber[][5, 10, 20, 30, 40]Array of available page size options for dropdown",{"id":11704,"title":5367,"titles":11705,"content":11706,"level":748},"/api-reference/components/table-components#events-1",[325,325,11696],"emit('update:page', value: number)       // Emitted when user changes page\nemit('update:pageCount', value: number)  // Emitted when user changes page size EventPayloadDescriptionupdate:pagenumberEmitted when user navigates to different page (clicking page buttons)update:pageCountnumberEmitted when user selects different page size from dropdown",{"id":11708,"title":183,"titles":11709,"content":11710,"level":748},"/api-reference/components/table-components#features-3",[325,325,11696],"Internationalization Support\nUses useT() composable for all displayed text, making it fully translatable Smart Range Display\nAutomatically calculates and displays \"Showing X to Y of Z results\" with proper edge case handling: Returns 0 when no items existCorrectly handles last page with fewer items than page size Page Size Selector\nDropdown for changing items per page (rows per page) Accessible Controls\nUPagination component with proper ARIA labels Loading State Management\nDisables controls during data fetching to prevent race conditions Computed Page Ranges\nAutomatically calculates pageFrom and pageTo based on current page and count",{"id":11712,"title":4173,"titles":11713,"content":11714,"level":748},"/api-reference/components/table-components#basic-usage-3",[325,325,11696],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonTable\n      :rows=\"data?.items || []\"\n      :columns=\"columns\"\n    />\n    \u003CTablePagination\n      :page=\"page\"\n      :page-count=\"pageCount\"\n      :total-items=\"data?.pagination?.totalItems || 0\"\n      :loading=\"pending\"\n      @update:page=\"handlePageChange\"\n      @update:page-count=\"handlePageCountChange\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst pageCount = ref(10)\n\nconst { data, pending, refresh } = await useCollectionQuery({\n  collection: 'users',\n  pagination: {\n    currentPage: page.value,\n    pageSize: pageCount.value\n  }\n})\n\nasync function handlePageChange(newPage: number) {\n  page.value = newPage\n  await refresh()\n}\n\nasync function handlePageCountChange(newCount: number) {\n  pageCount.value = newCount\n  page.value = 1 // Reset to first page\n  await refresh()\n}\n\u003C/script>",{"id":11716,"title":11717,"titles":11718,"content":11719,"level":748},"/api-reference/components/table-components#custom-page-sizes","Custom Page Sizes",[325,325,11696],"\u003Ctemplate>\n  \u003CTablePagination\n    :page=\"page\"\n    :page-count=\"pageCount\"\n    :total-items=\"total\"\n    :page-sizes=\"[10, 25, 50, 100]\"\n    @update:page=\"page = $event\"\n    @update:page-count=\"handlePageCountChange\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst pageCount = ref(10)\nconst total = ref(100)\n\nfunction handlePageCountChange(newCount: number) {\n  pageCount.value = newCount\n  page.value = 1 // Always reset to first page\n  // Fetch new data\n}\n\u003C/script>",{"id":11721,"title":11722,"titles":11723,"content":11724,"level":748},"/api-reference/components/table-components#with-loading-state","With Loading State",[325,325,11696],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonTable\n      :rows=\"rows\"\n      :columns=\"columns\"\n      :loading=\"loading\"\n    />\n    \u003CTablePagination\n      :page=\"page\"\n      :page-count=\"pageCount\"\n      :total-items=\"totalItems\"\n      :loading=\"loading\"\n      @update:page=\"loadPage\"\n      @update:page-count=\"loadPageCount\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst pageCount = ref(10)\nconst totalItems = ref(0)\nconst loading = ref(false)\nconst rows = ref([])\n\nasync function loadPage(newPage: number) {\n  loading.value = true\n  page.value = newPage\n  try {\n    const { data } = await $fetch('/api/users', {\n      query: { page: newPage, limit: pageCount.value }\n    })\n    rows.value = data.items\n    totalItems.value = data.total\n  } finally {\n    loading.value = false\n  }\n}\n\nasync function loadPageCount(newCount: number) {\n  pageCount.value = newCount\n  await loadPage(1) // Reset to first page\n}\n\u003C/script>",{"id":11726,"title":10098,"titles":11727,"content":11728,"level":748},"/api-reference/components/table-components#integration-with-croutontable-2",[325,325,11696],"TablePagination is automatically used in CroutonTable when pagination is enabled: \u003Ctemplate>\n  \u003CCroutonTable\n    :collection=\"collection\"\n    :rows=\"rows\"\n    :columns=\"columns\"\n    :server-pagination=\"true\"\n    :pagination-data=\"paginationData\"\n  />\n\u003C/template>",{"id":11730,"title":36,"titles":11731,"content":11732,"level":748},"/api-reference/components/table-components#troubleshooting-2",[325,325,11696],"Pagination controls disabled Problem: Buttons are grayed out and unclickableSolution: Check if loading prop is set to true Wrong page range displayed Problem: \"Showing 1 to 10 of 0 results\" even though items existSolution: Ensure totalItems prop reflects the actual total count, not just current page items Page reset doesn't work Problem: Changing page size doesn't reset to page 1Solution: Manually set page.value = 1 in the @update:page-count handler Page count out of sync Problem: Can navigate beyond the last pageSolution: Recalculate total pages: Math.ceil(totalItems / pageCount)",{"id":11734,"title":11735,"titles":11736,"content":11737,"level":449},"/api-reference/components/table-components#tableactions","TableActions",[325,325],"Provides batch action controls for table rows, including delete functionality and column visibility management. Implements a standard \"bulk actions toolbar\" pattern common in data tables.",{"id":11739,"title":4987,"titles":11740,"content":11741,"level":748},"/api-reference/components/table-components#props-4",[325,325,11735],"interface TableActionsProps {\n  selectedRows: any[]                    // Array of selected row objects (required)\n  collection: string                     // Collection name for routing (required)\n  table?: any                           // Table API instance from UTable (optional)\n  onDelete?: (ids: string[]) => void   // Custom delete handler (optional)\n  onColumnVisibilityChange?: (column: string, visible: boolean) => void  // Optional\n} PropTypeDefaultDescriptionselectedRowsany[]requiredArray of selected row objects (from table selection)collectionstringrequiredCollection identifier for delete operationstableanyundefinedTanStack Table API instance for column managementonDeleteFunctionundefinedCustom delete handler. If not provided, uses useCrouton().open()onColumnVisibilityChangeFunctionundefinedCustom handler for column visibility changes",{"id":11743,"title":5367,"titles":11744,"content":11745,"level":748},"/api-reference/components/table-components#events-2",[325,325,11735],"emit('delete', ids: string[])                                     // Emitted when delete is triggered\nemit('update:columnVisibility', column: string, visible: boolean) // Emitted when column visibility changes EventPayloadDescriptiondeletestring[]Emitted with array of IDs when delete button is clickedupdate:columnVisibilitycolumn: string, visible: booleanEmitted when user toggles column visibility in dropdown",{"id":11747,"title":183,"titles":11748,"content":11749,"level":748},"/api-reference/components/table-components#features-4",[325,325,11735],"Dynamic Delete Button State Shows count of selected itemsChanges color from neutral to error when items are selectedDisabled when no rows selectedPluralizes \"item/items\" correctly Flexible Delete Handling If onDelete prop provided: uses custom handlerIf not provided: opens Crouton delete modal via useCrouton().open('delete', ...) Column Visibility Management Dropdown menu showing all hideable columnsCheckbox toggles for each columnUses TanStack Table API for column managementOnly shows columns with getCanHide() returning true Smart Column Formatting\nUses upperFirst() from scule to format column IDs to display names (e.g., 'createdAt' → 'CreatedAt') Internationalization\nUses useT() for button labels",{"id":11751,"title":4173,"titles":11752,"content":11753,"level":748},"/api-reference/components/table-components#basic-usage-4",[325,325,11735],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cdiv class=\"flex items-center justify-between gap-3\">\n      \u003CTableSearch v-model=\"search\" />\n      \u003CTableActions\n        :selected-rows=\"selectedRows\"\n        collection=\"users\"\n        :table=\"tableRef\"\n        @delete=\"handleDelete\"\n      />\n    \u003C/div>\n    \u003CCroutonTable\n      v-model:row-selection=\"selectedRows\"\n      ref=\"tableRef\"\n      :rows=\"rows\"\n      :columns=\"columns\"\n      selection\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedRows = ref([])\nconst tableRef = ref()\n\nasync function handleDelete(ids: string[]) {\n  console.log('Deleted:', ids)\n  // Refresh data\n  await refresh()\n}\n\u003C/script>",{"id":11755,"title":11756,"titles":11757,"content":11758,"level":748},"/api-reference/components/table-components#custom-delete-handler","Custom Delete Handler",[325,325,11735],"\u003Ctemplate>\n  \u003CTableActions\n    :selected-rows=\"selectedRows\"\n    collection=\"users\"\n    :on-delete=\"customDelete\"\n    @delete=\"refreshTable\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedRows = ref([])\n\nasync function customDelete(ids: string[]) {\n  const confirmed = await showConfirmation()\n  if (!confirmed) return\n\n  await $fetch('/api/bulk-delete', {\n    method: 'DELETE',\n    body: { ids }\n  })\n\n  // Show success notification\n  toast.add({\n    title: 'Success',\n    description: `Deleted ${ids.length} items`\n  })\n}\n\nasync function refreshTable() {\n  // Refresh table data\n  selectedRows.value = []\n}\n\u003C/script>",{"id":11760,"title":11761,"titles":11762,"content":11763,"level":748},"/api-reference/components/table-components#column-visibility-control","Column Visibility Control",[325,325,11735],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003CTableActions\n      :selected-rows=\"selectedRows\"\n      collection=\"products\"\n      :table=\"tableRef\"\n      @update:column-visibility=\"handleColumnVisibilityChange\"\n    />\n    \u003CUTable\n      ref=\"tableRef\"\n      :data=\"rows\"\n      :columns=\"columns\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst tableRef = ref()\nconst selectedRows = ref([])\n\nfunction handleColumnVisibilityChange(column: string, visible: boolean) {\n  console.log(`Column ${column} visibility: ${visible}`)\n  // Optionally persist to local storage or API\n  localStorage.setItem(`column-${column}`, String(visible))\n}\n\u003C/script>",{"id":11765,"title":11766,"titles":11767,"content":11768,"level":748},"/api-reference/components/table-components#with-multiple-actions","With Multiple Actions",[325,325,11735],"\u003Ctemplate>\n  \u003Cdiv class=\"flex items-center gap-2\">\n    \u003CTableActions\n      :selected-rows=\"selectedRows\"\n      collection=\"articles\"\n      :table=\"tableRef\"\n      @delete=\"handleDelete\"\n    />\n\n    \u003C!-- Additional custom actions -->\n    \u003CUButton\n      v-if=\"selectedRows.length > 0\"\n      color=\"blue\"\n      variant=\"soft\"\n      @click=\"bulkPublish\"\n    >\n      Publish {{ selectedRows.length }} item{{ selectedRows.length > 1 ? 's' : '' }}\n    \u003C/UButton>\n\n    \u003CUButton\n      v-if=\"selectedRows.length > 0\"\n      color=\"gray\"\n      variant=\"soft\"\n      @click=\"bulkExport\"\n    >\n      Export Selected\n    \u003C/UButton>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedRows = ref([])\nconst tableRef = ref()\n\nasync function handleDelete(ids: string[]) {\n  // Handle delete\n}\n\nasync function bulkPublish() {\n  const ids = selectedRows.value.map(row => row.id)\n  await $fetch('/api/articles/bulk-publish', {\n    method: 'POST',\n    body: { ids }\n  })\n  selectedRows.value = []\n}\n\nasync function bulkExport() {\n  const ids = selectedRows.value.map(row => row.id)\n  window.location.href = `/api/export?ids=${ids.join(',')}`\n}\n\u003C/script>",{"id":11770,"title":10098,"titles":11771,"content":11772,"level":748},"/api-reference/components/table-components#integration-with-croutontable-3",[325,325,11735],"TableActions is automatically used in CroutonTable when selection is enabled: \u003Ctemplate>\n  \u003CCroutonTable\n    :collection=\"collection\"\n    :rows=\"rows\"\n    :columns=\"columns\"\n    selection  \u003C!-- Enables row selection and TableActions -->\n  />\n\u003C/template>",{"id":11774,"title":36,"titles":11775,"content":11776,"level":748},"/api-reference/components/table-components#troubleshooting-3",[325,325,11735],"Delete button always disabled Problem: Button is grayed out even when rows are selectedSolution: Ensure selectedRows is a non-empty array Column visibility not working Problem: Toggling columns doesn't hide/show themSolution: Ensure table prop is passed with the table ref from UTable Delete confirmation not showing Problem: Items are deleted immediately without confirmationSolution: The component relies on useCrouton() for confirmation. Provide a custom onDelete handler if you need custom confirmation logic. Wrong items being deleted Problem: Selected items don't match deleted itemsSolution: Ensure all rows have a unique id property. The component assumes row.id exists. Type errors with table prop Problem: TypeScript errors about table prop being anySolution: This is a known limitation. The component currently uses any type. You can safely ignore or cast to proper TanStack Table types.",{"id":11778,"title":11779,"titles":11780,"content":11781,"level":391},"/api-reference/components/table-components#croutontableactions","CroutonTableActions",[325],"Action buttons for table operations: bulk delete and column visibility toggle.",{"id":11783,"title":4987,"titles":11784,"content":11785,"level":449},"/api-reference/components/table-components#props-5",[325,11779],"interface TableActionsProps {\n  selectedRows: any[]\n  collection: string\n  table?: any // TanStack Table instance\n  onDelete?: (ids: string[]) => void\n} PropTypeDefaultDescriptionselectedRowsany[]-Array of selected table rowscollectionstring-Collection name for delete operationtableany-TanStack Table API instanceonDeleteFunction-Custom delete handler (optional)",{"id":11787,"title":5367,"titles":11788,"content":11789,"level":449},"/api-reference/components/table-components#events-3",[325,11779],"EventPayloadDescriptiondeletestring[]Emitted when delete is triggered with array of IDsupdate:columnVisibilitycolumn: string, visible: booleanEmitted when column visibility changes",{"id":11791,"title":183,"titles":11792,"content":11793,"level":449},"/api-reference/components/table-components#features-5",[325,11779],"Bulk Delete: Delete multiple selected rows at onceDynamic Button State: Delete button color changes when items selectedColumn Visibility: Dropdown menu to show/hide columnsi18n Support: Uses translation keys for labelsDisabled State: Delete button disabled when no rows selectedCustom Delete Handler: Override default delete behavior",{"id":11795,"title":1608,"titles":11796,"content":528,"level":449},"/api-reference/components/table-components#usage",[325,11779],{"id":11798,"title":4173,"titles":11799,"content":11800,"level":748},"/api-reference/components/table-components#basic-usage-5",[325,11779,1608],"\u003Ctemplate>\n  \u003CCroutonTableActions\n    :selected-rows=\"selectedRows\"\n    :collection=\"collection\"\n    :table=\"tableInstance\"\n    @delete=\"handleDelete\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedRows = ref([])\nconst collection = 'products'\nconst tableInstance = ref(null)\n\nconst handleDelete = (ids: string[]) => {\n  console.log('Deleting items:', ids)\n}\n\u003C/script>",{"id":11802,"title":11803,"titles":11804,"content":11805,"level":748},"/api-reference/components/table-components#with-custom-delete-handler","With Custom Delete Handler",[325,11779,1608],"\u003Ctemplate>\n  \u003CCroutonTableActions\n    :selected-rows=\"selected\"\n    collection=\"users\"\n    :on-delete=\"customDeleteHandler\"\n    @delete=\"afterDelete\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selected = ref([])\n\nconst customDeleteHandler = async (ids: string[]) => {\n  // Custom delete logic\n  await api.users.deleteBatch(ids)\n  toast.success(`Deleted ${ids.length} users`)\n}\n\nconst afterDelete = (ids: string[]) => {\n  // Refresh table or update UI\n  refreshTable()\n}\n\u003C/script>",{"id":11807,"title":11761,"titles":11808,"content":11809,"level":748},"/api-reference/components/table-components#column-visibility-control-1",[325,11779,1608],"\u003Ctemplate>\n  \u003CCroutonTableActions\n    :selected-rows=\"[]\"\n    collection=\"orders\"\n    :table=\"table\"\n    @update:columnVisibility=\"handleColumnToggle\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nimport { useVueTable } from '@tanstack/vue-table'\n\nconst table = useVueTable({\n  // ... table config\n})\n\nconst handleColumnToggle = (column: string, visible: boolean) => {\n  console.log(`Column ${column} visibility: ${visible}`)\n}\n\u003C/script>",{"id":11811,"title":10098,"titles":11812,"content":11813,"level":748},"/api-reference/components/table-components#integration-with-croutontable-4",[325,11779,1608],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonTableActions\n      :selected-rows=\"selectedRows\"\n      :collection=\"collection\"\n      :table=\"tableRef\"\n      @delete=\"refreshData\"\n    />\n    \n    \u003CCroutonTable\n      v-model:selected-rows=\"selectedRows\"\n      :rows=\"data\"\n      :columns=\"columns\"\n      @table-ready=\"(t) => tableRef = t\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedRows = ref([])\nconst tableRef = ref(null)\nconst collection = 'products'\n\u003C/script>",{"id":11815,"title":11816,"titles":11817,"content":11818,"level":449},"/api-reference/components/table-components#delete-button-states","Delete Button States",[325,11779],"StateColorVariantDisabledNo selectionneutralsubtletrueItems selectederrorsubtlefalse",{"id":11820,"title":11821,"titles":11822,"content":11823,"level":449},"/api-reference/components/table-components#column-visibility-menu","Column Visibility Menu",[325,11779],"The dropdown menu includes: All columns where column.getCanHide() returns trueCheckbox for each columnLabel auto-formatted with upperFirst() (e.g., \"firstName\" → \"FirstName\")Click to toggle visibility \u003C!-- Column menu structure -->\n\u003CUDropdownMenu :items=\"columnVisibilityItems\">\n  \u003CUButton\n    label=\"Display\"\n    color=\"neutral\"\n    variant=\"outline\"\n    trailing-icon=\"i-lucide-settings-2\"\n  />\n\u003C/UDropdownMenu> Translation Keys: The component uses tString('table.display') for the button label. Ensure your translation system has this key defined.",{"id":11825,"title":11826,"titles":11827,"content":11828,"level":391},"/api-reference/components/table-components#croutontablecheckbox","CroutonTableCheckbox",[325],"Wrapper component for table row selection checkbox with indeterminate support.",{"id":11830,"title":4987,"titles":11831,"content":11832,"level":449},"/api-reference/components/table-components#props-6",[325,11826],"PropTypeDefaultDescriptionmodelValueboolean | 'indeterminate'-Checkbox state (checked/unchecked/indeterminate)",{"id":11834,"title":5367,"titles":11835,"content":11836,"level":449},"/api-reference/components/table-components#events-4",[325,11826],"EventPayloadDescriptionupdate:modelValueboolean | 'indeterminate'Emitted when checkbox state changes",{"id":11838,"title":183,"titles":11839,"content":11840,"level":449},"/api-reference/components/table-components#features-6",[325,11826],"Indeterminate State: Supports three-state checkbox (checked/unchecked/indeterminate)UCheckbox Wrapper: Thin wrapper around Nuxt UI's UCheckboxType-Safe: TypeScript support for all three states",{"id":11842,"title":1608,"titles":11843,"content":528,"level":449},"/api-reference/components/table-components#usage-1",[325,11826],{"id":11845,"title":4173,"titles":11846,"content":11847,"level":748},"/api-reference/components/table-components#basic-usage-6",[325,11826,1608],"\u003Ctemplate>\n  \u003CCroutonTableCheckbox v-model=\"selected\" />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selected = ref(false)\n\u003C/script>",{"id":11849,"title":11850,"titles":11851,"content":11852,"level":748},"/api-reference/components/table-components#indeterminate-state","Indeterminate State",[325,11826,1608],"\u003Ctemplate>\n  \u003CCroutonTableCheckbox v-model=\"checkboxState\" />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst checkboxState = ref\u003Cboolean | 'indeterminate'>('indeterminate')\n\n// State can be:\n// - true (checked)\n// - false (unchecked)\n// - 'indeterminate' (partially checked, shown as dash/minus)\n\u003C/script>",{"id":11854,"title":11855,"titles":11856,"content":11857,"level":748},"/api-reference/components/table-components#header-select-all-checkbox","Header \"Select All\" Checkbox",[325,11826,1608],"\u003Ctemplate>\n  \u003Ctable>\n    \u003Cthead>\n      \u003Ctr>\n        \u003Cth>\n          \u003CCroutonTableCheckbox\n            :model-value=\"headerCheckboxState\"\n            @update:model-value=\"handleSelectAll\"\n          />\n        \u003C/th>\n      \u003C/tr>\n    \u003C/thead>\n    \u003Ctbody>\n      \u003Ctr v-for=\"row in rows\" :key=\"row.id\">\n        \u003Ctd>\n          \u003CCroutonTableCheckbox v-model=\"row.selected\" />\n        \u003C/td>\n      \u003C/tr>\n    \u003C/tbody>\n  \u003C/table>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst rows = ref([\n  { id: 1, selected: false },\n  { id: 2, selected: true },\n  { id: 3, selected: false }\n])\n\nconst headerCheckboxState = computed\u003Cboolean | 'indeterminate'>(() => {\n  const selectedCount = rows.value.filter(r => r.selected).length\n  if (selectedCount === 0) return false\n  if (selectedCount === rows.value.length) return true\n  return 'indeterminate'\n})\n\nconst handleSelectAll = (state: boolean | 'indeterminate') => {\n  const newState = state === true\n  rows.value.forEach(row => row.selected = newState)\n}\n\u003C/script>",{"id":11859,"title":11860,"titles":11861,"content":11862,"level":748},"/api-reference/components/table-components#with-tanstack-table","With TanStack Table",[325,11826,1608],"\u003Ctemplate>\n  \u003Ctable>\n    \u003Cthead>\n      \u003Ctr v-for=\"headerGroup in table.getHeaderGroups()\">\n        \u003Cth v-for=\"header in headerGroup.headers\">\n          \u003CCroutonTableCheckbox\n            v-if=\"header.id === 'select'\"\n            :model-value=\"table.getIsAllRowsSelected() ? true : \n                         table.getIsSomeRowsSelected() ? 'indeterminate' : false\"\n            @update:model-value=\"table.toggleAllRowsSelected()\"\n          />\n        \u003C/th>\n      \u003C/tr>\n    \u003C/thead>\n  \u003C/table>\n\u003C/template>",{"id":11864,"title":11865,"titles":11866,"content":11867,"level":449},"/api-reference/components/table-components#state-values","State Values",[325,11826],"ValueVisualMeaningfalseEmpty boxNot selectedtrueCheckmarkSelected'indeterminate'Dash/minusPartially selected TanStack Integration: This component is designed to work seamlessly with TanStack Table's selection state management.",{"id":11869,"title":11870,"titles":11871,"content":11872,"level":391},"/api-reference/components/table-components#croutontableheader","CroutonTableHeader",[325],"Header component for data tables with optional create button.",{"id":11874,"title":4987,"titles":11875,"content":11876,"level":449},"/api-reference/components/table-components#props-7",[325,11870],"PropTypeDefaultDescriptiontitlestring''Header title textcollectionstring''Collection name (used for create action)createButtonbooleanfalseShow create button",{"id":11878,"title":5372,"titles":11879,"content":11880,"level":449},"/api-reference/components/table-components#slots-2",[325,11870],"SlotDescriptionextraButtonsAdditional buttons in header right area",{"id":11882,"title":183,"titles":11883,"content":11884,"level":449},"/api-reference/components/table-components#features-7",[325,11870],"Dashboard Navbar: Built on UDashboardNavbarCreate Button: Opens create modal for collectionResponsive Label: Hides collection name on mobile (\u003C md)Extra Buttons Slot: Add custom header actionsAuto-Formatting: Formats collection name with capital singular",{"id":11886,"title":1608,"titles":11887,"content":528,"level":449},"/api-reference/components/table-components#usage-2",[325,11870],{"id":11889,"title":4173,"titles":11890,"content":11891,"level":748},"/api-reference/components/table-components#basic-usage-7",[325,11870,1608],"\u003Ctemplate>\n  \u003CCroutonTableHeader\n    title=\"All Products\"\n    collection=\"products\"\n    create-button\n  />\n\u003C/template>",{"id":11893,"title":11653,"titles":11894,"content":11895,"level":748},"/api-reference/components/table-components#with-extra-buttons-1",[325,11870,1608],"\u003Ctemplate>\n  \u003CCroutonTableHeader\n    title=\"User Management\"\n    collection=\"users\"\n    create-button\n  >\n    \u003Ctemplate #extraButtons>\n      \u003CUButton\n        color=\"neutral\"\n        variant=\"outline\"\n        icon=\"i-lucide-download\"\n        @click=\"exportUsers\"\n      >\n        Export\n      \u003C/UButton>\n      \u003CUButton\n        color=\"neutral\"\n        variant=\"outline\"\n        icon=\"i-lucide-filter\"\n        @click=\"openFilters\"\n      >\n        Filters\n      \u003C/UButton>\n    \u003C/template>\n  \u003C/CroutonTableHeader>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst exportUsers = () => {\n  // Export logic\n}\n\nconst openFilters = () => {\n  // Open filters modal\n}\n\u003C/script>",{"id":11897,"title":11898,"titles":11899,"content":11900,"level":748},"/api-reference/components/table-components#without-create-button","Without Create Button",[325,11870,1608],"\u003Ctemplate>\n  \u003CCroutonTableHeader\n    title=\"System Logs\"\n    collection=\"logs\"\n  />\n  \u003C!-- No create button shown -->\n\u003C/template>",{"id":11902,"title":11903,"titles":11904,"content":11905,"level":748},"/api-reference/components/table-components#complete-table-page-example","Complete Table Page Example",[325,11870,1608],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonTableHeader\n      title=\"Products\"\n      collection=\"products\"\n      create-button\n    >\n      \u003Ctemplate #extraButtons>\n        \u003CUButton\n          icon=\"i-lucide-upload\"\n          variant=\"outline\"\n          @click=\"importProducts\"\n        >\n          Import\n        \u003C/UButton>\n      \u003C/template>\n    \u003C/CroutonTableHeader>\n\n    \u003CCroutonTable\n      :rows=\"products\"\n      :columns=\"columns\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { open } = useCrouton()\nconst products = ref([])\n\nconst importProducts = () => {\n  // Import logic\n}\n\u003C/script>",{"id":11907,"title":11908,"titles":11909,"content":11910,"level":449},"/api-reference/components/table-components#create-button-behavior","Create Button Behavior",[325,11870],"When clicked, the create button: Calls useCrouton().open('create', collection)Opens the create modal/form for the specified collectionLogs debug info to console (collection name, button state)",{"id":11912,"title":11913,"titles":11914,"content":11915,"level":449},"/api-reference/components/table-components#responsive-design","Responsive Design",[325,11870],"Screen SizeButton LabelMobile (\u003C md)\"Create\"Desktop (md+)\"Create {CollectionSingular}\" Example: collection=\"products\" → Mobile: \"Create\", Desktop: \"Create Product\"collection=\"users\" → Mobile: \"Create\", Desktop: \"Create User\" Collection Formatting: Uses useFormatCollections().collectionWithCapitalSingular() to format collection names (e.g., \"products\" → \"Product\").",{"id":11917,"title":11918,"titles":11919,"content":11920,"level":391},"/api-reference/components/table-components#croutontablepagination","CroutonTablePagination",[325],"Pagination controls for tables with page size selector and result summary.",{"id":11922,"title":4987,"titles":11923,"content":11924,"level":449},"/api-reference/components/table-components#props-8",[325,11918],"interface TablePaginationProps {\n  page: number\n  pageCount: number\n  totalItems: number\n  loading?: boolean\n  pageSizes?: number[]\n} PropTypeDefaultDescriptionpagenumber-Current page number (1-indexed)pageCountnumber-Items per pagetotalItemsnumber-Total number of items across all pagesloadingbooleanfalseDisable pagination during loadingpageSizesnumber[][5, 10, 20, 30, 40]Available page size options",{"id":11926,"title":5367,"titles":11927,"content":11928,"level":449},"/api-reference/components/table-components#events-5",[325,11918],"EventPayloadDescriptionupdate:pagenumberEmitted when page changesupdate:pageCountnumberEmitted when page size changes",{"id":11930,"title":183,"titles":11931,"content":11932,"level":449},"/api-reference/components/table-components#features-8",[325,11918],"Page Navigation: UPagination component for page selectionPage Size Selector: Dropdown to change items per pageResult Summary: Shows \"Showing X to Y of Z results\"i18n Support: Uses translation keys for labelsLoading State: Disables controls when loadingBorder Top: Visual separator from table content",{"id":11934,"title":1608,"titles":11935,"content":528,"level":449},"/api-reference/components/table-components#usage-3",[325,11918],{"id":11937,"title":4173,"titles":11938,"content":11939,"level":748},"/api-reference/components/table-components#basic-usage-8",[325,11918,1608],"\u003Ctemplate>\n  \u003CCroutonTablePagination\n    v-model:page=\"currentPage\"\n    v-model:page-count=\"itemsPerPage\"\n    :total-items=\"totalItems\"\n    :loading=\"isLoading\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst currentPage = ref(1)\nconst itemsPerPage = ref(10)\nconst totalItems = ref(250)\nconst isLoading = ref(false)\n\nwatch([currentPage, itemsPerPage], () => {\n  fetchData()\n})\n\u003C/script>",{"id":11941,"title":11717,"titles":11942,"content":11943,"level":748},"/api-reference/components/table-components#custom-page-sizes-1",[325,11918,1608],"\u003Ctemplate>\n  \u003CCroutonTablePagination\n    v-model:page=\"page\"\n    v-model:page-count=\"pageSize\"\n    :total-items=\"total\"\n    :page-sizes=\"[10, 25, 50, 100]\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst pageSize = ref(25)\nconst total = ref(1000)\n\u003C/script>",{"id":11945,"title":11946,"titles":11947,"content":11948,"level":748},"/api-reference/components/table-components#with-table-integration","With Table Integration",[325,11918,1608],"\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003CCroutonTable\n      :rows=\"paginatedData\"\n      :columns=\"columns\"\n    />\n    \n    \u003CCroutonTablePagination\n      v-model:page=\"pagination.page\"\n      v-model:page-count=\"pagination.pageSize\"\n      :total-items=\"data.length\"\n      :loading=\"fetching\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst data = ref([]) // All data\nconst pagination = ref({ page: 1, pageSize: 20 })\nconst fetching = ref(false)\n\nconst paginatedData = computed(() => {\n  const start = (pagination.value.page - 1) * pagination.value.pageSize\n  const end = start + pagination.value.pageSize\n  return data.value.slice(start, end)\n})\n\nconst fetchData = async () => {\n  fetching.value = true\n  // Fetch data\n  fetching.value = false\n}\n\u003C/script>",{"id":11950,"title":4134,"titles":11951,"content":11952,"level":748},"/api-reference/components/table-components#server-side-pagination",[325,11918,1608],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonTable :rows=\"serverData\" :columns=\"columns\" />\n    \n    \u003CCroutonTablePagination\n      v-model:page=\"serverPage\"\n      v-model:page-count=\"serverPageSize\"\n      :total-items=\"serverTotal\"\n      :loading=\"serverLoading\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst serverPage = ref(1)\nconst serverPageSize = ref(10)\nconst serverTotal = ref(0)\nconst serverLoading = ref(false)\nconst serverData = ref([])\n\nconst fetchPage = async () => {\n  serverLoading.value = true\n  const response = await $fetch('/api/data', {\n    params: {\n      page: serverPage.value,\n      pageSize: serverPageSize.value\n    }\n  })\n  serverData.value = response.data\n  serverTotal.value = response.total\n  serverLoading.value = false\n}\n\nwatch([serverPage, serverPageSize], fetchPage, { immediate: true })\n\u003C/script>",{"id":11954,"title":11955,"titles":11956,"content":11957,"level":449},"/api-reference/components/table-components#result-summary-format","Result Summary Format",[325,11918],"The summary displays: Rows per page: [10 ▼]  Showing 11 to 20 of 250 results Calculations: pageFrom: (page - 1) * pageCount + 1pageTo: Math.min(page * pageCount, totalItems) Example with page=2, pageCount=10, totalItems=95: Shows: \"Showing 11 to 20 of 95 results\"",{"id":11959,"title":11960,"titles":11961,"content":11962,"level":449},"/api-reference/components/table-components#translation-keys","Translation Keys",[325,11918],"The component uses these translation keys: table.rowsPerPageColontable.rowsPerPage (for select label)table.showingtable.totable.oftable.results Sticky Footer: Use mt-auto class on parent container to keep pagination at bottom of available space.",{"id":11964,"title":11965,"titles":11966,"content":11967,"level":391},"/api-reference/components/table-components#croutontablesearch","CroutonTableSearch",[325],"Debounced search input for filtering table data.",{"id":11969,"title":4987,"titles":11970,"content":11971,"level":449},"/api-reference/components/table-components#props-9",[325,11965],"interface TableSearchProps {\n  modelValue: string\n  placeholder?: string\n  debounceMs?: number\n} PropTypeDefaultDescriptionmodelValuestring-Search query string (v-model)placeholderstring'Search...'Input placeholder textdebounceMsnumber300Debounce delay in milliseconds",{"id":11973,"title":5367,"titles":11974,"content":11975,"level":449},"/api-reference/components/table-components#events-6",[325,11965],"EventPayloadDescriptionupdate:modelValuestringEmitted after debounce when search value changes",{"id":11977,"title":183,"titles":11978,"content":11979,"level":449},"/api-reference/components/table-components#features-9",[325,11965],"Debounced Input: Uses VueUse useDebounceFn for performanceSearch Icon: Built-in magnifying glass iconMax Width: Constrained to max-w-sm (24rem)Configurable Delay: Adjust debounce timingEmpty State: Emits empty string when cleared",{"id":11981,"title":1608,"titles":11982,"content":528,"level":449},"/api-reference/components/table-components#usage-4",[325,11965],{"id":11984,"title":4173,"titles":11985,"content":11986,"level":748},"/api-reference/components/table-components#basic-usage-9",[325,11965,1608],"\u003Ctemplate>\n  \u003CCroutonTableSearch v-model=\"searchQuery\" />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst searchQuery = ref('')\n\nwatch(searchQuery, (query) => {\n  console.log('Searching for:', query)\n  // Fetch filtered data\n})\n\u003C/script>",{"id":11988,"title":11989,"titles":11990,"content":11991,"level":748},"/api-reference/components/table-components#custom-placeholder-and-debounce","Custom Placeholder and Debounce",[325,11965,1608],"\u003Ctemplate>\n  \u003CCroutonTableSearch\n    v-model=\"query\"\n    placeholder=\"Search products...\"\n    :debounce-ms=\"500\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst query = ref('')\n// Emits after 500ms of no typing\n\u003C/script>",{"id":11993,"title":11994,"titles":11995,"content":11996,"level":748},"/api-reference/components/table-components#integrated-with-table","Integrated with Table",[325,11965,1608],"\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003Cdiv class=\"flex justify-between\">\n      \u003CCroutonTableSearch\n        v-model=\"searchQuery\"\n        placeholder=\"Search users...\"\n      />\n      \u003CUButton color=\"primary\" @click=\"createUser\">\n        Create User\n      \u003C/UButton>\n    \u003C/div>\n\n    \u003CCroutonTable\n      :rows=\"filteredUsers\"\n      :columns=\"columns\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst searchQuery = ref('')\nconst users = ref([])\n\nconst filteredUsers = computed(() => {\n  if (!searchQuery.value) return users.value\n  \n  const query = searchQuery.value.toLowerCase()\n  return users.value.filter(user =>\n    user.name.toLowerCase().includes(query) ||\n    user.email.toLowerCase().includes(query)\n  )\n})\n\u003C/script>",{"id":11998,"title":11722,"titles":11999,"content":12000,"level":748},"/api-reference/components/table-components#with-loading-state-1",[325,11965,1608],"\u003Ctemplate>\n  \u003Cdiv class=\"relative\">\n    \u003CCroutonTableSearch\n      v-model=\"search\"\n      placeholder=\"Type to search...\"\n    />\n    \u003Cdiv\n      v-if=\"searching\"\n      class=\"absolute right-2 top-2\"\n    >\n      \u003CUIcon name=\"i-lucide-loader-2\" class=\"animate-spin\" />\n    \u003C/div>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst search = ref('')\nconst searching = ref(false)\n\nwatch(search, async (query) => {\n  if (!query) return\n  \n  searching.value = true\n  await fetchSearchResults(query)\n  searching.value = false\n})\n\u003C/script>",{"id":12002,"title":12003,"titles":12004,"content":12005,"level":748},"/api-reference/components/table-components#server-side-search","Server-Side Search",[325,11965,1608],"\u003Ctemplate>\n  \u003CCroutonTableSearch\n    v-model=\"serverSearch\"\n    placeholder=\"Search...\"\n    :debounce-ms=\"400\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst serverSearch = ref('')\n\nwatch(serverSearch, async (query) => {\n  const { data } = await useFetch('/api/search', {\n    params: { q: query }\n  })\n  // Update results\n})\n\u003C/script>",{"id":12007,"title":12008,"titles":12009,"content":12010,"level":449},"/api-reference/components/table-components#debounce-behavior","Debounce Behavior",[325,11965],"Without debouncing, every keystroke would trigger a search: User types: \"hello\"\nWithout debounce: 5 searches (h, he, hel, hell, hello)\nWith 300ms debounce: 1 search (hello) - after user stops typing Example debounce values: 100ms - Very responsive, still reduces load significantly300ms - Default, good balance500ms - Slower response, fewer API calls1000ms - Very slow, minimal API calls Performance: Debouncing is crucial for server-side searches to avoid excessive API calls. The default 300ms works well for most use cases.",{"id":12012,"title":1007,"titles":12013,"content":12014,"level":391},"/api-reference/components/table-components#related-resources",[325],"Table Composables - Table data managementNuxt UI Table - Base table componentTable Patterns - Table composition patterns html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":330,"title":329,"titles":12016,"content":12017,"level":385},[],"Helper components for loading states, errors, and special behaviors",{"id":12019,"title":329,"titles":12020,"content":12021,"level":391},"/api-reference/components/utility-components#utility-components",[329],"Utility components for common UI patterns like loading states and validation feedback.",{"id":12023,"title":12024,"titles":12025,"content":12026,"level":391},"/api-reference/components/utility-components#loading","Loading",[329],"A utility component that displays loading states during CRUD operations. Automatically responds to useCrouton() loading state changes and shows appropriate UI feedback. Auto-wired: The Loading component automatically reads from useCrouton() composable and requires no props. Simply include it in your templates where you want loading feedback.",{"id":12028,"title":4987,"titles":12029,"content":12030,"level":449},"/api-reference/components/utility-components#props",[329,12024],"No props - The component automatically reads loading state from useCrouton().",{"id":12032,"title":12033,"titles":12034,"content":12035,"level":449},"/api-reference/components/utility-components#internal-logic","Internal Logic",[329,12024],"The component connects to the global Crouton state and displays different UI based on the current loading value: // Loading states from useCrouton()\ntype LoadingState = \n  | 'notLoading'\n  | 'create_open'  // Opening create form\n  | 'create_send'  // Submitting create request\n  | 'update_open'  // Opening update form\n  | 'update_send'  // Submitting update request\n  | 'delete_send'  // Submitting delete request\n  | 'view_open'    // Opening view mode\n  | 'view_send'    // Loading view data",{"id":12037,"title":1608,"titles":12038,"content":528,"level":449},"/api-reference/components/utility-components#usage",[329,12024],{"id":12040,"title":4173,"titles":12041,"content":12042,"level":748},"/api-reference/components/utility-components#basic-usage",[329,12024,1608],"Simply add the component to your template: \u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonLoading />\n    \u003C!-- Your other content -->\n  \u003C/div>\n\u003C/template>",{"id":12044,"title":12045,"titles":12046,"content":12047,"level":748},"/api-reference/components/utility-components#in-a-form-layout","In a Form Layout",[329,12024,1608],"\u003Ctemplate>\n  \u003Cdiv class=\"space-y-6\">\n    \u003CCroutonLoading />\n    \n    \u003CUForm v-if=\"!loading\" :state=\"formState\" @submit=\"handleSubmit\">\n      \u003C!-- Form fields -->\n    \u003C/UForm>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { loading } = useCrouton()\n\u003C/script>",{"id":12049,"title":12050,"titles":12051,"content":12052,"level":748},"/api-reference/components/utility-components#in-a-modalslideover","In a Modal/Slideover",[329,12024,1608],"\u003Ctemplate>\n  \u003CUSlideover v-model=\"isOpen\">\n    \u003Ctemplate #content=\"{ close }\">\n      \u003Cdiv class=\"p-6\">\n        \u003Ch3 class=\"text-lg font-semibold mb-4\">Edit Item\u003C/h3>\n        \n        \u003CCroutonLoading />\n        \n        \u003CUForm v-if=\"!loading\" :state=\"state\" @submit=\"onSubmit\">\n          \u003C!-- Form fields -->\n        \u003C/UForm>\n      \u003C/div>\n    \u003C/template>\n  \u003C/USlideover>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { loading, open } = useCrouton()\nconst isOpen = ref(false)\n\u003C/script>",{"id":12054,"title":12055,"titles":12056,"content":12057,"level":449},"/api-reference/components/utility-components#loading-state-displays","Loading State Displays",[329,12024],"Form Opening States (create_open, update_open): Shows 5 skeleton loadersEach loader displays:\nLabel skeleton (h-6, w-40)Input skeleton (h-10, w-full)Simulates form field loading Submit States (create_send, update_send): Shows text: \"SENDING UPDATE\"Indicates active submission Other States: notLoading: Nothing displayeddelete_send: Nothing displayed (deletion uses confirmation modal)view_open, view_send: Nothing displayed",{"id":12059,"title":10089,"titles":12060,"content":12061,"level":449},"/api-reference/components/utility-components#visual-example",[329,12024],"When opening a form (create_open or update_open): ┌────────────────────────────────┐\n│ ████████░░░░░░░░ (label)       │\n│ ████████████████ (input)       │\n│                                │\n│ ████████░░░░░░░░ (label)       │\n│ ████████████████ (input)       │\n│                                │\n│ ████████░░░░░░░░ (label)       │\n│ ████████████████ (input)       │\n│                                │\n│ ████████░░░░░░░░ (label)       │\n│ ████████████████ (input)       │\n│                                │\n│ ████████░░░░░░░░ (label)       │\n│ ████████████████ (input)       │\n└────────────────────────────────┘",{"id":12063,"title":12064,"titles":12065,"content":12066,"level":449},"/api-reference/components/utility-components#integration-with-crouton-forms","Integration with Crouton Forms",[329,12024],"The Loading component works seamlessly with generated Crouton forms: \u003C!-- layers/products/collections/products/app/components/_Form.vue -->\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003C!-- Automatically shows loading state during form operations -->\n    \u003CCroutonLoading />\n    \n    \u003CCroutonForm\n      :schema=\"schema\"\n      :collection=\"collection\"\n      :items=\"items\"\n      :loading=\"loading\"\n      :action=\"action\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { loading } = useCrouton()\n// Loading state automatically updates during:\n// - Form opening (create_open, update_open)\n// - Form submission (create_send, update_send)\n\u003C/script>",{"id":12068,"title":1383,"titles":12069,"content":12070,"level":449},"/api-reference/components/utility-components#customization",[329,12024],"Since the component has no props, customization requires forking the component: \u003C!-- components/CustomLoading.vue -->\n\u003Ctemplate>\n  \u003Cdiv class=\"flex flex-col items-center gap-4\">\n    \u003C!-- Custom loading for form opening -->\n    \u003Ctemplate v-if=\"loading === 'create_open' || loading === 'update_open'\">\n      \u003Cdiv class=\"flex items-center gap-2 text-blue-600\">\n        \u003CUIcon name=\"i-lucide-loader-2\" class=\"animate-spin\" />\n        \u003Cspan>Loading form...\u003C/span>\n      \u003C/div>\n    \u003C/template>\n\n    \u003C!-- Custom loading for submission -->\n    \u003Ctemplate v-if=\"loading === 'create_send' || loading === 'update_send'\">\n      \u003Cdiv class=\"flex items-center gap-2 text-green-600\">\n        \u003CUIcon name=\"i-lucide-check-circle\" class=\"animate-pulse\" />\n        \u003Cspan>Saving changes...\u003C/span>\n      \u003C/div>\n    \u003C/template>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { loading } = useCrouton()\n\u003C/script>",{"id":12072,"title":36,"titles":12073,"content":12074,"level":449},"/api-reference/components/utility-components#troubleshooting",[329,12024],"Loading component not showing anything Problem: Component renders but no skeletons appearSolution: Check that useCrouton() is properly initialized. The loading state must be 'create_open', 'update_open', 'create_send', or 'update_send'. Skeletons show too long Problem: Loading skeletons remain visible after form loadsSolution: Ensure the Crouton state is properly transitioning from create_open/update_open to notLoading. Check for errors in form loading. \"SENDING UPDATE\" doesn't disappear Problem: Submit message persists after submissionSolution: Verify that your API endpoint is properly responding and the Crouton state is being reset. Check for uncaught errors in submission handlers. Want different loading states Problem: Need different UI for different loading scenariosSolution: Create a custom loading component (see Customization above) or use conditional rendering based on loading value:\n\u003Ctemplate>\n  \u003Cdiv v-if=\"loading === 'create_open'\">Custom create loader\u003C/div>\n  \u003Cdiv v-else-if=\"loading === 'update_send'\">Custom save indicator\u003C/div>\n\u003C/template>",{"id":12076,"title":12077,"titles":12078,"content":12079,"level":391},"/api-reference/components/utility-components#validationerrorsummary","ValidationErrorSummary",[329],"A validation error summary component that displays form validation errors grouped by tabs/sections. Provides clickable links to navigate to error locations with error counts per section. Form Validation Helper: Use this component at the top of tabbed forms to provide users with a clear overview of validation errors across all tabs.",{"id":12081,"title":4987,"titles":12082,"content":12083,"level":449},"/api-reference/components/utility-components#props-1",[329,12077],"interface Props {\n  tabErrors: Record\u003Cstring, number>\n  navigationItems: Array\u003C{ label: string; value: string }>\n} PropTypeRequiredDefaultDescriptiontabErrorsRecord\u003Cstring, number>Yes-Map of tab values to error counts (e.g., { 'basic': 2, 'advanced': 1 })navigationItemsArray\u003C{ label: string; value: string }>Yes-Array of navigation items matching your tab structure",{"id":12085,"title":5367,"titles":12086,"content":12087,"level":449},"/api-reference/components/utility-components#events",[329,12077],"interface Events {\n  'switch-tab': [tabValue: string]\n} EventPayloadDescriptionswitch-tabtabValue: stringEmitted when user clicks on a tab error link. Use to programmatically switch to the tab with errors.",{"id":12089,"title":12033,"titles":12090,"content":12091,"level":449},"/api-reference/components/utility-components#internal-logic-1",[329,12077],"The component: Filters tabErrors to only show tabs with errors (count > 0)Maps tab values to readable labels using navigationItemsCalculates total error count across all tabsDisplays a UAlert (error color) if any errors existRenders clickable links for each tab with errorsShows individual error counts per tab",{"id":12093,"title":1608,"titles":12094,"content":528,"level":449},"/api-reference/components/utility-components#usage-1",[329,12077],{"id":12096,"title":12097,"titles":12098,"content":12099,"level":748},"/api-reference/components/utility-components#basic-tabbed-form","Basic Tabbed Form",[329,12077,1608],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003C!-- Error summary at top -->\n    \u003CCroutonValidationErrorSummary\n      :tab-errors=\"tabErrors\"\n      :navigation-items=\"navigationItems\"\n      @switch-tab=\"handleSwitchTab\"\n    />\n\n    \u003C!-- Tab navigation -->\n    \u003CUTabs v-model=\"selectedTab\" :items=\"navigationItems\">\n      \u003Ctemplate #basic>\n        \u003CUFormField label=\"Name\" name=\"name\" :error=\"errors.name\">\n          \u003CUInput v-model=\"state.name\" />\n        \u003C/UFormField>\n        \u003C!-- More fields -->\n      \u003C/template>\n      \n      \u003Ctemplate #advanced>\n        \u003CUFormField label=\"Config\" name=\"config\" :error=\"errors.config\">\n          \u003CUInput v-model=\"state.config\" />\n        \u003C/UFormField>\n        \u003C!-- More fields -->\n      \u003C/template>\n    \u003C/UTabs>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst selectedTab = ref('basic')\n\nconst navigationItems = [\n  { label: 'Basic Info', value: 'basic' },\n  { label: 'Advanced', value: 'advanced' },\n  { label: 'Settings', value: 'settings' }\n]\n\n// Error tracking per tab\nconst errors = ref({\n  name: 'Required field',\n  config: null,\n  apiKey: 'Invalid format'\n})\n\n// Calculate errors per tab\nconst tabErrors = computed(() => ({\n  basic: errors.value.name ? 1 : 0,\n  advanced: errors.value.config ? 1 : 0,\n  settings: errors.value.apiKey ? 1 : 0\n}))\n\n// Handle tab switch from error summary\nconst handleSwitchTab = (tabValue: string) => {\n  selectedTab.value = tabValue\n}\n\u003C/script>",{"id":12101,"title":12102,"titles":12103,"content":12104,"level":748},"/api-reference/components/utility-components#with-zod-schema-validation","With Zod Schema Validation",[329,12077,1608],"Basic Zod integration: \u003Cscript setup lang=\"ts\">\nimport { z } from 'zod'\n\nconst schema = z.object({\n  email: z.string().email('Invalid email'),\n  username: z.string().min(3, 'Min 3 characters'),\n  bio: z.string().max(500, 'Max 500 characters'),\n  website: z.string().url('Invalid URL').optional()\n})\n\nconst formState = ref({\n  email: '',\n  username: '',\n  bio: '',\n  website: ''\n})\n\nconst validationErrors = ref\u003CZodError | null>(null)\n\nconst onSubmit = async (data: any) => {\n  try {\n    schema.parse(data)\n    validationErrors.value = null\n    // Submit data\n  } catch (err) {\n    if (err instanceof z.ZodError) {\n      validationErrors.value = err\n    }\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm :state=\"formState\" :schema=\"schema\" @submit=\"onSubmit\">\n    \u003CUFormField label=\"Email\" name=\"email\">\n      \u003CUInput v-model=\"formState.email\" type=\"email\" />\n    \u003C/UFormField>\n    \u003CUButton type=\"submit\">Save\u003C/UButton>\n  \u003C/UForm>\n\u003C/template>",{"id":12106,"title":12107,"titles":12108,"content":12109,"level":748},"/api-reference/components/utility-components#mapping-errors-to-tabs","Mapping Errors to Tabs",[329,12077,1608],"Track validation errors per tab section: \u003Cscript setup lang=\"ts\">\nconst validationErrors = ref\u003CZodError | null>(null)\n\n// Map field names to their tab groups\nconst tabErrors = computed(() => {\n  if (!validationErrors.value) return { general: 0, profile: 0 }\n\n  const errors = validationErrors.value.errors\n  const generalFields = ['email', 'username']\n  const profileFields = ['bio', 'website']\n\n  return {\n    general: errors.filter(e => generalFields.includes(e.path[0])).length,\n    profile: errors.filter(e => profileFields.includes(e.path[0])).length\n  }\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonValidationErrorSummary\n    :tab-errors=\"tabErrors\"\n    :navigation-items=\"tabs\"\n    @switch-tab=\"activeTab = $event\"\n  />\n\u003C/template>",{"id":12111,"title":12112,"titles":12113,"content":12114,"level":748},"/api-reference/components/utility-components#multi-step-wizard","Multi-Step Wizard",[329,12077,1608],"For a complete working example demonstrating a multi-step wizard with validation tracking, step navigation, and error summary, see this interactive demo: View Full Interactive Demo →Fork the demo to explore wizard form patterns. The complete example includes:Multi-step wizard with step progress indicatorsPer-step validation trackingValidationErrorSummary componentNavigation controls (Previous/Next)Error count badges per stepStep-by-step form submission Focused Example: Per-Step Error Tracking This snippet shows the key pattern for tracking validation errors per wizard step: \u003Cscript setup lang=\"ts\">\nconst currentStep = ref('personal')\n\nconst steps = [\n  { label: 'Personal Info', value: 'personal' },\n  { label: 'Company Details', value: 'company' },\n  { label: 'Preferences', value: 'preferences' }\n]\n\n// Track errors per step\nconst stepErrors = computed(() => ({\n  personal: [errors.value.name, errors.value.email].filter(Boolean).length,\n  company: [errors.value.company, errors.value.role].filter(Boolean).length,\n  preferences: [errors.value.timezone].filter(Boolean).length\n}))\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonValidationErrorSummary\n      :tab-errors=\"stepErrors\"\n      :navigation-items=\"steps\"\n      @switch-tab=\"currentStep = $event\"\n    />\n\n    \u003C!-- Step content with conditional rendering -->\n    \u003Cdiv v-if=\"currentStep === 'personal'\">\n      \u003C!-- Personal info fields... -->\n    \u003C/div>\n    \u003C!-- See interactive demo for complete step navigation -->\n  \u003C/div>\n\u003C/template>",{"id":12116,"title":10089,"titles":12117,"content":12118,"level":449},"/api-reference/components/utility-components#visual-example-1",[329,12077],"When there are validation errors: ┌─────────────────────────────────────────────────────────┐\n│ ⚠ Please fix 4 validation errors                       │\n│                                                          │\n│ Basic Info (2 errors)                                   │\n│ Advanced Settings (1 error)                             │\n│ API Configuration (1 error)                             │\n└─────────────────────────────────────────────────────────┘ Each section name is a clickable link that emits switch-tab event.",{"id":12120,"title":12121,"titles":12122,"content":12123,"level":449},"/api-reference/components/utility-components#display-behavior","Display Behavior",[329,12077],"When errors exist: Shows red/error-colored UAlertTitle: \"Please fix X validation error(s)\" (pluralized correctly)Icon: i-lucide-triangle-alertLists each tab with errors as clickable linkShows error count per tab with pluralization When no errors: Component renders nothing (hidden)No alert displayed",{"id":12125,"title":12126,"titles":12127,"content":12128,"level":449},"/api-reference/components/utility-components#error-calculation-pattern","Error Calculation Pattern",[329,12077],"Common pattern for calculating tab errors from validation state: // From Zod errors\nconst tabErrors = computed(() => {\n  if (!zodError.value) return {}\n  \n  const fieldToTab = {\n    name: 'basic',\n    email: 'basic',\n    apiKey: 'advanced',\n    webhookUrl: 'advanced',\n    timeout: 'settings'\n  }\n  \n  const counts: Record\u003Cstring, number> = {}\n  \n  zodError.value.errors.forEach(error => {\n    const field = error.path[0]\n    const tab = fieldToTab[field]\n    if (tab) {\n      counts[tab] = (counts[tab] || 0) + 1\n    }\n  })\n  \n  return counts\n})\n\n// From manual validation\nconst tabErrors = computed(() => {\n  const basic = [errors.name, errors.email].filter(Boolean).length\n  const advanced = [errors.apiKey, errors.webhook].filter(Boolean).length\n  const settings = [errors.timeout, errors.retries].filter(Boolean).length\n  \n  return { basic, advanced, settings }\n})",{"id":12130,"title":12131,"titles":12132,"content":12133,"level":449},"/api-reference/components/utility-components#integration-with-croutonform","Integration with CroutonForm",[329,12077],"Using ValidationErrorSummary with generated Crouton forms: \u003C!-- layers/products/collections/products/app/components/_Form.vue -->\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003C!-- Show validation errors at top -->\n    \u003CCroutonValidationErrorSummary\n      v-if=\"showTabs\"\n      :tab-errors=\"tabErrors\"\n      :navigation-items=\"tabNavigation\"\n      @switch-tab=\"activeTab = $event\"\n    />\n\n    \u003CCroutonForm\n      :schema=\"schema\"\n      :collection=\"collection\"\n      :items=\"items\"\n      :loading=\"loading\"\n      :action=\"action\"\n    >\n      \u003Ctemplate #form-content=\"{ state, errors }\">\n        \u003CUTabs v-model=\"activeTab\" :items=\"tabNavigation\">\n          \u003C!-- Tab content -->\n        \u003C/UTabs>\n      \u003C/template>\n    \u003C/CroutonForm>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst activeTab = ref('basic')\n\nconst tabNavigation = [\n  { label: 'Basic', value: 'basic' },\n  { label: 'Details', value: 'details' }\n]\n\n// Calculate errors from form state\nconst tabErrors = computed(() => {\n  // Your error calculation logic\n  return { basic: 0, details: 0 }\n})\n\u003C/script>",{"id":12135,"title":12136,"titles":12137,"content":12138,"level":449},"/api-reference/components/utility-components#styling-and-theming","Styling and Theming",[329,12077],"The component uses Nuxt UI's UAlert and UButton components with error color scheme: \u003C!-- Default styling -->\n\u003CUAlert\n  color=\"error\"\n  icon=\"i-lucide-triangle-alert\"\n  :title=\"errorTitle\"\n/>\n\n\u003C!-- Customize via Nuxt UI config -->\n\u003C!-- nuxt.config.ts -->\nexport default defineNuxtConfig({\n  ui: {\n    alert: {\n      color: {\n        error: {\n          solid: 'bg-red-50 dark:bg-red-950 text-red-900 dark:text-red-100'\n        }\n      }\n    }\n  }\n})",{"id":12140,"title":5551,"titles":12141,"content":12142,"level":449},"/api-reference/components/utility-components#accessibility",[329,12077],"Keyboard Navigation: All error links are keyboard accessibleTab key navigates between error linksEnter key activates tab switch Screen Readers: Alert role announced automaticallyError counts read aloudTab names clearly labeled ARIA Attributes: \u003C!-- Component includes proper ARIA -->\n\u003CUAlert role=\"alert\" aria-live=\"polite\">\n  \u003C!-- Error content -->\n\u003C/UAlert>",{"id":12144,"title":36,"titles":12145,"content":12146,"level":449},"/api-reference/components/utility-components#troubleshooting-1",[329,12077],"Error summary not appearing Problem: Component renders but no alert showsSolution: Check that tabErrors has at least one key with value > 0. The component only displays when errors exist. Tab names show as values instead of labels Problem: Seeing \"basic\" instead of \"Basic Info\"Solution: Ensure your navigationItems array includes matching value keys for all tabs in tabErrors. Example:\ntabErrors: { 'basic': 2 }  // value\nnavigationItems: [{ label: 'Basic Info', value: 'basic' }]  // must match Clicking error link doesn't switch tabs Problem: Click on error link does nothingSolution: Ensure you're listening to the @switch-tab event and updating your active tab:\n\u003CCroutonValidationErrorSummary\n  @switch-tab=\"activeTab = $event\"\n/> Error counts are incorrect Problem: Shows wrong number of errorsSolution: Verify your error counting logic. Each field should only be counted once per tab:\n// WRONG: Counting same error multiple times\nconst errors = [state.name, state.name].filter(Boolean).length\n\n// RIGHT: Count unique field errors\nconst errors = [state.name, state.email].filter(Boolean).length TypeScript errors with navigationItems Problem: Type errors about label/value propertiesSolution: Ensure your navigation items match the expected type:\nconst items: Array\u003C{ label: string; value: string }> = [\n  { label: 'Tab 1', value: 'tab1' }\n]",{"id":12148,"title":171,"titles":12149,"content":12150,"level":391},"/api-reference/components/utility-components#custom-components",[329],"While Nuxt Crouton provides these core components, you can create custom components for your specific needs:",{"id":12152,"title":12153,"titles":12154,"content":12155,"level":449},"/api-reference/components/utility-components#custom-list-component","Custom List Component",[329,171],"\u003C!-- components/ProductList.vue -->\n\u003Ctemplate>\n  \u003Cdiv class=\"grid grid-cols-3 gap-4\">\n    \u003Cdiv\n      v-for=\"product in products\"\n      :key=\"product.id\"\n      class=\"border rounded-lg p-4\"\n    >\n      \u003Cimg :src=\"product.imageUrl\" class=\"w-full h-48 object-cover\" />\n      \u003Ch3 class=\"font-bold mt-2\">{{ product.name }}\u003C/h3>\n      \u003Cp class=\"text-gray-600\">${{ product.price }}\u003C/p>\n      \u003CUButton @click=\"handleEdit(product.id)\" size=\"sm\" class=\"mt-2\">\n        Edit\n      \u003C/UButton>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  products: Product[]\n}>()\n\nconst { open } = useCrouton()\n\nconst handleEdit = (id: string) => {\n  open('update', 'shopProducts', [id])\n}\n\u003C/script>",{"id":12157,"title":12158,"titles":12159,"content":12160,"level":449},"/api-reference/components/utility-components#custom-form-button","Custom Form Button",[329,171],"\u003C!-- components/SaveButton.vue -->\n\u003Ctemplate>\n  \u003CUButton\n    type=\"submit\"\n    :loading=\"isLoading\"\n    :disabled=\"!isValid\"\n    color=\"primary\"\n    size=\"lg\"\n    block\n  >\n    \u003Ctemplate v-if=\"isLoading\">\n      Saving...\n    \u003C/template>\n    \u003Ctemplate v-else>\n      {{ label }}\n    \u003C/template>\n  \u003C/UButton>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  action: 'create' | 'update' | 'delete'\n  loading: string\n  valid: boolean\n}>()\n\nconst isLoading = computed(() => props.loading === props.action)\nconst isValid = computed(() => props.valid)\n\nconst label = computed(() => {\n  const labels = {\n    create: 'Create Item',\n    update: 'Save Changes',\n    delete: 'Confirm Delete'\n  }\n  return labels[props.action]\n})\n\u003C/script>",{"id":12162,"title":12163,"titles":12164,"content":12165,"level":391},"/api-reference/components/utility-components#croutoncollectionviewer","CroutonCollectionViewer",[329],"A collection viewer component with an integrated layout switcher that dynamically loads and renders collection-specific list components with different layout modes (table, list, grid, cards).",{"id":12167,"title":4987,"titles":12168,"content":12169,"level":449},"/api-reference/components/utility-components#props-2",[329,12163],"PropTypeDefaultDescriptioncollectionNamestringrequiredName of the collection to displaydefaultLayout'table' | 'list' | 'grid' | 'cards' | 'tree' | 'kanban' | 'workspace''table'Initial layout mode",{"id":12171,"title":183,"titles":12172,"content":12173,"level":449},"/api-reference/components/utility-components#features",[329,12163],"Dynamic Component Loading: Automatically resolves and loads the appropriate collection list componentLayout Switcher: Built-in UI for switching between table, list, grid, cards, tree, kanban, and workspace layoutsError Handling: Graceful error handling if component cannot be loadedAuto-Import Support: Works with Nuxt's auto-imported components",{"id":12175,"title":1608,"titles":12176,"content":12177,"level":449},"/api-reference/components/utility-components#usage-2",[329,12163],"\u003Ctemplate>\n  \u003CCroutonCollectionViewer\n    collection-name=\"products\"\n    default-layout=\"grid\"\n  />\n\u003C/template>",{"id":12179,"title":9640,"titles":12180,"content":12181,"level":449},"/api-reference/components/utility-components#component-resolution",[329,12163],"The viewer converts collection names to component names using PascalCase: products → ProductsListteamMembers → TeamMembersListshopOrders → ShopOrdersList Note: The referenced list component (e.g., ProductsList) must be available in your components directory or auto-imported by Nuxt.",{"id":12183,"title":12184,"titles":12185,"content":12186,"level":449},"/api-reference/components/utility-components#layout-options","Layout Options",[329,12163],"The switcher provides seven layout modes with corresponding icons: LayoutIconUse Casetablei-lucide-tableDetailed data with many columnslisti-lucide-listCompact row-by-row displaygridi-lucide-grid-3x3Grid of cards, good for imagescardsi-lucide-layout-gridLarge cards with rich contenttreei-lucide-git-branchHierarchical tree view for parent-child datakanbani-lucide-columns-3Kanban board view with draggable columnsworkspacei-lucide-panel-leftFull workspace view with inline editing",{"id":12188,"title":12189,"titles":12190,"content":12191,"level":391},"/api-reference/components/utility-components#croutonloading","CroutonLoading",[329],"Loading skeleton component for form creation and update states.",{"id":12193,"title":4987,"titles":12194,"content":12195,"level":449},"/api-reference/components/utility-components#props-3",[329,12189],"None (uses useCrouton().loading state)",{"id":12197,"title":183,"titles":12198,"content":12199,"level":449},"/api-reference/components/utility-components#features-1",[329,12189],"Auto-Detection: Reads loading state from useCrouton composableMultiple States: Supports create/update open and send statesSkeleton UI: Shows 5 form field skeletons during loadingMinimal: Simple component with no configuration needed",{"id":12201,"title":1608,"titles":12202,"content":528,"level":449},"/api-reference/components/utility-components#usage-3",[329,12189],{"id":12204,"title":4173,"titles":12205,"content":12206,"level":748},"/api-reference/components/utility-components#basic-usage-1",[329,12189,1608],"\u003Ctemplate>\n  \u003CCroutonLoading />\n  \u003C!-- Shows loading skeletons when useCrouton().loading matches states -->\n\u003C/template>",{"id":12208,"title":2973,"titles":12209,"content":12210,"level":748},"/api-reference/components/utility-components#loading-states",[329,12189,1608],"The component shows content when loading state is: 'create_open' - Form opening for creation'update_open' - Form opening for update'create_send' - Sending create request'update_send' - Sending update request \u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonLoading />\n    \u003C!-- When loading='create_open' or 'update_open': Shows 5 field skeletons\n         When loading='create_send' or 'update_send': Shows \"SENDING UPDATE\"\n         Otherwise: Shows nothing -->\n  \u003C/div>\n\u003C/template>",{"id":12212,"title":12213,"titles":12214,"content":12215,"level":748},"/api-reference/components/utility-components#integration-with-forms","Integration with Forms",[329,12189,1608],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonLoading />\n    \n    \u003CCroutonForm\n      v-if=\"!loading\"\n      :collection=\"collection\"\n      :active-item=\"item\"\n    />\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst { loading } = useCrouton()\n\u003C/script>",{"id":12217,"title":12218,"titles":12219,"content":12220,"level":449},"/api-reference/components/utility-components#skeleton-structure","Skeleton Structure",[329,12189],"When in create_open or update_open state: Renders 5 skeleton rowsEach row contains:\nLabel skeleton: h-6 w-40Input skeleton: h-10 w-fullGap: gap-2 \u003C!-- Internal structure -->\n\u003Cdiv v-for=\"i in 5\" class=\"flex flex-col gap-2 w-full\">\n  \u003CUSkeleton class=\"h-6 w-40\" />  \u003C!-- Label -->\n  \u003CUSkeleton class=\"h-10 w-full\" /> \u003C!-- Input -->\n\u003C/div> Simple Component: This is a minimal loading indicator. For more complex loading states, consider creating custom loading components for your specific use case.",{"id":12222,"title":12223,"titles":12224,"content":12225,"level":391},"/api-reference/components/utility-components#croutonvalidationerrorsummary","CroutonValidationErrorSummary",[329],"Displays validation error summary with clickable tabs to navigate to error locations.",{"id":12227,"title":4987,"titles":12228,"content":12229,"level":449},"/api-reference/components/utility-components#props-4",[329,12223],"interface Props {\n  tabErrors: Record\u003Cstring, number>\n  navigationItems: Array\u003C{ label: string; value: string }>\n} PropTypeDefaultDescriptiontabErrorsRecord\u003Cstring, number>-Map of tab values to error countsnavigationItemsArray-Navigation items with labels and values",{"id":12231,"title":5367,"titles":12232,"content":12233,"level":449},"/api-reference/components/utility-components#events-1",[329,12223],"EventPayloadDescriptionswitch-tabstringEmitted when user clicks a tab link with errors",{"id":12235,"title":183,"titles":12236,"content":12237,"level":449},"/api-reference/components/utility-components#features-2",[329,12223],"Error Summary: Shows total error count across all tabsTab Links: Clickable links to switch to tabs with errorsError Counts: Displays error count per tabConditional Rendering: Only shows when errors existAlert Style: Uses UAlert with error color and iconPluralization: Handles singular/plural (\"error\" vs \"errors\")",{"id":12239,"title":1608,"titles":12240,"content":528,"level":449},"/api-reference/components/utility-components#usage-4",[329,12223],{"id":12242,"title":4173,"titles":12243,"content":12244,"level":748},"/api-reference/components/utility-components#basic-usage-2",[329,12223,1608],"\u003Ctemplate>\n  \u003CCroutonValidationErrorSummary\n    :tab-errors=\"validationErrors\"\n    :navigation-items=\"formTabs\"\n    @switch-tab=\"handleTabSwitch\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst activeTab = ref('general')\n\nconst formTabs = [\n  { label: 'General', value: 'general' },\n  { label: 'Contact', value: 'contact' },\n  { label: 'Address', value: 'address' }\n]\n\nconst validationErrors = ref({\n  'general': 2,\n  'contact': 0,\n  'address': 3\n})\n\nconst handleTabSwitch = (tabValue: string) => {\n  activeTab.value = tabValue\n  // Optionally scroll to first error field\n}\n\u003C/script>",{"id":12246,"title":12247,"titles":12248,"content":12249,"level":748},"/api-reference/components/utility-components#integrated-with-form","Integrated with Form",[329,12223,1608],"\u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonValidationErrorSummary\n      :tab-errors=\"errors\"\n      :navigation-items=\"tabs\"\n      @switch-tab=\"activeSection = $event\"\n    />\n\n    \u003CCroutonFormLayout\n      v-model=\"activeSection\"\n      tabs\n      :navigation-items=\"tabs\"\n      :tab-errors=\"errors\"\n    >\n      \u003Ctemplate #main=\"{ activeSection }\">\n        \u003Cdiv v-show=\"activeSection === 'profile'\">\n          \u003CUFormGroup label=\"Name\" :error=\"errors.name\">\n            \u003CUInput v-model=\"form.name\" />\n          \u003C/UFormGroup>\n        \u003C/div>\n        \u003Cdiv v-show=\"activeSection === 'security'\">\n          \u003CUFormGroup label=\"Password\" :error=\"errors.password\">\n            \u003CUInput v-model=\"form.password\" type=\"password\" />\n          \u003C/UFormGroup>\n        \u003C/div>\n      \u003C/template>\n    \u003C/CroutonFormLayout>\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst activeSection = ref('profile')\n\nconst tabs = [\n  { label: 'Profile', value: 'profile' },\n  { label: 'Security', value: 'security' }\n]\n\nconst errors = ref({\n  'profile': 1,\n  'security': 2\n})\n\nconst form = ref({\n  name: '',\n  password: ''\n})\n\u003C/script>",{"id":12251,"title":12252,"titles":12253,"content":12254,"level":748},"/api-reference/components/utility-components#dynamic-error-calculation","Dynamic Error Calculation",[329,12223,1608],"\u003Ctemplate>\n  \u003CCroutonValidationErrorSummary\n    :tab-errors=\"errorsByTab\"\n    :navigation-items=\"sections\"\n    @switch-tab=\"goToTab\"\n  />\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nimport { z } from 'zod'\n\nconst sections = [\n  { label: 'Personal Info', value: 'personal' },\n  { label: 'Company', value: 'company' }\n]\n\nconst personalSchema = z.object({\n  firstName: z.string().min(1),\n  lastName: z.string().min(1),\n  email: z.string().email()\n})\n\nconst companySchema = z.object({\n  name: z.string().min(1),\n  website: z.string().url()\n})\n\nconst formData = ref({\n  firstName: '',\n  lastName: '',\n  email: 'invalid',\n  name: '',\n  website: 'not-a-url'\n})\n\nconst errorsByTab = computed(() => {\n  const personalErrors = personalSchema.safeParse({\n    firstName: formData.value.firstName,\n    lastName: formData.value.lastName,\n    email: formData.value.email\n  })\n  \n  const companyErrors = companySchema.safeParse({\n    name: formData.value.name,\n    website: formData.value.website\n  })\n  \n  return {\n    'personal': personalErrors.success ? 0 : personalErrors.error.errors.length,\n    'company': companyErrors.success ? 0 : companyErrors.error.errors.length\n  }\n})\n\nconst goToTab = (tab: string) => {\n  // Switch to tab and focus first error field\n  activeTab.value = tab\n}\n\u003C/script>",{"id":12256,"title":12257,"titles":12258,"content":12259,"level":748},"/api-reference/components/utility-components#with-auto-scroll","With Auto-Scroll",[329,12223,1608],"\u003Ctemplate>\n  \u003CCroutonValidationErrorSummary\n    :tab-errors=\"tabErrors\"\n    :navigation-items=\"tabs\"\n    @switch-tab=\"switchToTabAndScroll\"\n  />\n\n  \u003Cdiv ref=\"formContainer\">\n    \u003C!-- Form tabs content -->\n  \u003C/div>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nconst formContainer = ref\u003CHTMLElement>()\n\nconst switchToTabAndScroll = (tabValue: string) => {\n  // Switch tab\n  activeTab.value = tabValue\n  \n  // Wait for tab to render\n  nextTick(() => {\n    // Find first error field in new tab\n    const firstError = formContainer.value?.querySelector('[aria-invalid=\"true\"]')\n    firstError?.scrollIntoView({ behavior: 'smooth', block: 'center' })\n    \n    // Optional: Focus the field\n    if (firstError instanceof HTMLElement) {\n      firstError.focus()\n    }\n  })\n}\n\u003C/script>",{"id":12261,"title":12262,"titles":12263,"content":12264,"level":449},"/api-reference/components/utility-components#alert-structure","Alert Structure",[329,12223],"\u003CUAlert\n  color=\"error\"\n  icon=\"i-lucide-triangle-alert\"\n  title=\"Please fix X validation errors\"\n>\n  \u003Ctemplate #description>\n    \u003C!-- Tab links with error counts -->\n    \u003CUButton variant=\"link\" color=\"error\">\n      Profile (2 errors)\n    \u003C/UButton>\n    \u003CUButton variant=\"link\" color=\"error\">\n      Security (1 error)\n    \u003C/UButton>\n  \u003C/template>\n\u003C/UAlert>",{"id":12266,"title":12267,"titles":12268,"content":12269,"level":449},"/api-reference/components/utility-components#error-count-display","Error Count Display",[329,12223],"ScenarioTitleTab Display1 error total\"Please fix 1 validation error\"\"Tab Name (1 error)\"Multiple errors\"Please fix X validation errors\"\"Tab Name (X errors)\"Tab with 1 error-\"Tab Name (1 error)\"Tab with multiple-\"Tab Name (X errors)\" Zero Errors: Tabs with 0 errors are not shown in the summary. The entire component is hidden when all tabs have 0 errors.",{"id":12271,"title":1007,"titles":12272,"content":12273,"level":391},"/api-reference/components/utility-components#related-resources",[329],"Utility Composables - Utility helpersError Handling - Error handling patternsLoading States - Loading state patterns html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":340,"title":339,"titles":12275,"content":12276,"level":385},[],"Fetch and manage collection data with automatic caching and reactivity Query Examples: For complete useCollectionQuery patterns (basic, filtering, pagination, sorting, relations), see Querying Data.",{"id":12278,"title":12279,"titles":12280,"content":12281,"level":391},"/api-reference/composables/data-composables#usecollection-removed","useCollection (Removed)",[339],"Removed: useCollection() has been removed. Use useCollectionQuery() for data fetching or useCollections() for collection configuration.",{"id":12283,"title":300,"titles":12284,"content":12285,"level":391},"/api-reference/composables/data-composables#usecollectionitem",[339],"Fetch a single collection item by ID with support for both RESTful and query-based strategies.",{"id":12287,"title":5176,"titles":12288,"content":12289,"level":449},"/api-reference/composables/data-composables#type-signature",[339,300],"interface CollectionItemReturn\u003CT = any> {\n  item: ComputedRef\u003CT | null>\n  pending: Ref\u003Cboolean>\n  error: Ref\u003Cany>\n  refresh: () => Promise\u003Cvoid>\n}\n\nfunction useCollectionItem\u003CT = any>(\n  collection: string,\n  id: string | Ref\u003Cstring> | (() => string)\n): Promise\u003CCollectionItemReturn\u003CT>>",{"id":12291,"title":9079,"titles":12292,"content":12293,"level":449},"/api-reference/composables/data-composables#parameters",[339,300],"ParameterTypeRequiredDescriptioncollectionstringYesThe collection name (e.g., 'users', 'bookingsLocations')idstring | Ref\u003Cstring> | (() => string)YesItem ID - can be static string, reactive ref, or getter function",{"id":12295,"title":6217,"titles":12296,"content":12297,"level":449},"/api-reference/composables/data-composables#returns",[339,300],"PropertyTypeDescriptionitemComputedRef\u003CT | null>The fetched item (typed via generic)pendingRef\u003Cboolean>Loading state during fetcherrorRef\u003Cany>Error state if fetch failsrefresh() => Promise\u003Cvoid>Manual refetch function",{"id":12299,"title":1635,"titles":12300,"content":12301,"level":449},"/api-reference/composables/data-composables#how-it-works",[339,300],"Fetch Strategy Detection: Uses collection config to determine endpoint pattern\nRESTful: /api/teams/:teamId/:collection/:id (single object response)Query-based: /api/teams/:teamId/:collection?ids=:id (array response, extracts first)Reactive ID: Watches for ID changes and automatically refetchesTeam Context: Automatically resolves team ID from route or useTeam() composableProxy Transform: Applies external collection transforms if configured",{"id":12303,"title":12304,"titles":12305,"content":12306,"level":449},"/api-reference/composables/data-composables#fetch-strategies","Fetch Strategies",[339,300],"Collections can use two different fetch strategies defined in app.config.ts: // app.config.ts\nexport default defineAppConfig({\n  croutonCollections: {\n    // RESTful strategy (default for most collections)\n    users: {\n      fetchStrategy: 'restful', // GET /api/teams/123/users/456 → { id, name, ... }\n      // ...\n    },\n    \n    // Query-based strategy (for external APIs or batch endpoints)\n    bookingsBookings: {\n      fetchStrategy: 'query', // GET /api/teams/123/bookings?ids=456 → [{ id, name, ... }]\n      // ...\n    }\n  }\n}) Complete Examples: For comprehensive useCollectionItem patterns including basic usage, reactive IDs, error handling, and TypeScript examples, see the useCollectionItem API Reference.",{"id":12308,"title":12309,"titles":12310,"content":12311,"level":449},"/api-reference/composables/data-composables#in-card-components","In Card Components",[339,300],"Common pattern for rendering referenced items (e.g., showing location details in a booking): \u003Cscript setup lang=\"ts\">\n// ItemCardMini.vue (real example from codebase)\nconst props = defineProps\u003C{\n  id: string\n  collection: string\n}>()\n\nconst { item, pending, error } = await useCollectionItem(\n  props.collection,\n  computed(() => props.id)\n)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUBadge v-if=\"pending\" color=\"neutral\">\n    \u003CUSkeleton class=\"h-4 w-full\" />\n  \u003C/UBadge>\n  \n  \u003CUBadge v-else-if=\"item\" color=\"neutral\">\n    {{ item.title }}\n  \u003C/UBadge>\n  \n  \u003CUBadge v-else-if=\"error\" color=\"red\">\n    Error loading\n  \u003C/UBadge>\n\u003C/template> Usage: \u003Ctemplate>\n  \u003C!-- Show location name for booking.location ID -->\n  \u003CCroutonItemCardMini\n    :id=\"booking.location\"\n    collection=\"bookingsLocations\"\n  />\n\u003C/template>",{"id":12313,"title":12314,"titles":12315,"content":12316,"level":449},"/api-reference/composables/data-composables#with-external-collections-proxy","With External Collections (Proxy)",[339,300],"For collections that proxy external APIs: // app.config.ts\nexport default defineAppConfig({\n  croutonCollections: {\n    externalUsers: {\n      fetchStrategy: 'query',\n      proxy: {\n        enabled: true,\n        sourceEndpoint: 'external-users', // Proxies to external API\n        transform: (item) => ({\n          id: item.user_id,\n          title: item.full_name,\n          email: item.email_address\n        })\n      }\n    }\n  }\n}) \u003Cscript setup lang=\"ts\">\n// Transform is applied automatically\nconst { item, pending } = await useCollectionItem('externalUsers', '123')\n// item.value = { id: '123', title: 'John Doe', email: 'john@example.com' }\n\u003C/script>",{"id":12318,"title":12319,"titles":12320,"content":12321,"level":449},"/api-reference/composables/data-composables#team-context-resolution","Team Context Resolution",[339,300],"The composable automatically resolves the correct team ID: // In team-based route: /dashboard/:team/users/:id\nconst { item } = await useCollectionItem('users', '123')\n// → Fetches from: /api/teams/{resolvedTeamId}/users/123\n\n// In super-admin route: /super-admin/users/:id\nconst { item } = await useCollectionItem('users', '123')\n// → Fetches from: /api/super-admin/users/123 Resolution Strategy: Try useTeam() composable → returns currentTeam.id (preferred)Fallback to route.params.team (might be slug or ID)",{"id":12323,"title":2532,"titles":12324,"content":12325,"level":449},"/api-reference/composables/data-composables#cache-invalidation",[339,300],"After mutations, individual item caches are automatically refreshed: \u003Cscript setup lang=\"ts\">\n// Fetch user\nconst { item, pending } = await useCollectionItem('users', '123')\n\n// Mutate user\nconst { update } = useCollectionMutation('users')\nawait update('123', { name: 'Updated Name' })\n\n// ✅ item.value automatically refreshes with new data\n// No manual refresh needed!\n\u003C/script>",{"id":12327,"title":36,"titles":12328,"content":12329,"level":449},"/api-reference/composables/data-composables#troubleshooting",[339,300],"IssueSolutionItem not refetching when ID changesUse computed(() => props.id) or ref, not a plain stringWrong fetch strategy (404 errors)Check fetchStrategy in collection config (app.config.ts)Team context errorsEnsure route has :team param or useTeam() is availableTransform not appliedCheck proxy.enabled and proxy.transform in configSSR hydration mismatchUse computed(() => props.id) instead of arrow function () => props.id",{"id":12331,"title":12332,"titles":12333,"content":12334,"level":391},"/api-reference/composables/data-composables#usecollections","useCollections",[339],"Access the central collection registry and configuration management system.",{"id":12336,"title":5176,"titles":12337,"content":12338,"level":449},"/api-reference/composables/data-composables#type-signature-1",[339,12332],"interface CollectionConfig {\n  name?: string\n  layer?: string\n  componentName?: string\n  apiPath?: string\n  defaultPagination?: {\n    currentPage: number\n    pageSize: number\n    sortBy: string\n    sortDirection: 'asc' | 'desc'\n  }\n  references?: Record\u003Cstring, string>\n  dependentFieldComponents?: Record\u003Cstring, string>\n  [key: string]: any\n}\n\nfunction useCollections(): {\n  componentMap: Record\u003Cstring, string>\n  dependentFieldComponentMap: Record\u003Cstring, Record\u003Cstring, string>>\n  getConfig: (name: string) => CollectionConfig | undefined\n  configs: Record\u003Cstring, CollectionConfig>\n}",{"id":12340,"title":6217,"titles":12341,"content":12342,"level":449},"/api-reference/composables/data-composables#returns-1",[339,12332],"componentMap - Map of collection names to form component namesdependentFieldComponentMap - Map of collections to their custom field componentsgetConfig - Get configuration for a specific collectionconfigs - Full registry of all collection configurations",{"id":12344,"title":4173,"titles":12345,"content":12346,"level":449},"/api-reference/composables/data-composables#basic-usage",[339,12332],"\u003Cscript setup lang=\"ts\">\nconst { getConfig, configs } = useCollections()\n\n// Get configuration for a specific collection\nconst productsConfig = getConfig('shopProducts')\nconsole.log(productsConfig?.apiPath) // '/api/crouton-collection/shopProducts'\nconsole.log(productsConfig?.layer) // 'shop'\n\n// Access all registered collections\nconst allCollections = Object.keys(configs)\nconsole.log(allCollections) // ['shopProducts', 'shopCategories', ...]\n\u003C/script>",{"id":12348,"title":12349,"titles":12350,"content":12351,"level":449},"/api-reference/composables/data-composables#accessing-component-maps","Accessing Component Maps",[339,12332],"\u003Cscript setup lang=\"ts\">\nconst { componentMap } = useCollections()\n\n// Get form component name for a collection\nconst formComponent = componentMap['shopProducts']\nconsole.log(formComponent) // 'ShopProductsForm'\n\n// Dynamically load the form component\nconst FormComponent = resolveComponent(formComponent)\n\u003C/script>",{"id":12353,"title":12354,"titles":12355,"content":12356,"level":449},"/api-reference/composables/data-composables#working-with-references","Working with References",[339,12332],"\u003Cscript setup lang=\"ts\">\nconst { getConfig } = useCollections()\n\n// Check collection references for cache invalidation\nconst config = getConfig('shopProducts')\nif (config?.references) {\n  console.log(config.references)\n  // { categoryId: 'shopCategories', authorId: 'users' }\n  \n  // When a product is updated, also refresh:\n  // - shopCategories cache (if categoryId changed)\n  // - users cache (if authorId changed)\n}\n\u003C/script>",{"id":12358,"title":5028,"titles":12359,"content":12360,"level":449},"/api-reference/composables/data-composables#custom-field-components",[339,12332],"\u003Cscript setup lang=\"ts\">\nconst { dependentFieldComponentMap, getConfig } = useCollections()\n\n// Get custom component for dependent field\nconst productsMap = dependentFieldComponentMap['shopProducts']\nif (productsMap?.slots) {\n  console.log(productsMap.slots) // 'SlotSelect'\n  // FormDependentFieldLoader will use SlotSelect component\n}\n\n// Alternative: Access via config\nconst config = getConfig('shopProducts')\nconst slotComponent = config?.dependentFieldComponents?.slots\n\u003C/script>",{"id":12362,"title":12363,"titles":12364,"content":12365,"level":449},"/api-reference/composables/data-composables#collection-registry-pattern","Collection Registry Pattern",[339,12332],"Collections are registered in app.config.ts: // app.config.ts\nexport default defineAppConfig({\n  croutonCollections: {\n    shopProducts: {\n      name: 'shopProducts',\n      layer: 'shop',\n      apiPath: '/api/crouton-collection/shopProducts',\n      componentName: 'ShopProductsForm',\n      references: {\n        categoryId: 'shopCategories',  // Refresh categories when product changes\n        authorId: 'users'               // Refresh users when product changes\n      },\n      dependentFieldComponents: {\n        slots: 'SlotSelect'  // Custom component for 'slots' field\n      },\n      defaultPagination: {\n        currentPage: 1,\n        pageSize: 20,\n        sortBy: 'createdAt',\n        sortDirection: 'desc'\n      }\n    },\n    shopCategories: {\n      name: 'shopCategories',\n      layer: 'shop',\n      apiPath: '/api/crouton-collection/shopCategories',\n      componentName: 'ShopCategoriesForm'\n    }\n  }\n})",{"id":12367,"title":12368,"titles":12369,"content":12370,"level":449},"/api-reference/composables/data-composables#integration-with-data-fetching","Integration with Data Fetching",[339,12332],"\u003Cscript setup lang=\"ts\">\nconst { getConfig } = useCollections()\n\n// Get collection config\nconst config = getConfig('shopProducts')\nif (!config) {\n  throw new Error('Collection not registered')\n}\n\n// Use config for data fetching\nconst { items, pending } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({\n    page: 1,\n    pageSize: config.defaultPagination?.pageSize || 20\n  }))\n})\n\u003C/script>",{"id":12372,"title":12373,"titles":12374,"content":12375,"level":449},"/api-reference/composables/data-composables#checking-collection-existence","Checking Collection Existence",[339,12332],"\u003Cscript setup lang=\"ts\">\nconst { getConfig, configs } = useCollections()\n\n// Check if collection is registered\nconst isRegistered = (name: string) => {\n  return getConfig(name) !== undefined\n}\n\nif (isRegistered('shopProducts')) {\n  // Safe to use collection\n}\n\n// Get all collection names\nconst collectionNames = Object.keys(configs)\nconsole.log('Registered collections:', collectionNames)\n\u003C/script>",{"id":12377,"title":44,"titles":12378,"content":12379,"level":449},"/api-reference/composables/data-composables#best-practices",[339,12332],"DO: ✅ Use getConfig() to check if collection exists before querying✅ Register collections in app.config.ts via generator✅ Define references for automatic cache invalidation✅ Use dependentFieldComponents for custom field renderers DON'T: ❌ Modify componentMap or dependentFieldComponentMap directly❌ Assume a collection exists - always check with getConfig()❌ Manually manage collection data state (use useCollectionQuery instead)",{"id":12381,"title":36,"titles":12382,"content":12383,"level":449},"/api-reference/composables/data-composables#troubleshooting-1",[339,12332],"Problem: getConfig() returns undefined \u003Cscript setup lang=\"ts\">\nconst { getConfig } = useCollections()\nconst config = getConfig('myCollection')\n\nif (!config) {\n  // Collection not registered in app.config.ts\n  // Run: npx crouton-generate config crouton.config.js\n  throw new Error('Collection not found')\n}\n\u003C/script> Problem: Form component not loading \u003Cscript setup lang=\"ts\">\nconst { componentMap } = useCollections()\n\n// Check if component is registered\nif (!componentMap['shopProducts']) {\n  // Missing componentName in app.config.ts\n  // Check your collection config\n}\n\u003C/script>",{"id":12385,"title":12386,"titles":12387,"content":12388,"level":391},"/api-reference/composables/data-composables#collection-proxy-utilities","Collection Proxy Utilities",[339],"Standalone utility functions for external collection proxying with client-side data transformation. These are not a composable — they are exported as standalone functions.",{"id":12390,"title":5176,"titles":12391,"content":12392,"level":449},"/api-reference/composables/data-composables#type-signature-2",[339,12386],"interface ProxyConfig {\n  enabled: boolean\n  sourceEndpoint: string\n  transform: (item: any) => { id: string; title: string; [key: string]: any }\n}\n\nfunction applyProxyTransform(data: any, config: any): any\nfunction getProxiedEndpoint(config: any, apiPath: string): string",{"id":12394,"title":12395,"titles":12396,"content":12397,"level":449},"/api-reference/composables/data-composables#functions","Functions",[339,12386],"applyProxyTransform - Transform external data to Crouton formatgetProxiedEndpoint - Get the correct endpoint (proxied or standard)",{"id":12399,"title":12400,"titles":12401,"content":12402,"level":449},"/api-reference/composables/data-composables#basic-proxy-setup","Basic Proxy Setup",[339,12386],"// app.config.ts\nimport { defineExternalCollection } from '@fyit/crouton'\nimport { z } from 'zod'\n\nconst membersSchema = z.object({\n  id: z.string(),\n  title: z.string(),\n  email: z.string().optional()\n})\n\nexport default defineAppConfig({\n  croutonCollections: {\n    // Proxy existing 'members' API to work with Crouton\n    users: defineExternalCollection({\n      name: 'users',\n      schema: membersSchema,\n      proxy: {\n        enabled: true,\n        sourceEndpoint: 'members',  // Fetches from /api/teams/[id]/members\n        transform: (item) => ({\n          id: item.userId,           // Map userId → id\n          title: item.fullName,      // Map fullName → title\n          email: item.email\n        })\n      }\n    })\n  }\n})",{"id":12404,"title":12405,"titles":12406,"content":12407,"level":449},"/api-reference/composables/data-composables#transform-arrays","Transform Arrays",[339,12386],"\u003Cscript setup lang=\"ts\">\nconst { applyProxyTransform } = await import('@fyit/crouton')\n\n// External API response\nconst externalData = [\n  { userId: '1', fullName: 'Alice Smith', email: 'alice@example.com' },\n  { userId: '2', fullName: 'Bob Jones', email: 'bob@example.com' }\n]\n\n// Collection config with proxy\nconst config = {\n  proxy: {\n    enabled: true,\n    sourceEndpoint: 'members',\n    transform: (item: any) => ({\n      id: item.userId,\n      title: item.fullName,\n      email: item.email\n    })\n  }\n}\n\n// Apply transformation\nconst transformed = applyProxyTransform(externalData, config)\n// Result:\n// [\n//   { id: '1', title: 'Alice Smith', email: 'alice@example.com' },\n//   { id: '2', title: 'Bob Jones', email: 'bob@example.com' }\n// ]\n\u003C/script>",{"id":12409,"title":12410,"titles":12411,"content":12412,"level":449},"/api-reference/composables/data-composables#transform-single-items","Transform Single Items",[339,12386],"\u003Cscript setup lang=\"ts\">\nconst { applyProxyTransform } = await import('@fyit/crouton')\n\n// Single item from external API\nconst externalItem = {\n  userId: '1',\n  fullName: 'Alice Smith',\n  department: 'Engineering'\n}\n\nconst config = {\n  proxy: {\n    enabled: true,\n    sourceEndpoint: 'members',\n    transform: (item: any) => ({\n      id: item.userId,\n      title: `${item.fullName} (${item.department})`\n    })\n  }\n}\n\nconst transformed = applyProxyTransform(externalItem, config)\n// Result: { id: '1', title: 'Alice Smith (Engineering)' }\n\u003C/script>",{"id":12414,"title":12415,"titles":12416,"content":12417,"level":449},"/api-reference/composables/data-composables#get-proxied-endpoint","Get Proxied Endpoint",[339,12386],"\u003Cscript setup lang=\"ts\">\nconst { getProxiedEndpoint } = await import('@fyit/crouton')\n\n// Without proxy\nconst config1 = {\n  apiPath: 'users'\n}\nconst endpoint1 = getProxiedEndpoint(config1, 'users')\n// Returns: 'users' → /api/teams/[id]/users\n\n// With proxy enabled\nconst config2 = {\n  apiPath: 'users',\n  proxy: {\n    enabled: true,\n    sourceEndpoint: 'members'\n  }\n}\nconst endpoint2 = getProxiedEndpoint(config2, 'users')\n// Returns: 'members' → /api/teams/[id]/members\n\u003C/script>",{"id":12419,"title":12368,"titles":12420,"content":12421,"level":449},"/api-reference/composables/data-composables#integration-with-data-fetching-1",[339,12386],"\u003Cscript setup lang=\"ts\">\n// This is how useCollectionQuery uses the proxy internally\nconst { getConfig } = useCollections()\nconst { applyProxyTransform, getProxiedEndpoint } = await import('@fyit/crouton')\n\nconst collection = 'users'\nconst config = getConfig(collection)\n\n// Get the correct endpoint (proxied or standard)\nconst apiPath = getProxiedEndpoint(config, config.apiPath || collection)\n\n// Fetch from the proxied endpoint\nconst { data } = await $fetch(`/api/teams/123/${apiPath}`)\n\n// Transform the response\nconst transformedData = applyProxyTransform(data, config)\n\n// Now transformedData has Crouton format: { id, title, ... }\n\u003C/script>",{"id":12423,"title":12424,"titles":12425,"content":12426,"level":449},"/api-reference/composables/data-composables#complex-transform-examples","Complex Transform Examples",[339,12386],"Map nested properties: proxy: {\n  enabled: true,\n  sourceEndpoint: 'members',\n  transform: (item) => ({\n    id: item.user.id,\n    title: `${item.user.firstName} ${item.user.lastName}`,\n    email: item.user.contact.email,\n    role: item.membership.role,\n    joinedAt: item.membership.createdAt\n  })\n} Conditional transformations: proxy: {\n  enabled: true,\n  sourceEndpoint: 'products',\n  transform: (item) => ({\n    id: item.sku,\n    title: item.isActive ? `✅ ${item.name}` : `❌ ${item.name}`,\n    status: item.isActive ? 'active' : 'inactive',\n    price: item.pricing.amount / 100  // Convert cents to dollars\n  })\n} Combine multiple fields: proxy: {\n  enabled: true,\n  sourceEndpoint: 'bookings',\n  transform: (item) => ({\n    id: item.bookingId,\n    title: `${item.service} - ${new Date(item.startTime).toLocaleDateString()}`,\n    customerName: `${item.customer.firstName} ${item.customer.lastName}`,\n    duration: item.endTime - item.startTime\n  })\n}",{"id":12428,"title":2522,"titles":12429,"content":12430,"level":449},"/api-reference/composables/data-composables#error-handling",[339,12386],"The proxy automatically handles transform errors: \u003Cscript setup lang=\"ts\">\nconst { applyProxyTransform } = await import('@fyit/crouton')\n\n// Malformed data\nconst badData = [\n  { userId: '1', fullName: 'Alice' },  // Valid\n  null,                                  // Invalid\n  { userId: '2' }                        // Missing fullName\n]\n\nconst config = {\n  proxy: {\n    enabled: true,\n    sourceEndpoint: 'members',\n    transform: (item: any) => ({\n      id: item.userId,\n      title: item.fullName.toUpperCase()  // Will throw on missing fullName\n    })\n  }\n}\n\n// Transform with error handling\nconst result = applyProxyTransform(badData, config)\n// Logs error: \"[applyProxyTransform] Transform failed for item: ...\"\n// Returns partially transformed data (failed items remain unchanged)\n\u003C/script>",{"id":12432,"title":12433,"titles":12434,"content":12435,"level":449},"/api-reference/composables/data-composables#when-to-use-proxy","When to Use Proxy",[339,12386],"Use proxy when: ✅ Connecting to existing APIs (auth, external services)✅ API uses different field names (userId vs id)✅ Need to combine/compute fields for display✅ Working with third-party integrations Don't use proxy when: ❌ You control the API and can return Crouton format directly❌ No transformation needed❌ Data structure matches Crouton expectations",{"id":12437,"title":44,"titles":12438,"content":12439,"level":449},"/api-reference/composables/data-composables#best-practices-1",[339,12386],"DO: ✅ Always return { id, title, ...otherFields } from transform✅ Handle missing fields gracefully in transform function✅ Use defineExternalCollection() helper for proxy setup✅ Test transforms with real API data DON'T: ❌ Throw errors in transform functions (use try/catch)❌ Perform async operations in transform (use computed fields instead)❌ Mutate the original item data",{"id":12441,"title":36,"titles":12442,"content":12443,"level":449},"/api-reference/composables/data-composables#troubleshooting-2",[339,12386],"Problem: Transform not being applied // ❌ BAD: proxy.enabled is false or missing\nproxy: {\n  sourceEndpoint: 'members',\n  transform: (item) => ({ ... })\n}\n\n// ✅ GOOD: explicitly enable\nproxy: {\n  enabled: true,\n  sourceEndpoint: 'members',\n  transform: (item) => ({ ... })\n} Problem: Missing id or title field // ❌ BAD: Missing required fields\ntransform: (item) => ({\n  userId: item.id,  // Wrong! Must be 'id'\n  name: item.name   // Wrong! Must be 'title'\n})\n\n// ✅ GOOD: Include required fields\ntransform: (item) => ({\n  id: item.userId,\n  title: item.name,\n  // ... other fields\n})",{"id":12445,"title":12446,"titles":12447,"content":12448,"level":391},"/api-reference/composables/data-composables#useexternalcollection","useExternalCollection",[339],"Define and register external collections that are managed outside of Crouton (e.g., users from auth system, third-party APIs).",{"id":12450,"title":5176,"titles":12451,"content":12452,"level":449},"/api-reference/composables/data-composables#type-signature-3",[339,12446],"interface ExternalCollectionConfig {\n  name: string\n  schema: z.ZodSchema\n  apiPath?: string\n  fetchStrategy?: 'query' | 'restful'\n  readonly?: boolean\n  meta?: {\n    label?: string\n    description?: string\n  }\n  proxy?: {\n    enabled: boolean\n    sourceEndpoint: string\n    transform: (item: any) => { id: string; title: string; [key: string]: any }\n  }\n}\n\nfunction defineExternalCollection(config: ExternalCollectionConfig): CollectionConfig",{"id":12454,"title":9079,"titles":12455,"content":12456,"level":449},"/api-reference/composables/data-composables#parameters-1",[339,12446],"ParameterTypeRequiredDefaultDescriptionnamestringYes-Collection identifier (must match app.config.ts key)schemaz.ZodSchemaYes-Zod validation schema for itemsapiPathstringNonameAPI endpoint pathfetchStrategy'query' | 'restful'No'query'How to fetch single items: ?ids= vs /{id}readonlybooleanNotrueHide edit/delete in UI (read-only mode)meta.labelstringNo-Display label for collectionmeta.descriptionstringNo-Description of collectionproxy.enabledbooleanNo-Enable proxy to different endpointproxy.sourceEndpointstringNo-Endpoint to proxy (e.g., 'members' → /api/teams/id/members)proxy.transformfunctionNo-Transform source data to Crouton format",{"id":12458,"title":6217,"titles":12459,"content":12460,"level":449},"/api-reference/composables/data-composables#returns-2",[339,12446],"A collection configuration object compatible with Crouton registry. Returns object with: name: Collection namelayer: 'external'apiPath: API endpointfetchStrategy: Query or REST strategyreadonly: Read-only flagcomponentName: null (read-only)schema: Zod schema for validationdefaultValues: Empty objectcolumns: Empty arraymeta: Metadata objectproxy: Proxy configuration",{"id":12462,"title":4173,"titles":12463,"content":12464,"level":449},"/api-reference/composables/data-composables#basic-usage-1",[339,12446],"Simple external users collection: // utils/collections.ts\nimport { z } from 'zod'\nimport { defineExternalCollection } from '@fyit/crouton'\n\nconst userSchema = z.object({\n  id: z.string(),\n  title: z.string(), // Required for display in selects\n  email: z.string().email().optional(),\n  avatarUrl: z.string().optional()\n})\n\nexport const usersConfig = defineExternalCollection({\n  name: 'users',\n  schema: userSchema,\n  readonly: true, // Don't allow editing\n  meta: {\n    label: 'Team Users',\n    description: 'Users from your authentication system'\n  }\n}) Register in app.config: // app.config.ts\nimport { usersConfig } from '~/utils/collections'\n\nexport default defineAppConfig({\n  croutonCollections: {\n    users: usersConfig,\n    // ... other collections\n  }\n})",{"id":12466,"title":12467,"titles":12468,"content":12469,"level":449},"/api-reference/composables/data-composables#with-custom-api-path","With Custom API Path",[339,12446],"Map collection name to different endpoint: export const authUsersConfig = defineExternalCollection({\n  name: 'authUsers',\n  schema: userSchema,\n  apiPath: 'api/auth/users', // Different from collection name\n  readonly: true\n})",{"id":12471,"title":12472,"titles":12473,"content":12474,"level":449},"/api-reference/composables/data-composables#rest-fetch-strategy","REST Fetch Strategy",[339,12446],"Use RESTful endpoints for single item fetching: // Users collection using /api/users/:id\nexport const usersConfig = defineExternalCollection({\n  name: 'users',\n  schema: userSchema,\n  fetchStrategy: 'restful', // Use /api/users/123 instead of /api/users?ids=123\n  readonly: true\n})",{"id":12476,"title":12477,"titles":12478,"content":12479,"level":449},"/api-reference/composables/data-composables#with-proxy-configuration","With Proxy Configuration",[339,12446],"Fetch from nested endpoint with transformation: const teamMembersConfig = defineExternalCollection({\n  name: 'teamMembers',\n  schema: z.object({\n    id: z.string(),\n    title: z.string(),\n    role: z.string().optional()\n  }),\n  readonly: true,\n  proxy: {\n    enabled: true,\n    // Endpoint transforms: 'members' → /api/teams/:teamId/members\n    sourceEndpoint: 'members',\n    // Transform from source format to Crouton format\n    transform: (item: any) => ({\n      id: item.userId,\n      title: item.userName,\n      role: item.userRole\n    })\n  }\n})",{"id":12481,"title":12482,"titles":12483,"content":12484,"level":449},"/api-reference/composables/data-composables#using-in-croutonreferenceselect","Using in CroutonReferenceSelect",[339,12446],"Reference external collection in forms: \u003Cscript setup lang=\"ts\">\nimport { usersConfig } from '~/utils/collections'\n\nconst formData = ref({\n  assignedUser: null\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- CroutonReferenceSelect automatically finds 'users' external collection -->\n  \u003CCroutonReferenceSelect \n    v-model=\"formData.assignedUser\"\n    collection=\"users\"\n    label=\"Assign to User\"\n  />\n\u003C/template>",{"id":12486,"title":12487,"titles":12488,"content":12489,"level":449},"/api-reference/composables/data-composables#multiple-external-collections","Multiple External Collections",[339,12446],"For complete schema definitions and examples, see the Basic Usage section above. Register several external sources: // app.config.ts\nimport { z } from 'zod'\nimport { defineExternalCollection } from '@fyit/crouton'\n\n// Define schemas (see Basic Usage for full examples)\nconst userSchema = z.object({ /* ... */ })\nconst departmentSchema = z.object({ /* ... */ })\n\nexport default defineAppConfig({\n  croutonCollections: {\n    users: defineExternalCollection({\n      name: 'users',\n      schema: userSchema,\n      meta: { label: 'Team Users' }\n    }),\n    departments: defineExternalCollection({\n      name: 'departments',\n      schema: departmentSchema,\n      meta: { label: 'Organization Departments' }\n    })\n  }\n})",{"id":12491,"title":44,"titles":12492,"content":12493,"level":449},"/api-reference/composables/data-composables#best-practices-2",[339,12446],"DO: ✅ Always include title field in schema (required for UI display)✅ Set readonly: true for external system data (prevent accidental mutations)✅ Provide clear meta.label and meta.description✅ Use query strategy for most cases (simpler)✅ Test schema validation with actual API responses✅ Use proxy with transform for complex data structures DON'T: ❌ Forget title field (breaks CroutonReferenceSelect)❌ Set readonly: false for system collections (causes data inconsistency)❌ Use complex nested schemas (transform at proxy layer instead)❌ Assume API returns exact Crouton format (use proxy.transform)❌ Mix external and managed collections in same references",{"id":12495,"title":1650,"titles":12496,"content":528,"level":449},"/api-reference/composables/data-composables#common-patterns",[339,12446],{"id":12498,"title":12499,"titles":12500,"content":12501,"level":748},"/api-reference/composables/data-composables#authentication-system-users","Authentication System Users",[339,12446,1650],"export const authUsersConfig = defineExternalCollection({\n  name: 'authUsers',\n  schema: z.object({\n    id: z.string().uuid(),\n    title: z.string(), // Display name\n    email: z.string().email(),\n    role: z.enum(['admin', 'user', 'guest']).optional()\n  }),\n  apiPath: 'api/auth/users',\n  readonly: true\n})",{"id":12503,"title":12504,"titles":12505,"content":12506,"level":748},"/api-reference/composables/data-composables#third-party-api-integration","Third-Party API Integration",[339,12446,1650],"export const externalVendorsConfig = defineExternalCollection({\n  name: 'vendors',\n  schema: z.object({\n    id: z.string(),\n    title: z.string(),\n    vendorCode: z.string().optional()\n  }),\n  apiPath: 'https://external-api.com/vendors',\n  fetchStrategy: 'restful'\n})",{"id":12508,"title":12509,"titles":12510,"content":12511,"level":748},"/api-reference/composables/data-composables#nested-endpoint-transformation","Nested Endpoint Transformation",[339,12446,1650],"export const projectMembersConfig = defineExternalCollection({\n  name: 'projectMembers',\n  schema: z.object({\n    id: z.string(),\n    title: z.string()\n  }),\n  proxy: {\n    enabled: true,\n    sourceEndpoint: 'members',\n    transform: (member: any) => ({\n      id: member.memberId.toString(),\n      title: `${member.firstName} ${member.lastName}`\n    })\n  }\n})",{"id":12513,"title":36,"titles":12514,"content":12515,"level":449},"/api-reference/composables/data-composables#troubleshooting-3",[339,12446],"IssueCauseSolutionCroutonReferenceSelect emptytitle field missing from schemaAdd required title: z.string() to schemaAPI 404 errorsWrong apiPath or endpointVerify apiPath matches actual API routeTransform not runningproxy.enabled false or not setEnsure proxy.enabled: trueData shows incorrectlySchema validation failingValidate schema matches actual API responseCan't edit itemsreadonly: true (expected)Set readonly: false if editable (risk: data inconsistency)",{"id":12517,"title":1007,"titles":12518,"content":12519,"level":391},"/api-reference/composables/data-composables#related-resources",[339],"Query Composables - Advanced data fetchingMutation Composables - Data mutationsCollections Guide - Understanding collections html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":344,"title":343,"titles":12521,"content":12522,"level":385},[],"Manage form state and CRUD operations with modal/slideover support Working with Forms: For complete form patterns and component usage, see Form Patterns and Form Components API.",{"id":12524,"title":12525,"titles":12526,"content":12527,"level":391},"/api-reference/composables/form-composables#usecrouton","useCrouton",[343],"Modal and form state management for opening and controlling CRUD forms.",{"id":12529,"title":5176,"titles":12530,"content":12531,"level":449},"/api-reference/composables/form-composables#type-signature",[343,12525],"function useCrouton(): {\n  open: (\n    action: 'create' | 'update' | 'delete',\n    collection: string,\n    ids?: string[],\n    container?: 'slideover' | 'modal' | 'dialog' | 'inline',\n---\n\n\n---\n\n## useCrouton\n\nGlobal modal and form state management for handling CRUD operations with support for nested forms up to 5 levels deep.\n\n### Type Signature\n\n```typescript\ninterface CroutonState {\n  id: string\n  action: 'create' | 'update' | 'delete' | 'view' | undefined\n  collection: string | null\n  activeItem: any\n  items: any[]\n  loading: 'notLoading' | 'create_send' | 'update_send' | 'delete_send' | 'view_send' | 'create_open' | 'update_open' | 'delete_open' | 'view_open'\n  isOpen: boolean\n  containerType: 'slideover' | 'modal' | 'dialog' | 'inline'\n}\n\ninterface PaginationState {\n  currentPage: number\n  pageSize: number\n  sortBy: string\n  sortDirection: 'asc' | 'desc'\n  totalItems?: number\n  totalPages?: number\n}\n\nfunction useCrouton(): {\n  // State\n  showCrouton: ComputedRef\u003Cboolean>\n  loading: ComputedRef\u003CLoadingState>\n  action: ComputedRef\u003CCroutonAction>\n  items: ComputedRef\u003Cany[]>\n  activeItem: ComputedRef\u003Cany>\n  activeCollection: ComputedRef\u003Cstring | null>\n  croutonStates: Ref\u003CCroutonState[]>\n  pagination: Ref\u003CRecord\u003Cstring, PaginationState>>\n  \n  // Methods\n  open(\n    action: CroutonAction,\n    collection: string,\n    ids?: string[],\n    containerType?: 'slideover' | 'modal' | 'dialog' | 'inline',\n    initialData?: any\n  ): Promise\u003Cvoid>\n  close(stateId?: string): void\n  closeAll(): void\n  removeState(stateId: string): void\n  reset(): void\n  setPagination(collection: string, data: Partial\u003CPaginationState>): void\n  getPagination(collection: string): PaginationState\n  getDefaultPagination(collection: string): PaginationState\n}",{"id":12533,"title":9079,"titles":12534,"content":12535,"level":449},"/api-reference/composables/form-composables#parameters",[343,12525],"ParameterTypeRequiredDescriptionactionCroutonActionYesOperation type: 'create', 'update', 'delete', or 'view'collectionstringYesCollection name (e.g., 'users', 'bookings')idsstring[]NoItem IDs for update/delete operationscontainerType'slideover' | 'modal' | 'dialog' | 'inline'NoContainer type (default: 'slideover')initialDataanyNoPre-populated data for create forms",{"id":12537,"title":6217,"titles":12538,"content":12539,"level":449},"/api-reference/composables/form-composables#returns",[343,12525],"PropertyTypeDescriptionshowCroutonComputedRef\u003Cboolean>Whether any form is openloadingComputedRef\u003CLoadingState>Loading state of topmost formactionComputedRef\u003CCroutonAction>Action of topmost formactiveItemComputedRef\u003Cany>Item being edited (from topmost form)activeCollectionComputedRef\u003Cstring | null>Collection of topmost formitemsComputedRef\u003Cany[]>Items for deletion (from topmost form)croutonStatesRef\u003CCroutonState[]>Array of all open form states (for templates)paginationRef\u003CRecord\u003Cstring, PaginationState>>Pagination state per collection",{"id":12541,"title":1635,"titles":12542,"content":12543,"level":449},"/api-reference/composables/form-composables#how-it-works",[343,12525],"Global State Stack: useCrouton() maintains an array of form states, enabling up to 5 levels of nested forms: Level 1: users (update) ← croutonStates[0]\n  Level 2: categories (create) ← croutonStates[1]\n    Level 3: tags (create) ← croutonStates[2] Each level has its own: Unique ID for animation handlingSeparate loading stateIndependent active item dataContainer type (modal/slideover/dialog) Closing Behavior: When a form closes: close() sets isOpen = false (triggers exit animation)Animation completes (300ms transition)Component calls removeState() to remove from arrayTopmost form now visible again",{"id":12545,"title":12546,"titles":12547,"content":12548,"level":449},"/api-reference/composables/form-composables#basic-usage-create-form","Basic Usage - Create Form",[343,12525],"\u003Cscript setup lang=\"ts\">\nconst { open, showCrouton } = useCrouton()\n\nconst handleCreateClick = () => {\n  open('create', 'users', [])\n  // Form opens in slideover\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUButton @click=\"handleCreateClick\">\n    Create User\n  \u003C/UButton>\n  \n  \u003CCroutonForm v-if=\"showCrouton\" />\n\u003C/template>",{"id":12550,"title":12551,"titles":12552,"content":12553,"level":449},"/api-reference/composables/form-composables#update-form-with-pre-filled-data","Update Form with Pre-filled Data",[343,12525],"\u003Cscript setup lang=\"ts\">\nconst { open } = useCrouton()\n\nconst handleEditClick = (userId: string) => {\n  // Fetches user data automatically\n  open('update', 'users', [userId], 'slideover')\n  // Form opens with user pre-populated\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUButton\n    v-for=\"user in users\"\n    :key=\"user.id\"\n    @click=\"handleEditClick(user.id)\"\n  >\n    Edit {{ user.name }}\n  \u003C/UButton>\n\u003C/template>",{"id":12555,"title":12556,"titles":12557,"content":12558,"level":449},"/api-reference/composables/form-composables#modal-vs-slideover","Modal vs Slideover",[343,12525],"\u003Cscript setup lang=\"ts\">\nconst { open } = useCrouton()\n\n// Centered modal - single, focused form\nconst handleQuickEdit = (id: string) => {\n  open('update', 'users', [id], 'modal')\n}\n\n// Side slideover - for complex nested workflows\nconst handleDetailedEdit = (id: string) => {\n  open('update', 'users', [id], 'slideover')\n}\n\n// Dialog - minimal UI for destructive actions\nconst handleDeleteConfirm = (ids: string[]) => {\n  open('delete', 'users', ids, 'dialog')\n}\n\u003C/script>",{"id":12560,"title":12561,"titles":12562,"content":12563,"level":449},"/api-reference/composables/form-composables#delete-with-confirmation","Delete with Confirmation",[343,12525],"\u003Cscript setup lang=\"ts\">\nconst { open } = useCrouton()\n\nconst handleDeleteMultiple = (selectedIds: string[]) => {\n  open('delete', 'users', selectedIds, 'dialog')\n  // Shows: \"Delete 3 users?\"\n  // User confirms and items are deleted\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonTable\n    @delete:items=\"handleDeleteMultiple\"\n  />\n\u003C/template>",{"id":12565,"title":12566,"titles":12567,"content":12568,"level":449},"/api-reference/composables/form-composables#nested-forms-create-category-inside-product","Nested Forms (Create Category Inside Product)",[343,12525],"Complex workflow: Edit product → Add new category → Back to product \u003C!-- ProductsForm.vue -->\n\u003Cscript setup lang=\"ts\">\nconst { open } = useCrouton()\nconst state = ref({ name: '', category: null })\n\nconst handleAddCategory = () => {\n  // Level 1: Editing product\n  // Click \"Add Category\" button\n  // → Level 2: Create category form opens (nested)\n  open('create', 'categories', [], 'slideover')\n}\n\nconst handleSubmit = () => {\n  // After category is created and form closes:\n  // Level 2 removed from stack\n  // User is back in product form\n  // New category is pre-selected\n  const { create } = useCollectionMutation('products')\n  await create(state.value)\n  close()\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonFormField label=\"Category\">\n    \u003CCroutonFormReferenceSelect\n      v-model=\"state.category\"\n      collection=\"categories\"\n    />\n    \n    \u003CUButton\n      size=\"sm\"\n      color=\"gray\"\n      variant=\"ghost\"\n      @click=\"handleAddCategory\"\n    >\n      + Add Category\n    \u003C/UButton>\n  \u003C/CroutonFormField>\n  \n  \u003CUButton @click=\"handleSubmit\">\n    Create Product\n  \u003C/UButton>\n\u003C/template>",{"id":12570,"title":12571,"titles":12572,"content":12573,"level":449},"/api-reference/composables/form-composables#_5-level-deep-nesting-example","5-Level Deep Nesting Example",[343,12525],"While 5 levels are technically supported, UX best practice suggests limiting to 2-3 levels: \u003Cscript setup lang=\"ts\">\nconst { open, croutonStates } = useCrouton()\n\n// Level 1: Edit Order\nconst editOrder = () => open('update', 'orders', ['ord-123'])\n\n// Level 2: Add Payment Method\nconst addPayment = () => open('create', 'paymentMethods', [], 'slideover')\n\n// Level 3: Configure Address\nconst configAddress = () => open('create', 'addresses', [], 'slideover')\n\n// Level 4: Select Country (nested in address)\nconst selectCountry = () => open('update', 'countries', ['us'], 'modal')\n\n// Level 5: View Region Details\nconst viewRegion = () => open('view', 'regions', ['west-coast'], 'slideover')\n\n// Can monitor nesting depth\nconst currentDepth = computed(() => croutonStates.value.length)\n// currentDepth.value = 5\n\u003C/script>",{"id":12575,"title":12576,"titles":12577,"content":12578,"level":449},"/api-reference/composables/form-composables#form-submission-and-closing","Form Submission and Closing",[343,12525],"\u003Cscript setup lang=\"ts\">\nconst { close } = useCrouton()\nconst { create, update } = useCollectionMutation('users')\n\nconst handleSubmit = async (formData: any) => {\n  try {\n    if (props.action === 'create') {\n      await create(formData)\n      // Success toast shown by mutation\n    } else if (props.action === 'update') {\n      await update(formData.id, formData)\n    }\n  } catch (error) {\n    // Error toast shown by mutation\n    return\n  }\n  \n  // Only close on success\n  close()\n  // Animation triggers, form slides out\n}\n\u003C/script>",{"id":12580,"title":12581,"titles":12582,"content":12583,"level":449},"/api-reference/composables/form-composables#pagination-management","Pagination Management",[343,12525],"\u003Cscript setup lang=\"ts\">\nconst { getPagination, setPagination } = useCrouton()\n\n// Get current pagination for users collection\nconst usersPagination = getPagination('users')\n// → { currentPage: 1, pageSize: 10, sortBy: 'createdAt', sortDirection: 'desc' }\n\n// Update pagination (e.g., user changes sort)\nconst handleSort = (sortBy: string) => {\n  setPagination('users', {\n    currentPage: 1,  // Reset to first page when sorting\n    sortBy\n  })\n  // Table automatically refetches with new sort\n}\n\n// Get collection-specific defaults from config\nconst defaults = getDefaultPagination('users')\n\u003C/script>",{"id":12585,"title":12586,"titles":12587,"content":12588,"level":449},"/api-reference/composables/form-composables#state-structure-deep-dive","State Structure Deep Dive",[343,12525],"Understanding how state works is crucial for debugging: // useCrouton internally maintains:\nconst croutonStates = ref\u003CCroutonState[]>([])\n\n// When you call open():\n// 1. New state object created with unique ID\n// 2. Pushed to array\n// 3. For update/view, item data fetched\n// 4. State marked as loaded\n\n// Computed properties always read from TOP of stack:\nconst action = computed(\n  () => croutonStates.value[croutonStates.value.length - 1]?.action || null\n)\n\n// This means nested forms don't interfere with parent form state\n// Each level has independent data",{"id":12590,"title":2522,"titles":12591,"content":12592,"level":449},"/api-reference/composables/form-composables#error-handling",[343,12525],"\u003Cscript setup lang=\"ts\">\nconst { open } = useCrouton()\nconst { foundErrors } = useCroutonError()\n\nconst handleOpenWithErrorCheck = async () => {\n  // Prevents opening if errors exist from previous operation\n  if (foundErrors()) {\n    console.log('Cannot open form: previous operation has errors')\n    return\n  }\n  \n  // Safe to open\n  open('create', 'users', [])\n}\n\u003C/script>",{"id":12594,"title":12595,"titles":12596,"content":12597,"level":449},"/api-reference/composables/form-composables#backward-compatibility","Backward Compatibility",[343,12525],"For backward compatibility, the topmost form's state is exposed via computed properties: \u003Cscript setup lang=\"ts\">\nconst { \n  showCrouton,  // computed: true if any form open\n  action,       // computed: action of topmost form\n  activeItem,   // computed: item of topmost form\n  activeCollection,\n  items         // computed: items for delete (topmost form)\n} = useCrouton()\n\n// These all reference the LAST item in croutonStates array\n// Useful for global _Form.vue component rendering\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonForm\n    v-if=\"showCrouton\"\n    :action=\"action\"\n    :active-item=\"activeItem\"\n    :collection=\"activeCollection\"\n  />\n\u003C/template>",{"id":12599,"title":44,"titles":12600,"content":12601,"level":449},"/api-reference/composables/form-composables#best-practices",[343,12525],"DO: ✅ Keep nesting to 2-3 levels for UX✅ Use slideovers for nested workflows✅ Use modals for simple, focused edits✅ Use dialogs for destructive confirmations✅ Let animations complete before operations✅ Close forms on successful submission✅ Show loading states during fetches DON'T: ❌ Nest deeper than 5 levels (will warn and prevent)❌ Open multiple modals simultaneously❌ Assume item data before fetch completes❌ Skip error handling❌ Mutate croutonStates directly (use provided methods)",{"id":12603,"title":36,"titles":12604,"content":12605,"level":449},"/api-reference/composables/form-composables#troubleshooting",[343,12525],"IssueSolutionForm doesn't openCheck collection exists, run npx crouton-generate config crouton.config.jsForm opens but no dataWait for loading to finish before accessing activeItemNested form closes both levelsComponent isn't calling close() with specific stateIdMax depth warningLimit to 3-5 levels, redesign workflow for simpler nestingState leaks between formsUse computed properties, not direct array access",{"id":12607,"title":1007,"titles":12608,"content":12609,"level":391},"/api-reference/composables/form-composables#related-resources",[343],"Form Components - Form UI componentsMutation Composables - Data mutationsNuxt UI Forms - Nuxt UI form components html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"id":334,"title":333,"titles":12611,"content":12612,"level":385},[],"Complete reference for all Nuxt Crouton composables Nuxt Crouton provides a comprehensive set of composables for data fetching, mutations, forms, tables, and utilities. These composables follow Nuxt's composables conventions and integrate seamlessly with Vue's Composition API.",{"id":12614,"title":603,"titles":12615,"content":528,"level":391},"/api-reference/composables#quick-reference",[333],{"id":12617,"title":339,"titles":12618,"content":12619,"level":449},"/api-reference/composables#data-composables",[333,603],"Fetch and manage collection data with automatic caching and reactivity. ComposablePurposeCategoryuseCollectionSimplified collection fetching (legacy pattern)DatauseCollectionItemFetch and manage a single collection itemDatauseCollectionsAccess all registered collections metadataDatauseCollectionProxyCreate reactive proxy for collection operationsDatauseExternalCollectionFetch data from external APIsData",{"id":12621,"title":351,"titles":12622,"content":12623,"level":449},"/api-reference/composables#query-composables",[333,603],"Fetch collection data with advanced query capabilities. ComposablePurposeCategoryuseCollectionQueryFetch with automatic caching and reactivityQuery",{"id":12625,"title":347,"titles":12626,"content":12627,"level":449},"/api-reference/composables#mutation-composables",[333,603],"Create, update, and delete collection data with automatic cache invalidation. ComposablePurposeCategoryuseCollectionMutationCRUD operations for a specific collectionMutationuseCroutonMutateOne-off mutations (any collection)Mutation",{"id":12629,"title":343,"titles":12630,"content":12631,"level":449},"/api-reference/composables#form-composables",[333,603],"Manage form state and CRUD operations with modal/slideover support. ComposablePurposeCategoryuseCroutonGlobal modal and form state managementForm",{"id":12633,"title":355,"titles":12634,"content":12635,"level":449},"/api-reference/composables#table-composables",[333,603],"Build interactive data tables with sorting, filtering, and pagination. ComposablePurposeCategoryuseTableColumnsDefine and configure table columnsTableuseTableDataManage table data with sorting and paginationTableuseTableSearchAdd search functionality to tablesTable",{"id":12637,"title":359,"titles":12638,"content":12639,"level":449},"/api-reference/composables#utility-composables",[333,603],"Specialized helpers for translations, assets, formatting, and more. ComposablePurposeCategoryuseFormatCollectionsFormat collection metadata for UI displayUtilityuseEntityTranslationsHandle entity-specific translationsUtilityuseAssetUploadManage file uploads with progress trackingUtilityuseTeamContextMulti-tenancy team context managementUtilityuseUsersUser management operationsUtilityuseCroutonErrorCentralized error handlingUtilityuseTTranslation helper with collection contextUtilityuseDependentFieldResolverResolve dependent field valuesUtilityuseExpandableSlideoverManage expandable slideover stateUtility",{"id":12641,"title":10805,"titles":12642,"content":12643,"level":391},"/api-reference/composables#detailed-documentation",[333],"Click any category below to view complete documentation with type signatures, parameters, examples, and best practices: Fetch and manage collection data with automatic caching and reactivityAdvanced data fetching with query parameters and reactivityCreate, update, and delete operations with cache invalidationManage form state and CRUD operations with modal supportBuild interactive data tables with sorting and filteringSpecialized helpers for translations, assets, and more",{"id":12645,"title":1007,"titles":12646,"content":12647,"level":391},"/api-reference/composables#related-resources",[333],"Nuxt Composables Guide - Learn about Nuxt's composables conventionsVue Composition API - Understanding Vue's Composition APIComponents Reference - UI components documentationTypeScript Types - Type definitions and interfaces",{"id":348,"title":347,"titles":12649,"content":12650,"level":385},[],"Create, update, and delete collection data with automatic cache invalidation",{"id":12652,"title":12653,"titles":12654,"content":12655,"level":391},"/api-reference/composables/mutation-composables#usecollectionmutation","useCollectionMutation",[347],"Mutate collection data with optimized API calls and automatic cache invalidation.",{"id":12657,"title":5176,"titles":12658,"content":12659,"level":449},"/api-reference/composables/mutation-composables#type-signature",[347,12653],"function useCollectionMutation(collection: string): {\n  create: (data: any) => Promise\u003Cany>\n  update: (id: string, data: any) => Promise\u003Cany>\n  deleteItems: (ids: string[]) => Promise\u003Cvoid>\n  delete: (ids: string[]) => Promise\u003Cvoid>  // Alias for deleteItems\n  isReady: ComputedRef\u003Cboolean>\n}",{"id":12661,"title":9079,"titles":12662,"content":12663,"level":449},"/api-reference/composables/mutation-composables#parameters",[347,12653],"collection (string) - The collection name",{"id":12665,"title":6217,"titles":12666,"content":12667,"level":449},"/api-reference/composables/mutation-composables#returns",[347,12653],"create - Create new itemupdate - Update existing itemdeleteItems - Delete one or more itemsdelete - Alias for deleteItemsisReady - Whether the mutation handler is ready (team context resolved)",{"id":12669,"title":1608,"titles":12670,"content":12671,"level":449},"/api-reference/composables/mutation-composables#usage",[347,12653],"\u003Cscript setup lang=\"ts\">\nconst { create, update, deleteItems } = useCollectionMutation('shopProducts')\n\n// Create\nawait create({\n  name: 'New Product',\n  price: 29.99\n})\n\n// Update\nawait update('product-123', {\n  name: 'Updated Name'\n})\n\n// Delete\nawait deleteItems(['id1', 'id2'])\n\u003C/script>",{"id":12673,"title":11435,"titles":12674,"content":12675,"level":449},"/api-reference/composables/mutation-composables#in-generated-forms",[347,12653],"\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003CShopProductsFormProps>()\nconst { create, update, deleteItems } = useCollectionMutation(props.collection)\n\nconst handleSubmit = async () => {\n  if (props.action === 'create') {\n    await create(state.value)\n  } else if (props.action === 'update') {\n    await update(state.value.id, state.value)\n  } else if (props.action === 'delete') {\n    await deleteItems(props.items)\n  }\n  close()\n}\n\u003C/script>",{"id":12677,"title":12678,"titles":12679,"content":12680,"level":449},"/api-reference/composables/mutation-composables#when-to-use","When to Use",[347,12653],"Best for: Generated formsRepeated operations on the same collectionMulti-step wizardsBulk operations (same collection) Use useCroutonMutate() instead for: One-off actionsQuick toggle buttonsUtility functions",{"id":12682,"title":12683,"titles":12684,"content":12685,"level":391},"/api-reference/composables/mutation-composables#usecroutonmutate","useCroutonMutate",[347],"Quick mutation API for one-off operations across any collection. (Already documented above, but here's the expanded version)",{"id":12687,"title":5176,"titles":12688,"content":12689,"level":449},"/api-reference/composables/mutation-composables#type-signature-1",[347,12683],"function useCroutonMutate(): {\n  mutate: (\n    action: 'create' | 'update' | 'delete',\n    collection: string,\n    data: any\n  ) => Promise\u003Cany>\n}",{"id":12691,"title":6217,"titles":12692,"content":12693,"level":449},"/api-reference/composables/mutation-composables#returns-1",[347,12683],"mutate - Generic mutation function for any collection",{"id":12695,"title":12696,"titles":12697,"content":12698,"level":449},"/api-reference/composables/mutation-composables#basic-operations","Basic Operations",[347,12683],"\u003Cscript setup lang=\"ts\">\nconst { mutate } = useCroutonMutate()\n\n// Create new item\nconst newProduct = await mutate('create', 'shopProducts', {\n  name: 'New Product',\n  price: 29.99,\n  categoryId: 'cat-123'\n})\n\n// Update existing item\nconst updated = await mutate('update', 'shopProducts', {\n  id: 'product-123',\n  name: 'Updated Name',\n  price: 34.99\n})\n\n// Delete items\nawait mutate('delete', 'shopProducts', ['id1', 'id2'])\n// Or single item\nawait mutate('delete', 'shopProducts', 'id1')\n\u003C/script>",{"id":12700,"title":12701,"titles":12702,"content":12703,"level":449},"/api-reference/composables/mutation-composables#quick-toggle-actions","Quick Toggle Actions",[347,12683],"Perfect for inline buttons and quick state changes: \u003Cscript setup lang=\"ts\">\nconst { mutate } = useCroutonMutate()\n\nconst toggleFeatured = async (product: Product) => {\n  await mutate('update', 'shopProducts', {\n    id: product.id,\n    featured: !product.featured\n  })\n}\n\nconst togglePublished = async (post: Post) => {\n  await mutate('update', 'blogPosts', {\n    id: post.id,\n    published: !post.published,\n    publishedAt: post.published ? null : new Date().toISOString()\n  })\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUButton @click=\"toggleFeatured(product)\">\n    {{ product.featured ? '⭐ Unfeature' : '☆ Feature' }}\n  \u003C/UButton>\n  \u003CUButton @click=\"togglePublished(post)\">\n    {{ post.published ? 'Unpublish' : 'Publish' }}\n  \u003C/UButton>\n\u003C/template>",{"id":12705,"title":12706,"titles":12707,"content":12708,"level":449},"/api-reference/composables/mutation-composables#cross-collection-operations","Cross-Collection Operations",[347,12683],"Mutate different collections without creating multiple composable instances: \u003Cscript setup lang=\"ts\">\nconst { mutate } = useCroutonMutate()\n\nconst archiveProject = async (projectId: string) => {\n  // Update project status\n  await mutate('update', 'projects', {\n    id: projectId,\n    status: 'archived',\n    archivedAt: new Date().toISOString()\n  })\n  \n  // Create audit log entry\n  await mutate('create', 'auditLogs', {\n    action: 'project.archived',\n    projectId,\n    userId: user.value.id\n  })\n  \n  // Send notification\n  await mutate('create', 'notifications', {\n    type: 'project.archived',\n    recipientId: project.value.ownerId,\n    message: `Project ${project.value.name} was archived`\n  })\n}\n\u003C/script>",{"id":12710,"title":12711,"titles":12712,"content":12713,"level":449},"/api-reference/composables/mutation-composables#utility-functions","Utility Functions",[347,12683],"Use in composables for reusable logic: // composables/useProductActions.ts\nexport function useProductActions() {\n  const { mutate } = useCroutonMutate()\n  \n  const duplicateProduct = async (productId: string) => {\n    // Fetch original\n    const original = await $fetch(`/api/teams/123/shopProducts/${productId}`)\n    \n    // Create duplicate\n    return await mutate('create', 'shopProducts', {\n      ...original,\n      name: `${original.name} (Copy)`,\n      id: undefined  // Let server generate new ID\n    })\n  }\n  \n  const bulkUpdatePrices = async (productIds: string[], multiplier: number) => {\n    for (const id of productIds) {\n      const product = await $fetch(`/api/teams/123/shopProducts/${id}`)\n      await mutate('update', 'shopProducts', {\n        id,\n        price: product.price * multiplier\n      })\n    }\n  }\n  \n  return {\n    duplicateProduct,\n    bulkUpdatePrices\n  }\n}",{"id":12715,"title":2522,"titles":12716,"content":12717,"level":449},"/api-reference/composables/mutation-composables#error-handling",[347,12683],"\u003Cscript setup lang=\"ts\">\nconst { mutate } = useCroutonMutate()\nconst toast = useToast()\n\nconst handleQuickDelete = async (id: string) => {\n  try {\n    await mutate('delete', 'shopProducts', [id])\n    \n    toast.add({\n      title: 'Product deleted',\n      color: 'green'\n    })\n  } catch (error) {\n    toast.add({\n      title: 'Delete failed',\n      description: error.message,\n      color: 'red'\n    })\n  }\n}\n\u003C/script>",{"id":12719,"title":12720,"titles":12721,"content":12722,"level":449},"/api-reference/composables/mutation-composables#validation","Validation",[347,12683],"Update operations require an id: // ❌ ERROR: Update requires data.id\nawait mutate('update', 'shopProducts', {\n  name: 'Updated Name'\n})\n// Throws: \"Update requires data.id\"\n\n// ✅ GOOD: Include id\nawait mutate('update', 'shopProducts', {\n  id: 'product-123',\n  name: 'Updated Name'\n})",{"id":12724,"title":12725,"titles":12726,"content":12727,"level":449},"/api-reference/composables/mutation-composables#comparison-table","Comparison Table",[347,12683],"ScenarioUse ThisToggle buttonuseCroutonMutate() ✅Quick add/deleteuseCroutonMutate() ✅Utility functionuseCroutonMutate() ✅Cross-collection mutationsuseCroutonMutate() ✅Generated formuseCollectionMutation()Multi-step wizarduseCollectionMutation()Repeated operations (same collection)useCollectionMutation()",{"id":12729,"title":12730,"titles":12731,"content":12732,"level":449},"/api-reference/composables/mutation-composables#integration-with-other-composables","Integration with Other Composables",[347,12683],"\u003Cscript setup lang=\"ts\">\nconst { mutate } = useCroutonMutate()\nconst { items, refresh } = await useCollectionQuery('shopProducts')\n\n// After mutation, manually refresh query\nconst quickCreate = async () => {\n  await mutate('create', 'shopProducts', { name: 'New Product' })\n  await refresh()  // Update the list\n}\n\n// Or rely on automatic cache invalidation\nconst quickUpdate = async (id: string) => {\n  await mutate('update', 'shopProducts', {\n    id,\n    featured: true\n  })\n  // Cache automatically refreshes!\n}\n\u003C/script>",{"id":12734,"title":44,"titles":12735,"content":12736,"level":449},"/api-reference/composables/mutation-composables#best-practices",[347,12683],"DO: ✅ Use for one-off, quick mutations✅ Use for cross-collection operations✅ Use in utility functions and composables✅ Handle errors with try/catch DON'T: ❌ Use for forms (use useCollectionMutation() instead)❌ Forget the id field for updates❌ Use for repeated operations on same collection",{"id":12738,"title":1007,"titles":12739,"content":12740,"level":391},"/api-reference/composables/mutation-composables#related-resources",[347],"Query Composables - Data fetching patternsForm Composables - Form state managementNuxt Data Fetching - Nuxt's data patterns html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":352,"title":351,"titles":12742,"content":12743,"level":385},[],"Fetch collection data with automatic caching and reactivity",{"id":12745,"title":12746,"titles":12747,"content":12748,"level":391},"/api-reference/composables/query-composables#usecollectionquery","useCollectionQuery",[351],"Fetch collection data with automatic caching and reactivity.",{"id":12750,"title":5176,"titles":12751,"content":12752,"level":449},"/api-reference/composables/query-composables#type-signature",[351,12746],"function useCollectionQuery\u003CT>(\n  collection: string,\n  options?: {\n    query?: ComputedRef\u003CRecord\u003Cstring, any>>\n    watch?: boolean\n  }\n): Promise\u003C{\n  items: ComputedRef\u003CT[]>\n  data: Ref\u003Cany>\n  pending: Ref\u003Cboolean>\n  error: Ref\u003Cany>\n  refresh: () => Promise\u003Cvoid>\n}>",{"id":12754,"title":9079,"titles":12755,"content":12756,"level":449},"/api-reference/composables/query-composables#parameters",[351,12746],"collection (string) - The collection name (e.g., 'shopProducts')options (object, optional)\nquery (ComputedRef) - Reactive query parameterswatch (boolean) - Enable automatic refetching on query changes",{"id":12758,"title":6217,"titles":12759,"content":12760,"level":449},"/api-reference/composables/query-composables#returns",[351,12746],"items - Computed array of collection itemsdata - Raw response datapending - Loading stateerror - Error staterefresh - Manual refetch function",{"id":12762,"title":4173,"titles":12763,"content":12764,"level":449},"/api-reference/composables/query-composables#basic-usage",[351,12746],"\u003Cscript setup lang=\"ts\">\nconst { items, pending, error, refresh } = await useCollectionQuery('shopProducts')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"pending\">Loading...\u003C/div>\n  \u003Cdiv v-else-if=\"error\">Error: {{ error }}\u003C/div>\n  \u003Cdiv v-else>\n    \u003Cdiv v-for=\"product in items\" :key=\"product.id\">\n      {{ product.name }}\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":12766,"title":12767,"titles":12768,"content":12769,"level":449},"/api-reference/composables/query-composables#with-query-parameters","With Query Parameters",[351,12746],"\u003Cscript setup lang=\"ts\">\nconst page = ref(1)\nconst search = ref('')\n\nconst { items, pending } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({\n    page: page.value,\n    search: search.value\n  }))\n})\n\u003C/script>",{"id":12771,"title":12772,"titles":12773,"content":12774,"level":449},"/api-reference/composables/query-composables#with-translations","With Translations",[351,12746],"\u003Cscript setup lang=\"ts\">\nconst { locale } = useI18n()\n\nconst { items } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({\n    locale: locale.value\n  }))\n})\n\n// Auto-refetches when locale changes!\n\u003C/script>",{"id":12776,"title":9361,"titles":12777,"content":12778,"level":449},"/api-reference/composables/query-composables#caching-behavior",[351,12746],"Each unique query gets its own cache entry: // Different cache keys for different queries\ncollection:shopProducts:{}                      // All products\ncollection:shopProducts:{\"page\":1}              // Page 1\ncollection:shopProducts:{\"page\":2}              // Page 2\ncollection:shopProducts:{\"locale\":\"en\"}         // English products\ncollection:shopProducts:{\"page\":1,\"locale\":\"fr\"} // Page 1, French\n\n// After mutation, all matching caches refresh automatically\nawait create({ name: 'New Product' })\n// → Triggers refetch for all shopProducts queries",{"id":12780,"title":1007,"titles":12781,"content":12782,"level":391},"/api-reference/composables/query-composables#related-resources",[351],"Data Composables - Collection data managementMutation Composables - Create, update, delete operationsNuxt Data Fetching - Nuxt's data fetching patternsVue Reactivity - Understanding Vue reactivity html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":356,"title":355,"titles":12784,"content":12785,"level":385},[],"Build interactive data tables with sorting, filtering, and pagination Working with Tables: For complete table patterns and component usage, see Table Patterns and Table Components API.",{"id":12787,"title":9286,"titles":12788,"content":12789,"level":391},"/api-reference/composables/table-composables#usetablecolumns",[355],"Manages table column configuration with automatic selection column, localized headers, and conditional default columns.",{"id":12791,"title":5176,"titles":12792,"content":12793,"level":449},"/api-reference/composables/table-composables#type-signature",[355,9286],"interface UseTableColumnsOptions {\n  columns: TableColumn[]\n  hideDefaultColumns?: {\n    createdAt?: boolean\n    updatedAt?: boolean\n    createdBy?: boolean\n    updatedBy?: boolean\n    select?: boolean\n    presence?: boolean\n    actions?: boolean\n  }\n}\n\nfunction useTableColumns(options: UseTableColumnsOptions): {\n  allColumns: ComputedRef\u003CTableColumn[]>\n}",{"id":12795,"title":9079,"titles":12796,"content":12797,"level":449},"/api-reference/composables/table-composables#parameters",[355,9286],"ParameterTypeRequiredDescriptioncolumnsTableColumn[]YesArray of column definitions for the tablehideDefaultColumnsobjectNoObject with boolean flags to hide default columnshideDefaultColumns.createdAtbooleanNoHide 'createdAt' column (default: false)hideDefaultColumns.updatedAtbooleanNoHide 'updatedAt' column (default: false)hideDefaultColumns.createdBybooleanNoHide 'createdBy' column (default: false)hideDefaultColumns.updatedBybooleanNoHide 'updatedBy' column (default: false)hideDefaultColumns.selectbooleanNoHide 'select' checkbox column (default: false)hideDefaultColumns.presencebooleanNoHide 'presence' collab column (default: false)hideDefaultColumns.actionsbooleanNoHide 'actions' column (default: false)",{"id":12799,"title":6217,"titles":12800,"content":12801,"level":449},"/api-reference/composables/table-composables#returns",[355,9286],"PropertyTypeDescriptionallColumnsComputedRef\u003CTableColumn[]>Computed array of all columns including selection column and default columns",{"id":12803,"title":12804,"titles":12805,"content":12806,"level":449},"/api-reference/composables/table-composables#return-values-detail","Return Values Detail",[355,9286],"allColumns: A computed reactive reference containing: Selection column (first): CroutonTableCheckbox for row selection with \"select all\" headerActions column (second): Placed after the select column but before user columnsCustom columns: Your provided columns in the order specifiedDefault columns (appended): createdAt, updatedAt, createdBy, updatedBy (unless hidden)",{"id":12808,"title":4173,"titles":12809,"content":12810,"level":449},"/api-reference/composables/table-composables#basic-usage",[355,9286],"Simple table with all default columns: \u003Cscript setup lang=\"ts\">\nimport type { TableColumn } from '@crouton/types'\n\nconst { allColumns } = useTableColumns({\n  columns: [\n    {\n      accessorKey: 'name',\n      id: 'name',\n      header: 'Product Name',\n      sortable: true\n    },\n    {\n      accessorKey: 'price',\n      id: 'price',\n      header: 'Price',\n      sortable: true\n    }\n  ]\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonTable :columns=\"allColumns\" :rows=\"products\" />\n\u003C/template>",{"id":12812,"title":12813,"titles":12814,"content":12815,"level":449},"/api-reference/composables/table-composables#hide-specific-default-columns","Hide Specific Default Columns",[355,9286],"Remove timestamps and audit columns: \u003Cscript setup lang=\"ts\">\nconst { allColumns } = useTableColumns({\n  columns: [\n    {\n      accessorKey: 'name',\n      id: 'name',\n      header: 'Name'\n    }\n  ],\n  hideDefaultColumns: {\n    createdAt: true,\n    updatedAt: true,\n    createdBy: true,\n    updatedBy: true\n  }\n})\n\u003C/script>",{"id":12817,"title":12818,"titles":12819,"content":12820,"level":449},"/api-reference/composables/table-composables#hide-actions-column","Hide Actions Column",[355,9286],"For read-only tables: \u003Cscript setup lang=\"ts\">\nconst { allColumns } = useTableColumns({\n  columns: [\n    {\n      accessorKey: 'name',\n      id: 'name',\n      header: 'Name'\n    },\n    {\n      accessorKey: 'email',\n      id: 'email',\n      header: 'Email'\n    }\n  ],\n  hideDefaultColumns: {\n    actions: true\n  }\n})\n\u003C/script>",{"id":12822,"title":12823,"titles":12824,"content":12825,"level":449},"/api-reference/composables/table-composables#custom-column-configuration","Custom Column Configuration",[355,9286],"With complex column definitions: \u003Cscript setup lang=\"ts\">\nconst { allColumns } = useTableColumns({\n  columns: [\n    {\n      accessorKey: 'name',\n      id: 'name',\n      header: 'Name',\n      sortable: true,\n      size: 200\n    },\n    {\n      accessorKey: 'status',\n      id: 'status',\n      header: 'Status',\n      cell: ({ row }: any) => h('span', {\n        class: row.original.status === 'active' \n          ? 'text-green-600' \n          : 'text-gray-500'\n      }, row.original.status)\n    }\n  ],\n  hideDefaultColumns: {\n    createdBy: true,\n    updatedBy: true\n  }\n})\n\u003C/script>",{"id":12827,"title":12828,"titles":12829,"content":12830,"level":449},"/api-reference/composables/table-composables#integration-with-usetabledata","Integration with useTableData",[355,9286],"Combine with useTableData for complete table functionality: \u003Cscript setup lang=\"ts\">\nimport { ref, computed } from 'vue'\n\nconst rows = ref([\n  { id: 1, name: 'Item 1', status: 'active' },\n  { id: 2, name: 'Item 2', status: 'inactive' }\n])\nconst search = ref('')\nconst sort = ref({ column: 'name', direction: 'asc' })\nconst page = ref(1)\nconst pageCount = ref(10)\n\nconst { allColumns } = useTableColumns({\n  columns: [\n    { accessorKey: 'name', id: 'name', header: 'Name', sortable: true },\n    { accessorKey: 'status', id: 'status', header: 'Status' }\n  ]\n})\n\nconst { slicedRows, pageTotalToShow } = useTableData({\n  rows,\n  search,\n  sort,\n  page,\n  pageCount,\n  serverPagination: false\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonTable \n    :columns=\"allColumns\"\n    :rows=\"slicedRows\"\n    :total-items=\"pageTotalToShow\"\n  />\n\u003C/template>",{"id":12832,"title":44,"titles":12833,"content":12834,"level":449},"/api-reference/composables/table-composables#best-practices",[355,9286],"DO: ✅ Use to ensure consistent column structure across tables✅ Hide audit columns for read-only views✅ Define custom columns with proper TypeScript types✅ Use accessorKey for simple data access✅ Provide sortable flag for relevant columns✅ Combine with useTableData for complete solution DON'T: ❌ Define selection column manually (it's added automatically)❌ Forget to provide column headers (affects accessibility)❌ Hide all default columns without reason (users expect timestamps)❌ Use complex cell renderers without memoization❌ Mix different table column standards",{"id":12836,"title":36,"titles":12837,"content":12838,"level":449},"/api-reference/composables/table-composables#troubleshooting",[355,9286],"IssueCauseSolutionSelection column missingNot using CroutonTableCheckboxuseTableColumns handles this automaticallyDefault columns still visiblehideDefaultColumns not workingVerify property names: createdAt, updatedAt, etc. (camelCase)Headers not localizedTranslation keys missingEnsure useT() provides 'table.createdAt', 'table.actions' keysColumn order wrongColumns appended after customDefine custom columns first, defaults added lastSortable not workingTanStack Table not configuredEnsure parent table component handles sorting",{"id":12840,"title":9281,"titles":12841,"content":12842,"level":391},"/api-reference/composables/table-composables#usetabledata",[355],"Manages table data transformation including search, filtering, sorting, and pagination (both client and server-side).",{"id":12844,"title":5176,"titles":12845,"content":12846,"level":449},"/api-reference/composables/table-composables#type-signature-1",[355,9281],"interface UseTableDataOptions {\n  rows: Ref\u003Cany[]>\n  search: Ref\u003Cstring>\n  sort: Ref\u003CTableSort>\n  page: Ref\u003Cnumber>\n  pageCount: Ref\u003Cnumber>\n  serverPagination: boolean\n  paginationData?: PaginationData | null\n}\n\nfunction useTableData(options: UseTableDataOptions): {\n  searchedRows: ComputedRef\u003Cany[]>\n  slicedRows: ComputedRef\u003Cany[]>\n  pageTotalToShow: ComputedRef\u003Cnumber>\n  pageFrom: ComputedRef\u003Cnumber>\n  pageTo: ComputedRef\u003Cnumber>\n  itemCountFromServer: ComputedRef\u003Cnumber>\n}",{"id":12848,"title":9079,"titles":12849,"content":12850,"level":449},"/api-reference/composables/table-composables#parameters-1",[355,9281],"ParameterTypeRequiredDescriptionrowsRef\u003Cany[]>YesReactive array of table rowssearchRef\u003Cstring>YesReactive search query stringsortRef\u003CTableSort>YesReactive sort configurationpageRef\u003Cnumber>YesCurrent page number (1-indexed)pageCountRef\u003Cnumber>YesItems per pageserverPaginationbooleanYesWhether pagination happens on serverpaginationDataPaginationData | nullNoServer pagination metadata (totalItems, etc.)",{"id":12852,"title":6217,"titles":12853,"content":12854,"level":449},"/api-reference/composables/table-composables#returns-1",[355,9281],"PropertyTypeDescriptionsearchedRowsComputedRef\u003Cany[]>All rows matching search queryslicedRowsComputedRef\u003Cany[]>Rows for current page after searchpageTotalToShowComputedRef\u003Cnumber>Total count after search filteringpageFromComputedRef\u003Cnumber>Index of first item on current page (1-indexed)pageToComputedRef\u003Cnumber>Index of last item on current pageitemCountFromServerComputedRef\u003Cnumber>Total items from server (for server pagination)",{"id":12856,"title":12804,"titles":12857,"content":12858,"level":449},"/api-reference/composables/table-composables#return-values-detail-1",[355,9281],"searchedRows: Filtered rows based on search.value. Searches all object values case-insensitively. slicedRows: Client pagination: searchedRows sliced by page and pageCountServer pagination: rows (already paginated from server) pageTotalToShow: Server pagination: itemCountFromServerClient pagination with search: searchedRows.lengthClient pagination without search: rows.length pageFrom: Starting index (1-based). Example: page 2 with pageCount 10 = 11 pageTo: Ending index (1-based). Clamped to actual total. itemCountFromServer: From paginationData.totalItems or rows.length",{"id":12860,"title":12861,"titles":12862,"content":12863,"level":449},"/api-reference/composables/table-composables#basic-usage-client-side-pagination","Basic Usage - Client-Side Pagination",[355,9281],"Simple local pagination: \u003Cscript setup lang=\"ts\">\nimport { ref, computed } from 'vue'\n\nconst rows = ref([\n  { id: 1, name: 'Alice', email: 'alice@example.com' },\n  { id: 2, name: 'Bob', email: 'bob@example.com' },\n  // ... more rows\n])\n\nconst search = ref('')\nconst sort = ref({ column: 'name', direction: 'asc' })\nconst page = ref(1)\nconst pageCount = ref(10)\n\nconst { slicedRows, pageFrom, pageTo, pageTotalToShow } = useTableData({\n  rows,\n  search,\n  sort,\n  page,\n  pageCount,\n  serverPagination: false\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003CCroutonTable :rows=\"slicedRows\" />\n    \u003Cp>Showing {{ pageFrom }} to {{ pageTo }} of {{ pageTotalToShow }}\u003C/p>\n  \u003C/div>\n\u003C/template>",{"id":12865,"title":4134,"titles":12866,"content":12867,"level":449},"/api-reference/composables/table-composables#server-side-pagination",[355,9281],"Efficient pagination for large datasets: \u003Cscript setup lang=\"ts\">\nimport { ref } from 'vue'\n\nconst rows = ref([])\nconst search = ref('')\nconst sort = ref({ column: 'name', direction: 'asc' })\nconst page = ref(1)\nconst pageCount = ref(10)\nconst paginationData = ref({ totalItems: 0 })\n\n// Fetch from server when page/search changes\nconst fetchData = async () => {\n  const { data } = await $fetch('/api/items', {\n    query: {\n      page: page.value,\n      pageSize: pageCount.value,\n      search: search.value,\n      sortBy: sort.value.column,\n      sortDirection: sort.value.direction\n    }\n  })\n  rows.value = data.items\n  paginationData.value = { totalItems: data.total }\n}\n\nconst { slicedRows, pageFrom, pageTo, pageTotalToShow } = useTableData({\n  rows,\n  search,\n  sort,\n  page,\n  pageCount,\n  serverPagination: true,\n  paginationData: paginationData.value\n})\n\nwatch([page, search, sort], () => fetchData())\nonMounted(() => fetchData())\n\u003C/script>",{"id":12869,"title":12870,"titles":12871,"content":12872,"level":449},"/api-reference/composables/table-composables#client-search-with-pagination","Client Search with Pagination",[355,9281],"Search within current page results: \u003Cscript setup lang=\"ts\">\nconst rows = ref([...myData])\nconst search = ref('')\nconst page = ref(1)\nconst pageCount = ref(10)\n\nconst { slicedRows, searchedRows, pageTotalToShow } = useTableData({\n  rows,\n  search,\n  sort: ref({ column: 'name', direction: 'asc' }),\n  page,\n  pageCount,\n  serverPagination: false\n})\n\n// Show filtered count\nconst resultText = computed(() => {\n  return `${pageTotalToShow.value} results (searched across ${rows.value.length} items)`\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003CUInput v-model=\"search\" placeholder=\"Search...\" />\n    \u003Cp class=\"text-sm text-gray-600\">{{ resultText }}\u003C/p>\n    \u003CCroutonTable :rows=\"slicedRows\" />\n  \u003C/div>\n\u003C/template>",{"id":12874,"title":12875,"titles":12876,"content":12877,"level":449},"/api-reference/composables/table-composables#pagination-ui","Pagination UI",[355,9281],"Display pagination controls: \u003Cscript setup lang=\"ts\">\nconst rows = ref([...myData])\nconst search = ref('')\nconst sort = ref({ column: 'name', direction: 'asc' })\nconst page = ref(1)\nconst pageCount = ref(10)\n\nconst { slicedRows, pageFrom, pageTo, pageTotalToShow } = useTableData({\n  rows,\n  search,\n  sort,\n  page,\n  pageCount,\n  serverPagination: false\n})\n\nconst totalPages = computed(() => Math.ceil(pageTotalToShow.value / pageCount.value))\n\nconst nextPage = () => {\n  if (page.value \u003C totalPages.value) page.value++\n}\n\nconst prevPage = () => {\n  if (page.value > 1) page.value--\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003CCroutonTable :rows=\"slicedRows\" />\n    \n    \u003Cdiv class=\"flex items-center justify-between\">\n      \u003Cp class=\"text-sm\">\n        Showing {{ pageFrom }} to {{ pageTo }} of {{ pageTotalToShow }}\n      \u003C/p>\n      \u003Cdiv class=\"flex gap-2\">\n        \u003CUButton @click=\"prevPage\" :disabled=\"page === 1\">Previous\u003C/UButton>\n        \u003Cspan>Page {{ page }} of {{ totalPages }}\u003C/span>\n        \u003CUButton @click=\"nextPage\" :disabled=\"page === totalPages\">Next\u003C/UButton>\n      \u003C/div>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":12879,"title":44,"titles":12880,"content":12881,"level":449},"/api-reference/composables/table-composables#best-practices-1",[355,9281],"DO: ✅ Use server pagination for datasets over 1000 items✅ Debounce search input for server pagination✅ Reset page to 1 when search changes✅ Provide clear result counts to users✅ Handle loading/error states during pagination✅ Memoize computed values with computed() DON'T: ❌ Use client pagination for large datasets (performance killer)❌ Search all fields if you can search specific fields on server❌ Forget to update totalItems from server response❌ Mix server and client search (confusing results)❌ Assume search works on nested objects (it searches values)",{"id":12883,"title":36,"titles":12884,"content":12885,"level":449},"/api-reference/composables/table-composables#troubleshooting-1",[355,9281],"IssueCauseSolutionPage data wrong after searchPage number not resetWatch search changes and set page.value = 1Server pagination not workingserverPagination false or paginationData not updatedSet serverPagination: true and update paginationDataSearch finds nothingCase sensitivity or nested fieldsSearch is case-insensitive across all top-level valuesPagination counts offServer totalItems mismatchEnsure server returns correct totalItemsPerformance degradationClient pagination with large datasetSwitch to serverPagination: true",{"id":12887,"title":9291,"titles":12888,"content":12889,"level":391},"/api-reference/composables/table-composables#usetablesearch",[355],"Manages table search state with automatic debouncing and error handling.",{"id":12891,"title":5176,"titles":12892,"content":12893,"level":449},"/api-reference/composables/table-composables#type-signature-2",[355,9291],"interface UseTableSearchOptions {\n  initialValue?: string\n  debounceMs?: number\n  onSearch?: (value: string) => void | Promise\u003Cvoid>\n}\n\nfunction useTableSearch(options?: UseTableSearchOptions): {\n  search: Readonly\u003CRef\u003Cstring>>\n  isSearching: Readonly\u003CRef\u003Cboolean>>\n  handleSearch: (value: string) => void\n  clearSearch: () => void\n}",{"id":12895,"title":9079,"titles":12896,"content":12897,"level":449},"/api-reference/composables/table-composables#parameters-2",[355,9291],"ParameterTypeRequiredDefaultDescriptioninitialValuestringNo''Initial search textdebounceMsnumberNo300Debounce delay in millisecondsonSearch(value: string) => void | Promise\u003Cvoid>NoundefinedCallback function when search executes",{"id":12899,"title":6217,"titles":12900,"content":12901,"level":449},"/api-reference/composables/table-composables#returns-2",[355,9291],"PropertyTypeDescriptionsearchReadonly\u003CRef\u003Cstring>>Current search string (readonly)isSearchingReadonly\u003CRef\u003Cboolean>>Whether async search is in progresshandleSearch(value: string) => voidDebounced search handlerclearSearch() => voidClear search and call onSearch('')",{"id":12903,"title":12804,"titles":12904,"content":12905,"level":449},"/api-reference/composables/table-composables#return-values-detail-2",[355,9291],"search: Readonly ref containing current search query. Use .value to access. isSearching: Readonly ref indicating if async onSearch callback is executing. Useful for showing loading spinner. handleSearch: Debounced function that updates search and calls onSearch. Debounce delay specified by debounceMs. clearSearch: Resets search to '' and calls onSearch if provided. Does not debounce.",{"id":12907,"title":4173,"titles":12908,"content":12909,"level":449},"/api-reference/composables/table-composables#basic-usage-1",[355,9291],"Simple search input: \u003Cscript setup lang=\"ts\">\nconst { search, handleSearch, clearSearch } = useTableSearch({\n  debounceMs: 300\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"flex gap-2\">\n    \u003CUInput\n      :value=\"search\"\n      placeholder=\"Search...\"\n      @input=\"(e) => handleSearch(e.target.value)\"\n    />\n    \u003CUButton @click=\"clearSearch\">Clear\u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":12911,"title":12912,"titles":12913,"content":12914,"level":449},"/api-reference/composables/table-composables#with-server-search","With Server Search",[355,9291],"Search that queries the server: \u003Cscript setup lang=\"ts\">\nconst results = ref([])\n\nconst { search, isSearching, handleSearch } = useTableSearch({\n  debounceMs: 500,\n  onSearch: async (query) => {\n    if (query === '') {\n      results.value = []\n      return\n    }\n    \n    try {\n      const { data } = await $fetch('/api/search', {\n        query: { q: query }\n      })\n      results.value = data\n    } catch (error) {\n      console.error('Search failed:', error)\n    }\n  }\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cdiv class=\"flex gap-2\">\n      \u003CUInput\n        :value=\"search\"\n        placeholder=\"Search...\"\n        @input=\"(e) => handleSearch(e.target.value)\"\n      />\n      \u003CUIcon v-if=\"isSearching\" class=\"animate-spin\" />\n    \u003C/div>\n    \u003Cdiv v-if=\"results.length > 0\" class=\"mt-4\">\n      \u003Cdiv v-for=\"result in results\" :key=\"result.id\" class=\"p-2 border\">\n        {{ result.title }}\n      \u003C/div>\n    \u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":12916,"title":12917,"titles":12918,"content":12919,"level":449},"/api-reference/composables/table-composables#search-with-url-updates","Search with URL Updates",[355,9291],"Sync search with URL query parameters: \u003Cscript setup lang=\"ts\">\nconst route = useRoute()\nconst router = useRouter()\nconst initialSearch = route.query.q as string || ''\n\nconst { search, handleSearch, clearSearch } = useTableSearch({\n  initialValue: initialSearch,\n  debounceMs: 300,\n  onSearch: async (query) => {\n    // Update URL\n    await router.push({\n      query: { ...route.query, q: query || undefined }\n    })\n  }\n})\n\n// Sync when query param changes\nwatch(() => route.query.q, (newQuery) => {\n  if (newQuery !== search.value) {\n    handleSearch(newQuery as string || '')\n  }\n})\n\u003C/script>",{"id":12921,"title":12922,"titles":12923,"content":12924,"level":449},"/api-reference/composables/table-composables#search-with-async-validation","Search with Async Validation",[355,9291],"Show search status and validation: \u003Cscript setup lang=\"ts\">\nconst searchStatus = ref('')\nconst searchError = ref('')\n\nconst { search, isSearching, handleSearch, clearSearch } = useTableSearch({\n  onSearch: async (query) => {\n    searchStatus.value = 'Searching...'\n    searchError.value = ''\n    \n    try {\n      // Validate query\n      if (query.length \u003C 2) {\n        searchStatus.value = 'Enter at least 2 characters'\n        return\n      }\n      \n      // Perform search\n      const results = await $fetch('/api/search', { query: { q: query } })\n      searchStatus.value = `Found ${results.length} results`\n    } catch (error) {\n      searchError.value = 'Search failed. Please try again.'\n      searchStatus.value = ''\n    }\n  }\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003CUInput\n      :value=\"search\"\n      placeholder=\"Search...\"\n      @input=\"(e) => handleSearch(e.target.value)\"\n      :disabled=\"isSearching\"\n    />\n    \n    \u003Cdiv v-if=\"isSearching\" class=\"text-sm text-gray-600 mt-2\">\n      Searching...\n    \u003C/div>\n    \u003Cdiv v-else-if=\"searchError\" class=\"text-sm text-red-600 mt-2\">\n      {{ searchError }}\n    \u003C/div>\n    \u003Cdiv v-else-if=\"searchStatus\" class=\"text-sm text-gray-600 mt-2\">\n      {{ searchStatus }}\n    \u003C/div>\n    \n    \u003CUButton \n      v-if=\"search\"\n      @click=\"clearSearch\"\n      variant=\"ghost\"\n      size=\"sm\"\n    >\n      Clear\n    \u003C/UButton>\n  \u003C/div>\n\u003C/template>",{"id":12926,"title":44,"titles":12927,"content":12928,"level":449},"/api-reference/composables/table-composables#best-practices-2",[355,9291],"DO: ✅ Use debounce to reduce server load (300-500ms typical)✅ Show isSearching state with loading indicator✅ Handle errors in onSearch callback✅ Use clearSearch for quick reset✅ Validate query length before searching✅ Provide user feedback on search results DON'T: ❌ Use debounceMs \u003C 200 (too aggressive)❌ Perform expensive operations on every keystroke❌ Forget to handle empty search (should clear results)❌ Ignore isSearching state (users think nothing is happening)❌ Search without server-side validation",{"id":12930,"title":36,"titles":12931,"content":12932,"level":449},"/api-reference/composables/table-composables#troubleshooting-2",[355,9291],"IssueCauseSolutionSearch fires too oftendebounceMs too lowIncrease to 300-500msSearch fires not at allhandleSearch not calledEnsure bound to @input event properlyisSearching never resetsError in onSearch callbackAdd try/catch and finally blockClear button doesn't resetclearSearch not updating parent stateParent must watch search.value and reset stateDebounce not workingDirect mutation of search.valueAlways use handleSearch() method",{"id":12934,"title":1007,"titles":12935,"content":12936,"level":391},"/api-reference/composables/table-composables#related-resources",[355],"Table Components - Table UI componentsQuery Composables - Data fetching for tablesNuxt UI Table - Nuxt UI table component html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"id":360,"title":359,"titles":12938,"content":12939,"level":385},[],"Specialized helpers for translations, assets, formatting, and more Query Examples: For complete useCollectionQuery patterns (basic, filtering, pagination, sorting, relations), see Querying Data.",{"id":12941,"title":12942,"titles":12943,"content":12944,"level":391},"/api-reference/composables/utility-composables#useformatcollections","useFormatCollections",[359],"Format collection names for display with intelligent pluralization and layer prefix handling. (Already partially documented above, but here's the expanded version)",{"id":12946,"title":5176,"titles":12947,"content":12948,"level":449},"/api-reference/composables/utility-composables#type-signature",[359,12942],"function useFormatCollections(): {\n  collectionWithCapital: (val: string) => string\n  collectionWithCapitalSingular: (val: string) => string\n  stripLayerPrefix: (val: string) => string\n  camelToTitleCase: (val: string) => string\n  toPascalCase: (val: string) => string\n}",{"id":12950,"title":6217,"titles":12951,"content":12952,"level":449},"/api-reference/composables/utility-composables#returns",[359,12942],"collectionWithCapital - Format collection name to Title Case (plural)collectionWithCapitalSingular - Format collection name to singular Title CasestripLayerPrefix - Remove layer prefix from collection namecamelToTitleCase - Convert camelCase to Title Case with spacestoPascalCase - Convert to PascalCase",{"id":12954,"title":4173,"titles":12955,"content":12956,"level":449},"/api-reference/composables/utility-composables#basic-usage",[359,12942],"\u003Cscript setup lang=\"ts\">\nconst { collectionWithCapitalSingular } = useFormatCollections()\n\n// Format collection names\nconsole.log(collectionWithCapitalSingular('shopProducts'))   // 'Product'\nconsole.log(collectionWithCapitalSingular('blogPosts'))      // 'Post'\nconsole.log(collectionWithCapitalSingular('adminUsers'))     // 'User'\nconsole.log(collectionWithCapitalSingular('shopCategories')) // 'Category'\n\u003C/script>",{"id":12958,"title":12959,"titles":12960,"content":12961,"level":449},"/api-reference/composables/utility-composables#pluralization-rules","Pluralization Rules",[359,12942],"The composable handles various English pluralization patterns: \u003Cscript setup lang=\"ts\">\nconst { collectionWithCapitalSingular } = useFormatCollections()\n\n// Standard -s removal\ncollectionWithCapitalSingular('products')    // 'Product'\ncollectionWithCapitalSingular('users')       // 'User'\n\n// -ies → -y\ncollectionWithCapitalSingular('categories')  // 'Category'\ncollectionWithCapitalSingular('companies')   // 'Company'\n\n// Sibilants: -xes, -ches, -shes, -sses, -zes → remove -es\ncollectionWithCapitalSingular('boxes')       // 'Box'\ncollectionWithCapitalSingular('watches')     // 'Watch'\ncollectionWithCapitalSingular('brushes')     // 'Brush'\ncollectionWithCapitalSingular('classes')     // 'Class'\n\n// -oes → -o (conditional)\ncollectionWithCapitalSingular('heroes')      // 'Hero'\ncollectionWithCapitalSingular('tomatoes')    // 'Tomato'\n\u003C/script>",{"id":12963,"title":12964,"titles":12965,"content":12966,"level":449},"/api-reference/composables/utility-composables#layer-prefix-stripping","Layer Prefix Stripping",[359,12942],"Automatically removes layer prefixes based on app.config.ts registry: // app.config.ts\nexport default defineAppConfig({\n  croutonCollections: {\n    shopProducts: { name: 'shopProducts', layer: 'shop' },\n    shopCategories: { name: 'shopCategories', layer: 'shop' },\n    blogPosts: { name: 'blogPosts', layer: 'blog' },\n    adminUsers: { name: 'adminUsers', layer: 'admin' }\n  }\n}) \u003Cscript setup lang=\"ts\">\nconst { stripLayerPrefix, collectionWithCapitalSingular } = useFormatCollections()\n\n// Strip layer prefix\nstripLayerPrefix('shopProducts')    // 'products'\nstripLayerPrefix('blogPosts')       // 'posts'\nstripLayerPrefix('adminUsers')      // 'users'\n\n// Combined with singularization\ncollectionWithCapitalSingular('shopProducts')    // 'Product' (not 'Shop Product')\ncollectionWithCapitalSingular('blogPosts')       // 'Post' (not 'Blog Post')\ncollectionWithCapitalSingular('adminUsers')      // 'User' (not 'Admin User')\n\u003C/script>",{"id":12968,"title":12969,"titles":12970,"content":12971,"level":449},"/api-reference/composables/utility-composables#title-case-conversion","Title Case Conversion",[359,12942],"\u003Cscript setup lang=\"ts\">\nconst { camelToTitleCase } = useFormatCollections()\n\n// Convert camelCase to Title Case with spaces\ncamelToTitleCase('shopProducts')      // 'Shop Products'\ncamelToTitleCase('userPreferences')   // 'User Preferences'\ncamelToTitleCase('apiKeys')           // 'Api Keys'\ncamelToTitleCase('blogPostComments')  // 'Blog Post Comments'\n\u003C/script>",{"id":12973,"title":12974,"titles":12975,"content":12976,"level":449},"/api-reference/composables/utility-composables#pascalcase-conversion","PascalCase Conversion",[359,12942],"\u003Cscript setup lang=\"ts\">\nconst { toPascalCase } = useFormatCollections()\n\n// Convert to PascalCase\ntoPascalCase('shopProducts')    // 'ShopProducts'\ntoPascalCase('userSettings')    // 'UserSettings'\ntoPascalCase('blogPosts')       // 'BlogPosts'\n\u003C/script>",{"id":12978,"title":12979,"titles":12980,"content":12981,"level":449},"/api-reference/composables/utility-composables#plural-vs-singular","Plural vs Singular",[359,12942],"\u003Cscript setup lang=\"ts\">\nconst {\n  collectionWithCapital,          // Plural\n  collectionWithCapitalSingular   // Singular\n} = useFormatCollections()\n\nconst collection = 'shopProducts'\n\n// Plural form (for lists, tables)\ncollectionWithCapital(collection)         // 'Products'\n\n// Singular form (for buttons, forms)\ncollectionWithCapitalSingular(collection) // 'Product'\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- List heading -->\n  \u003Ch1>{{ collectionWithCapital('shopProducts') }}\u003C/h1>\n  \u003C!-- \"Products\" -->\n  \n  \u003C!-- Create button -->\n  \u003CUButton>Create {{ collectionWithCapitalSingular('shopProducts') }}\u003C/UButton>\n  \u003C!-- \"Create Product\" -->\n\u003C/template>",{"id":12983,"title":12984,"titles":12985,"content":12986,"level":449},"/api-reference/composables/utility-composables#dynamic-button-labels","Dynamic Button Labels",[359,12942],"Used internally by CroutonButton for automatic labeling: \u003Cscript setup lang=\"ts\">\nconst { collectionWithCapitalSingular } = useFormatCollections()\n\nconst getButtonLabel = (action: string, collection: string) => {\n  const singular = collectionWithCapitalSingular(collection)\n  \n  return {\n    create: `Create ${singular}`,\n    update: `Update ${singular}`,\n    delete: `Delete ${singular}`,\n    view: `View ${singular}`\n  }[action]\n}\n\nconsole.log(getButtonLabel('create', 'shopProducts'))  // 'Create Product'\nconsole.log(getButtonLabel('update', 'blogPosts'))     // 'Update Post'\nconsole.log(getButtonLabel('delete', 'adminUsers'))    // 'Delete User'\n\u003C/script>",{"id":12988,"title":12989,"titles":12990,"content":12991,"level":449},"/api-reference/composables/utility-composables#page-titles-and-headings","Page Titles and Headings",[359,12942],"\u003Cscript setup lang=\"ts\">\nconst { collectionWithCapital, collectionWithCapitalSingular } = useFormatCollections()\n\nconst route = useRoute()\nconst collection = 'shopProducts'\n\nconst getPageTitle = () => {\n  if (route.name?.includes('index')) {\n    return collectionWithCapital(collection)        // 'Products' (list page)\n  } else if (route.name?.includes('create')) {\n    return `New ${collectionWithCapitalSingular(collection)}`  // 'New Product'\n  } else if (route.name?.includes('edit')) {\n    return `Edit ${collectionWithCapitalSingular(collection)}` // 'Edit Product'\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CNuxtLayout>\n    \u003Ctemplate #header>\n      \u003Ch1>{{ getPageTitle() }}\u003C/h1>\n    \u003C/template>\n  \u003C/NuxtLayout>\n\u003C/template>",{"id":12993,"title":12994,"titles":12995,"content":12996,"level":449},"/api-reference/composables/utility-composables#breadcrumbs","Breadcrumbs",[359,12942],"\u003Cscript setup lang=\"ts\">\nconst { collectionWithCapital, collectionWithCapitalSingular } = useFormatCollections()\n\nconst breadcrumbs = computed(() => {\n  const collection = route.params.collection as string\n  const id = route.params.id\n  \n  return [\n    { label: 'Home', to: '/' },\n    { label: collectionWithCapital(collection), to: `/${collection}` },\n    id && { label: collectionWithCapitalSingular(collection), to: `/${collection}/${id}` }\n  ].filter(Boolean)\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- Breadcrumbs: Home > Products > Product -->\n  \u003Cnav>\n    \u003Cspan v-for=\"(crumb, i) in breadcrumbs\" :key=\"i\">\n      \u003CNuxtLink :to=\"crumb.to\">{{ crumb.label }}\u003C/NuxtLink>\n      \u003Cspan v-if=\"i \u003C breadcrumbs.length - 1\"> > \u003C/span>\n    \u003C/span>\n  \u003C/nav>\n\u003C/template>",{"id":12998,"title":12999,"titles":13000,"content":13001,"level":449},"/api-reference/composables/utility-composables#notification-messages","Notification Messages",[359,12942],"\u003Cscript setup lang=\"ts\">\nconst { collectionWithCapitalSingular } = useFormatCollections()\nconst toast = useToast()\n\nconst notifySuccess = (action: string, collection: string) => {\n  const singular = collectionWithCapitalSingular(collection)\n  \n  const messages = {\n    create: `${singular} created successfully`,\n    update: `${singular} updated successfully`,\n    delete: `${singular} deleted successfully`\n  }\n  \n  toast.add({\n    title: messages[action],\n    color: 'green'\n  })\n}\n\n// Usage\nawait mutate('create', 'shopProducts', data)\nnotifySuccess('create', 'shopProducts')\n// Shows: \"Product created successfully\"\n\u003C/script>",{"id":13003,"title":13004,"titles":13005,"content":13006,"level":449},"/api-reference/composables/utility-composables#edge-cases","Edge Cases",[359,12942],"\u003Cscript setup lang=\"ts\">\nconst { collectionWithCapitalSingular } = useFormatCollections()\n\n// Edge cases handled correctly\ncollectionWithCapitalSingular('')             // ''\ncollectionWithCapitalSingular('s')            // 's' (single char)\ncollectionWithCapitalSingular('news')         // 'new' (false plural)\ncollectionWithCapitalSingular('sheep')        // 'sheep' (irregular, stays same)\n\u003C/script>",{"id":13008,"title":44,"titles":13009,"content":13010,"level":449},"/api-reference/composables/utility-composables#best-practices",[359,12942],"DO: ✅ Use collectionWithCapitalSingular() for button labels✅ Use collectionWithCapital() for list headings✅ Use in notification messages for consistency✅ Rely on automatic layer prefix stripping DON'T: ❌ Manually strip prefixes or pluralize (use the composable)❌ Hardcode collection display names❌ Forget to handle empty strings",{"id":13012,"title":13013,"titles":13014,"content":13015,"level":449},"/api-reference/composables/utility-composables#integration-example","Integration Example",[359,12942],"Complete example showing all formatting methods: \u003Cscript setup lang=\"ts\">\nconst {\n  collectionWithCapital,\n  collectionWithCapitalSingular,\n  stripLayerPrefix,\n  camelToTitleCase,\n  toPascalCase\n} = useFormatCollections()\n\nconst collection = 'shopProducts'\n\nconst labels = {\n  listTitle: collectionWithCapital(collection),          // 'Products'\n  createButton: `New ${collectionWithCapitalSingular(collection)}`,  // 'New Product'\n  tableTitle: camelToTitleCase(collection),              // 'Shop Products'\n  componentName: `${toPascalCase(collection)}Form`,      // 'ShopProductsForm'\n  apiPath: stripLayerPrefix(collection)                  // 'products'\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>{{ labels.listTitle }}\u003C/h1>\n    \u003CUButton>{{ labels.createButton }}\u003C/UButton>\n    \u003CUTable :title=\"labels.tableTitle\" />\n  \u003C/div>\n\u003C/template>",{"id":13017,"title":12525,"titles":13018,"content":13019,"level":391},"/api-reference/composables/utility-composables#usecrouton",[359],"Global modal/form state management with nested form support, pagination, and automatic team context resolution.",{"id":13021,"title":5176,"titles":13022,"content":13023,"level":449},"/api-reference/composables/utility-composables#type-signature-1",[359,12525],"type CroutonAction = 'create' | 'update' | 'delete' | 'view' | null\ntype LoadingState = 'notLoading' | 'create_send' | 'update_send' | 'delete_send' | 'view_send' | 'create_open' | 'update_open' | 'delete_open' | 'view_open'\n\nfunction useCrouton(): {\n  // Modal state\n  showCrouton: ComputedRef\u003Cboolean>\n  loading: ComputedRef\u003CLoadingState>\n  action: ComputedRef\u003CCroutonAction>\n  items: ComputedRef\u003Cany[]>\n  activeItem: ComputedRef\u003Cany>\n  activeCollection: ComputedRef\u003Cstring | null>\n  croutonStates: Ref\u003CCroutonState[]>\n\n  // Modal actions\n  open: (action: CroutonAction, collection: string, ids?: string[], container?: 'slideover' | 'modal' | 'dialog' | 'inline', initialData?: any) => Promise\u003Cvoid>\n  close: (stateId?: string) => void\n  closeAll: () => void\n  removeState: (stateId: string) => void\n  reset: () => void\n\n  // Pagination\n  pagination: Ref\u003CPaginationMap>\n  setPagination: (collection: string, paginationData: Partial\u003CPaginationState>) => void\n  getPagination: (collection: string) => PaginationState\n  getDefaultPagination: (collection: string) => PaginationState\n}",{"id":13025,"title":6217,"titles":13026,"content":13027,"level":449},"/api-reference/composables/utility-composables#returns-1",[359,12525],"PropertyTypeDescriptionshowCroutonComputedRef\u003Cboolean>Whether any form is currently openloadingComputedRef\u003CLoadingState>Current loading state of the topmost formactionComputedRef\u003CCroutonAction>Current action being performed (create, update, delete, view)itemsComputedRef\u003Cany[]>Items array (used for delete operations with multiple IDs)activeItemComputedRef\u003Cany>Currently active item being edited/viewedactiveCollectionComputedRef\u003Cstring | null>Collection name of the topmost formcroutonStatesRef\u003CCroutonState[]>Array of all open form states (for nested forms)openFunctionOpen a new form modal/slideovercloseFunctionClose the current or specified formcloseAllFunctionClose all open formsremoveStateFunctionRemove a state from the array (called after animation)resetFunctionReset all form states (for navigation scenarios)paginationRef\u003CPaginationMap>Pagination state for all collectionssetPaginationFunctionUpdate pagination for a collectiongetPaginationFunctionGet pagination for a collectiongetDefaultPaginationFunctionGet default pagination settings",{"id":13029,"title":2370,"titles":13030,"content":2372,"level":449},"/api-reference/composables/utility-composables#opening-forms",[359,12525],{"id":13032,"title":2375,"titles":13033,"content":13034,"level":449},"/api-reference/composables/utility-composables#container-types",[359,12525],"// Slideover (default)\nopen('create', 'shopProducts', [], 'slideover')\n\n// Modal\nopen('create', 'shopProducts', [], 'modal')\n\n// Dialog\nopen('delete', 'shopProducts', ['id1'], 'dialog')",{"id":13036,"title":2400,"titles":13037,"content":13038,"level":449},"/api-reference/composables/utility-composables#nested-forms",[359,12525],"\u003Cscript setup lang=\"ts\">\n// Open product form\nopen('create', 'shopProducts')\n\n// From inside product form, open category form\nopen('create', 'shopCategories')  // Opens on top of product form\n\n// Supports up to 5 levels deep\n\u003C/script>",{"id":13040,"title":2405,"titles":13041,"content":2407,"level":449},"/api-reference/composables/utility-composables#programmatic-control",[359,12525],{"id":13043,"title":13044,"titles":13045,"content":13046,"level":449},"/api-reference/composables/utility-composables#with-initial-data","With Initial Data",[359,12525],"\u003Cscript setup lang=\"ts\">\nconst { open } = useCrouton()\n\n// Pre-populate form with default values\nopen('create', 'shopProducts', [], 'slideover', {\n  categoryId: 'default-category',\n  price: 0,\n  inStock: true\n})\n\u003C/script>",{"id":13048,"title":13049,"titles":13050,"content":13051,"level":391},"/api-reference/composables/utility-composables#usecroutonshortcuts","useCroutonShortcuts",[359],"Keyboard shortcuts for power-user CRUD operations with platform-aware bindings and visual hints.",{"id":13053,"title":5176,"titles":13054,"content":13055,"level":449},"/api-reference/composables/utility-composables#type-signature-2",[359,13049],"interface CroutonShortcutConfig {\n  create: string   // Open create form\n  save: string     // Submit current form\n  close: string    // Close current form/modal\n  delete: string   // Delete selected items\n  search: string   // Focus search input\n}\n\ninterface UseCroutonShortcutsOptions {\n  collection: string\n  shortcuts?: Partial\u003CCroutonShortcutConfig>\n  disabled?: MaybeRef\u003Cboolean>\n  selected?: Ref\u003Cstring[]>\n  searchRef?: Ref\u003CHTMLInputElement | null>\n  handlers?: {\n    onSave?: () => void | Promise\u003Cvoid>\n    onDelete?: (ids: string[]) => void | Promise\u003Cvoid>\n    onCreate?: () => void\n  }\n}\n\nfunction useCroutonShortcuts(options: UseCroutonShortcutsOptions): {\n  shortcuts: CroutonShortcutConfig\n  formatShortcut: (action: keyof CroutonShortcutConfig) => string\n  pause: () => void\n  resume: () => void\n  isActive: Ref\u003Cboolean>\n}",{"id":13057,"title":13058,"titles":13059,"content":13060,"level":449},"/api-reference/composables/utility-composables#default-shortcuts","Default Shortcuts",[359,13049],"ActionMacWindows/LinuxContextCreate⌘NCtrl+NWhen no form is openSave⌘SCtrl+SWhen form is openCloseEscapeEscapeCloses form/modalDelete⌘⌫Ctrl+BackspaceWith selectionSearch⌘K or /Ctrl+K or /Focus search input",{"id":13062,"title":4173,"titles":13063,"content":13064,"level":449},"/api-reference/composables/utility-composables#basic-usage-1",[359,13049],"\u003Cscript setup lang=\"ts\">\nconst selected = ref\u003Cstring[]>([])\nconst searchRef = ref\u003CHTMLInputElement | null>(null)\n\nconst { formatShortcut } = useCroutonShortcuts({\n  collection: 'shopProducts',\n  selected,\n  searchRef,\n  handlers: {\n    onSave: () => formRef.value?.submit(),\n    onDelete: async (ids) => {\n      if (confirm(`Delete ${ids.length} items?`)) {\n        await deleteItems(ids)\n        selected.value = []\n      }\n    }\n  }\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"flex justify-between mb-4\">\n    \u003CUInput ref=\"searchRef\" placeholder=\"Search...\" />\n\n    \u003CUButton @click=\"open('create', 'shopProducts')\">\n      New Product\n      \u003CCroutonShortcutHint :shortcut=\"formatShortcut('create')\" subtle />\n    \u003C/UButton>\n  \u003C/div>\n\n  \u003CCroutonCollection\n    v-model:selected=\"selected\"\n    collection=\"shopProducts\"\n    selectable\n  />\n\u003C/template>",{"id":13066,"title":13067,"titles":13068,"content":13069,"level":449},"/api-reference/composables/utility-composables#custom-shortcuts","Custom Shortcuts",[359,13049],"Override default key bindings: \u003Cscript setup lang=\"ts\">\nuseCroutonShortcuts({\n  collection: 'posts',\n  shortcuts: {\n    create: 'Meta+Shift+n',  // ⌘⇧N instead of ⌘N\n    search: 'Meta+f',        // ⌘F instead of ⌘K\n  }\n})\n\u003C/script>",{"id":13071,"title":13072,"titles":13073,"content":13074,"level":449},"/api-reference/composables/utility-composables#pauseresume","Pause/Resume",[359,13049],"Temporarily disable shortcuts (e.g., during custom modal): \u003Cscript setup lang=\"ts\">\nconst isCustomModalOpen = ref(false)\nconst { pause, resume, isActive } = useCroutonShortcuts({\n  collection: 'posts',\n  disabled: isCustomModalOpen, // Option 1: Reactive disable\n})\n\n// Option 2: Manual control\nconst openCustomModal = () => {\n  pause()\n  isCustomModalOpen.value = true\n}\n\nconst closeCustomModal = () => {\n  isCustomModalOpen.value = false\n  resume()\n}\n\u003C/script>",{"id":13076,"title":13077,"titles":13078,"content":13079,"level":449},"/api-reference/composables/utility-composables#display-shortcut-hints","Display Shortcut Hints",[359,13049],"Use the CroutonShortcutHint component: \u003Cscript setup lang=\"ts\">\nconst { formatShortcut } = useCroutonShortcuts({ collection: 'posts' })\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- Inline with button -->\n  \u003CUButton>\n    Save\n    \u003CCroutonShortcutHint :shortcut=\"formatShortcut('save')\" subtle />\n  \u003C/UButton>\n\n  \u003C!-- Standalone -->\n  \u003Cdiv class=\"flex items-center gap-2\">\n    \u003Cspan>Press\u003C/span>\n    \u003CCroutonShortcutHint :shortcut=\"formatShortcut('search')\" />\n    \u003Cspan>to search\u003C/span>\n  \u003C/div>\n\u003C/template>",{"id":13081,"title":13082,"titles":13083,"content":13084,"level":449},"/api-reference/composables/utility-composables#context-aware-behavior","Context-Aware Behavior",[359,13049],"Shortcuts are automatically disabled when: User is typing in an \u003Cinput> or \u003Ctextarea>User is in a contenteditable elementdisabled option is truepause() has been called \u003Cscript setup lang=\"ts\">\n// Shortcuts won't fire when user is typing in search\nconst searchRef = ref\u003CHTMLInputElement | null>(null)\n\nuseCroutonShortcuts({\n  collection: 'posts',\n  searchRef,\n  // ⌘N won't fire while typing in searchRef\n})\n\u003C/script>",{"id":13086,"title":13087,"titles":13088,"content":13089,"level":449},"/api-reference/composables/utility-composables#form-save-handler","Form Save Handler",[359,13049],"Connect to form submission: \u003Cscript setup lang=\"ts\">\nconst formRef = ref()\n\nuseCroutonShortcuts({\n  collection: 'posts',\n  handlers: {\n    onSave: () => {\n      // Trigger form submission on ⌘S\n      formRef.value?.submit()\n    }\n  }\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm ref=\"formRef\" @submit=\"handleSubmit\">\n    \u003C!-- form fields -->\n  \u003C/UForm>\n\u003C/template>",{"id":13091,"title":44,"titles":13092,"content":13093,"level":449},"/api-reference/composables/utility-composables#best-practices-1",[359,13049],"DO: ✅ Provide visual hints with CroutonShortcutHint for discoverability✅ Use formatShortcut() for platform-aware display✅ Connect onSave to form submission for ⌘S support✅ Connect onDelete with confirmation dialog✅ Disable during custom modals that capture focus DON'T: ❌ Override common browser shortcuts (⌘C, ⌘V, ⌘Z)❌ Forget to handle delete confirmation❌ Leave shortcuts active during text editing",{"id":13095,"title":36,"titles":13096,"content":13097,"level":449},"/api-reference/composables/utility-composables#troubleshooting",[359,13049],"IssueSolutionShortcuts not firingCheck isActive value, ensure not typing in input⌘S opens browser saveVerify onSave handler is providedDelete fires without selectionCheck selected ref is populatedWrong platform symbolsEnsure using formatShortcut() not hardcoded strings",{"id":13099,"title":13100,"titles":13101,"content":13102,"level":391},"/api-reference/composables/utility-composables#useentitytranslations","useEntityTranslations",[359],"Package Required: This composable is part of the @fyit/crouton-i18n package. It is not available in the core @fyit/crouton package. Install the i18n package to use this composable. Display translated field values with automatic locale fallback.",{"id":13104,"title":5176,"titles":13105,"content":5205,"level":449},"/api-reference/composables/utility-composables#type-signature-3",[359,13100],{"id":13107,"title":6217,"titles":13108,"content":13109,"level":449},"/api-reference/composables/utility-composables#returns-2",[359,13100],"t - Translation function with fallback",{"id":13111,"title":1608,"titles":13112,"content":13113,"level":449},"/api-reference/composables/utility-composables#usage",[359,13100],"\u003Cscript setup lang=\"ts\">\nconst { t } = useEntityTranslations()\nconst { locale } = useI18n()\n\nconst product = {\n  name: 'Product',\n  translations: {\n    en: { name: 'Product', description: 'English description' },\n    nl: { name: 'Product', description: 'Nederlandse beschrijving' },\n    fr: { name: 'Produit', description: 'Description française' }\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>{{ t(product, 'name') }}\u003C/h1>\n    \u003Cp>{{ t(product, 'description') }}\u003C/p>\n  \u003C/div>\n\u003C/template>",{"id":13115,"title":5212,"titles":13116,"content":13117,"level":449},"/api-reference/composables/utility-composables#fallback-behavior",[359,13100],"The t() function follows this priority: Current locale translation: entity.translations[currentLocale][field]English translation: entity.translations.en[field]Base field value: entity[field]Empty string: '' Example: // User's locale: 'nl' (Dutch)\nconst product = {\n  name: 'Product',\n  description: 'English description',\n  translations: {\n    en: { name: 'Product', description: 'English description' },\n    nl: { name: 'Product' } // Missing 'description'\n  }\n}\n\nt(product, 'name')        // 'Product' (Dutch available)\nt(product, 'description') // 'English description' (fallback to EN)\nt(product, 'price')       // '' (field doesn't exist)",{"id":13119,"title":1481,"titles":13120,"content":13121,"level":449},"/api-reference/composables/utility-composables#with-usecollectionquery",[359,13100],"Combine with data fetching for automatic translated displays: \u003Cscript setup lang=\"ts\">\nconst { locale } = useI18n()\nconst { items: products } = await useCollectionQuery('shopProducts', {\n  query: computed(() => ({\n    locale: locale.value  // Auto-refetch when locale changes\n  }))\n})\n\nconst { t } = useEntityTranslations()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-for=\"product in products\" :key=\"product.id\">\n    \u003Ch2>{{ t(product, 'name') }}\u003C/h2>\n    \u003Cp>{{ t(product, 'description') }}\u003C/p>\n  \u003C/div>\n\u003C/template> When the user switches languages, useCollectionQuery automatically refetches data and t() displays the correct translation.",{"id":13123,"title":13124,"titles":13125,"content":13126,"level":391},"/api-reference/composables/utility-composables#useassetupload","useAssetUpload",[359],"Package Required: This composable is part of the @fyit/crouton-assets package. It is not available in the core @fyit/crouton package. Install the assets package to use this composable. Programmatic asset upload with metadata tracking.",{"id":13128,"title":5176,"titles":13129,"content":13130,"level":449},"/api-reference/composables/utility-composables#type-signature-4",[359,13124],"function useAssetUpload(): {\n  uploadAsset: (\n    file: File,\n    metadata?: AssetMetadata,\n    collection?: string\n  ) => Promise\u003CUploadAssetResult>\n  uploadAssets: (\n    files: File[],\n    metadata?: AssetMetadata,\n    collection?: string\n  ) => Promise\u003CUploadAssetResult[]>\n  uploading: Readonly\u003CRef\u003Cboolean>>\n  error: Readonly\u003CRef\u003CError | null>>\n}\n\ninterface AssetMetadata {\n  alt?: string\n  filename?: string\n}\n\ninterface UploadAssetResult {\n  id: string\n  pathname: string\n  filename: string\n  contentType: string\n  size: number\n  alt?: string\n}",{"id":13132,"title":6217,"titles":13133,"content":13134,"level":449},"/api-reference/composables/utility-composables#returns-3",[359,13124],"uploadAsset - Upload single file with metadatauploadAssets - Upload multiple files in paralleluploading - Loading state (readonly)error - Error state (readonly)",{"id":13136,"title":4173,"titles":13137,"content":13138,"level":449},"/api-reference/composables/utility-composables#basic-usage-2",[359,13124],"\u003Cscript setup lang=\"ts\">\nconst { uploadAsset, uploading, error } = useAssetUpload()\n\nconst handleFileSelect = async (event: Event) => {\n  const target = event.target as HTMLInputElement\n  const file = target.files?.[0]\n  if (!file) return\n\n  try {\n    const asset = await uploadAsset(file, {\n      alt: 'Product image'\n    })\n\n    console.log('Uploaded:', asset.id)\n    console.log('URL:', `/images/${asset.pathname}`)\n  } catch (err) {\n    console.error('Upload failed:', error.value)\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cinput type=\"file\" @change=\"handleFileSelect\" />\n    \u003Cdiv v-if=\"uploading\">Uploading...\u003C/div>\n  \u003C/div>\n\u003C/template>",{"id":13140,"title":13141,"titles":13142,"content":13143,"level":449},"/api-reference/composables/utility-composables#with-custom-metadata","With Custom Metadata",[359,13124],"\u003Cscript setup lang=\"ts\">\nconst { uploadAsset } = useAssetUpload()\n\nconst uploadProductImage = async (file: File, productName: string) => {\n  const asset = await uploadAsset(file, {\n    alt: `${productName} product image`,\n    filename: `${productName.toLowerCase().replace(/\\s+/g, '-')}.jpg`\n  })\n\n  return asset.id\n}\n\u003C/script>",{"id":13145,"title":13146,"titles":13147,"content":13148,"level":449},"/api-reference/composables/utility-composables#multiple-file-upload","Multiple File Upload",[359,13124],"\u003Cscript setup lang=\"ts\">\nconst { uploadAssets, uploading } = useAssetUpload()\n\nconst handleMultipleFiles = async (event: Event) => {\n  const target = event.target as HTMLInputElement\n  const files = Array.from(target.files || [])\n\n  const assets = await uploadAssets(files, {\n    alt: 'Gallery image'\n  })\n\n  console.log(`Uploaded ${assets.length} files`)\n  return assets.map(a => a.id)\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cinput type=\"file\" multiple @change=\"handleMultipleFiles\" />\n  \u003Cdiv v-if=\"uploading\">Uploading files...\u003C/div>\n\u003C/template>",{"id":13150,"title":13151,"titles":13152,"content":13153,"level":449},"/api-reference/composables/utility-composables#custom-collection","Custom Collection",[359,13124],"By default, uploads go to the assets collection. You can specify a different collection: \u003Cscript setup lang=\"ts\">\nconst { uploadAsset } = useAssetUpload()\n\n// Upload to custom 'productImages' collection\nconst asset = await uploadAsset(file, { alt: 'Product' }, 'productImages')\n\u003C/script>",{"id":13155,"title":2522,"titles":13156,"content":13157,"level":449},"/api-reference/composables/utility-composables#error-handling",[359,13124],"\u003Cscript setup lang=\"ts\">\nconst { uploadAsset, uploading, error } = useAssetUpload()\nconst toast = useToast()\n\nconst handleUpload = async (file: File) => {\n  try {\n    const asset = await uploadAsset(file, {\n      alt: 'User uploaded image'\n    })\n\n    toast.add({\n      title: 'Upload successful',\n      description: `File ${asset.filename} uploaded`,\n      color: 'green'\n    })\n\n    return asset\n  } catch (err) {\n    toast.add({\n      title: 'Upload failed',\n      description: error.value?.message || 'Unknown error',\n      color: 'red'\n    })\n  }\n}\n\u003C/script>",{"id":13159,"title":12213,"titles":13160,"content":13161,"level":449},"/api-reference/composables/utility-composables#integration-with-forms",[359,13124],"Combine with form state for seamless asset management: \u003Cscript setup lang=\"ts\">\nconst { uploadAsset } = useAssetUpload()\nconst state = ref({\n  name: '',\n  imageId: ''\n})\n\nconst handleImageUpload = async (file: File) => {\n  const asset = await uploadAsset(file, {\n    alt: state.value.name || 'Product image'\n  })\n\n  // Assign asset ID to form\n  state.value.imageId = asset.id\n}\n\nconst handleSubmit = async () => {\n  // Create product with asset reference\n  await $fetch('/api/teams/123/shopProducts', {\n    method: 'POST',\n    body: state.value\n  })\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm :state=\"state\" @submit=\"handleSubmit\">\n    \u003CUFormField label=\"Product Name\" name=\"name\">\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n\n    \u003CUFormField label=\"Product Image\" name=\"imageId\">\n      \u003CCroutonAssetPicker v-model=\"state.imageId\" />\n    \u003C/UFormField>\n\n    \u003CUButton type=\"submit\">Create Product\u003C/UButton>\n  \u003C/UForm>\n\u003C/template>",{"id":13163,"title":12678,"titles":13164,"content":13165,"level":449},"/api-reference/composables/utility-composables#when-to-use",[359,13124],"Use useAssetUpload() when: Building custom upload UIProgrammatic file uploadsDrag-and-drop interfacesBatch processing filesCustom upload workflows Use CroutonAssetUploader component when: Simple form-based uploadsUsing generated CRUD formsStandard upload modal needed Use CroutonImageUpload component when: Quick one-off uploadsStoring URLs directly (not using asset library)Simple file picker needed",{"id":13167,"title":13168,"titles":13169,"content":13170,"level":391},"/api-reference/composables/utility-composables#useteamcontext","useTeamContext",[359],"Resolve team identifiers for multi-tenancy API path construction with automatic fallback strategies.",{"id":13172,"title":5176,"titles":13173,"content":13174,"level":449},"/api-reference/composables/utility-composables#type-signature-5",[359,13168],"function useTeamContext(): {\n  getTeamId: () => string | undefined\n  getTeamSlug: () => string | undefined\n}",{"id":13176,"title":6217,"titles":13177,"content":13178,"level":449},"/api-reference/composables/utility-composables#returns-4",[359,13168],"PropertyTypeDescriptiongetTeamId() => string | undefinedGet team ID for API calls (preferred identifier)getTeamSlug() => string | undefinedGet team slug for display purposes",{"id":13180,"title":1635,"titles":13181,"content":13182,"level":449},"/api-reference/composables/utility-composables#how-it-works",[359,13168],"Smart Team Resolution Strategy: Preferred: Try useTeam() composable → returns currentTeam.id (team ID)Fallback: Use route.params.team (might be slug or ID from URL) This allows Crouton to work seamlessly with multi-tenant apps that use team slugs in routes but need team IDs for API calls.",{"id":13184,"title":4173,"titles":13185,"content":13186,"level":449},"/api-reference/composables/utility-composables#basic-usage-3",[359,13168],"\u003Cscript setup lang=\"ts\">\nconst { getTeamId } = useTeamContext()\n\n// Get team identifier for API paths\nconst teamId = getTeamId()\nconst apiPath = `/api/teams/${teamId}/members`\n\nconsole.log('Team ID:', teamId)\n// → \"team_abc123\" (from useTeam() if available)\n// → \"my-team-slug\" (from route params as fallback)\n\u003C/script>",{"id":13188,"title":13189,"titles":13190,"content":13191,"level":449},"/api-reference/composables/utility-composables#with-data-fetching","With Data Fetching",[359,13168],"Used internally by useCollectionQuery and useCollectionMutation: \u003Cscript setup lang=\"ts\">\n// useCollectionQuery uses useTeamContext internally\nconst { items, pending } = await useCollectionQuery('shopProducts')\n\n// Automatically resolves to correct endpoint:\n// - /api/teams/{teamId}/shopProducts (if useTeam() available)\n// - /api/teams/{slug}/shopProducts (fallback to route param)\n\u003C/script>",{"id":13193,"title":13194,"titles":13195,"content":13196,"level":449},"/api-reference/composables/utility-composables#get-team-slug-for-display","Get Team Slug for Display",[359,13168],"\u003Cscript setup lang=\"ts\">\nconst { getTeamId, getTeamSlug } = useTeamContext()\n\nconst teamId = getTeamId()    // \"team_abc123\"\nconst teamSlug = getTeamSlug() // \"my-team\"\n\n// Use ID for API calls, slug for display\nconst apiPath = `/api/teams/${teamId}/settings`\nconst displayName = `Team: ${teamSlug}`\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>{{ displayName }}\u003C/h1>\n    \u003C!-- Shows: \"Team: my-team\" -->\n  \u003C/div>\n\u003C/template>",{"id":13198,"title":13199,"titles":13200,"content":13201,"level":449},"/api-reference/composables/utility-composables#multi-tenancy-patterns","Multi-Tenancy Patterns",[359,13168],"Pattern 1: App with useTeam() composable (recommended) // composables/useTeam.ts (in your app)\nexport function useTeam() {\n  return {\n    currentTeam: computed(() => ({\n      id: 'team_abc123',\n      slug: 'my-team',\n      name: 'My Team'\n    }))\n  }\n}\n\n// Crouton automatically detects this composable\nconst { getTeamId } = useTeamContext()\nconst teamId = getTeamId() // → \"team_abc123\" (preferred!) Pattern 2: Route-based team context (fallback) \u003Cscript setup lang=\"ts\">\n// Route: /dashboard/:team/products\n// URL: /dashboard/my-team/products\n\nconst { getTeamId } = useTeamContext()\nconst teamId = getTeamId() // → \"my-team\" (from route.params.team)\n\u003C/script>",{"id":13203,"title":13204,"titles":13205,"content":13206,"level":449},"/api-reference/composables/utility-composables#super-admin-routes","Super-Admin Routes",[359,13168],"Handles super-admin contexts automatically: \u003Cscript setup lang=\"ts\">\n// Route: /super-admin/users\nconst route = useRoute()\nconst { getTeamId } = useTeamContext()\n\nif (route.path.includes('/super-admin/')) {\n  // useCollectionQuery detects super-admin context\n  // → Uses /api/super-admin/users instead of /api/teams/{id}/users\n}\n\u003C/script>",{"id":13208,"title":13209,"titles":13210,"content":13211,"level":449},"/api-reference/composables/utility-composables#building-custom-api-paths","Building Custom API Paths",[359,13168],"\u003Cscript setup lang=\"ts\">\nconst { getTeamId } = useTeamContext()\n\nconst buildTeamApiPath = (resource: string): string => {\n  const teamId = getTeamId()\n  \n  if (!teamId) {\n    throw new Error('Team context not available')\n  }\n  \n  return `/api/teams/${teamId}/${resource}`\n}\n\n// Usage\nconst membersPath = buildTeamApiPath('members')\n// → \"/api/teams/team_abc123/members\"\n\nconst settingsPath = buildTeamApiPath('settings')\n// → \"/api/teams/team_abc123/settings\"\n\u003C/script>",{"id":13213,"title":2522,"titles":13214,"content":13215,"level":449},"/api-reference/composables/utility-composables#error-handling-1",[359,13168],"\u003Cscript setup lang=\"ts\">\nconst { getTeamId } = useTeamContext()\n\nconst fetchTeamData = async () => {\n  const teamId = getTeamId()\n  \n  if (!teamId) {\n    console.error('Team context not available', {\n      routePath: route.path,\n      routeParams: route.params\n    })\n    \n    // Handle missing team context\n    navigateTo('/select-team')\n    return\n  }\n  \n  // Proceed with API call\n  const data = await $fetch(`/api/teams/${teamId}/data`)\n}\n\u003C/script>",{"id":13217,"title":13218,"titles":13219,"content":13220,"level":449},"/api-reference/composables/utility-composables#integration-with-collection-queries","Integration with Collection Queries",[359,13168],"Crouton composables automatically use useTeamContext: \u003Cscript setup lang=\"ts\">\n// These automatically resolve team context:\nconst { items } = await useCollectionQuery('shopProducts')\nconst { create } = useCollectionMutation('shopProducts')\n\n// Internally calls useTeamContext() to build:\n// → /api/teams/{resolvedTeamId}/shopProducts\n\u003C/script>",{"id":13222,"title":13223,"titles":13224,"content":13225,"level":449},"/api-reference/composables/utility-composables#team-context-switching","Team Context Switching",[359,13168],"When users switch teams, data automatically refetches: \u003Cscript setup lang=\"ts\">\nconst { currentTeam, switchTeam } = useTeam() // Your app's composable\nconst { items, refresh } = await useCollectionQuery('shopProducts')\n\nconst handleTeamSwitch = async (newTeamId: string) => {\n  await switchTeam(newTeamId)\n  \n  // Manually refresh if needed (or rely on watch)\n  await refresh()\n  \n  // All API calls now use new team context\n}\n\u003C/script>",{"id":13227,"title":13228,"titles":13229,"content":13230,"level":449},"/api-reference/composables/utility-composables#debug-team-resolution","Debug Team Resolution",[359,13168],"\u003Cscript setup lang=\"ts\">\nconst { getTeamId, getTeamSlug } = useTeamContext()\nconst route = useRoute()\n\n// Debug team resolution\nconsole.log('Team Resolution:', {\n  teamId: getTeamId(),\n  teamSlug: getTeamSlug(),\n  routeParam: route.params.team,\n  routePath: route.path\n})\n\n// Example output:\n// {\n//   teamId: \"team_abc123\",\n//   teamSlug: \"my-team\",\n//   routeParam: \"my-team\",\n//   routePath: \"/dashboard/my-team/products\"\n// }\n\u003C/script>",{"id":13232,"title":44,"titles":13233,"content":13234,"level":449},"/api-reference/composables/utility-composables#best-practices-2",[359,13168],"DO: ✅ Implement useTeam() in your app for proper ID resolution✅ Use getTeamId() for API calls (preferred identifier)✅ Use getTeamSlug() for display purposes✅ Check for undefined before making API calls✅ Use consistent team param naming in routes (:team) DON'T: ❌ Manually construct team API paths (use composables)❌ Assume team context is always available❌ Mix team IDs and slugs in API calls❌ Forget to validate team context before API operations",{"id":13236,"title":36,"titles":13237,"content":13238,"level":449},"/api-reference/composables/utility-composables#troubleshooting-1",[359,13168],"IssueSolutiongetTeamId() returns undefinedCheck that route has :team param or implement useTeam()Using slug instead of IDImplement useTeam() composable to return team ID404 errors on API callsVerify team ID resolution matches server expectationsTeam context not switchingEnsure currentTeam is reactive in useTeam()",{"id":13240,"title":13241,"titles":13242,"content":13243,"level":391},"/api-reference/composables/utility-composables#useusers","useUsers",[359],"External collection connector for integrating user management systems with Crouton's reference system.",{"id":13245,"title":5176,"titles":13246,"content":13247,"level":449},"/api-reference/composables/utility-composables#type-signature-6",[359,13241],"interface UserSchema {\n  id: string\n  title: string\n  email?: string\n  avatarUrl?: string\n  role?: string\n}\n\nfunction useUsers(): ExternalCollectionConfig",{"id":13249,"title":6217,"titles":13250,"content":13251,"level":449},"/api-reference/composables/utility-composables#returns-5",[359,13241],"External collection configuration object for registering in app.config.ts.",{"id":13253,"title":13254,"titles":13255,"content":13256,"level":449},"/api-reference/composables/utility-composables#purpose","Purpose",[359,13241],"This composable provides a reference implementation for connecting external user systems (e.g., authentication providers, auth databases) to Crouton's reference fields. Use cases: Connect auth system users to Crouton formsEnable user selection in reference fieldsTrack createdBy and updatedBy fieldsDisplay user info in admin panels",{"id":13258,"title":13259,"titles":13260,"content":13261,"level":449},"/api-reference/composables/utility-composables#setup-instructions","Setup Instructions",[359,13241],"Step 1: Copy the composable to your project // app/composables/useUsers.ts\nimport { z } from 'zod'\nimport { defineExternalCollection } from '@fyit/crouton'\n\nconst userSchema = z.object({\n  id: z.string(),\n  title: z.string(), // Required for CroutonReferenceSelect\n  email: z.string().optional(),\n  avatarUrl: z.string().optional(),\n  role: z.string().optional()\n})\n\nexport const usersConfig = defineExternalCollection({\n  name: 'users',\n  schema: userSchema,\n  meta: {\n    label: 'Users',\n    description: 'External user collection from auth system'\n  }\n})\n\nexport const useUsers = () => usersConfig Step 2: Create the API endpoint // server/api/teams/[id]/users/index.get.ts\nimport { getActiveTeamMembers } from '~/server/database/queries/teams'\nimport { validateTeamOwnership } from '~/server/utils/teamValidation'\n\n// createExternalCollectionHandler is auto-imported from nuxt-crouton\nexport default createExternalCollectionHandler(\n  async (event) => {\n    const teamId = getRouterParam(event, 'id')\n    await validateTeamOwnership(event, teamId!)\n    return await getActiveTeamMembers(teamId!)\n  },\n  (member) => ({\n    id: member.userId,\n    title: member.name,\n    email: member.email,\n    avatarUrl: member.avatarUrl,\n    role: member.role\n  })\n) Step 3: Register in app.config.ts // app.config.ts\nimport { usersConfig } from './composables/useUsers'\n\nexport default defineAppConfig({\n  croutonCollections: {\n    users: usersConfig,\n    // ... other collections\n  }\n}) Step 4: Use :users prefix in collection schemas // layers/shop/collections/products.collection.json\n{\n  \"schema\": {\n    \"createdBy\": {\n      \"type\": \"string\",\n      \"refTarget\": \":users\",\n      \"meta\": {\n        \"label\": \"Created By\",\n        \"readOnly\": true\n      }\n    },\n    \"updatedBy\": {\n      \"type\": \"string\",\n      \"refTarget\": \":users\",\n      \"meta\": {\n        \"label\": \"Updated By\"\n      }\n    }\n  }\n}",{"id":13263,"title":13264,"titles":13265,"content":13266,"level":449},"/api-reference/composables/utility-composables#usage-in-forms","Usage in Forms",[359,13241],"Once registered, users appear in reference selects automatically: \u003Cscript setup lang=\"ts\">\nconst state = ref({\n  title: 'New Product',\n  assignedTo: '', // User ID from `:users` collection\n  reviewedBy: ''\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm :state=\"state\">\n    \u003C!-- Reference select auto-generated by Crouton -->\n    \u003CCroutonReferenceSelect\n      v-model=\"state.assignedTo\"\n      collection=\"users\"\n      label=\"Assigned To\"\n    />\n    \n    \u003CCroutonReferenceSelect\n      v-model=\"state.reviewedBy\"\n      collection=\"users\"\n      label=\"Reviewed By\"\n    />\n  \u003C/UForm>\n\u003C/template>",{"id":13268,"title":13269,"titles":13270,"content":13271,"level":449},"/api-reference/composables/utility-composables#auto-populate-user-fields","Auto-populate User Fields",[359,13241],"Pattern: Auto-populate createdBy and updatedBy server-side // server/api/teams/[id]/products.post.ts\nexport default defineEventHandler(async (event) => {\n  const session = await getUserSession(event)\n  const body = await readBody(event)\n  \n  // Auto-populate user fields\n  body.createdBy = session.user.id\n  body.updatedBy = session.user.id\n  body.createdAt = new Date().toISOString()\n  body.updatedAt = new Date().toISOString()\n  \n  return await createProduct(body)\n})\n\n// server/api/teams/[id]/products/[productId].patch.ts\nexport default defineEventHandler(async (event) => {\n  const session = await getUserSession(event)\n  const body = await readBody(event)\n  \n  // Update tracking\n  body.updatedBy = session.user.id\n  body.updatedAt = new Date().toISOString()\n  \n  return await updateProduct(event.context.params.productId, body)\n})",{"id":13273,"title":13274,"titles":13275,"content":13276,"level":449},"/api-reference/composables/utility-composables#display-user-info","Display User Info",[359,13241],"Pattern: Display user cards in tables \u003Cscript setup lang=\"ts\">\nconst { items } = await useCollectionQuery('shopProducts')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonTable :items=\"items\" collection=\"shopProducts\">\n    \u003C!-- Auto-renders user info for createdBy field -->\n    \u003Ctemplate #createdBy-cell=\"{ row }\">\n      \u003CCroutonUsersCardMini\n        v-if=\"row.original.createdByUser\"\n        :item=\"row.original.createdByUser\"\n        :name=\"true\"\n      />\n    \u003C/template>\n    \n    \u003Ctemplate #updatedBy-cell=\"{ row }\">\n      \u003CCroutonUsersCardMini\n        v-if=\"row.original.updatedByUser\"\n        :item=\"row.original.updatedByUser\"\n        :name=\"true\"\n      />\n    \u003C/template>\n  \u003C/CroutonTable>\n\u003C/template>",{"id":13278,"title":13279,"titles":13280,"content":13281,"level":449},"/api-reference/composables/utility-composables#schema-customization","Schema Customization",[359,13241],"For the base userSchema definition, see the Setup Instructions section above. Extend the user schema with custom fields: // app/composables/useUsers.ts\nconst userSchema = z.object({\n  // ... base fields (id, title, email, avatarUrl, role)\n\n  // Custom fields\n  department: z.string().optional(),\n  jobTitle: z.string().optional(),\n  isActive: z.boolean().optional(),\n  permissions: z.array(z.string()).optional()\n})",{"id":13283,"title":13284,"titles":13285,"content":13286,"level":449},"/api-reference/composables/utility-composables#transform-external-data","Transform External Data",[359,13241],"If your auth system uses different field names: // server/api/teams/[id]/users/index.get.ts\nexport default createExternalCollectionHandler(\n  async (event) => {\n    const teamId = getRouterParam(event, 'id')\n    return await fetchAuthSystemUsers(teamId)\n  },\n  (authUser) => ({\n    // Transform to Crouton format\n    id: authUser.user_id,           // user_id → id\n    title: authUser.full_name,      // full_name → title\n    email: authUser.email_address,  // email_address → email\n    avatarUrl: authUser.profile_pic, // profile_pic → avatarUrl\n    role: authUser.user_role        // user_role → role\n  })\n)",{"id":13288,"title":13289,"titles":13290,"content":13291,"level":449},"/api-reference/composables/utility-composables#read-only-collection","Read-Only Collection",[359,13241],"Users collection is read-only by default (no create/edit/delete buttons): export const usersConfig = defineExternalCollection({\n  name: 'users',\n  schema: userSchema,\n  readonly: true, // Default behavior (can be omitted)\n  meta: {\n    label: 'Users',\n    description: 'External user collection'\n  }\n}) To make it editable (if managing users in Crouton): readonly: false // Show edit/delete buttons in CardMini",{"id":13293,"title":13294,"titles":13295,"content":13296,"level":449},"/api-reference/composables/utility-composables#fetch-strategy","Fetch Strategy",[359,13241],"Users collection uses query-based fetching by default: export const usersConfig = defineExternalCollection({\n  name: 'users',\n  schema: userSchema,\n  fetchStrategy: 'query' // Default: uses ?ids= for single items\n}) If your API supports RESTful fetching: fetchStrategy: 'restful' // Uses /users/{id} for single items",{"id":13298,"title":13299,"titles":13300,"content":13301,"level":449},"/api-reference/composables/utility-composables#filter-users-by-role","Filter Users by Role",[359,13241],"// server/api/teams/[id]/users/index.get.ts\nexport default createExternalCollectionHandler(\n  async (event) => {\n    const teamId = getRouterParam(event, 'id')\n    const query = getQuery(event)\n    const role = query.role as string | undefined\n    \n    const users = await getActiveTeamMembers(teamId!)\n    \n    // Filter by role if specified\n    if (role) {\n      return users.filter(u => u.role === role)\n    }\n    \n    return users\n  },\n  (member) => ({\n    id: member.userId,\n    title: member.name,\n    email: member.email,\n    role: member.role\n  })\n) Usage: \u003Cscript setup lang=\"ts\">\n// Fetch only admin users\nconst { items: admins } = await useCollectionQuery('users', {\n  query: computed(() => ({ role: 'admin' }))\n})\n\u003C/script>",{"id":13303,"title":13304,"titles":13305,"content":13306,"level":449},"/api-reference/composables/utility-composables#integration-with-audit-trails","Integration with Audit Trails",[359,13241],"Common pattern for tracking who created/updated records: // server/database/schemas/products.ts\nexport const products = pgTable('products', {\n  id: text('id').primaryKey(),\n  name: text('name').notNull(),\n  price: decimal('price').notNull(),\n  \n  // Audit fields\n  createdAt: timestamp('created_at').defaultNow(),\n  createdBy: text('created_by').references(() => users.id),\n  updatedAt: timestamp('updated_at').defaultNow(),\n  updatedBy: text('updated_by').references(() => users.id)\n})",{"id":13308,"title":44,"titles":13309,"content":13310,"level":449},"/api-reference/composables/utility-composables#best-practices-3",[359,13241],"DO: ✅ Use :users prefix for external user references✅ Transform auth system data to Crouton format (id, title)✅ Auto-populate user fields server-side✅ Keep readonly: true unless managing users in Crouton✅ Include avatarUrl for better UX DON'T: ❌ Store sensitive data (passwords, tokens) in user schema❌ Expose all auth system fields to client❌ Forget to validate team ownership in API endpoints❌ Mix user IDs from different auth systems",{"id":13312,"title":36,"titles":13313,"content":13314,"level":449},"/api-reference/composables/utility-composables#troubleshooting-2",[359,13241],"IssueSolutionUsers not appearing in selectCheck API endpoint returns { id, title } format404 on user fetchVerify fetchStrategy matches API implementationDuplicate user collectionsUse :users prefix to avoid collision with Crouton-managed usersMissing title fieldTransform name or full_name to title in handler",{"id":13316,"title":13317,"titles":13318,"content":13319,"level":391},"/api-reference/composables/utility-composables#usecroutonerror","useCroutonError",[359],"Global error checking and user feedback system for blocking invalid operations.",{"id":13321,"title":5176,"titles":13322,"content":13323,"level":449},"/api-reference/composables/utility-composables#type-signature-7",[359,13317],"function useCroutonError(): {\n  foundErrors: () => boolean\n  activeToast: Ref\u003Cboolean>\n  toastVibration: Ref\u003Cboolean>\n}",{"id":13325,"title":6217,"titles":13326,"content":13327,"level":449},"/api-reference/composables/utility-composables#returns-6",[359,13317],"PropertyTypeDescriptionfoundErrors() => booleanCheck for blocking errors (network, auth)activeToastRef\u003Cboolean>Whether error toast is currently displayedtoastVibrationRef\u003Cboolean>Vibration state for duplicate error prevention",{"id":13329,"title":1635,"titles":13330,"content":13331,"level":449},"/api-reference/composables/utility-composables#how-it-works-1",[359,13317],"Error Checking Sequence: Network Status: Check useNetwork().isOnlineAuthentication: Check useUserSession().loggedInToast Display: Show error toast if issues foundDuplicate Prevention: Vibrate toast instead of showing multiple",{"id":13333,"title":4173,"titles":13334,"content":13335,"level":449},"/api-reference/composables/utility-composables#basic-usage-4",[359,13317],"\u003Cscript setup lang=\"ts\">\nconst { foundErrors } = useCroutonError()\n\nconst handleSave = async () => {\n  // Block operation if errors detected\n  if (foundErrors()) {\n    console.log('Operation blocked due to errors')\n    return\n  }\n  \n  // Proceed with save\n  await saveTodo()\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUButton @click=\"handleSave\">\n    Save Changes\n  \u003C/UButton>\n\u003C/template>",{"id":13337,"title":13338,"titles":13339,"content":13340,"level":449},"/api-reference/composables/utility-composables#integrated-with-usecrouton","Integrated with useCrouton",[359,13317],"useCrouton() automatically checks for errors before opening forms: \u003Cscript setup lang=\"ts\">\nconst { open } = useCrouton()\n\n// This internally calls foundErrors() before opening\nconst handleCreate = () => {\n  open('create', 'shopProducts')\n  \n  // If user is offline → shows error toast, blocks modal\n  // If user is not logged in → shows error toast, blocks modal\n  // Otherwise → opens create form\n}\n\u003C/script>",{"id":13342,"title":13343,"titles":13344,"content":13345,"level":449},"/api-reference/composables/utility-composables#manual-error-checking","Manual Error Checking",[359,13317],"\u003Cscript setup lang=\"ts\">\nconst { foundErrors } = useCroutonError()\n\nconst performCriticalOperation = async () => {\n  // Check before expensive operations\n  if (foundErrors()) {\n    console.log('Aborting critical operation')\n    return { success: false, error: 'Precondition failed' }\n  }\n  \n  // Proceed with operation\n  const result = await $fetch('/api/critical-action', {\n    method: 'POST'\n  })\n  \n  return { success: true, result }\n}\n\u003C/script>",{"id":13347,"title":13348,"titles":13349,"content":13350,"level":449},"/api-reference/composables/utility-composables#error-types-checked","Error Types Checked",[359,13317],"1. Network Connectivity // Checks: useNetwork().isOnline\nif (!isOnline) {\n  // Shows toast: \"Check your connection status.\"\n  return true\n} 2. Authentication Status // Checks: useUserSession().loggedIn\nif (!loggedIn) {\n  // Shows toast: \"You are not logged in.\"\n  return true\n}",{"id":13352,"title":13353,"titles":13354,"content":13355,"level":449},"/api-reference/composables/utility-composables#toast-duplication-prevention","Toast Duplication Prevention",[359,13317],"The composable prevents showing multiple error toasts: \u003Cscript setup lang=\"ts\">\nconst { foundErrors, activeToast, toastVibration } = useCroutonError()\n\n// First call: Shows toast\nfoundErrors() // → activeToast = true, shows error\n\n// Second call (within 500ms): Vibrates toast instead\nfoundErrors() // → toastVibration = true, no new toast\n\n// After 500ms: activeToast resets, can show new toast\n\u003C/script>",{"id":13357,"title":13358,"titles":13359,"content":13360,"level":449},"/api-reference/composables/utility-composables#custom-error-handling","Custom Error Handling",[359,13317],"Extend error checking for custom conditions: \u003Cscript setup lang=\"ts\">\nconst { foundErrors } = useCroutonError()\n\nconst checkCustomErrors = (): boolean => {\n  // Check standard errors first\n  if (foundErrors()) {\n    return true\n  }\n  \n  // Check custom conditions\n  const userPermissions = useUserPermissions()\n  if (!userPermissions.canCreateProducts) {\n    const toast = useToast()\n    toast.add({\n      title: 'Permission denied',\n      description: 'You do not have permission to create products',\n      color: 'error'\n    })\n    return true\n  }\n  \n  // Check team subscription\n  const { currentTeam } = useTeam()\n  if (!currentTeam.value?.isSubscribed) {\n    const toast = useToast()\n    toast.add({\n      title: 'Subscription required',\n      description: 'Upgrade your plan to create more products',\n      color: 'error'\n    })\n    return true\n  }\n  \n  return false\n}\n\u003C/script>",{"id":13362,"title":12213,"titles":13363,"content":13364,"level":449},"/api-reference/composables/utility-composables#integration-with-forms-1",[359,13317],"\u003Cscript setup lang=\"ts\">\nconst { foundErrors } = useCroutonError()\nconst { create } = useCollectionMutation('shopProducts')\n\nconst state = ref({\n  name: '',\n  price: 0\n})\n\nconst handleSubmit = async () => {\n  // Block submit if errors exist\n  if (foundErrors()) {\n    return\n  }\n  \n  try {\n    await create(state.value)\n    navigateTo('/products')\n  } catch (error) {\n    console.error('Create failed:', error)\n  }\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm :state=\"state\" @submit=\"handleSubmit\">\n    \u003CUFormField label=\"Name\" name=\"name\">\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n    \n    \u003CUButton type=\"submit\">\n      Create Product\n    \u003C/UButton>\n  \u003C/UForm>\n\u003C/template>",{"id":13366,"title":13367,"titles":13368,"content":13369,"level":449},"/api-reference/composables/utility-composables#batch-operations","Batch Operations",[359,13317],"\u003Cscript setup lang=\"ts\">\nconst { foundErrors } = useCroutonError()\n\nconst bulkDelete = async (ids: string[]) => {\n  // Check once before batch operation\n  if (foundErrors()) {\n    return { success: false, deleted: 0 }\n  }\n  \n  let deleted = 0\n  for (const id of ids) {\n    try {\n      await $fetch(`/api/items/${id}`, { method: 'DELETE' })\n      deleted++\n    } catch (error) {\n      console.error(`Failed to delete ${id}:`, error)\n    }\n  }\n  \n  return { success: true, deleted }\n}\n\u003C/script>",{"id":13371,"title":13372,"titles":13373,"content":13374,"level":449},"/api-reference/composables/utility-composables#visual-toast-vibration","Visual Toast Vibration",[359,13317],"The vibration state can be used for UI feedback: \u003Cscript setup lang=\"ts\">\nconst { activeToast, toastVibration } = useCroutonError()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv\n    v-if=\"activeToast\"\n    class=\"error-toast\"\n    :class=\"{ 'vibrate': toastVibration }\"\n  >\n    Error notification\n  \u003C/div>\n\u003C/template>\n\n\u003Cstyle scoped>\n.vibrate {\n  animation: shake 0.5s;\n}\n\n@keyframes shake {\n  0%, 100% { transform: translateX(0); }\n  25% { transform: translateX(-10px); }\n  75% { transform: translateX(10px); }\n}\n\u003C/style>",{"id":13376,"title":13377,"titles":13378,"content":13379,"level":449},"/api-reference/composables/utility-composables#debug-error-checking","Debug Error Checking",[359,13317],"\u003Cscript setup lang=\"ts\">\nconst { foundErrors } = useCroutonError()\n\nconst debugErrors = () => {\n  const network = useNetwork()\n  const session = useUserSession()\n  \n  console.log('Error Check Debug:', {\n    isOnline: network.isOnline.value,\n    loggedIn: session.loggedIn?.value,\n    willBlock: foundErrors()\n  })\n}\n\n// Call before critical operations\ndebugErrors()\n\u003C/script>",{"id":13381,"title":44,"titles":13382,"content":13383,"level":449},"/api-reference/composables/utility-composables#best-practices-4",[359,13317],"DO: ✅ Call foundErrors() before data mutations✅ Call foundErrors() before expensive operations✅ Rely on automatic checking in useCrouton()✅ Extend with custom error conditions for your app DON'T: ❌ Bypass error checking for \"quick\" operations❌ Show custom error toasts while activeToast is true❌ Forget to return early when foundErrors() is true❌ Use for validation errors (use form validation instead)",{"id":13385,"title":12678,"titles":13386,"content":13387,"level":449},"/api-reference/composables/utility-composables#when-to-use-1",[359,13317],"ScenarioUse foundErrors()Opening CRUD forms✅ Automatic (via useCrouton)Manual mutations✅ Call before mutationBatch operations✅ Call once before batchForm validation❌ Use Zod schemas insteadAPI errors (500, 404)❌ Use try/catch insteadPermission checks⚠️ Extend with custom checks",{"id":13389,"title":36,"titles":13390,"content":13391,"level":449},"/api-reference/composables/utility-composables#troubleshooting-3",[359,13317],"IssueSolutionToast not showingCheck useToast() is available and Nuxt UI configuredMultiple toasts appearingWait for activeToast to reset (automatic after close)False positivesDebug with console logs, check useNetwork() and useUserSession()Custom errors not blockedExtend checkCustomErrors() pattern above",{"id":13393,"title":13394,"titles":13395,"content":13396,"level":391},"/api-reference/composables/utility-composables#uset","useT",[359],"Stub vs Full Implementation: The core @fyit/crouton package provides a stub implementation of useT() with English fallbacks for common UI strings. For full i18n support (database-backed translations, multiple locales, translation management UI), install @fyit/crouton-i18n. Translation helper composable with fallback support for UI strings and content.",{"id":13398,"title":5176,"titles":13399,"content":13400,"level":449},"/api-reference/composables/utility-composables#type-signature-8",[359,13394],"function useT(): {\n  t: (key: string, options?: any) => string\n  tString: (key: string, options?: any) => string\n  tContent: (entity: any, field: string, preferredLocale?: string) => string\n  tInfo: (key: string, options?: any) => TranslationInfo\n  hasTranslation: (key: string) => boolean\n  getAvailableLocales: (key: string) => string[]\n  getTranslationMeta: (key: string) => TranslationMeta\n  refreshTranslations: () => Promise\u003Cvoid>\n  locale: Ref\u003Cstring>\n  isDev: boolean\n  devModeEnabled: Ref\u003Cboolean>\n}",{"id":13402,"title":6217,"titles":13403,"content":13404,"level":449},"/api-reference/composables/utility-composables#returns-7",[359,13394],"PropertyTypeDescriptiont(key: string, options?: any) => stringMain translation functiontString(key: string, options?: any) => stringString-only translation (same as t)tContent(entity: any, field: string, locale?: string) => stringTranslate entity field contenttInfo(key: string) => TranslationInfoGet translation metadatahasTranslation(key: string) => booleanCheck if translation existsgetAvailableLocales(key: string) => string[]Get locales for keygetTranslationMeta(key: string) => TranslationMetaGet translation metadatarefreshTranslations() => Promise\u003Cvoid>Reload translationslocaleRef\u003Cstring>Current localeisDevbooleanDevelopment mode flagdevModeEnabledRef\u003Cboolean>Translation dev mode",{"id":13406,"title":1635,"titles":13407,"content":13408,"level":449},"/api-reference/composables/utility-composables#how-it-works-2",[359,13394],"Two Modes: Stub Mode (Base Layer): Returns English fallbacks for common UI stringsFull i18n Mode: Activated when @fyit/crouton-i18n layer is installed",{"id":13410,"title":13411,"titles":13412,"content":13413,"level":449},"/api-reference/composables/utility-composables#basic-usage-stub-mode","Basic Usage (Stub Mode)",[359,13394],"\u003Cscript setup lang=\"ts\">\nconst { t } = useT()\n\n// Common UI strings have English fallbacks\nconst searchLabel = t('table.search')        // → \"Search\"\nconst saveLabel = t('common.save')           // → \"Save\"\nconst cancelLabel = t('common.cancel')       // → \"Cancel\"\n\n// Unknown keys return the key itself\nconst unknownLabel = t('my.custom.key')      // → \"my.custom.key\"\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUInput :placeholder=\"searchLabel\" />\n  \u003CUButton>{{ saveLabel }}\u003C/UButton>\n  \u003CUButton variant=\"ghost\">{{ cancelLabel }}\u003C/UButton>\n\u003C/template>",{"id":13415,"title":13416,"titles":13417,"content":13418,"level":449},"/api-reference/composables/utility-composables#built-in-fallback-keys","Built-in Fallback Keys",[359,13394],"Table Translations: 'table.search' → 'Search'\n'table.selectAll' → 'Select all'\n'table.selectRow' → 'Select row'\n'table.createdAt' → 'Created At'\n'table.updatedAt' → 'Updated At'\n'table.createdBy' → 'Created By'\n'table.updatedBy' → 'Updated By'\n'table.actions' → 'Actions'\n'table.display' → 'Display'\n'table.rowsPerPage' → 'Rows per page'\n'table.rowsPerPageColon' → 'Rows per page:'\n'table.showing' → 'Showing'\n'table.to' → 'to'\n'table.of' → 'of'\n'table.results' → 'results' Common Translations: 'common.loading' → 'Loading'\n'common.save' → 'Save'\n'common.cancel' → 'Cancel'\n'common.delete' → 'Delete'\n'common.edit' → 'Edit'\n'common.create' → 'Create'\n'common.update' → 'Update'",{"id":13420,"title":13421,"titles":13422,"content":13423,"level":449},"/api-reference/composables/utility-composables#using-in-components","Using in Components",[359,13394],"Table Search: \u003Cscript setup lang=\"ts\">\nconst { t } = useT()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonTableSearch\n    v-model=\"search\"\n    :placeholder=\"t('table.search')\"\n  />\n\u003C/template> Button Labels: \u003Cscript setup lang=\"ts\">\nconst { t } = useT()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUButton>{{ t('common.save') }}\u003C/UButton>\n  \u003CUButton color=\"gray\" variant=\"ghost\">{{ t('common.cancel') }}\u003C/UButton>\n  \u003CUButton color=\"red\">{{ t('common.delete') }}\u003C/UButton>\n\u003C/template> Loading States: \u003Cscript setup lang=\"ts\">\nconst { t } = useT()\nconst { pending } = await useCollectionQuery('shopProducts')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"pending\">\n    {{ t('common.loading') }}\n  \u003C/div>\n\u003C/template>",{"id":13425,"title":13426,"titles":13427,"content":13428,"level":449},"/api-reference/composables/utility-composables#content-translation-tcontent","Content Translation (tContent)",[359,13394],"Translate entity fields (for multilingual content): \u003Cscript setup lang=\"ts\">\nconst { tContent } = useT()\n\nconst product = {\n  name: 'Default Name',\n  description: 'Default Description',\n  translations: {\n    en: {\n      name: 'Product',\n      description: 'English description'\n    },\n    nl: {\n      name: 'Product',\n      description: 'Nederlandse beschrijving'\n    },\n    fr: {\n      name: 'Produit',\n      description: 'Description française'\n    }\n  }\n}\n\n// Get translated content\nconst name = tContent(product, 'name')             // Current locale\nconst nameNl = tContent(product, 'name', 'nl')     // Specific locale\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Ch1>{{ tContent(product, 'name') }}\u003C/h1>\n  \u003Cp>{{ tContent(product, 'description') }}\u003C/p>\n\u003C/template>",{"id":13430,"title":5212,"titles":13431,"content":13432,"level":449},"/api-reference/composables/utility-composables#fallback-behavior-1",[359,13394],"When translation is missing, tContent falls back through: Current locale translationEnglish translationBase field valueEmpty string const { tContent, locale } = useT()\nlocale.value = 'nl' // Dutch\n\nconst product = {\n  name: 'Default Name',\n  translations: {\n    en: { name: 'English Name' }\n    // Missing 'nl' translation\n  }\n}\n\ntContent(product, 'name')\n// → Tries 'nl' (not found)\n// → Falls back to 'en': \"English Name\"",{"id":13434,"title":13435,"titles":13436,"content":13437,"level":449},"/api-reference/composables/utility-composables#check-translation-existence","Check Translation Existence",[359,13394],"\u003Cscript setup lang=\"ts\">\nconst { hasTranslation } = useT()\n\nif (hasTranslation('table.search')) {\n  console.log('Translation exists')\n} else {\n  console.log('Using fallback')\n}\n\n// In stub mode: always returns false\n// In i18n mode: checks translation registry\n\u003C/script>",{"id":13439,"title":13440,"titles":13441,"content":13442,"level":449},"/api-reference/composables/utility-composables#get-available-locales","Get Available Locales",[359,13394],"\u003Cscript setup lang=\"ts\">\nconst { getAvailableLocales } = useT()\n\nconst locales = getAvailableLocales('table.search')\n// Stub mode: ['en']\n// i18n mode: ['en', 'nl', 'fr', ...] (actual available locales)\n\u003C/script>",{"id":13444,"title":13445,"titles":13446,"content":13447,"level":449},"/api-reference/composables/utility-composables#translation-metadata","Translation Metadata",[359,13394],"\u003Cscript setup lang=\"ts\">\nconst { tInfo } = useT()\n\nconst info = tInfo('table.search')\nconsole.log(info)\n// {\n//   key: 'table.search',\n//   value: 'Search',\n//   mode: 'system',\n//   category: 'ui',\n//   isMissing: false,\n//   hasTeamOverride: false\n// }\n\u003C/script>",{"id":13449,"title":13450,"titles":13451,"content":13452,"level":449},"/api-reference/composables/utility-composables#full-i18n-mode","Full i18n Mode",[359,13394],"When @fyit/crouton-i18n is installed, useT() is automatically replaced with full i18n functionality: pnpm add @fyit/crouton-i18n Additional features in i18n mode: Database-backed translationsTeam-specific overridesTranslation management UILocale switchingMissing translation detectionTranslation dev mode",{"id":13454,"title":13455,"titles":13456,"content":13457,"level":449},"/api-reference/composables/utility-composables#integration-with-tables","Integration with Tables",[359,13394],"Used automatically in CroutonTable: \u003C!-- Internal implementation -->\n\u003Cscript setup lang=\"ts\">\nconst { t, tString } = useT()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CCroutonTableSearch\n    v-model=\"search\"\n    :placeholder=\"tString('table.search')\"\n  />\n  \n  \u003Cdiv class=\"pagination-info\">\n    {{ t('table.showing') }} {{ start }} {{ t('table.to') }} {{ end }} {{ t('table.of') }} {{ total }} {{ t('table.results') }}\n  \u003C/div>\n\u003C/template>",{"id":13459,"title":13460,"titles":13461,"content":13462,"level":449},"/api-reference/composables/utility-composables#custom-fallbacks","Custom Fallbacks",[359,13394],"Extend fallback map for your app: // app/composables/useT.ts (override stub)\nexport function useT() {\n  const translate = (key: string): string => {\n    const fallbacks: Record\u003Cstring, string> = {\n      // Crouton defaults\n      'table.search': 'Search',\n      'common.save': 'Save',\n      \n      // Your custom fallbacks\n      'app.welcome': 'Welcome to My App',\n      'app.logout': 'Log Out',\n      'product.title': 'Product Name'\n    }\n    \n    return fallbacks[key] || key\n  }\n  \n  return {\n    t: translate,\n    tString: translate,\n    // ... other methods\n  }\n}",{"id":13464,"title":44,"titles":13465,"content":13466,"level":449},"/api-reference/composables/utility-composables#best-practices-5",[359,13394],"DO: ✅ Use t() for all UI strings (consistent API)✅ Use tContent() for multilingual entity fields✅ Define fallbacks for common app strings✅ Install i18n layer for production apps✅ Use tString() when type needs to be string (rare) DON'T: ❌ Hardcode strings in components❌ Mix t() and hardcoded strings❌ Forget to check stub mode limitations❌ Use for dynamic user-generated content",{"id":13468,"title":13469,"titles":13470,"content":13471,"level":449},"/api-reference/composables/utility-composables#when-to-upgrade-to-i18n-layer","When to Upgrade to i18n Layer",[359,13394],"Use stub mode when: ✅ Building prototypes or MVPs✅ Single-language apps (English only)✅ Don't need translation management UI Upgrade to i18n layer when: ✅ Supporting multiple languages✅ Need team-specific translations✅ Want translation management UI✅ Need to track missing translations✅ Building production multi-tenant apps",{"id":13473,"title":36,"titles":13474,"content":13475,"level":449},"/api-reference/composables/utility-composables#troubleshooting-4",[359,13394],"IssueSolutionTranslation returns keyKey not in fallback map, add to fallbacks or install i18n layertContent returns emptyCheck entity has translations object and field existsLocale not switchingInstall @fyit/crouton-i18n for full supportMissing translationsUse i18n layer's translation management UI",{"id":13477,"title":13478,"titles":13479,"content":13480,"level":391},"/api-reference/composables/utility-composables#usedependentfieldresolver","useDependentFieldResolver",[359],"Resolves a field value from a parent collection's JSON array field. Used for complex data relationships where a field stores an ID that references an object within another collection's JSON array.",{"id":13482,"title":5176,"titles":13483,"content":13484,"level":449},"/api-reference/composables/utility-composables#type-signature-9",[359,13478],"interface DependentFieldResolverOptions {\n  valueId: string | Ref\u003Cstring> | (() => string)\n  parentId: string | Ref\u003Cstring> | (() => string)\n  parentCollection: string\n  parentField: string\n}\n\ninterface DependentFieldResolverReturn\u003CT = any> {\n  resolvedValue: ComputedRef\u003CT | null>\n  pending: Ref\u003Cboolean>\n  error: Ref\u003Cany>\n}\n\nasync function useDependentFieldResolver\u003CT = any>(\n  options: DependentFieldResolverOptions\n): Promise\u003CDependentFieldResolverReturn\u003CT>>",{"id":13486,"title":9079,"titles":13487,"content":13488,"level":449},"/api-reference/composables/utility-composables#parameters",[359,13478],"ParameterTypeRequiredDescriptionvalueIdstring | Ref\u003Cstring> | (() => string)YesID to search for in parent's array fieldparentIdstring | Ref\u003Cstring> | (() => string)YesParent collection item IDparentCollectionstringYesCollection containing the parent (e.g., 'bookingsLocations')parentFieldstringYesField name in parent containing the array (e.g., 'slots')",{"id":13490,"title":6217,"titles":13491,"content":13492,"level":449},"/api-reference/composables/utility-composables#returns-8",[359,13478],"PropertyTypeDescriptionresolvedValueComputedRef\u003CT | null>The found object from parent's array, or nullpendingRef\u003Cboolean>Whether parent item is still loadingerrorRef\u003Cany>Error if parent fetch failed",{"id":13494,"title":13495,"titles":13496,"content":13497,"level":449},"/api-reference/composables/utility-composables#use-case-example","Use Case Example",[359,13478],"Problem: A booking has a slotId field that references a slot object in a location's slots array. How do you display the slot details? // bookingsBookings collection\n{\n  id: 'booking-123',\n  location: 'location-456',\n  slotId: 'tvmNIE0CGmS7uxQe0y0YM',  // ← ID to resolve\n  // ...\n}\n\n// bookingsLocations collection\n{\n  id: 'location-456',\n  name: 'Main Location',\n  slots: [  // ← JSON array containing slot objects\n    {\n      id: 'tvmNIE0CGmS7uxQe0y0YM',\n      label: 'Room 123',\n      value: '123',\n      capacity: 10\n    },\n    {\n      id: 'another-slot-id',\n      label: 'Room 456',\n      value: '456',\n      capacity: 8\n    }\n  ]\n}",{"id":13499,"title":4173,"titles":13500,"content":13501,"level":449},"/api-reference/composables/utility-composables#basic-usage-5",[359,13478],"Resolve a booking's slot details: \u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  bookingId: string\n  bookingData: any\n}>()\n\n// Resolve slot details from location's slots array\nconst { resolvedValue: slot, pending, error } = await useDependentFieldResolver({\n  valueId: props.bookingData.slotId,\n  parentId: props.bookingData.location,\n  parentCollection: 'bookingsLocations',\n  parentField: 'slots'\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"pending\">Loading slot details...\u003C/div>\n  \u003Cdiv v-else-if=\"error\">Error: {{ error.message }}\u003C/div>\n  \u003Cdiv v-else-if=\"slot\">\n    \u003Ch3>{{ slot.label }}\u003C/h3>\n    \u003Cp>Capacity: {{ slot.capacity }}\u003C/p>\n  \u003C/div>\n\u003C/template>",{"id":13503,"title":13504,"titles":13505,"content":13506,"level":449},"/api-reference/composables/utility-composables#with-reactive-values","With Reactive Values",[359,13478],"When IDs come from reactive sources (refs, props, computed): \u003Cscript setup lang=\"ts\">\nconst slotId = ref('tvmNIE0CGmS7uxQe0y0YM')\nconst locationId = ref('location-456')\n\n// Reactively resolves when either ID changes\nconst { resolvedValue: slot, pending } = await useDependentFieldResolver({\n  valueId: slotId,\n  parentId: locationId,\n  parentCollection: 'bookingsLocations',\n  parentField: 'slots'\n})\n\n// Change IDs to resolve different slot\nconst selectDifferentSlot = (newSlotId: string) => {\n  slotId.value = newSlotId\n  // resolvedValue automatically updates\n}\n\u003C/script>",{"id":13508,"title":13509,"titles":13510,"content":13511,"level":449},"/api-reference/composables/utility-composables#with-getter-functions","With Getter Functions",[359,13478],"For lazy evaluation: \u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  booking: any\n}>()\n\nconst { resolvedValue: slot } = await useDependentFieldResolver({\n  valueId: () => props.booking.slotId,\n  parentId: () => props.booking.location,\n  parentCollection: 'bookingsLocations',\n  parentField: 'slots'\n})\n\u003C/script>",{"id":13513,"title":13514,"titles":13515,"content":13516,"level":449},"/api-reference/composables/utility-composables#display-slot-in-template","Display Slot in Template",[359,13478],"\u003Cscript setup lang=\"ts\">\nconst { resolvedValue: slot, pending, error } = await useDependentFieldResolver({\n  valueId: 'tvmNIE0CGmS7uxQe0y0YM',\n  parentId: 'location-456',\n  parentCollection: 'bookingsLocations',\n  parentField: 'slots'\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUCard v-if=\"!pending && slot\" class=\"p-4\">\n    \u003Ctemplate #header>\n      \u003Cdiv class=\"flex items-center gap-2\">\n        \u003Ci-lucide-clock class=\"w-5 h-5\" />\n        \u003Cspan>{{ slot.label }}\u003C/span>\n      \u003C/div>\n    \u003C/template>\n    \n    \u003Cdiv class=\"space-y-2\">\n      \u003Cdiv class=\"flex justify-between\">\n        \u003Cspan class=\"text-gray-600\">Capacity:\u003C/span>\n        \u003Cspan class=\"font-semibold\">{{ slot.capacity }} people\u003C/span>\n      \u003C/div>\n      \n      \u003Cdiv class=\"flex justify-between\">\n        \u003Cspan class=\"text-gray-600\">Value:\u003C/span>\n        \u003Cspan class=\"font-semibold\">{{ slot.value }}\u003C/span>\n      \u003C/div>\n    \u003C/div>\n  \u003C/UCard>\n  \n  \u003CUSkeleton v-else-if=\"pending\" class=\"h-32\" />\n  \n  \u003CUAlert\n    v-else-if=\"error\"\n    icon=\"i-lucide-alert-circle\"\n    color=\"red\"\n    title=\"Failed to load slot details\"\n  />\n\u003C/template>",{"id":13518,"title":13519,"titles":13520,"content":13521,"level":449},"/api-reference/composables/utility-composables#complex-nested-resolution","Complex Nested Resolution",[359,13478],"Resolve deeply nested arrays: \u003Cscript setup lang=\"ts\">\n// Parent item has nested array\nconst parent = {\n  id: 'parent-123',\n  categories: [\n    {\n      id: 'cat-1',\n      name: 'Category 1',\n      options: [\n        { id: 'opt-1', label: 'Option 1' }\n      ]\n    }\n  ]\n}\n\n// Resolve category\nconst { resolvedValue: category } = await useDependentFieldResolver({\n  valueId: 'cat-1',\n  parentId: 'parent-123',\n  parentCollection: 'products',\n  parentField: 'categories'\n})\n// → { id: 'cat-1', name: 'Category 1', options: [...] }\n\n// To get option from category, use another resolver\nconst { resolvedValue: option } = await useDependentFieldResolver({\n  valueId: 'opt-1',\n  parentId: 'parent-123',\n  parentCollection: 'products',\n  // First access category from categories array\n  parentField: 'categories' // Would need custom logic for nested\n})\n\u003C/script>",{"id":13523,"title":13524,"titles":13525,"content":13526,"level":449},"/api-reference/composables/utility-composables#with-reference-display-component","With Reference Display Component",[359,13478],"Common pattern for displaying resolved references: \u003C!-- ResolvedSlotBadge.vue -->\n\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  slotId: string\n  locationId: string\n}>()\n\nconst { resolvedValue: slot, pending, error } = await useDependentFieldResolver({\n  valueId: computed(() => props.slotId),\n  parentId: computed(() => props.locationId),\n  parentCollection: 'bookingsLocations',\n  parentField: 'slots'\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUBadge\n    v-if=\"!pending && slot\"\n    color=\"blue\"\n    variant=\"soft\"\n  >\n    {{ slot.label }}\n  \u003C/UBadge>\n  \n  \u003CUSkeleton v-else-if=\"pending\" class=\"h-6 w-24\" />\n  \n  \u003CUBadge v-else-if=\"error\" color=\"red\">\n    Error loading\n  \u003C/UBadge>\n\u003C/template> Usage in table or list: \u003Ctemplate>\n  \u003Cdiv v-for=\"booking in bookings\" :key=\"booking.id\">\n    \u003CResolvedSlotBadge\n      :slot-id=\"booking.slotId\"\n      :location-id=\"booking.location\"\n    />\n  \u003C/div>\n\u003C/template>",{"id":13528,"title":2522,"titles":13529,"content":13530,"level":449},"/api-reference/composables/utility-composables#error-handling-2",[359,13478],"\u003Cscript setup lang=\"ts\">\nconst { resolvedValue: slot, error, pending } = await useDependentFieldResolver({\n  valueId: 'invalid-slot-id',\n  parentId: 'location-456',\n  parentCollection: 'bookingsLocations',\n  parentField: 'slots'\n})\n\nwatchEffect(() => {\n  if (error.value) {\n    console.error('Parent fetch failed:', error.value)\n    // Parent collection unreachable or doesn't exist\n  }\n  \n  if (resolvedValue.value === null && !pending.value && !error.value) {\n    console.warn('Slot ID not found in array')\n    // slotId exists but no matching object in slots array\n  }\n})\n\u003C/script>",{"id":13532,"title":13533,"titles":13534,"content":13535,"level":449},"/api-reference/composables/utility-composables#algorithm-deep-dive","Algorithm Deep Dive",[359,13478],"How the resolution works internally: // 1. Fetch parent item\nconst { item: parentItem } = await useCollectionItem(parentCollection, parentId)\n\n// 2. Extract array field\nconst arrayField = parentItem.value[parentField] // e.g., slots array\n\n// 3. Find by ID\nconst found = arrayField.find(item => item.id === valueId)\n\n// 4. Return with reactivity\nreturn {\n  resolvedValue: computed(() => found),\n  pending,\n  error\n}\n\n// Reactivity: If valueId or parentId changes, refetch parent and search again",{"id":13537,"title":44,"titles":13538,"content":13539,"level":449},"/api-reference/composables/utility-composables#best-practices-6",[359,13478],"DO: ✅ Use for displaying resolved references in tables/lists✅ Pass reactive values (ref, computed, function) for automatic updates✅ Handle pending state while loading✅ Display error state if resolution fails✅ Use with card components for detail display DON'T: ❌ Use for simple ID → title mapping (use useCollectionItem instead)❌ Resolve multiple levels in a chain (redesign data structure)❌ Forget to handle null when ID doesn't match array❌ Leave pending state unhandled (shows \"Error loading\" to users)",{"id":13541,"title":36,"titles":13542,"content":13543,"level":449},"/api-reference/composables/utility-composables#troubleshooting-5",[359,13478],"IssueCauseSolutionAlways returns nullField name wrong or not an arrayCheck parentField name matches collection schemaParent fails to loadCollection unreachable or ID invalidVerify parentCollection exists and ID is correctUpdates don't reflectPassing static string instead of refUse ref, computed, or getter functionPerformance slowResolving too many items in listCache results or batch queries differently",{"id":13545,"title":9296,"titles":13546,"content":13547,"level":391},"/api-reference/composables/utility-composables#useexpandableslideover",[359],"Manages expandable slideover state with smooth transitions between sidebar and fullscreen modes.",{"id":13549,"title":5176,"titles":13550,"content":13551,"level":449},"/api-reference/composables/utility-composables#type-signature-10",[359,9296],"interface UseExpandableSlideoverOptions {\n  defaultExpanded?: boolean\n  maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '4xl' | '7xl' | 'full'\n  closeOnExpand?: boolean\n}\n\nfunction useExpandableSlideover(options?: UseExpandableSlideoverOptions): {\n  // State\n  isOpen: Ref\u003Cboolean>\n  isExpanded: Ref\u003Cboolean>\n  \n  // Actions\n  toggleExpand(): void\n  expand(): void\n  collapse(): void\n  open(expanded?: boolean): void\n  close(): void\n  \n  // UI Configuration\n  slideoverUi: ComputedRef\u003C{\n    overlay: string\n    content: string\n    wrapper: string\n    body: string\n    header: string\n  }>\n  side: ComputedRef\u003C'right'>\n  expandIcon: ComputedRef\u003Cstring>\n  expandTooltip: ComputedRef\u003Cstring>\n}",{"id":13553,"title":9079,"titles":13554,"content":13555,"level":449},"/api-reference/composables/utility-composables#parameters-1",[359,9296],"ParameterTypeRequiredDefaultDescriptiondefaultExpandedbooleanNofalseStart expanded or collapsedmaxWidth'sm' | 'md' | 'lg' | 'xl' | '2xl' | '4xl' | '7xl' | 'full'No'xl'Max width in sidebar modecloseOnExpandbooleanNofalseClose slideover after expand",{"id":13557,"title":6217,"titles":13558,"content":13559,"level":449},"/api-reference/composables/utility-composables#returns-9",[359,9296],"PropertyTypeDescriptionisOpenRef\u003Cboolean>Whether slideover is visibleisExpandedRef\u003Cboolean>Whether in fullscreen modetoggleExpand()voidToggle between modes with animationexpand()voidSwitch to fullscreencollapse()voidSwitch to sidebaropen(expanded?)voidOpen slideover, optionally expandedclose()voidClose with animationslideoverUiComputedRef\u003C...>Tailwind classes for different modessideComputedRef\u003C'right'>Always 'right'expandIconComputedRef\u003Cstring>Icon for expand/collapse buttonexpandTooltipComputedRef\u003Cstring>Tooltip text for button",{"id":13561,"title":4173,"titles":13562,"content":13563,"level":449},"/api-reference/composables/utility-composables#basic-usage-6",[359,9296],"Simple sidebar with expand button: \u003Cscript setup lang=\"ts\">\nconst {\n  isOpen,\n  isExpanded,\n  toggleExpand,\n  expand,\n  collapse,\n  open,\n  close,\n  expandIcon,\n  expandTooltip\n} = useExpandableSlideover({ maxWidth: '2xl' })\n\nconst handleOpenForm = () => {\n  open() // Open in sidebar mode\n}\n\nconst handleOpenImmersive = () => {\n  open(true) // Open already expanded\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"flex gap-4\">\n    \u003CUButton @click=\"handleOpenForm\">\n      Open in Sidebar\n    \u003C/UButton>\n    \n    \u003CUButton @click=\"handleOpenImmersive\">\n      Open Fullscreen\n    \u003C/UButton>\n  \u003C/div>\n  \n  \u003CUSlideover v-model=\"isOpen\" side=\"right\">\n    \u003Ctemplate #header=\"{ close: closeSlideover }\">\n      \u003Cdiv class=\"flex items-center justify-between\">\n        \u003Ch2 class=\"text-lg font-semibold\">Form Title\u003C/h2>\n        \n        \u003Cdiv class=\"flex gap-2\">\n          \u003CUTooltip :text=\"expandTooltip\">\n            \u003CUButton\n              :icon=\"expandIcon\"\n              color=\"gray\"\n              variant=\"ghost\"\n              @click=\"toggleExpand\"\n            />\n          \u003C/UTooltip>\n          \n          \u003CUButton\n            icon=\"i-lucide-x\"\n            color=\"gray\"\n            variant=\"ghost\"\n            @click=\"closeSlideover\"\n          />\n        \u003C/div>\n      \u003C/div>\n    \u003C/template>\n    \n    \u003Ctemplate #content>\n      \u003C!-- Form content -->\n      \u003Cdiv class=\"p-6\">\n        \u003CYourFormComponent />\n      \u003C/div>\n    \u003C/template>\n  \u003C/USlideover>\n\u003C/template>",{"id":13565,"title":13566,"titles":13567,"content":13568,"level":449},"/api-reference/composables/utility-composables#width-presets","Width Presets",[359,9296],"Control sidebar width when not expanded: \u003Cscript setup lang=\"ts\">\n// Small sidebar\nconst { isExpanded } = useExpandableSlideover({ maxWidth: 'sm' })\n// → max-w-sm = 384px\n\n// Medium sidebar\nconst { isExpanded } = useExpandableSlideover({ maxWidth: 'md' })\n// → max-w-md = 448px\n\n// Large sidebar (default-ish)\nconst { isExpanded } = useExpandableSlideover({ maxWidth: 'xl' })\n// → max-w-xl = 672px\n\n// Extra large sidebar\nconst { isExpanded } = useExpandableSlideover({ maxWidth: '2xl' })\n// → max-w-2xl = 768px\n\n// Full width sidebar (uses entire right side)\nconst { isExpanded } = useExpandableSlideover({ maxWidth: 'full' })\n// → 100% of viewport width\n\u003C/script>",{"id":13570,"title":13571,"titles":13572,"content":13573,"level":449},"/api-reference/composables/utility-composables#start-expanded","Start Expanded",[359,9296],"Open directly in fullscreen mode: \u003Cscript setup lang=\"ts\">\nconst { isOpen, open } = useExpandableSlideover({\n  defaultExpanded: true\n})\n\nconst handleOpen = () => {\n  open(true) // Opens already expanded\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUSlideover v-model=\"isOpen\">\n    \u003C!-- Content renders in fullscreen immediately -->\n  \u003C/USlideover>\n\u003C/template>",{"id":13575,"title":13576,"titles":13577,"content":13578,"level":449},"/api-reference/composables/utility-composables#close-on-expand","Close on Expand",[359,9296],"Useful for immersive editing experiences: \u003Cscript setup lang=\"ts\">\nconst { open, toggleExpand } = useExpandableSlideover({\n  closeOnExpand: true\n})\n\n// When user clicks expand button:\n// 1. Expand to fullscreen\n// 2. Wait 300ms\n// 3. Close sideover panel\n// 4. Form is now fullscreen in new container\n\u003C/script>",{"id":13580,"title":13581,"titles":13582,"content":13583,"level":449},"/api-reference/composables/utility-composables#expandcollapse-transitions","Expand/Collapse Transitions",[359,9296],"Smooth 300ms animations between modes: \u003Cscript setup lang=\"ts\">\nconst {\n  isExpanded,\n  toggleExpand,\n  slideoverUi\n} = useExpandableSlideover()\n\n// slideoverUi provides animated classes:\n// - transition-[max-width,width] duration-300 ease-in-out\n// - transition-all duration-300 ease-in-out\n\n// When isExpanded changes:\n// 1. Width animates smoothly (300ms)\n// 2. Overlay opacity transitions\n// 3. Content padding adjusts\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- USlideover automatically applies slideoverUi classes -->\n  \u003CUSlideover\n    v-model=\"isOpen\"\n    side=\"right\"\n    :ui=\"slideoverUi\"\n  >\n    \u003Ctemplate #content>\n      \u003Cdiv :class=\"slideoverUi.body\">\n        \u003C!-- Content grows/shrinks with sidebar -->\n        \u003CYourContent />\n      \u003C/div>\n    \u003C/template>\n  \u003C/USlideover>\n\u003C/template>",{"id":13585,"title":13586,"titles":13587,"content":13588,"level":449},"/api-reference/composables/utility-composables#icon-and-tooltip-management","Icon and Tooltip Management",[359,9296],"Display button with context-aware label: \u003Cscript setup lang=\"ts\">\nconst {\n  isExpanded,\n  expandIcon,\n  expandTooltip,\n  toggleExpand\n} = useExpandableSlideover()\n\n// When collapsed:\n// expandIcon = 'i-lucide-maximize-2'\n// expandTooltip = 'Expand to fullscreen'\n\n// When expanded:\n// expandIcon = 'i-lucide-minimize-2'\n// expandTooltip = 'Collapse to sidebar'\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUButton\n    :icon=\"expandIcon\"\n    :ui=\"{ base: 'group' }\"\n    @click=\"toggleExpand\"\n  >\n    \u003CUTooltip :text=\"expandTooltip\" :shortcuts=\"['F11']\">\n      \u003Ctemplate #default>\n        \u003C!-- Button shows appropriate icon/tooltip -->\n      \u003C/template>\n    \u003C/UTooltip>\n  \u003C/UButton>\n\u003C/template>",{"id":13590,"title":13591,"titles":13592,"content":13593,"level":449},"/api-reference/composables/utility-composables#complete-form-example","Complete Form Example",[359,9296],"Full form with expandable sidebar: \u003Cscript setup lang=\"ts\">\nimport { ref } from 'vue'\n\nconst {\n  isOpen,\n  isExpanded,\n  open,\n  close,\n  toggleExpand,\n  expandIcon,\n  expandTooltip,\n  slideoverUi\n} = useExpandableSlideover({ maxWidth: '2xl' })\n\nconst formData = ref({\n  name: '',\n  email: '',\n  bio: ''\n})\n\nconst handleSubmit = () => {\n  // Save form data\n  console.log('Saving:', formData.value)\n  close()\n}\n\nconst handleOpenEdit = () => {\n  open() // Open in sidebar\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"space-y-4\">\n    \u003CUButton @click=\"handleOpenEdit\">\n      Open Editor\n    \u003C/UButton>\n    \n    \u003CUSlideover\n      v-model=\"isOpen\"\n      side=\"right\"\n      :ui=\"slideoverUi\"\n    >\n      \u003Ctemplate #header=\"{ close: closeSlideover }\">\n        \u003Cdiv class=\"flex items-center justify-between w-full\">\n          \u003Ch2 class=\"text-lg font-semibold\">\n            {{ isExpanded ? 'Full Editor' : 'Quick Edit' }}\n          \u003C/h2>\n          \n          \u003Cdiv class=\"flex gap-2\">\n            \u003CUTooltip :text=\"expandTooltip\">\n              \u003CUButton\n                :icon=\"expandIcon\"\n                color=\"gray\"\n                variant=\"ghost\"\n                size=\"sm\"\n                @click=\"toggleExpand\"\n              />\n            \u003C/UTooltip>\n            \n            \u003CUButton\n              icon=\"i-lucide-x\"\n              color=\"gray\"\n              variant=\"ghost\"\n              size=\"sm\"\n              @click=\"closeSlideover\"\n            />\n          \u003C/div>\n        \u003C/div>\n      \u003C/template>\n      \n      \u003Ctemplate #content>\n        \u003Cdiv :class=\"slideoverUi.body\">\n          \u003CUForm :state=\"formData\" @submit=\"handleSubmit\">\n            \u003CUFormField label=\"Name\" name=\"name\">\n              \u003CUInput v-model=\"formData.name\" />\n            \u003C/UFormField>\n            \n            \u003CUFormField label=\"Email\" name=\"email\">\n              \u003CUInput v-model=\"formData.email\" type=\"email\" />\n            \u003C/UFormField>\n            \n            \u003CUFormField label=\"Bio\" name=\"bio\">\n              \u003CUTextarea v-model=\"formData.bio\" rows=\"6\" />\n            \u003C/UFormField>\n            \n            \u003Cdiv class=\"flex gap-2 mt-6\">\n              \u003CUButton\n                type=\"submit\"\n                color=\"primary\"\n              >\n                Save Changes\n              \u003C/UButton>\n              \n              \u003CUButton\n                color=\"gray\"\n                variant=\"ghost\"\n                @click=\"close\"\n              >\n                Cancel\n              \u003C/UButton>\n            \u003C/div>\n          \u003C/UForm>\n        \u003C/div>\n      \u003C/template>\n    \u003C/USlideover>\n  \u003C/div>\n\u003C/template>",{"id":13595,"title":13596,"titles":13597,"content":13598,"level":449},"/api-reference/composables/utility-composables#fullscreen-mode-styling","Fullscreen Mode Styling",[359,9296],"When expanded, the modal takes full viewport: \u003Cscript setup lang=\"ts\">\nconst { isExpanded, slideoverUi } = useExpandableSlideover()\n\n// In fullscreen mode:\n// - No max-width constraint (w-full max-w-none)\n// - 80% backdrop opacity (vs 50% in sidebar)\n// - Larger padding (p-6 vs p-4)\n// - Smooth width transition (duration-300)\n\n// Sidebar mode:\n// - max-w-2xl (or your configured width)\n// - Standard backdrop\n// - Standard padding\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- USlideover with auto-applied slideoverUi -->\n  \u003CUSlideover v-model=\"isOpen\" :ui=\"slideoverUi\">\n    \u003Ctemplate #content>\n      \u003C!-- Automatically gets correct classes for current mode -->\n    \u003C/template>\n  \u003C/USlideover>\n\u003C/template>",{"id":13600,"title":13601,"titles":13602,"content":13603,"level":449},"/api-reference/composables/utility-composables#nested-slideovers","Nested Slideovers",[359,9296],"Works with multiple slideover instances: \u003Cscript setup lang=\"ts\">\n// Main form\nconst main = useExpandableSlideover({ maxWidth: '2xl' })\n\n// Nested detail view\nconst detail = useExpandableSlideover({ maxWidth: 'lg' })\n\nconst handleOpenDetail = () => {\n  // Main still open and behind\n  detail.open(false)\n}\n\nconst handleCloseDetail = () => {\n  detail.close()\n  // Back to main form\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003C!-- Main slideover -->\n  \u003CUSlideover v-model=\"main.isOpen\" side=\"right\" :ui=\"main.slideoverUi\">\n    \u003Ctemplate #header>\n      \u003Ch2>Main Form\u003C/h2>\n    \u003C/template>\n    \n    \u003Ctemplate #content>\n      \u003Cdiv class=\"p-6 space-y-4\">\n        \u003CUButton @click=\"handleOpenDetail\">\n          View Details\n        \u003C/UButton>\n      \u003C/div>\n    \u003C/template>\n  \u003C/USlideover>\n  \n  \u003C!-- Detail slideover (nested) -->\n  \u003CUSlideover v-model=\"detail.isOpen\" side=\"right\" :ui=\"detail.slideoverUi\">\n    \u003Ctemplate #header>\n      \u003Ch2>Detail View\u003C/h2>\n    \u003C/template>\n    \n    \u003Ctemplate #content>\n      \u003C!-- Renders on top of main -->\n    \u003C/template>\n  \u003C/USlideover>\n\u003C/template>",{"id":13605,"title":10600,"titles":13606,"content":13607,"level":449},"/api-reference/composables/utility-composables#responsive-behavior",[359,9296],"Adjust width based on screen size: \u003Cscript setup lang=\"ts\">\nconst { windowWidth } = useWindowSize()\n\n// Wider on desktop, narrower on mobile\nconst maxWidth = computed(() => {\n  if (windowWidth.value \u003C 640) return 'sm'\n  if (windowWidth.value \u003C 1024) return 'md'\n  return '2xl'\n})\n\nconst slideover = useExpandableSlideover({\n  maxWidth: maxWidth.value\n})\n\u003C/script>",{"id":13609,"title":44,"titles":13610,"content":13611,"level":449},"/api-reference/composables/utility-composables#best-practices-7",[359,9296],"DO: ✅ Use for complex forms with lots of content✅ Provide expand button for better UX✅ Handle close animations before heavy operations✅ Show appropriate tooltips for expand/collapse✅ Use consistent maxWidth across app DON'T: ❌ Nest more than 2 levels deep (gets confusing)❌ Auto-expand on small screens (sidebar better for mobile)❌ Force fullscreen if not needed (sidebar is usually fine)❌ Mix with modal overlays (stick to slideovers)",{"id":13613,"title":36,"titles":13614,"content":13615,"level":449},"/api-reference/composables/utility-composables#troubleshooting-6",[359,9296],"IssueSolutionExpand button doesn't appearCheck slideoverUi passed to USlideover, verify Lucide icons availableWidth doesn't change on toggleVerify transition-[max-width] Tailwind directive enabledAnimation feels jerkyCheck no CSS conflicts, ensure duration-300 appliedBackdrop too dark/lightAdjust bg-gray-900/80 and backdrop-blur-sm in slideoverUi",{"id":13617,"title":1007,"titles":13618,"content":13619,"level":391},"/api-reference/composables/utility-composables#related-resources",[359],"Internationalization Guide - Translation systemAsset Management Guide - File upload and managementNuxt Composables - Nuxt composables guide html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}",{"id":281,"title":285,"titles":13621,"content":13622,"level":385},[],"Complete API documentation for Nuxt Crouton composables, components, and types",{"id":13624,"title":285,"titles":13625,"content":13626,"level":385},"/api-reference#api-reference",[],"Complete reference documentation for all Nuxt Crouton APIs.",{"id":13628,"title":13629,"titles":13630,"content":13631,"level":391},"/api-reference#sections","Sections",[285],"Data fetching, mutations, forms, and utility composables.Table, form, modal, layout, and utility components.TypeScript types and interfaces for collections, schemas, and configurations.Server-side utilities, API handlers, and database helpers.Low-level APIs for advanced customization.Single item fetching and management composable.",{"id":371,"title":370,"titles":13633,"content":13634,"level":385},[],"Naming conventions, file organization, and coding standards for Nuxt Crouton This guide documents the conventions used throughout Nuxt Crouton. Following these conventions ensures consistency, predictability, and easier collaboration.",{"id":13636,"title":13637,"titles":13638,"content":528,"level":391},"/reference/conventions#naming-conventions","Naming Conventions",[370],{"id":13640,"title":13641,"titles":13642,"content":13643,"level":449},"/reference/conventions#collection-names","Collection Names",[370,13637],"Collections should follow these rules: Use plural names: products, users, ordersUse camelCase for multi-word names: blogPosts, orderItems, userProfilesBe descriptive: userProfiles is better than just profilesAvoid abbreviations: categories not catsAvoid generic names: Don't use items, data, records Examples: ✅ Good: products, blogPosts, orderItems\n❌ Bad: product (singular), blog_posts (snake_case), items (too generic), prods (abbreviated)",{"id":13645,"title":13646,"titles":13647,"content":13648,"level":449},"/reference/conventions#field-names","Field Names",[370,13637],"Field names should: Use camelCase: firstName, createdAt, isActiveBe descriptive: emailAddress not email if you also have emailVerifiedUse conventional names:\nDates: createdAt, updatedAt, publishedAt, deletedAtBooleans: isActive, hasAccess, canEditRelationships: userId, categoryId, authorId Examples: {\n  \"firstName\": { \"type\": \"string\" },        // ✅ Good\n  \"isPublished\": { \"type\": \"boolean\" },     // ✅ Good\n  \"publishedAt\": { \"type\": \"date\" },        // ✅ Good\n  \"authorId\": { \"type\": \"string\", \"refTarget\": \"authors\" },  // ✅ Good\n\n  \"first_name\": { \"type\": \"string\" },       // ❌ Bad: snake_case\n  \"published\": { \"type\": \"boolean\" },       // ❌ Bad: not clear it's a boolean\n  \"pub_date\": { \"type\": \"date\" },          // ❌ Bad: abbreviated\n  \"author\": { \"type\": \"string\" }            // ❌ Bad: should be authorId\n}",{"id":13650,"title":13651,"titles":13652,"content":13653,"level":449},"/reference/conventions#file-names","File Names",[370,13637],"Generated and custom files follow these patterns: Component Files: Generated components: _Form.vue, List.vueCustom components: ProductCard.vue, OrderStatusBadge.vue, UserAvatar.vueUse PascalCase for all Vue components Composable Files: Generated composables: useShopProducts.ts, useShopOrders.ts (layer prefix + collection name)Custom composables: useProductHelpers.ts, useOrderFilters.tsUse camelCase starting with use Utility Files: Use camelCase: formatters.ts, validators.ts, productHelpers.ts Type Files: Use camelCase: products.ts, orders.ts, shared.ts",{"id":13655,"title":13656,"titles":13657,"content":13658,"level":449},"/reference/conventions#layer-names","Layer Names",[370,13637],"Layer directory names should: Match collection names: If collection is products, layer is layers/products/Use plural, camelCase: layers/blogPosts/, layers/orderItems/Group by domain for multiple collections: layers/shop/ for products, categories, orders",{"id":13660,"title":1297,"titles":13661,"content":528,"level":391},"/reference/conventions#file-organization",[370],{"id":13663,"title":13664,"titles":13665,"content":13666,"level":449},"/reference/conventions#standard-layer-structure","Standard Layer Structure",[370,1297],"Every generated layer follows this structure: layers/[layer-name]/collections/[collection-name]/\n├── app/\n│   ├── components/\n│   │   ├── _Form.vue              # Generated form component\n│   │   └── List.vue               # Generated list component\n│   │\n│   └── composables/\n│       └── use[LayerCollection].ts  # Generated CRUD composable\n│\n├── server/\n│   ├── api/teams/[id]/[layer]-[collection]/\n│   │   ├── index.get.ts           # List endpoint\n│   │   ├── index.post.ts          # Create endpoint\n│   │   ├── [collectionId].patch.ts # Update endpoint (PATCH)\n│   │   └── [collectionId].delete.ts # Delete endpoint\n│   │\n│   └── database/\n│       └── schema.ts              # Drizzle schema\n│\n├── types.ts                       # TypeScript types\n└── nuxt.config.ts                 # Layer config",{"id":13668,"title":13669,"titles":13670,"content":13671,"level":449},"/reference/conventions#custom-components-placement","Custom Components Placement",[370,1297],"Place custom components in organized subdirectories: layers/shop/collections/products/app/components/\n├── _Form.vue                    # Generated\n├── List.vue                     # Generated\n├── fields/                       # Custom field components\n│   ├── PriceField.vue\n│   ├── StockField.vue\n│   └── CategoryField.vue\n└── cards/                        # Custom card layouts\n    ├── ProductCard.vue\n    └── ProductCardMini.vue",{"id":13673,"title":13674,"titles":13675,"content":13676,"level":449},"/reference/conventions#schema-files","Schema Files",[370,1297],"Schema files should be organized in a schemas/ directory using JSON format: schemas/\n├── products.json        # Single collection schema\n├── categories.json\n├── shop/                # Domain-grouped schemas\n│   ├── products.json\n│   ├── orders.json\n│   └── orderItems.json\n└── blog/\n    ├── posts.json\n    ├── authors.json\n    └── tags.json",{"id":13678,"title":13679,"titles":13680,"content":528,"level":391},"/reference/conventions#collection-schema-patterns","Collection Schema Patterns",[370],{"id":13682,"title":13683,"titles":13684,"content":13685,"level":449},"/reference/conventions#basic-schema-structure","Basic Schema Structure",[370,13679],"{\n  \"title\": {\n    \"type\": \"string\",\n    \"meta\": { \"required\": true, \"label\": \"Product Name\" }\n  },\n  \"price\": {\n    \"type\": \"number\",\n    \"meta\": { \"required\": true }\n  },\n  \"categoryId\": {\n    \"type\": \"string\",\n    \"refTarget\": \"categories\"\n  }\n}",{"id":13687,"title":3010,"titles":13688,"content":13689,"level":449},"/reference/conventions#auto-generated-fields",[370,13679],"NEVER define these fields in your schema - they're auto-generated: id - Always generated (UUID or nanoid)teamId - Always generated (team-scoped by default)owner - Always generated (team-scoped by default)createdAt - Generated when useMetadata: true (default)updatedAt - Generated when useMetadata: true (default)createdBy - Generated when useMetadata: true (default)updatedBy - Generated when useMetadata: true (default) Common Mistake: Defining auto-generated fields in your schema causes duplicate key errors during build.// ❌ BAD - These are auto-generated!\n{\n  \"id\": { \"type\": \"string\" },\n  \"createdAt\": { \"type\": \"date\" },\n  \"teamId\": { \"type\": \"string\" }\n}\n\n// ✅ GOOD - Only define your custom fields\n{\n  \"title\": { \"type\": \"string\" },\n  \"description\": { \"type\": \"text\" },\n  \"price\": { \"type\": \"number\" }\n}",{"id":13691,"title":13692,"titles":13693,"content":13694,"level":449},"/reference/conventions#field-type-conventions","Field Type Conventions",[370,13679],"Text Fields: { \"title\": { \"type\": \"string\" } }\n{ \"description\": { \"type\": \"text\" } } Number Fields: { \"quantity\": { \"type\": \"number\" } }\n{ \"price\": { \"type\": \"decimal\", \"meta\": { \"precision\": 10, \"scale\": 2 } } } Boolean Fields: { \"isActive\": { \"type\": \"boolean\", \"meta\": { \"default\": true } } } Date Fields: { \"publishedAt\": { \"type\": \"date\" } } Reference Fields (use string type with refTarget): { \"categoryId\": { \"type\": \"string\", \"refTarget\": \"categories\" } } Other Types: json, repeater, array, image, file",{"id":13696,"title":13697,"titles":13698,"content":528,"level":391},"/reference/conventions#component-conventions","Component Conventions",[370],{"id":13700,"title":13701,"titles":13702,"content":13703,"level":449},"/reference/conventions#customization-patterns","Customization Patterns",[370,13697],"Override generated components by creating your own component with the same name in the collection directory. The generated _Form.vue and List.vue can be freely edited since they live in your project.",{"id":13705,"title":13706,"titles":13707,"content":13708,"level":449},"/reference/conventions#props-naming","Props Naming",[370,13697],"Component props follow Vue conventions: Use camelCase in script: modelValue, errorMessage, showModalUse kebab-case in templates: model-value, error-message, show-modalPrefix boolean props: isActive, hasError, canEdit",{"id":13710,"title":13711,"titles":13712,"content":528,"level":391},"/reference/conventions#api-endpoint-patterns","API Endpoint Patterns",[370],{"id":13714,"title":13715,"titles":13716,"content":13717,"level":449},"/reference/conventions#restful-conventions","RESTful Conventions",[370,13711],"Generated API endpoints follow REST conventions: GET    /api/teams/:teamId/shop-products          # List all\nPOST   /api/teams/:teamId/shop-products          # Create new\nPATCH  /api/teams/:teamId/shop-products/:id      # Update existing\nDELETE /api/teams/:teamId/shop-products/:id      # Delete",{"id":13719,"title":5720,"titles":13720,"content":13721,"level":449},"/reference/conventions#query-parameters",[370,13711],"Standard query parameters: page - Page number (1-indexed)limit - Items per pagesearch - Search querysort - Sort field (e.g., title, -createdAt for descending)filter - JSON filter object Example: GET /api/products?page=2&limit=20&search=laptop&sort=-price",{"id":13723,"title":13724,"titles":13725,"content":528,"level":391},"/reference/conventions#typescript-conventions","TypeScript Conventions",[370],{"id":13727,"title":13728,"titles":13729,"content":13730,"level":449},"/reference/conventions#type-naming","Type Naming",[370,13724],"Collection types: PascalCase matching collection: Product, BlogPost, OrderItemInput types: Suffix with Input: ProductInput, CreateProductInputQuery types: Suffix with Query: ProductQuery, ProductsQueryResponse types: Suffix with Response: ProductResponse, ProductsResponse Example: // Collection type\nexport interface Product {\n  id: string\n  title: string\n  price: number\n  createdAt: Date\n}\n\n// Input type\nexport interface CreateProductInput {\n  title: string\n  price: number\n}\n\n// Query type\nexport interface ProductsQuery {\n  page?: number\n  limit?: number\n  search?: string\n}\n\n// Response type\nexport interface ProductsResponse {\n  items: Product[]\n  total: number\n  page: number\n}",{"id":13732,"title":13733,"titles":13734,"content":13735,"level":449},"/reference/conventions#import-conventions","Import Conventions",[370,13724],"// ✅ Good - Organized imports\nimport { ref, computed } from 'vue'\nimport type { Product } from '~/layers/shop/collections/products/types'\nimport { useProducts } from '~/layers/shop/collections/products/app/composables/useShopProducts'\n\n// ❌ Bad - Mixed order, no type imports\nimport { useProducts } from '~/layers/shop/collections/products/app/composables/useShopProducts'\nimport { ref, computed } from 'vue'\nimport { Product } from '~/layers/shop/collections/products/types' // Should be type import",{"id":13737,"title":13738,"titles":13739,"content":528,"level":391},"/reference/conventions#code-style","Code Style",[370],{"id":13741,"title":13742,"titles":13743,"content":13744,"level":449},"/reference/conventions#vue-component-structure","Vue Component Structure",[370,13738],"Components should follow this order: \u003C!-- 1. Script setup -->\n\u003Cscript setup lang=\"ts\">\n// 1. Imports\nimport { ref, computed } from 'vue'\nimport type { Product } from '~/types'\n\n// 2. Props\ninterface Props {\n  product: Product\n}\nconst props = defineProps\u003CProps>()\n\n// 3. Emits\nconst emit = defineEmits\u003C{\n  'update': [product: Product]\n}>()\n\n// 4. Composables\nconst { mutate } = useCroutonMutate()\n\n// 5. Reactive state\nconst isEditing = ref(false)\n\n// 6. Computed\nconst displayPrice = computed(() => `$${props.product.price.toFixed(2)}`)\n\n// 7. Methods\nconst handleSave = async () => {\n  // ...\n}\n\u003C/script>\n\n\u003C!-- 2. Template -->\n\u003Ctemplate>\n  \u003C!-- Component markup -->\n\u003C/template>\n\n\u003C!-- 3. Styles (if needed) -->\n\u003Cstyle scoped>\n/* Component styles */\n\u003C/style>",{"id":13746,"title":13747,"titles":13748,"content":13749,"level":449},"/reference/conventions#composable-structure","Composable Structure",[370,13738],"// layers/products/composables/useProducts.ts\n\nimport { ref } from 'vue'\nimport type { Product } from '~/types'\n\nexport const useProducts = () => {\n  // 1. State\n  const products = ref\u003CProduct[]>([])\n  const loading = ref(false)\n\n  // 2. Methods\n  const fetchProducts = async () => {\n    loading.value = true\n    try {\n      products.value = await $fetch('/api/products')\n    } finally {\n      loading.value = false\n    }\n  }\n\n  // 3. Return public API\n  return {\n    products: readonly(products),\n    loading: readonly(loading),\n    fetchProducts\n  }\n}",{"id":13751,"title":13752,"titles":13753,"content":13754,"level":391},"/reference/conventions#best-practices-summary","Best Practices Summary",[370],"Follow naming conventions: Plural collections, camelCase fields, PascalCase componentsOrganize files consistently: Use standard layer structureNever define auto-generated fields: Let Nuxt Crouton add id, createdAt, etc.Use TypeScript: Type everything for safety and better DXFollow Vue conventions: Component structure, prop naming, import orderKeep customizations separate: Custom components in subdirectoriesUse descriptive names: Avoid abbreviations and generic names",{"id":13756,"title":1007,"titles":13757,"content":13758,"level":391},"/reference/conventions#related-resources",[370],"Best Practices - General best practicesSchema Format - Detailed schema referenceArchitecture - Understanding layersTypeScript Types - Type definitions reference html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":375,"title":374,"titles":13760,"content":13761,"level":385},[],"Common questions and answers about Nuxt Crouton Query Examples: For complete useCollectionQuery patterns (basic, filtering, pagination, sorting, relations), see Querying Data. This page answers the most frequently asked questions about Nuxt Crouton. For detailed troubleshooting, see the Troubleshooting Guide.",{"id":13763,"title":13764,"titles":13765,"content":528,"level":391},"/reference/faq#generation-setup","Generation & Setup",[374],{"id":13767,"title":13768,"titles":13769,"content":13770,"level":449},"/reference/faq#q-what-fields-are-auto-generated-and-should-not-be-in-my-schema","Q: What fields are auto-generated and should NOT be in my schema?",[374,13764],"A: Never define these fields in your schema files: id - Always auto-generated (UUID or nanoid)teamId, owner - Always auto-generated (team-scoped by default)createdAt, updatedAt, createdBy, updatedBy - Generated when useMetadata: true (default) Defining these manually causes duplicate key errors during build. Common mistake:{\n  \"id\": { \"type\": \"string\" },         // ❌ Remove\n  \"createdAt\": { \"type\": \"date\" },    // ❌ Remove\n  \"teamId\": { \"type\": \"string\" },     // ❌ Remove\n  \"title\": { \"type\": \"string\" }       // ✅ Keep\n} See: Schema Format - Auto-Generated Fields",{"id":13772,"title":13773,"titles":13774,"content":13775,"level":449},"/reference/faq#q-how-do-i-regenerate-a-collection-without-losing-customizations","Q: How do I regenerate a collection without losing customizations?",[374,13764],"A: Nuxt Crouton generates code in layers, which you can freely customize. Regeneration is safe as long as you: Keep custom components separate - Place them in subdirectories like components/custom/Use slots for overrides - Override specific fields/columns via slots instead of editing generated filesCommit before regenerating - Always commit working code before running the generator # Safe regeneration workflow\ngit add .\ngit commit -m \"Before regeneration\"\nnpx crouton config ./crouton.config.js --only products --force See: Customization",{"id":13777,"title":13778,"titles":13779,"content":13780,"level":449},"/reference/faq#q-should-collection-names-be-singular-or-plural","Q: Should collection names be singular or plural?",[374,13764],"A: Always use plural names: ✅ Correct: products, blogPosts, orderItems\n❌ Wrong: product (singular), blog_post (snake_case), orderItem (singular) See: Conventions - Collection Names",{"id":13782,"title":13783,"titles":13784,"content":13785,"level":449},"/reference/faq#q-what-naming-convention-should-i-use-for-fields","Q: What naming convention should I use for fields?",[374,13764],"A: Use camelCase for all field names: {\n  \"firstName\": { \"type\": \"string\" },      // ✅ Correct\n  \"isActive\": { \"type\": \"boolean\" },      // ✅ Correct\n  \"publishedAt\": { \"type\": \"date\" },      // ✅ Correct\n\n  \"first_name\": { \"type\": \"string\" },     // ❌ Wrong (snake_case)\n  \"FirstName\": { \"type\": \"string\" }       // ❌ Wrong (PascalCase)\n} See: Conventions - Field Names",{"id":13787,"title":1302,"titles":13788,"content":528,"level":391},"/reference/faq#data-operations",[374],{"id":13790,"title":13791,"titles":13792,"content":13793,"level":449},"/reference/faq#q-why-isnt-my-data-updating-after-savedelete","Q: Why isn't my data updating after save/delete?",[374,1302],"A: This is usually a cache invalidation issue. Check: Collection name matches between query and mutationUsing useCollectionMutation() which auto-invalidates cache // ✅ Correct - auto-invalidation\nconst { create, update } = useCollectionMutation('products')\nawait create({ title: 'New Product' })\n\n// ✅ Also correct - useCroutonMutate auto-invalidates too\nconst { mutate } = useCroutonMutate()\nawait mutate('create', 'products', data)\n// Cache is auto-invalidated internally, no manual refresh needed See: Troubleshooting - Data Not Updating",{"id":13795,"title":13796,"titles":13797,"content":13798,"level":449},"/reference/faq#q-how-do-i-handle-loading-and-error-states","Q: How do I handle loading and error states?",[374,1302],"A: All query composables return pending and error properties. Use these to conditionally render loading spinners, error messages, and retry buttons. For the complete pattern, see Best Practices - Handle Loading States.",{"id":13800,"title":13801,"titles":13802,"content":13803,"level":449},"/reference/faq#q-when-should-i-use-usecollectionmutation-vs-usecroutonmutate","Q: When should I use useCollectionMutation() vs useCroutonMutate()?",[374,1302],"A: Use useCollectionMutation() for: Generated formsRepeated operations on same collectionMulti-step wizardsBulk operations Use useCroutonMutate() for: Quick toggle buttonsOne-off actionsDifferent collections in same component Examples: See Mutation Composables API for complete usage patterns. See: Best Practices - Choose the Right Mutation Method",{"id":13805,"title":1383,"titles":13806,"content":528,"level":391},"/reference/faq#customization",[374],{"id":13808,"title":13809,"titles":13810,"content":13811,"level":449},"/reference/faq#q-how-do-i-customize-a-single-form-field","Q: How do I customize a single form field?",[374,1383],"A: Edit the generated _Form.vue component directly. It lives in your project at layers/[layer]/collections/[collection]/app/components/_Form.vue and is yours to modify. You can change field components, add custom logic, or rearrange the layout. See: Customization - Custom Components",{"id":13813,"title":13814,"titles":13815,"content":13816,"level":449},"/reference/faq#q-how-do-i-customize-the-list-view","Q: How do I customize the list view?",[374,1383],"A: Edit the generated List.vue component directly. It lives in your project at layers/[layer]/collections/[collection]/app/components/List.vue and is yours to modify. See: Customization - Custom Columns",{"id":13818,"title":13819,"titles":13820,"content":13821,"level":449},"/reference/faq#q-can-i-completely-replace-a-generated-component","Q: Can I completely replace a generated component?",[374,1383],"A: Yes, create a component with the same name in your layer: layers/shop/collections/products/app/components/\n├── _Form.vue   ← Generated form, yours to edit\n└── List.vue    ← Generated list, yours to edit Since generated code lives in your project, you can edit it directly. See: Customization Overview",{"id":13823,"title":13824,"titles":13825,"content":528,"level":391},"/reference/faq#typescript-development","TypeScript & Development",[374],{"id":13827,"title":13828,"titles":13829,"content":13830,"level":449},"/reference/faq#q-why-am-i-getting-cannot-find-module-errors-after-generation","Q: Why am I getting \"Cannot find module\" errors after generation?",[374,13824],"A: TypeScript server needs to be restarted: In VS Code: Press Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows)Type \"Restart TS Server\"Select \"TypeScript: Restart TS Server\" Or clear cache: rm -rf .nuxt\nnpx nuxt prepare See: Troubleshooting - Type Errors",{"id":13832,"title":13833,"titles":13834,"content":13835,"level":449},"/reference/faq#q-how-do-i-get-type-safety-for-queries","Q: How do I get type safety for queries?",[374,13824],"A: Use TypeScript generics with useCollectionQuery\u003CYourType>() to get full type safety. Import your type from the layer's types file and pass it as a generic parameter. See: Best Practices - Type Your Queries.",{"id":13837,"title":13838,"titles":13839,"content":13840,"level":449},"/reference/faq#q-why-arent-tailwind-classes-working-in-layer-components","Q: Why aren't Tailwind classes working in layer components?",[374,13824],"A: Tailwind v4 doesn't auto-scan node_modules. Add @source directive: /* app/assets/css/tailwind.css */\n@import \"tailwindcss\";\n@import \"@nuxt/ui\";\n\n/* Scan Nuxt Crouton layers */\n@source \"../../../node_modules/@fyit/crouton*/app/**/*.{vue,js,ts}\"; Then restart dev server: pnpm dev See: Troubleshooting - Tailwind Classes",{"id":13842,"title":13843,"titles":13844,"content":528,"level":391},"/reference/faq#relations-references","Relations & References",[374],{"id":13846,"title":13847,"titles":13848,"content":13849,"level":449},"/reference/faq#q-how-do-i-define-a-relationship-between-collections","Q: How do I define a relationship between collections?",[374,13843],"A: Use string type with refTarget: {\n  \"authorId\": { \"type\": \"string\", \"refTarget\": \"users\" }\n} See: Patterns - Relations",{"id":13851,"title":13852,"titles":13853,"content":13854,"level":449},"/reference/faq#q-how-do-i-query-related-data","Q: How do I query related data?",[374,13843],"A: Use server-side joins in your API endpoints to load related data. See Querying with Relations for the complete pattern and Patterns - Relations for advanced usage.",{"id":13856,"title":13857,"titles":13858,"content":528,"level":391},"/reference/faq#forms-validation","Forms & Validation",[374],{"id":13860,"title":13861,"titles":13862,"content":13863,"level":449},"/reference/faq#q-why-arent-validation-errors-showing","Q: Why aren't validation errors showing?",[374,13857],"A: Check that: Schema is passed to form: :schema=\"schema\"Field names match schema keysState is reactive (ref() or reactive()) \u003Cscript setup lang=\"ts\">\nimport { z } from 'zod'\n\nconst schema = z.object({\n  name: z.string().min(1, 'Required'),\n  price: z.number().min(0)\n})\n\nconst state = ref({ name: '', price: 0 })\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUForm :state=\"state\" :schema=\"schema\" @submit=\"handleSubmit\">\n    \u003CUFormField label=\"Name\" name=\"name\">\n      \u003CUInput v-model=\"state.name\" />\n    \u003C/UFormField>\n  \u003C/UForm>\n\u003C/template> See: Troubleshooting - Validation Errors",{"id":13865,"title":13866,"titles":13867,"content":13868,"level":449},"/reference/faq#q-how-do-i-create-conditional-fields","Q: How do I create conditional fields?",[374,13857],"A: Use v-if with reactive state: \u003Cscript setup lang=\"ts\">\nconst productType = ref('physical')\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CUFormField label=\"Type\" name=\"type\">\n    \u003CUSelect v-model=\"productType\" :options=\"['physical', 'digital']\" />\n  \u003C/UFormField>\n\n  \u003C!-- Only show for physical products -->\n  \u003CUFormField v-if=\"productType === 'physical'\" label=\"Weight\" name=\"weight\">\n    \u003CUInput v-model=\"formData.weight\" type=\"number\" />\n  \u003C/UFormField>\n\u003C/template> See: Patterns - Forms",{"id":13870,"title":1364,"titles":13871,"content":528,"level":391},"/reference/faq#performance",[374],{"id":13873,"title":13874,"titles":13875,"content":13876,"level":449},"/reference/faq#q-how-do-i-optimize-for-large-datasets","Q: How do I optimize for large datasets?",[374,1364],"A: Use these strategies: Implement pagination - See Querying with PaginationUse server-side filtering - Filter in your database, not the frontendOptimize relations - Use server-side joins instead of multiple queries See: Troubleshooting - Performance Issues",{"id":13878,"title":183,"titles":13879,"content":528,"level":391},"/reference/faq#features",[374],{"id":13881,"title":13882,"titles":13883,"content":13884,"level":449},"/reference/faq#q-which-features-are-production-ready","Q: Which features are production-ready?",[374,183],"A: Stable ✅ - Production-ready (Internationalization)Beta 🔬 - Safe for non-critical use (Rich Text, Maps, Connectors, DevTools)Experimental ⚠️ - Use with caution (Assets, Events) See: Features Overview",{"id":13886,"title":13887,"titles":13888,"content":13889,"level":449},"/reference/faq#q-how-do-i-enable-internationalization-i18n","Q: How do I enable internationalization (i18n)?",[374,183],"A: Extend the i18n layer: // nuxt.config.ts\nexport default defineNuxtConfig({\n  extends: [\n    '@fyit/crouton',\n    '@fyit/crouton-i18n'\n  ]\n}) Mark fields as translatable: // crouton.config.js\nexport default {\n  translations: {\n    collections: {\n      products: ['name', 'description']\n    }\n  }\n} Query with locale:\nBind your query to the i18n locale reactively (see Querying with Filters for the pattern). See: Internationalization",{"id":13891,"title":68,"titles":13892,"content":528,"level":391},"/reference/faq#deployment",[374],{"id":13894,"title":13895,"titles":13896,"content":13897,"level":449},"/reference/faq#q-do-i-need-to-regenerate-code-before-deploying","Q: Do I need to regenerate code before deploying?",[374,68],"A: No, generated code is committed to your repository. Just deploy as normal: pnpm build",{"id":13899,"title":13900,"titles":13901,"content":13902,"level":449},"/reference/faq#q-what-environment-variables-do-i-need","Q: What environment variables do I need?",[374,68],"A: Depends on your setup: Database: NuxtHub with D1/SQLite (configured via hub: { db: 'sqlite' } in nuxt.config.ts, no DATABASE_URL needed) Team-based auth: BETTER_AUTH_SECRET - Session encryption (32+ chars)BETTER_AUTH_URL - Production URL Features: i18n: NUXT_PUBLIC_I18N_DEFAULT_LOCALEAssets: Storage provider credentials (S3, R2, etc.) Check your layer's documentation for specific requirements.",{"id":13904,"title":13905,"titles":13906,"content":528,"level":391},"/reference/faq#getting-help","Getting Help",[374],{"id":13908,"title":13909,"titles":13910,"content":13911,"level":449},"/reference/faq#q-where-can-i-get-help-if-my-question-isnt-answered-here","Q: Where can I get help if my question isn't answered here?",[374,13905],"A: Check Troubleshooting Guide for detailed solutionsSearch GitHub Issues for similar problemsAsk in GitHub Discussions for general questionsCreate an issue with:\nNuxt Crouton versionNuxt versionError messagesMinimal reproduction steps",{"id":13913,"title":1007,"titles":13914,"content":13915,"level":391},"/reference/faq#related-resources",[374],"Troubleshooting Guide - Detailed problem-solvingBest Practices - Recommended patternsConventions - Naming and organization standardsGlossary - Term definitions html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":379,"title":378,"titles":13917,"content":13918,"level":385},[],"Definitions of key terms and concepts in Nuxt Crouton This glossary defines the key terms and concepts used throughout Nuxt Crouton documentation.",{"id":13920,"title":13921,"titles":13922,"content":528,"level":391},"/reference/glossary#a","A",[378],{"id":13924,"title":13925,"titles":13926,"content":13927,"level":449},"/reference/glossary#api-endpoint","API Endpoint",[378,13921],"Server-side route that handles HTTP requests for a collection. Nuxt Crouton generates RESTful endpoints for CRUD operations: GET /api/teams/[teamId]/[layer]-[collection] - List itemsPOST /api/teams/[teamId]/[layer]-[collection] - Create itemPATCH /api/teams/[teamId]/[layer]-[collection]/[id] - Update itemDELETE /api/teams/[teamId]/[layer]-[collection]/[id] - Delete item See also: Server API",{"id":13929,"title":3010,"titles":13930,"content":13931,"level":449},"/reference/glossary#auto-generated-fields",[378,13921],"Fields automatically added to every collection by the generator: id - Always added (primary key)teamId, owner - Always added (team-scoped by default)createdAt, updatedAt, createdBy, updatedBy - Added when useMetadata: true (default) Important: Never define these manually in your schema. See also: Schema Format",{"id":13933,"title":13934,"titles":13935,"content":528,"level":391},"/reference/glossary#c","C",[378],{"id":13937,"title":2532,"titles":13938,"content":13939,"level":449},"/reference/glossary#cache-invalidation",[378,13934],"Process of clearing cached data when underlying data changes. Nuxt Crouton automatically invalidates caches after mutations using refreshNuxtData(). Example: // Invalidate all queries for a collection\nawait refreshNuxtData((key) => key.startsWith('collection:products:')) See also: Caching",{"id":13941,"title":13942,"titles":13943,"content":13944,"level":449},"/reference/glossary#collection","Collection",[378,13934],"The fundamental building block of Nuxt Crouton. A collection represents a data model (like products, users, or posts) and includes: Database schema (via Drizzle ORM)Generated CRUD componentsAPI endpointsTypeScript typesComposables for data operations Naming: Always use plural names (products, not product) See also: Collections",{"id":13946,"title":13947,"titles":13948,"content":13949,"level":449},"/reference/glossary#collection-schema","Collection Schema",[378,13934],"JSON file that defines the structure of a collection, including: Field names and typesValidation rulesRelationships to other collections (via refTarget)Metadata (label, required, etc.) Location: schemas/[name].json See also: Schema Format",{"id":13951,"title":13952,"titles":13953,"content":13954,"level":449},"/reference/glossary#component-override","Component Override",[378,13934],"Replacing a generated component with your own custom implementation by creating a component with the same name in your layer. Example: layers/shop/collections/products/app/components/\n└── _Form.vue  ← Generated form, yours to customize See also: Customization",{"id":13956,"title":7869,"titles":13957,"content":13958,"level":449},"/reference/glossary#composable",[378,13934],"Vue Composition API function that encapsulates reusable logic. Nuxt Crouton provides composables for data operations: useCollectionQuery() - Fetch datauseCollectionMutation() - Create/update/deleteuseCrouton() - Open forms in modals/slideovers/dialogsuseNotify() - User notifications Naming: Always start with use prefix See also: Composables API",{"id":13960,"title":13961,"titles":13962,"content":13963,"level":449},"/reference/glossary#core-layer","Core Layer",[378,13934],"The base Nuxt Crouton framework code in node_modules/@fyit/crouton. This code is never modified directly. See also: Architecture",{"id":13965,"title":13966,"titles":13967,"content":13968,"level":449},"/reference/glossary#crud","CRUD",[378,13934],"Create, Read, Update, Delete - The four basic operations for persistent storage. Nuxt Crouton auto-generates CRUD operations for every collection.",{"id":13970,"title":13971,"titles":13972,"content":528,"level":391},"/reference/glossary#d","D",[378],{"id":13974,"title":13975,"titles":13976,"content":13977,"level":449},"/reference/glossary#domain-driven-design-ddd","Domain-Driven Design (DDD)",[378,13971],"Software design approach that organizes code by business domain rather than technical function. Nuxt Crouton uses layers to implement DDD. Example: layers/\n├── shop/      # Shop domain (products, orders)\n├── blog/      # Blog domain (posts, authors)\n└── auth/      # Auth domain (users, sessions) See also: Architecture",{"id":13979,"title":13980,"titles":13981,"content":13982,"level":449},"/reference/glossary#drizzle-orm","Drizzle ORM",[378,13971],"TypeScript ORM (Object-Relational Mapping) used by Nuxt Crouton for database operations. Provides type-safe query builder. External: Drizzle ORM Docs See also: Drizzle Integration",{"id":13984,"title":13985,"titles":13986,"content":528,"level":391},"/reference/glossary#f","F",[378],{"id":13988,"title":13989,"titles":13990,"content":13991,"level":449},"/reference/glossary#field","Field",[378,13985],"A property of a collection that represents a single piece of data (like title, price, createdAt). Field Types: Text: string, textNumber: number, decimalBoolean: booleanDate: dateStructured: json, repeater, arrayMedia: image, file References use type: \"string\" with a refTarget property. Naming: Use camelCase (firstName, isActive, publishedAt) See also: Schema Format",{"id":13993,"title":13994,"titles":13995,"content":13996,"level":449},"/reference/glossary#form-component","Form Component",[378,13985],"Auto-generated Vue component that renders a form for creating/editing collection items. Includes validation, error handling, and save/cancel actions. Component: _Form.vue (generated per collection) Customization: Edit the generated _Form.vue directly, as it lives in your project. See also: Forms",{"id":13998,"title":13999,"titles":14000,"content":528,"level":391},"/reference/glossary#g","G",[378],{"id":14002,"title":14003,"titles":14004,"content":14005,"level":449},"/reference/glossary#generated-code","Generated Code",[378,13999],"Code automatically created by the Nuxt Crouton CLI based on your collection schemas. Includes components, composables, API routes, and database schema. Location: layers/[collection-name]/ Regeneration: Safe to regenerate without losing customizations if you follow conventions See also: Generated Code",{"id":14007,"title":14008,"titles":14009,"content":14010,"level":449},"/reference/glossary#generated-layer","Generated Layer",[378,13999],"A Nuxt layer created by the Nuxt Crouton generator for a specific collection or domain. Contains all generated code for that collection. Example: layers/products/  ← Generated layer for products collection\n├── components/\n├── composables/\n├── server/\n└── types/ See also: Layers",{"id":14012,"title":14013,"titles":14014,"content":528,"level":391},"/reference/glossary#i","I",[378],{"id":14016,"title":191,"titles":14017,"content":14018,"level":449},"/reference/glossary#internationalization-i18n",[378,14013],"Multi-language support feature that allows translating collection fields into multiple languages. Status: Stable ✅ See also: Internationalization",{"id":14020,"title":14021,"titles":14022,"content":528,"level":391},"/reference/glossary#l","L",[378],{"id":14024,"title":14025,"titles":14026,"content":14027,"level":449},"/reference/glossary#layer","Layer",[378,14021],"Nuxt's mechanism for extending applications with reusable code. Nuxt Crouton uses layers to organize collections by domain. Types: Core Layer: Framework code (@fyit/crouton)Generated Layer: Auto-generated collection code (layers/products/)Feature Layer: Optional features (@fyit/crouton-i18n) External: Nuxt Layers Guide See also: Architecture",{"id":14029,"title":14030,"titles":14031,"content":528,"level":391},"/reference/glossary#m","M",[378],{"id":14033,"title":14034,"titles":14035,"content":14036,"level":449},"/reference/glossary#migration","Migration",[378,14030],"Database schema change managed by Drizzle ORM. Generated automatically when schemas change. See also: Migration Guide",{"id":14038,"title":14039,"titles":14040,"content":14041,"level":449},"/reference/glossary#mutation","Mutation",[378,14030],"Operation that modifies data (create, update, delete). In Nuxt Crouton, handled by useCollectionMutation() or useCroutonMutate(). See also: Mutation Composables API, Data Operations",{"id":14043,"title":14044,"titles":14045,"content":528,"level":391},"/reference/glossary#n","N",[378],{"id":14047,"title":14048,"titles":14049,"content":14050,"level":449},"/reference/glossary#nuxt-ui","Nuxt UI",[378,14044],"Component library used by Nuxt Crouton for UI elements (forms, tables, modals, buttons, etc.). Version: Nuxt Crouton uses Nuxt UI v4 External: Nuxt UI Docs",{"id":14052,"title":14053,"titles":14054,"content":528,"level":391},"/reference/glossary#o","O",[378],{"id":14056,"title":14057,"titles":14058,"content":14059,"level":449},"/reference/glossary#override","Override",[378,14053],"See Component Override",{"id":14061,"title":14062,"titles":14063,"content":528,"level":391},"/reference/glossary#p","P",[378],{"id":14065,"title":11558,"titles":14066,"content":14067,"level":449},"/reference/glossary#pagination",[378,14062],"Splitting large datasets into smaller pages for performance and usability. Pagination Examples: For complete pagination patterns, see Querying Data. See also: Patterns - Tables",{"id":14069,"title":14070,"titles":14071,"content":528,"level":391},"/reference/glossary#q","Q",[378],{"id":14073,"title":14074,"titles":14075,"content":14076,"level":449},"/reference/glossary#query","Query",[378,14070],"Operation that retrieves data without modifying it. In Nuxt Crouton, handled by useCollectionQuery(). Query Examples: For complete useCollectionQuery patterns including filters and sorting, see Querying Data. See also: Querying Data",{"id":14078,"title":14079,"titles":14080,"content":528,"level":391},"/reference/glossary#r","R",[378],{"id":14082,"title":14083,"titles":14084,"content":14085,"level":449},"/reference/glossary#reference-field","Reference Field",[378,14079],"A field that creates a relationship between collections. Uses type: \"string\" with a refTarget property to specify the target collection. Example: {\n  \"authorId\": { \"type\": \"string\", \"refTarget\": \"users\" }\n} See also: Relations",{"id":14087,"title":14088,"titles":14089,"content":14090,"level":449},"/reference/glossary#reftarget","refTarget",[378,14079],"Property of a reference field that specifies which collection it points to. Must be a plural collection name. Example: { \"categoryId\": { \"type\": \"string\", \"refTarget\": \"categories\" } } See also: Relations",{"id":14092,"title":14093,"titles":14094,"content":14095,"level":449},"/reference/glossary#relation","Relation",[378,14079],"Connection between two collections, implemented using reference fields. Types: One-to-Many: One user has many postsMany-to-Many: Many posts have many tags See also: Relations",{"id":14097,"title":14098,"titles":14099,"content":528,"level":391},"/reference/glossary#s","S",[378],{"id":14101,"title":14102,"titles":14103,"content":14104,"level":449},"/reference/glossary#schema","Schema",[378,14098],"See Collection Schema",{"id":14106,"title":14107,"titles":14108,"content":14109,"level":449},"/reference/glossary#slot","Slot",[378,14098],"Vue mechanism for customizing component content. Generated components like _Form.vue and List.vue live in your project and can be edited directly for customization. See also: Customization",{"id":14111,"title":8356,"titles":14112,"content":14113,"level":449},"/reference/glossary#stability-status",[378,14098],"Label indicating production-readiness of a feature: Stable ✅ - Production-ready, API stableBeta 🔬 - Feature-complete, minor changes possibleExperimental ⚠️ - Under development, API may change See also: Features Overview",{"id":14115,"title":14116,"titles":14117,"content":528,"level":391},"/reference/glossary#t","T",[378],{"id":14119,"title":14120,"titles":14121,"content":14122,"level":449},"/reference/glossary#list-component","List Component",[378,14116],"Auto-generated Vue component that displays collection items with sorting, filtering, and pagination. Component: List.vue (generated per collection) Customization: Edit the generated List.vue directly, as it lives in your project. See also: Tables",{"id":14124,"title":14125,"titles":14126,"content":14127,"level":449},"/reference/glossary#type-safety","Type Safety",[378,14116],"TypeScript feature that catches errors at compile-time. Nuxt Crouton generates TypeScript types for all collections. Example: import type { Product } from '~/layers/shop/collections/products/types'\nconst { items } = await useCollectionQuery\u003CProduct>('products')\n// items is typed as Product[] See also: TypeScript Types",{"id":14129,"title":14130,"titles":14131,"content":528,"level":391},"/reference/glossary#v","V",[378],{"id":14133,"title":12720,"titles":14134,"content":14135,"level":449},"/reference/glossary#validation",[378,14130],"Checking data against rules before saving. Nuxt Crouton uses Zod for schema validation. Example: import { z } from 'zod'\n\nconst schema = z.object({\n  title: z.string().min(1, 'Required'),\n  price: z.number().min(0, 'Must be positive')\n}) See also: Forms",{"id":14137,"title":14138,"titles":14139,"content":528,"level":391},"/reference/glossary#y","Y",[378],{"id":14141,"title":14142,"titles":14143,"content":14144,"level":449},"/reference/glossary#json-schema","JSON Schema",[378,14138],"JSON format used for collection schema definitions. Example: {\n  \"title\": { \"type\": \"string\", \"meta\": { \"required\": true } },\n  \"description\": { \"type\": \"text\" }\n} See also: Schema Format",{"id":14146,"title":1007,"titles":14147,"content":14148,"level":391},"/reference/glossary#related-resources",[378],"Conventions - Naming and coding standardsFAQ - Common questionsTroubleshooting - Problem-solving guide html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"id":364,"title":363,"titles":14150,"content":14151,"level":385},[],"Conventions, FAQ, and glossary for Nuxt Crouton",{"id":14153,"title":363,"titles":14154,"content":14155,"level":385},"/reference#reference",[],"Quick lookup resources for Nuxt Crouton development.",{"id":14157,"title":2256,"titles":14158,"content":14159,"level":391},"/reference#contents",[363],"Naming conventions, file structure, and code style guidelines.Frequently asked questions and common issues.Definitions of terms used throughout the documentation.",{"packages":14161},[14162,14167,14170,14173,14176,14179,14183,14186,14189,14192,14196,14199,14202,14205],{"name":14163,"repo":14164,"priority":14165,"enabled":14166},"nuxt","nuxt/nuxt","critical",true,{"name":14168,"repo":14169,"priority":14165,"enabled":14166},"vue","vuejs/core",{"name":14171,"repo":14172,"priority":14165,"enabled":14166},"@nuxt/ui","nuxt/ui",{"name":14174,"repo":14175,"priority":14165,"enabled":14166},"drizzle-orm","drizzle-team/drizzle-orm",{"name":14177,"repo":14178,"priority":14165,"enabled":14166},"better-auth","better-auth/better-auth",{"name":14180,"repo":14181,"priority":14182,"enabled":14166},"@vueuse/core","vueuse/vueuse","high",{"name":14184,"repo":14185,"priority":14182,"enabled":14166},"ai","vercel/ai",{"name":14187,"repo":14188,"priority":14182,"enabled":14166},"stripe","stripe/stripe-node",{"name":14190,"repo":14191,"priority":14182,"enabled":14166},"typescript","microsoft/TypeScript",{"name":14193,"repo":14194,"priority":14195,"enabled":14166},"@nuxtjs/i18n","nuxt-modules/i18n","medium",{"name":14197,"repo":14198,"priority":14195,"enabled":14166},"@nuxthub/core","nuxt-hub/core",{"name":14200,"repo":14201,"priority":14195,"enabled":14166},"zod","colinhacks/zod",{"name":14203,"repo":14204,"priority":14195,"enabled":14166},"@nuxt/content","nuxt/content",{"name":14206,"repo":14207,"priority":14195,"enabled":14166},"nitro","nitrojs/nitro",{"releases":14209,"total":14655,"lastSyncedAt":14656,"pagination":14657},[14210,14223,14233,14242,14251,14260,14269,14278,14287,14296,14305,14314,14323,14332,14341,14350,14359,14368,14376,14385,14394,14403,14412,14421,14430,14439,14447,14456,14464,14472,14481,14489,14497,14505,14514,14523,14532,14541,14549,14557,14565,14574,14583,14592,14601,14610,14619,14628,14637,14646],{"id":14211,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14212,"name":14212,"body":14213,"htmlUrl":14214,"publishedAt":14215,"isPrerelease":27,"summary":14216,"breakingChanges":14217,"newFeatures":14218,"importance":14219,"relevanceScore":14220,"processedAt":14221,"model":14222},"vercel/ai:307279192","ai@6.0.156","### Patch Changes\n\n-   Updated dependencies [08c5ac3]\n    -   @ai-sdk/gateway@3.0.95\n","https://github.com/vercel/ai/releases/tag/ai%406.0.156","2026-04-09T21:14:43Z","AI summary unavailable. See full release notes.",[],[],"minor",50,"2026-04-10T06:34:38.461Z","claude-3-5-haiku-20241022",{"id":14224,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14225,"name":14225,"body":14226,"htmlUrl":14227,"publishedAt":14228,"isPrerelease":27,"summary":14229,"breakingChanges":14230,"newFeatures":14231,"importance":14219,"relevanceScore":14220,"processedAt":14232,"model":14222},"vercel/ai:307279179","@ai-sdk/vue@3.0.156","### Patch Changes\n\n-   ai@6.0.156\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/vue%403.0.156","2026-04-09T21:14:40Z","See full release notes for details.",[],[],"2026-04-10T06:34:40.054Z",{"id":14234,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14235,"name":14235,"body":14236,"htmlUrl":14237,"publishedAt":14238,"isPrerelease":27,"summary":14216,"breakingChanges":14239,"newFeatures":14240,"importance":14219,"relevanceScore":14220,"processedAt":14241,"model":14222},"vercel/ai:307276140","ai@5.0.172","### Patch Changes\n\n-   Updated dependencies [a5317d0]\n    -   @ai-sdk/gateway@2.0.76\n","https://github.com/vercel/ai/releases/tag/ai%405.0.172","2026-04-09T21:06:29Z",[],[],"2026-04-10T06:34:39.555Z",{"id":14243,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14244,"name":14244,"body":14245,"htmlUrl":14246,"publishedAt":14247,"isPrerelease":27,"summary":14216,"breakingChanges":14248,"newFeatures":14249,"importance":14219,"relevanceScore":14220,"processedAt":14250,"model":14222},"vercel/ai:307257149","ai@6.0.155","### Patch Changes\n\n-   06764c5: fix(ai): skip passing invalid JSON inputs to response messages\n","https://github.com/vercel/ai/releases/tag/ai%406.0.155","2026-04-09T20:21:27Z",[],[],"2026-04-10T06:34:39.010Z",{"id":14252,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14253,"name":14253,"body":14254,"htmlUrl":14255,"publishedAt":14256,"isPrerelease":27,"summary":14216,"breakingChanges":14257,"newFeatures":14258,"importance":14219,"relevanceScore":14220,"processedAt":14259,"model":14222},"vercel/ai:307257069","@ai-sdk/vue@3.0.155","### Patch Changes\n\n-   Updated dependencies [06764c5]\n    -   ai@6.0.155\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/vue%403.0.155","2026-04-09T20:21:15Z",[],[],"2026-04-10T06:34:40.600Z",{"id":14261,"packageName":14177,"packageRepo":14178,"packagePriority":14165,"tagName":14262,"name":14262,"body":14263,"htmlUrl":14264,"publishedAt":14265,"isPrerelease":27,"summary":14216,"breakingChanges":14266,"newFeatures":14267,"importance":14219,"relevanceScore":14220,"processedAt":14268,"model":14222},"better-auth/better-auth:307104464","v1.6.2","## `better-auth`\n\n### ❗ Breaking Changes\n\n- Prevented unverified TOTP enrollment from blocking sign-in ([#8711](https://github.com/better-auth/better-auth/pull/8711))\n> **Migration:** Adds a `verified` column to the `twoFactor` table (defaults to `true`). Existing rows are unaffected. No data migration required.\n\n### Features\n\n- Included enabled 2FA methods in sign-in redirect response ([#8772](https://github.com/better-auth/better-auth/pull/8772))\n\n### Bug Fixes\n\n- Fixed OAuth state verification against cookie-stored nonce to prevent CSRF ([#8949](https://github.com/better-auth/better-auth/pull/8949))\n- Fixed infinite router refresh loops in `nextCookies()` by replacing cookie probe with header-based RSC detection ([#9059](https://github.com/better-auth/better-auth/pull/9059))\n- Fixed cross-provider account collision in link-social callback ([#8983](https://github.com/better-auth/better-auth/pull/8983))\n- Included `RelayState` in signed SAML AuthnRequests ([#9058](https://github.com/better-auth/better-auth/pull/9058))\n\nFor detailed changes, see [`CHANGELOG`](https://github.com/better-auth/better-auth/blob/3c12c2043a0be4bbc4438f32e115c381550edce3/packages/better-auth/CHANGELOG.md)\n\n## `@better-auth/oauth-provider`\n\n### Bug Fixes\n\n- Fixed multi-valued query params collapsing through prompt redirects ([#9060](https://github.com/better-auth/better-auth/pull/9060))\n- Rejected `skip_consent` at schema level in dynamic client registration ([#8998](https://github.com/better-auth/better-auth/pull/8998))\n\nFor detailed changes, see [`CHANGELOG`](https://github.com/better-auth/better-auth/blob/3c12c2043a0be4bbc4438f32e115c381550edce3/packages/oauth-provider/CHANGELOG.md)\n\n## `@better-auth/sso`\n\n### Bug Fixes\n\n- Fixed SAMLResponse decoding failures caused by line-wrapped base64 ([#8968](https://github.com/better-auth/better-auth/pull/8968))\n\nFor detailed changes, see [`CHANGELOG`](https://github.com/better-auth/better-auth/blob/3c12c2043a0be4bbc4438f32e115c381550edce3/packages/sso/CHANGELOG.md)\n\n## Contributors\n\nThanks to everyone who contributed to this release:\n\n@aarmful, @cyphercodes, @dvanmali, @gustavovalverde, @jaydeep-pipaliya, @ping-maxwell\n\n**Full changelog:** [`v1.6.1...v1.6.2`](https://github.com/better-auth/better-auth/compare/v1.6.1...v1.6.2)\n\n","https://github.com/better-auth/better-auth/releases/tag/v1.6.2","2026-04-09T14:20:45Z",[],[],"2026-04-10T06:34:34.419Z",{"id":14270,"packageName":14187,"packageRepo":14188,"packagePriority":14182,"tagName":14271,"name":14271,"body":14272,"htmlUrl":14273,"publishedAt":14274,"isPrerelease":14166,"summary":14216,"breakingChanges":14275,"newFeatures":14276,"importance":14219,"relevanceScore":14220,"processedAt":14277,"model":14222},"stripe/stripe-node:306787338","v22.1.0-alpha.2","* [#2657](https://github.com/stripe/stripe-node/pull/2657) Update generated code for private-preview\n  * Add support for `payment_record` on `ApplicationFee.fee_source`\n  * Add support for `beneficiary_account`, `beneficiary_details`, `sender_account`, and `sender_details` on `ChargeCaptureParams.payment_details.money_services.account_funding`, `ChargeUpdateParams.payment_details.money_services.account_funding`, `PaymentIntentCaptureParams.payment_details.money_services.account_funding`, `PaymentIntentConfirmParams.payment_details.money_services.account_funding`, `PaymentIntentCreateParams.payment_details.money_services.account_funding`, and `PaymentIntentUpdateParams.payment_details.money_services.account_funding`\n  * Change type of `ChargeCaptureParams.payment_details.money_services.transaction_type`, `ChargeUpdateParams.payment_details.money_services.transaction_type`, `PaymentIntentCaptureParams.payment_details.money_services.transaction_type`, `PaymentIntentConfirmParams.payment_details.money_services.transaction_type`, `PaymentIntentCreateParams.payment_details.money_services.transaction_type`, and `PaymentIntentUpdateParams.payment_details.money_services.transaction_type` from `literal('account_funding')` to `emptyable(literal('account_funding'))`\n  * Add support for `bizum` on `Invoice.payment_settings.payment_method_options`, `InvoiceCreateParams.payment_settings.payment_method_options`, `InvoiceUpdateParams.payment_settings.payment_method_options`, `QuotePreviewInvoice.payment_settings.payment_method_options`, `Subscription.payment_settings.payment_method_options`, `SubscriptionCreateParams.payment_settings.payment_method_options`, and `SubscriptionUpdateParams.payment_settings.payment_method_options`\n  * ⚠️ Add support for new value `bizum` on enums `Invoice.payment_settings.payment_method_types`, `InvoiceCreateParams.payment_settings.payment_method_types`, `InvoiceUpdateParams.payment_settings.payment_method_types`, `QuotePreviewInvoice.payment_settings.payment_method_types`, `Subscription.payment_settings.payment_method_types`, `SubscriptionCreateParams.payment_settings.payment_method_types`, and `SubscriptionUpdateParams.payment_settings.payment_method_types`\n  * Add support for `quantity_precision` on `PaymentIntentAmountDetailsLineItem`, `PaymentIntentCaptureParams.amount_details.line_items[]`, `PaymentIntentConfirmParams.amount_details.line_items[]`, `PaymentIntentCreateParams.amount_details.line_items[]`, `PaymentIntentDecrementAuthorizationParams.amount_details.line_items[]`, `PaymentIntentIncrementAuthorizationParams.amount_details.line_items[]`, and `PaymentIntentUpdateParams.amount_details.line_items[]`\n  * Add support for `liquid_asset` and `wallet` on `PaymentIntentConfirmParams.payment_method_options.card.payment_details.money_services.account_funding`, `PaymentIntentConfirmParams.payment_method_options.card_present.payment_details.money_services.account_funding`, `PaymentIntentCreateParams.payment_method_options.card.payment_details.money_services.account_funding`, `PaymentIntentCreateParams.payment_method_options.card_present.payment_details.money_services.account_funding`, `PaymentIntentUpdateParams.payment_method_options.card.payment_details.money_services.account_funding`, and `PaymentIntentUpdateParams.payment_method_options.card_present.payment_details.money_services.account_funding`\n  * Add support for `shared_payment_granted_token` on `PaymentMethod`\n  * ⚠️ Change type of `Radar.CustomerEvaluation.event_type` from `string` to `enum('login'|'registration')`\n  * ⚠️ Change type of `Radar.CustomerEvaluation.signals.account_sharing.risk_level` and `Radar.CustomerEvaluation.signals.multi_accounting.risk_level` from `string` to `enum`\n  * Add support for `data` on `Radar.PaymentEvaluation.client_device_metadata_details` and `Radar.PaymentEvaluationCreateParams.client_device_metadata_details`\n  * Add support for `sunbit` on `SharedPayment.GrantedToken.payment_method_details`\n  * ⚠️ Add support for new value `sunbit` on enum `SharedPayment.GrantedToken.payment_method_details.type`\n  * ⚠️ Remove support for values `bm_crn`, `bo_tin`, `bt_tpn`, `co_nit`, `ec_ruc`, `eg_tin`, `gh_tin`, `gy_tin`, `hn_rtn`, `jm_trn`, `jo_crn`, `ke_pin`, `ky_crn`, `lk_tin`, `mo_tin`, `mv_tin`, `ng_tin`, `pa_ruc`, `ph_tin`, `py_ruc`, `sl_tin`, `sv_nit`, `uy_ruc`, `vg_cn`, and `za_tin` from enums `V2.Core.Account.identity.business_details.id_numbers[].type`, `V2.Core.AccountCreateParams.identity.business_details.id_numbers[].type`, `V2.Core.AccountTokenCreateParams.identity.business_details.id_numbers[].type`, and `V2.Core.AccountUpdateParams.identity.business_details.id_numbers[].type`\n  * ⚠️ Remove support for values `bm_pp`, `bo_ci`, `bt_cid`, `eg_tin`, `gh_pin`, `gy_tin`, `hn_rtn`, `jm_trn`, `jo_pin`, `ky_pp`, `lk_nic`, `mo_bir`, `mt_nic`, `mv_tin`, `pa_ruc`, `ph_tin`, `py_ruc`, `si_pin`, `sv_nit`, and `vg_pp` from enums `V2.Core.Account.identity.individual.id_numbers[].type`, `V2.Core.AccountCreateParams.identity.individual.id_numbers[].type`, `V2.Core.AccountPerson.id_numbers[].type`, `V2.Core.AccountPersonCreateParams.id_numbers[].type`, `V2.Core.AccountPersonTokenCreateParams.id_numbers[].type`, `V2.Core.AccountPersonUpdateParams.id_numbers[].type`, `V2.Core.AccountTokenCreateParams.identity.individual.id_numbers[].type`, and `V2.Core.AccountUpdateParams.identity.individual.id_numbers[].type`\n  * Add support for error type `CannotProceedError`\n\nSee [the changelog for more details](https://github.com/stripe/stripe-node/blob/v22.1.0-alpha.2/CHANGELOG.md).\n","https://github.com/stripe/stripe-node/releases/tag/v22.1.0-alpha.2","2026-04-08T20:43:54Z",[],[],"2026-04-09T06:31:14.737Z",{"id":14279,"packageName":14177,"packageRepo":14178,"packagePriority":14165,"tagName":14280,"name":14280,"body":14281,"htmlUrl":14282,"publishedAt":14283,"isPrerelease":27,"summary":14216,"breakingChanges":14284,"newFeatures":14285,"importance":14219,"relevanceScore":14220,"processedAt":14286,"model":14222},"better-auth/better-auth:306759326","v1.6.1","> Install: `npm i better-auth@latest`\n\n## Core\n\n- endpoint instrumentation to always use route template ([#9023](https://github.com/better-auth/better-auth/pull/9023)) by @jonathansamines\n- restore getSession accessibility in generic Auth\u003CO> context ([#9017](https://github.com/better-auth/better-auth/pull/9017)) by @bytaesu\n- Update endpoint instrumentation to always use endpoint routes\n- use `INVALID_PASSWORD` for all `checkPassword` failures ([#8902](https://github.com/better-auth/better-auth/pull/8902)) by @ping-maxwell\n\n**Full changelog**: [`v1.6.0...v1.6.1`](https://github.com/better-auth/better-auth/compare/v1.6.0...v1.6.1)","https://github.com/better-auth/better-auth/releases/tag/v1.6.1","2026-04-08T19:31:17Z",[],[],"2026-04-09T06:31:07.484Z",{"id":14288,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14289,"name":14289,"body":14290,"htmlUrl":14291,"publishedAt":14292,"isPrerelease":27,"summary":14216,"breakingChanges":14293,"newFeatures":14294,"importance":14219,"relevanceScore":14220,"processedAt":14295,"model":14222},"vercel/ai:306755534","ai@5.0.171","### Patch Changes\n\n-   Updated dependencies [f03fec2]\n    -   @ai-sdk/gateway@2.0.75\n","https://github.com/vercel/ai/releases/tag/ai%405.0.171","2026-04-08T19:20:23Z",[],[],"2026-04-09T06:31:12.640Z",{"id":14297,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14298,"name":14298,"body":14299,"htmlUrl":14300,"publishedAt":14301,"isPrerelease":27,"summary":14216,"breakingChanges":14302,"newFeatures":14303,"importance":14219,"relevanceScore":14220,"processedAt":14304,"model":14222},"vercel/ai:306708308","ai@6.0.154","### Patch Changes\n\n-   Updated dependencies [37a378e]\n    -   @ai-sdk/gateway@3.0.94\n","https://github.com/vercel/ai/releases/tag/ai%406.0.154","2026-04-08T17:36:55Z",[],[],"2026-04-09T06:31:11.897Z",{"id":14306,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14307,"name":14307,"body":14308,"htmlUrl":14309,"publishedAt":14310,"isPrerelease":27,"summary":14216,"breakingChanges":14311,"newFeatures":14312,"importance":14219,"relevanceScore":14220,"processedAt":14313,"model":14222},"vercel/ai:306708285","@ai-sdk/xai@3.0.82","### Patch Changes\n\n-   72ebb54: fix (provider/xai): handle mid-stream error chunks\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/xai%403.0.82","2026-04-08T17:36:52Z",[],[],"2026-04-09T06:31:13.198Z",{"id":14315,"packageName":14187,"packageRepo":14188,"packagePriority":14182,"tagName":14316,"name":14316,"body":14317,"htmlUrl":14318,"publishedAt":14319,"isPrerelease":14166,"summary":14216,"breakingChanges":14320,"newFeatures":14321,"importance":14219,"relevanceScore":14220,"processedAt":14322,"model":14222},"stripe/stripe-node:306653545","v22.1.0-beta.2","Please review the [changelog for 22.0.1](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md#2201---2026-04-08) for more information about changes in this release.\n\nSee [the changelog for more details](https://github.com/stripe/stripe-node/blob/v22.1.0-beta.2/CHANGELOG.md).\n","https://github.com/stripe/stripe-node/releases/tag/v22.1.0-beta.2","2026-04-08T15:25:47Z",[],[],"2026-04-09T06:31:14.176Z",{"id":14324,"packageName":14187,"packageRepo":14188,"packagePriority":14182,"tagName":14325,"name":14325,"body":14326,"htmlUrl":14327,"publishedAt":14328,"isPrerelease":27,"summary":14216,"breakingChanges":14329,"newFeatures":14330,"importance":14219,"relevanceScore":14220,"processedAt":14331,"model":14222},"stripe/stripe-node:306652755","v22.0.1","* [#2669](https://github.com/stripe/stripe-node/pull/2669) Add constructor based initialization for CJS based TypeScript imports\n  * Initialization of Stripe class with `new` keyword is now possible for CJS based TypeScript project. Resolves: [2660](https://github.com/stripe/stripe-node/issues/2660)\r\n  ```ts\r\n  import Stripe = require('stripe');\r\n  // ✅ Both statements work\r\n  const stripeNew: Stripe.Stripe = new Stripe('sk_test_...');\r\n  const stripeCalled: Stripe.Stripe = Stripe('sk_test_...');\r\n  ```\n* [#2664](https://github.com/stripe/stripe-node/pull/2664) Fixed nested service param exports in the Stripe namespace\n  * Resolves: [2658](https://github.com/stripe/stripe-node/issues/2658),  [2662](https://github.com/stripe/stripe-node/issues/2662)\n* [#2667](https://github.com/stripe/stripe-node/pull/2667) Add type safety to Stripe constructor config (no runtime change)\n  - Fixed some compile-time checks (no runtime changes)\r\n    - Fixed `Stripe` constructor config parameter to use `StripeConfig` type instead of `Record\u003Cstring, unknown>`, restoring compile-time type safety.\r\n    - Added missing `authenticator` property to `StripeConfig`.\r\n    - Fixed `Stripe.API_VERSION` to retain the literal API version type.\r\n    - Fixed `StripeConfig.stripeContext` to accept `StripeContext` objects in addition to strings.\r\n* [#2663](https://github.com/stripe/stripe-node/pull/2663) Throw a more descriptive error when calling `rawRequest` with absolute urls\n* [#2652](https://github.com/stripe/stripe-node/pull/2652) Added `string[]` to `WebhookHeader` type for compatibility with express\n  * Added `string[]` to the type of `signature` param in `stripe.webhooks.construct_event` method.\n\nSee [the changelog for more details](https://github.com/stripe/stripe-node/blob/v22.0.1/CHANGELOG.md).\n","https://github.com/stripe/stripe-node/releases/tag/v22.0.1","2026-04-08T15:23:58Z",[],[],"2026-04-09T06:31:15.309Z",{"id":14333,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14334,"name":14334,"body":14335,"htmlUrl":14336,"publishedAt":14337,"isPrerelease":27,"summary":14216,"breakingChanges":14338,"newFeatures":14339,"importance":14219,"relevanceScore":14220,"processedAt":14340,"model":14222},"vercel/ai:306407505","@ai-sdk/xai@2.0.66","### Patch Changes\n\n-   92e25ae: fix (provider/xai): add response.incomplete and response.failed streaming event handling\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/xai%402.0.66","2026-04-08T05:35:03Z",[],[],"2026-04-08T06:29:57.384Z",{"id":14342,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14343,"name":14343,"body":14344,"htmlUrl":14345,"publishedAt":14346,"isPrerelease":27,"summary":14216,"breakingChanges":14347,"newFeatures":14348,"importance":14219,"relevanceScore":14220,"processedAt":14349,"model":14222},"vercel/ai:306403649","@ai-sdk/xai@3.0.81","### Patch Changes\n\n-   c1cc97f: fix (provider/xai): add response.incomplete and response.failed streaming event handling\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/xai%403.0.81","2026-04-08T05:16:17Z",[],[],"2026-04-08T06:29:56.834Z",{"id":14351,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14352,"name":14352,"body":14353,"htmlUrl":14354,"publishedAt":14355,"isPrerelease":27,"summary":14216,"breakingChanges":14356,"newFeatures":14357,"importance":14219,"relevanceScore":14220,"processedAt":14358,"model":14222},"vercel/ai:306397561","@ai-sdk/vue@3.0.153","### Patch Changes\n\n-   Updated dependencies [f152133]\n    -   ai@6.0.153\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/vue%403.0.153","2026-04-08T04:43:44Z",[],[],"2026-04-08T06:29:57.936Z",{"id":14360,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14361,"name":14361,"body":14362,"htmlUrl":14363,"publishedAt":14364,"isPrerelease":27,"summary":14216,"breakingChanges":14365,"newFeatures":14366,"importance":14219,"relevanceScore":14220,"processedAt":14367,"model":14222},"vercel/ai:306397521","ai@6.0.153","### Patch Changes\n\n-   f152133: feat (ai/core): support plain string model IDs in `rerank()` function\n\n    The `rerank()` function now accepts plain model strings (e.g., `'cohere/rerank-v3.5'`) in addition to `RerankingModel` objects, matching the behavior of `generateText`, `embed`, and other core functions.\n","https://github.com/vercel/ai/releases/tag/ai%406.0.153","2026-04-08T04:43:35Z",[],[],"2026-04-08T06:29:56.248Z",{"id":14369,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14370,"name":14370,"body":14353,"htmlUrl":14371,"publishedAt":14372,"isPrerelease":27,"summary":14216,"breakingChanges":14373,"newFeatures":14374,"importance":14219,"relevanceScore":14220,"processedAt":14375,"model":14222},"vercel/ai:306397508","@ai-sdk/svelte@4.0.153","https://github.com/vercel/ai/releases/tag/%40ai-sdk/svelte%404.0.153","2026-04-08T04:43:32Z",[],[],"2026-04-08T06:29:58.499Z",{"id":14377,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14378,"name":14378,"body":14379,"htmlUrl":14380,"publishedAt":14381,"isPrerelease":27,"summary":14216,"breakingChanges":14382,"newFeatures":14383,"importance":14219,"relevanceScore":14220,"processedAt":14384,"model":14222},"vercel/ai:305861158","ai@5.0.169","### Patch Changes\n\n-   Updated dependencies [c6fcbfa]\n    -   @ai-sdk/gateway@2.0.73\n","https://github.com/vercel/ai/releases/tag/ai%405.0.169","2026-04-06T23:15:56Z",[],[],"2026-04-07T06:28:57.933Z",{"id":14386,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14387,"name":14387,"body":14388,"htmlUrl":14389,"publishedAt":14390,"isPrerelease":27,"summary":14216,"breakingChanges":14391,"newFeatures":14392,"importance":14219,"relevanceScore":14220,"processedAt":14393,"model":14222},"vercel/ai:305792984","ai@6.0.149","### Patch Changes\n\n-   Updated dependencies [3aca847]\n    -   @ai-sdk/gateway@3.0.91\n","https://github.com/vercel/ai/releases/tag/ai%406.0.149","2026-04-06T19:26:38Z",[],[],"2026-04-07T06:28:56.261Z",{"id":14395,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14396,"name":14396,"body":14397,"htmlUrl":14398,"publishedAt":14399,"isPrerelease":27,"summary":14216,"breakingChanges":14400,"newFeatures":14401,"importance":14219,"relevanceScore":14220,"processedAt":14402,"model":14222},"vercel/ai:305771976","ai@6.0.148","### Patch Changes\n\n-   Updated dependencies [e923a24]\n    -   @ai-sdk/gateway@3.0.90\n","https://github.com/vercel/ai/releases/tag/ai%406.0.148","2026-04-06T18:25:12Z",[],[],"2026-04-07T06:28:56.835Z",{"id":14404,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14405,"name":14405,"body":14406,"htmlUrl":14407,"publishedAt":14408,"isPrerelease":27,"summary":14216,"breakingChanges":14409,"newFeatures":14410,"importance":14219,"relevanceScore":14220,"processedAt":14411,"model":14222},"vercel/ai:305747148","ai@5.0.168","### Patch Changes\n\n-   Updated dependencies [a064eef]\n    -   @ai-sdk/gateway@2.0.72\n","https://github.com/vercel/ai/releases/tag/ai%405.0.168","2026-04-06T17:17:46Z",[],[],"2026-04-07T06:28:58.474Z",{"id":14413,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14414,"name":14414,"body":14415,"htmlUrl":14416,"publishedAt":14417,"isPrerelease":27,"summary":14216,"breakingChanges":14418,"newFeatures":14419,"importance":14219,"relevanceScore":14220,"processedAt":14420,"model":14222},"vercel/ai:305729653","ai@6.0.147","### Patch Changes\n\n-   Updated dependencies [6247886]\n    -   @ai-sdk/provider-utils@4.0.23\n    -   @ai-sdk/gateway@3.0.89\n","https://github.com/vercel/ai/releases/tag/ai%406.0.147","2026-04-06T16:27:34Z",[],[],"2026-04-07T06:28:57.381Z",{"id":14422,"packageName":14177,"packageRepo":14178,"packagePriority":14165,"tagName":14423,"name":14423,"body":14424,"htmlUrl":14425,"publishedAt":14426,"isPrerelease":27,"summary":14216,"breakingChanges":14427,"newFeatures":14428,"importance":14219,"relevanceScore":14220,"processedAt":14429,"model":14222},"better-auth/better-auth:305728617","v1.6.0","> Install: `npm i better-auth@latest`\n\n## Core\n\n- **BREAKING:** align fresh age with session creation time ([#8762](https://github.com/better-auth/better-auth/pull/8762)) by @bytaesu\n- compare account cookie by provider accountId instead of internal id ([#8786](https://github.com/better-auth/better-auth/pull/8786)) by @bytaesu\n- don't mark redirect APIErrors as span errors ([#8850](https://github.com/better-auth/better-auth/pull/8850)) by @GoPro16\n- enforce authorization on SCIM management endpoints and normalize passkey ownership ([#8843](https://github.com/better-auth/better-auth/pull/8843)) by @gustavovalverde\n- expose plugin version ([#8750](https://github.com/better-auth/better-auth/pull/8750)) by @jonathansamines\n- normalize missing resolver path ([#8589](https://github.com/better-auth/better-auth/pull/8589)) by @mrgrauel\n- prevent `any` from collapsing base type and client inference ([#8981](https://github.com/better-auth/better-auth/pull/8981)) by @bytaesu\n- set stateless cookieCache maxAge to match session expiresIn ([#8648](https://github.com/better-auth/better-auth/pull/8648)) by @himself65\n- turbo caching, enforce lockfile integrity, expand pre-commit hooks ([#8892](https://github.com/better-auth/better-auth/pull/8892)) by @gustavovalverde\n- use non-blocking scrypt from `@better-auth/utils` ([#8685](https://github.com/better-auth/better-auth/pull/8685)) by @bytaesu\n\n## Database\n\n- add case insensitive queries support ([#8556](https://github.com/better-auth/better-auth/pull/8556)) by @jonathansamines\n- drizzle-adapter failing date transformation ([#8289](https://github.com/better-auth/better-auth/pull/8289)) by @ping-maxwell\n- generate session id when using secondary storage without database ([#8927](https://github.com/better-auth/better-auth/pull/8927)) by @bytaesu\n- remove deprecated `numUpdatedOrDeletedRows` from D1 dialect ([#8798](https://github.com/better-auth/better-auth/pull/8798)) by @bytaesu\n- use IS NULL / IS NOT NULL for null value comparisons ([#8660](https://github.com/better-auth/better-auth/pull/8660)) by @olliethedev\n\n## OAuth\n\n- add dedicated `secret` option to reduce shared key exposure surface ([#8699](https://github.com/better-auth/better-auth/pull/8699)) by @bytaesu\n- opt into FedCM to suppress Google GSI deprecation warnings ([#8720](https://github.com/better-auth/better-auth/pull/8720)) by @himself65\n- prevent double-hashing of state when storeIdentifier is hashed ([#8980](https://github.com/better-auth/better-auth/pull/8980)) by @bytaesu\n- read callback params from body for form_post ([#8895](https://github.com/better-auth/better-auth/pull/8895)) by @bytaesu\n\n## Credentials\n\n- add pre-auth registration and extensions ([#7154](https://github.com/better-auth/better-auth/pull/7154)) by @gustavovalverde\n- dont set other username prop in updateUser ([#7570](https://github.com/better-auth/better-auth/pull/7570)) by @ping-maxwell\n- enforce username uniqueness in updateUser ([#8731](https://github.com/better-auth/better-auth/pull/8731)) by @aarmful\n- rethrow phone sendOTP failures ([#8842](https://github.com/better-auth/better-auth/pull/8842)) by @GautamBytes\n- return additional fields in `/magic-link/verify` ([#7223](https://github.com/better-auth/better-auth/pull/7223)) by @himself65\n- trigger sessionSignal on req-email-change ([#8816](https://github.com/better-auth/better-auth/pull/8816)) by @ping-maxwell\n- use message ([#8751](https://github.com/better-auth/better-auth/pull/8751)) by @okisdev\n\n## Identity\n\n- enforce DB-backed sessions with secondary storage ([#8894](https://github.com/better-auth/better-auth/pull/8894)) by @GautamBytes\n- handle dynamic baseURL config in init ([#8649](https://github.com/better-auth/better-auth/pull/8649)) by @himself65\n- let customIdTokenClaims override acr and auth_time ([#8633](https://github.com/better-auth/better-auth/pull/8633)) by @gustavovalverde\n- normalize auth_time timestamps ([#8761](https://github.com/better-auth/better-auth/pull/8761)) by @gustavovalverde\n- provisionUser inconsistency and option to run on every login ([#8818](https://github.com/better-auth/better-auth/pull/8818)) by @formatlos\n- return JSON redirects from post-login OAuth continuation ([#8815](https://github.com/better-auth/better-auth/pull/8815)) by @gustavovalverde\n- scope loss on PAR, loopback redirect matching, DCR skip_consent ([#8632](https://github.com/better-auth/better-auth/pull/8632)) by @gustavovalverde\n\n## Organization\n\n- resolve duplicate operationId in admin plugin endpoints ([#8570](https://github.com/better-auth/better-auth/pull/8570)) by @Sigmabrogz\n\n## Security\n\n- add enable option ([#8728](https://github.com/better-auth/better-auth/pull/8728)) by @aarmful\n- allow passwordless 2FA management ([#7243](https://github.com/better-auth/better-auth/pull/7243)) by @gustavovalverde\n- misleading rate limit IP warning ([#8617](https://github.com/better-auth/better-auth/pull/8617)) by @GautamBytes\n\n## Enterprise\n\n- **BREAKING:** enable InResponseTo validation by default for SAML flows ([#8736](https://github.com/better-auth/better-auth/pull/8736)) by @bytaesu\n- Add logging for when code validation fails in oidc callback ([#8693](https://github.com/better-auth/better-auth/pull/8693)) by @OscarCornish\n- patch Dependabot security issues ([#8838](https://github.com/better-auth/better-auth/pull/8838)) by @gustavovalverde\n- skip state cookie check for SAML ACS cross-site POST ([#8735](https://github.com/better-auth/better-auth/pull/8735)) by @bytaesu\n\n## Payments\n\n- return correct priceId for annual subscriptions in list ([#8810](https://github.com/better-auth/better-auth/pull/8810)) by @bytaesu\n\n## Devtools\n\n- migrate MCP server URL to `mcp.better-auth.com` ([#8747](https://github.com/better-auth/better-auth/pull/8747)) by @bytaesu\n- remove `using` keyword ([#8756](https://github.com/better-auth/better-auth/pull/8756)) by @ping-maxwell\n- treat omitted `required` as `true` in Drizzle and Prisma generators ([#8614](https://github.com/better-auth/better-auth/pull/8614)) by @bytaesu\n\n**Full changelog**: [`v1.5.6...v1.6.0`](https://github.com/better-auth/better-auth/compare/v1.5.6...v1.6.0)","https://github.com/better-auth/better-auth/releases/tag/v1.6.0","2026-04-06T16:24:58Z",[],[],"2026-04-07T06:28:52.045Z",{"id":14431,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14432,"name":14432,"body":14433,"htmlUrl":14434,"publishedAt":14435,"isPrerelease":27,"summary":14229,"breakingChanges":14436,"newFeatures":14437,"importance":14219,"relevanceScore":14220,"processedAt":14438,"model":14222},"vercel/ai:305480359","@ai-sdk/rsc@1.0.169","### Patch Changes\n\n-   ai@5.0.167\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/rsc%401.0.169","2026-04-05T21:43:49Z",[],[],"2026-04-06T06:35:21.050Z",{"id":14440,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14441,"name":14441,"body":14433,"htmlUrl":14442,"publishedAt":14443,"isPrerelease":27,"summary":14229,"breakingChanges":14444,"newFeatures":14445,"importance":14219,"relevanceScore":14220,"processedAt":14446,"model":14222},"vercel/ai:305480355","@ai-sdk/vue@2.0.167","https://github.com/vercel/ai/releases/tag/%40ai-sdk/vue%402.0.167","2026-04-05T21:43:46Z",[],[],"2026-04-06T06:35:20.048Z",{"id":14448,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14449,"name":14449,"body":14450,"htmlUrl":14451,"publishedAt":14452,"isPrerelease":27,"summary":14216,"breakingChanges":14453,"newFeatures":14454,"importance":14219,"relevanceScore":14220,"processedAt":14455,"model":14222},"vercel/ai:305480340","ai@5.0.167","### Patch Changes\n\n-   Updated dependencies [6b4ceaa]\n    -   @ai-sdk/gateway@2.0.71\n","https://github.com/vercel/ai/releases/tag/ai%405.0.167","2026-04-05T21:43:40Z",[],[],"2026-04-06T06:35:19.548Z",{"id":14457,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14458,"name":14458,"body":14433,"htmlUrl":14459,"publishedAt":14460,"isPrerelease":27,"summary":14229,"breakingChanges":14461,"newFeatures":14462,"importance":14219,"relevanceScore":14220,"processedAt":14463,"model":14222},"vercel/ai:305480335","@ai-sdk/react@2.0.169","https://github.com/vercel/ai/releases/tag/%40ai-sdk/react%402.0.169","2026-04-05T21:43:37Z",[],[],"2026-04-06T06:35:21.551Z",{"id":14465,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14466,"name":14466,"body":14433,"htmlUrl":14467,"publishedAt":14468,"isPrerelease":27,"summary":14229,"breakingChanges":14469,"newFeatures":14470,"importance":14219,"relevanceScore":14220,"processedAt":14471,"model":14222},"vercel/ai:305480332","@ai-sdk/svelte@3.0.167","https://github.com/vercel/ai/releases/tag/%40ai-sdk/svelte%403.0.167","2026-04-05T21:43:34Z",[],[],"2026-04-06T06:35:20.549Z",{"id":14473,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14474,"name":14474,"body":14475,"htmlUrl":14476,"publishedAt":14477,"isPrerelease":27,"summary":14229,"breakingChanges":14478,"newFeatures":14479,"importance":14219,"relevanceScore":14220,"processedAt":14480,"model":14222},"vercel/ai:305190368","@ai-sdk/svelte@4.0.146","### Patch Changes\n\n-   ai@6.0.146\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/svelte%404.0.146","2026-04-04T00:45:18Z",[],[],"2026-04-04T06:20:01.380Z",{"id":14482,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14483,"name":14483,"body":14475,"htmlUrl":14484,"publishedAt":14485,"isPrerelease":27,"summary":14229,"breakingChanges":14486,"newFeatures":14487,"importance":14219,"relevanceScore":14220,"processedAt":14488,"model":14222},"vercel/ai:305190357","@ai-sdk/vue@3.0.146","https://github.com/vercel/ai/releases/tag/%40ai-sdk/vue%403.0.146","2026-04-04T00:45:12Z",[],[],"2026-04-04T06:20:00.880Z",{"id":14490,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14491,"name":14491,"body":14475,"htmlUrl":14492,"publishedAt":14493,"isPrerelease":27,"summary":14229,"breakingChanges":14494,"newFeatures":14495,"importance":14219,"relevanceScore":14220,"processedAt":14496,"model":14222},"vercel/ai:305190322","@ai-sdk/react@3.0.148","https://github.com/vercel/ai/releases/tag/%40ai-sdk/react%403.0.148","2026-04-04T00:45:09Z",[],[],"2026-04-04T06:20:02.381Z",{"id":14498,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14499,"name":14499,"body":14475,"htmlUrl":14500,"publishedAt":14501,"isPrerelease":27,"summary":14229,"breakingChanges":14502,"newFeatures":14503,"importance":14219,"relevanceScore":14220,"processedAt":14504,"model":14222},"vercel/ai:305190315","@ai-sdk/rsc@2.0.146","https://github.com/vercel/ai/releases/tag/%40ai-sdk/rsc%402.0.146","2026-04-04T00:45:06Z",[],[],"2026-04-04T06:20:01.881Z",{"id":14506,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14507,"name":14507,"body":14508,"htmlUrl":14509,"publishedAt":14510,"isPrerelease":27,"summary":14216,"breakingChanges":14511,"newFeatures":14512,"importance":14219,"relevanceScore":14220,"processedAt":14513,"model":14222},"vercel/ai:305190293","ai@6.0.146","### Patch Changes\n\n-   Updated dependencies [5f439a1]\n    -   @ai-sdk/gateway@3.0.88\n","https://github.com/vercel/ai/releases/tag/ai%406.0.146","2026-04-04T00:45:00Z",[],[],"2026-04-04T06:20:00.380Z",{"id":14515,"packageName":14171,"packageRepo":14172,"packagePriority":14165,"tagName":14516,"name":14516,"body":14517,"htmlUrl":14518,"publishedAt":14519,"isPrerelease":27,"summary":14216,"breakingChanges":14520,"newFeatures":14521,"importance":14219,"relevanceScore":14220,"processedAt":14522,"model":14222},"nuxt/ui:305076096","v4.6.1","## :bug: Bug Fixes\r\n\r\n* **ai:** use `part.state` for streaming detection and deprecate `isReasoningStreaming` ([d2d7543](https://github.com/nuxt/ui/commit/d2d7543b7fdeecd44639602aba5c13bc5bfa1e8e))\r\n* **ChatMessage:** hide files slot when no file parts exist ([9cddc8e](https://github.com/nuxt/ui/commit/9cddc8e228896b197006878f689b44b11bebddeb))\r\n* **ChatMessages:** keep indicator visible until first content arrives ([195cce8](https://github.com/nuxt/ui/commit/195cce85f1b7a4eed866de1ac08e6d4040926381))\r\n* **ChatMessages:** reset scroll icon when messages are cleared ([#6239](https://github.com/nuxt/ui/issues/6239)) ([4ba3eef](https://github.com/nuxt/ui/commit/4ba3eef1f42cf558c26801365ce45f048b43a894))\r\n* **ChatPrompt:** guard enter during composition ([#6280](https://github.com/nuxt/ui/issues/6280)) ([a911ca8](https://github.com/nuxt/ui/commit/a911ca8aa822efe5fd3618bf8fb71fb304f5c32d))\r\n* **DashboardSidebar:** always pass `collapsed: false` in mobile menu slots ([957a0f5](https://github.com/nuxt/ui/commit/957a0f5589ab0b0f5c129ca84999a507edff55cb)), closes [#6157](https://github.com/nuxt/ui/issues/6157)\r\n* **Modal/Slideover/Drawer:** suppress reka ui title and description warnings ([3451b8d](https://github.com/nuxt/ui/commit/3451b8d9d303c2f5b1586cc0ddea7ac9a35fee77)), closes [#6240](https://github.com/nuxt/ui/issues/6240)\r\n* **module:** inline defaultVariants and prefix in dev template ([314e23b](https://github.com/nuxt/ui/commit/314e23b6043d5dd987793c498e45814fac407588))\r\n* **module:** transpile `reka-ui` to prevent injection errors ([#6286](https://github.com/nuxt/ui/issues/6286)) ([b822c43](https://github.com/nuxt/ui/commit/b822c433c310ee3b0dd315bbf05dbb83475f1cba))\r\n\r\n## New Contributors\r\n* @fabianpnke made their first contribution in https://github.com/nuxt/ui/pull/6243\r\n* @wicii2120 made their first contribution in https://github.com/nuxt/ui/pull/6280\r\n\r\n**Full Changelog**: https://github.com/nuxt/ui/compare/v4.6.0...v4.6.1","https://github.com/nuxt/ui/releases/tag/v4.6.1","2026-04-03T15:11:59Z",[],[],"2026-04-04T06:19:53.262Z",{"id":14524,"packageName":14168,"packageRepo":14169,"packagePriority":14165,"tagName":14525,"name":14525,"body":14526,"htmlUrl":14527,"publishedAt":14528,"isPrerelease":27,"summary":14216,"breakingChanges":14529,"newFeatures":14530,"importance":14219,"relevanceScore":14220,"processedAt":14531,"model":14222},"vuejs/core:304951648","v3.5.32","For stable releases, please refer to [CHANGELOG.md](https://github.com/vuejs/core/blob/main/CHANGELOG.md) for details.\nFor pre-releases, please refer to [CHANGELOG.md](https://github.com/vuejs/core/blob/minor/CHANGELOG.md) of the `minor` branch.","https://github.com/vuejs/core/releases/tag/v3.5.32","2026-04-03T05:41:45Z",[],[],"2026-04-03T06:23:43.306Z",{"id":14533,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14534,"name":14534,"body":14535,"htmlUrl":14536,"publishedAt":14537,"isPrerelease":27,"summary":14229,"breakingChanges":14538,"newFeatures":14539,"importance":14219,"relevanceScore":14220,"processedAt":14540,"model":14222},"vercel/ai:304917123","@ai-sdk/rsc@2.0.145","### Patch Changes\n\n-   ai@6.0.145\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/rsc%402.0.145","2026-04-03T01:36:38Z",[],[],"2026-04-03T06:23:53.645Z",{"id":14542,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14543,"name":14543,"body":14535,"htmlUrl":14544,"publishedAt":14545,"isPrerelease":27,"summary":14229,"breakingChanges":14546,"newFeatures":14547,"importance":14219,"relevanceScore":14220,"processedAt":14548,"model":14222},"vercel/ai:304917107","@ai-sdk/svelte@4.0.145","https://github.com/vercel/ai/releases/tag/%40ai-sdk/svelte%404.0.145","2026-04-03T01:36:32Z",[],[],"2026-04-03T06:23:53.144Z",{"id":14550,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14551,"name":14551,"body":14535,"htmlUrl":14552,"publishedAt":14553,"isPrerelease":27,"summary":14229,"breakingChanges":14554,"newFeatures":14555,"importance":14219,"relevanceScore":14220,"processedAt":14556,"model":14222},"vercel/ai:304917097","@ai-sdk/react@3.0.147","https://github.com/vercel/ai/releases/tag/%40ai-sdk/react%403.0.147","2026-04-03T01:36:26Z",[],[],"2026-04-03T06:23:54.144Z",{"id":14558,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14559,"name":14559,"body":14535,"htmlUrl":14560,"publishedAt":14561,"isPrerelease":27,"summary":14229,"breakingChanges":14562,"newFeatures":14563,"importance":14219,"relevanceScore":14220,"processedAt":14564,"model":14222},"vercel/ai:304917077","@ai-sdk/vue@3.0.145","https://github.com/vercel/ai/releases/tag/%40ai-sdk/vue%403.0.145","2026-04-03T01:36:17Z",[],[],"2026-04-03T06:23:52.643Z",{"id":14566,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14567,"name":14567,"body":14568,"htmlUrl":14569,"publishedAt":14570,"isPrerelease":27,"summary":14216,"breakingChanges":14571,"newFeatures":14572,"importance":14219,"relevanceScore":14220,"processedAt":14573,"model":14222},"vercel/ai:304917061","ai@6.0.145","### Patch Changes\n\n-   Updated dependencies [ffd431a]\n    -   @ai-sdk/gateway@3.0.87\n","https://github.com/vercel/ai/releases/tag/ai%406.0.145","2026-04-03T01:36:08Z",[],[],"2026-04-03T06:23:52.143Z",{"id":14575,"packageName":14187,"packageRepo":14188,"packagePriority":14182,"tagName":14576,"name":14576,"body":14577,"htmlUrl":14578,"publishedAt":14579,"isPrerelease":14166,"summary":14216,"breakingChanges":14580,"newFeatures":14581,"importance":14219,"relevanceScore":14220,"processedAt":14582,"model":14222},"stripe/stripe-node:304903829","v22.1.0-alpha.1","This release changes the pinned API version to 2026-04-01.preview and contains additional breaking changes. See the [GA changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md#2200---2026-04-02) for more information.\n\n* [#2629](https://github.com/stripe/stripe-node/pull/2629) Update generated code for private-preview\n  * Add support for new resources `SharedPayment.IssuedToken` and `V2.Data.Reporting.QueryRun`\n  * Add support for `create` and `retrieve` methods on resource `V2.Data.Reporting.QueryRun`\n  * Add support for `pause` and `resume` methods on resource `V2.Payments.OffSessionPayment`\n  * Add support for `tenant_keys`, `tenant_operator`, and `tenant_values` on `Billing.MeterListMeterEventSummariesParams`\n  * Add support for `fleet_data` on `ChargeCaptureParams.payment_details`, `ChargeUpdateParams.payment_details`, `PaymentIntent.payment_details`, `PaymentIntentAmountDetailsLineItem.payment_method_options.card`, `PaymentIntentCaptureParams.amount_details.line_items[].payment_method_options.card`, `PaymentIntentCaptureParams.payment_details`, `PaymentIntentConfirmParams.amount_details.line_items[].payment_method_options.card`, `PaymentIntentConfirmParams.payment_details`, `PaymentIntentCreateParams.amount_details.line_items[].payment_method_options.card`, `PaymentIntentCreateParams.payment_details`, `PaymentIntentDecrementAuthorizationParams.amount_details.line_items[].payment_method_options.card`, `PaymentIntentIncrementAuthorizationParams.amount_details.line_items[].payment_method_options.card`, `PaymentIntentUpdateParams.amount_details.line_items[].payment_method_options.card`, and `PaymentIntentUpdateParams.payment_details`\n  * Add support for `money_services` on `ChargeCaptureParams.payment_details`, `ChargeUpdateParams.payment_details`, `PaymentIntentCaptureParams.payment_details`, `PaymentIntentConfirmParams.payment_details`, `PaymentIntentCreateParams.payment_details`, and `PaymentIntentUpdateParams.payment_details`\n  * Add support for `payment_method_options` on `DelegatedCheckout.RequestedSessionCreateParams`, `DelegatedCheckout.RequestedSessionUpdateParams`, and `DelegatedCheckout.RequestedSession`\n  * ⚠️ Remove support for `payment_method_data` on `DelegatedCheckout.RequestedSessionConfirmParams`, `DelegatedCheckout.RequestedSessionCreateParams`, and `DelegatedCheckout.RequestedSessionUpdateParams`\n  * Add support for `card_brands` and `payment_method_types` on `DelegatedCheckout.RequestedSession.seller_details`\n  * ⚠️ Change type of `DelegatedCheckout.RequestedSession.shared_payment_issued_token` from `string` to `expandable(SharedPayment.IssuedToken)`\n  * ⚠️ Add support for new value `requires_action` on enum `DelegatedCheckout.RequestedSession.status`\n  * Add support for `check_scan` on `Invoice.payment_settings.payment_method_options`, `InvoiceCreateParams.payment_settings.payment_method_options`, `InvoiceUpdateParams.payment_settings.payment_method_options`, `QuotePreviewInvoice.payment_settings.payment_method_options`, `Subscription.payment_settings.payment_method_options`, `SubscriptionCreateParams.payment_settings.payment_method_options`, and `SubscriptionUpdateParams.payment_settings.payment_method_options`\n  * ⚠️ Add support for new value `check_scan` on enums `Invoice.payment_settings.payment_method_types`, `InvoiceCreateParams.payment_settings.payment_method_types`, `InvoiceUpdateParams.payment_settings.payment_method_types`, `QuotePreviewInvoice.payment_settings.payment_method_types`, `Subscription.payment_settings.payment_method_types`, `SubscriptionCreateParams.payment_settings.payment_method_types`, and `SubscriptionUpdateParams.payment_settings.payment_method_types`\n  * Add support for `processor_details` on `PaymentAttemptRecordReportFailedParams`, `PaymentAttemptRecordReportGuaranteedParams`, `PaymentRecordReportPaymentAttemptFailedParams`, `PaymentRecordReportPaymentAttemptGuaranteedParams`, `PaymentRecordReportPaymentAttemptParams.failed`, `PaymentRecordReportPaymentAttemptParams.guaranteed`, `PaymentRecordReportPaymentParams.failed`, and `PaymentRecordReportPaymentParams.guaranteed`\n  * Add support for `payment_details` on `PaymentIntentConfirmParams.payment_method_options.card_present`, `PaymentIntentConfirmParams.payment_method_options.card`, `PaymentIntentCreateParams.payment_method_options.card_present`, `PaymentIntentCreateParams.payment_method_options.card`, `PaymentIntentUpdateParams.payment_method_options.card_present`, and `PaymentIntentUpdateParams.payment_method_options.card`\n  * ⚠️ Remove support for `bill_from` on `QuotePreviewSubscriptionSchedule.billing_schedules[]`, `Subscription.billing_schedules[]`, and `SubscriptionSchedule.billing_schedules[]`\n  * Add support for `agent_details`, `payment_method_details`, and `risk_details` on `SharedPayment.GrantedToken`\n  * Add support for `paper_checks` on `V2.Account.configuration.recipient_data.features`, `V2.AccountCreateParams.configuration.recipient_data.features`, `V2.AccountUpdateParams.configuration.recipient_data.features`, `V2.Core.Account.configuration.recipient.capabilities`, `V2.Core.Account.configuration.storer.capabilities.outbound_payments`, `V2.Core.AccountCreateParams.configuration.recipient.capabilities`, `V2.Core.AccountCreateParams.configuration.storer.capabilities.outbound_payments`, `V2.Core.AccountUpdateParams.configuration.recipient.capabilities`, and `V2.Core.AccountUpdateParams.configuration.storer.capabilities.outbound_payments`\n  * ⚠️ Add support for new value `paper_checks` on enum `V2.Account.configuration.supportable_features.recipient_data`\n  * ⚠️ Add support for new value `paper_checks` on enum `V2.Account.requirements[].impact.required_for_features`\n  * ⚠️ Change type of `V2.Billing.Cadence.settings_data.collection.payment_method_options.konbini`, `V2.Billing.CollectionSetting.payment_method_options.konbini`, `V2.Billing.CollectionSettingCreateParams.payment_method_options.konbini`, `V2.Billing.CollectionSettingUpdateParams.payment_method_options.konbini`, and `V2.Billing.CollectionSettingVersion.payment_method_options.konbini` from `map(string: dynamic)` to `an object`\n  * ⚠️ Change type of `V2.Billing.Cadence.settings_data.collection.payment_method_options.sepa_debit`, `V2.Billing.CollectionSetting.payment_method_options.sepa_debit`, `V2.Billing.CollectionSettingCreateParams.payment_method_options.sepa_debit`, `V2.Billing.CollectionSettingUpdateParams.payment_method_options.sepa_debit`, and `V2.Billing.CollectionSettingVersion.payment_method_options.sepa_debit` from `map(string: dynamic)` to `an object`\n  * Add support for `id` on `V2.Billing.CadenceSpendModifier.max_billing_period_spend.amount.custom_pricing_unit`, `V2.Billing.IntentAction.apply.spend_modifier_rule.max_billing_period_spend.amount.custom_pricing_unit`, and `V2.Billing.IntentCreateParams.actions[].apply.spend_modifier_rule.max_billing_period_spend.amount.custom_pricing_unit`\n  * ⚠️ Add support for new values `outbound_payments.paper_checks` and `paper_checks` on enums `V2.Core.Account.future_requirements.entries[].impact.restricts_capabilities[].capability` and `V2.Core.Account.requirements.entries[].impact.restricts_capabilities[].capability`\n  * ⚠️ Add support for new values `bm_crn`, `bo_tin`, `bt_tpn`, `co_nit`, `ec_ruc`, `eg_tin`, `gh_tin`, `gy_tin`, `hn_rtn`, `jm_trn`, `jo_crn`, `ke_pin`, `ky_crn`, `lk_tin`, `mo_tin`, `mv_tin`, `ng_tin`, `pa_ruc`, `ph_tin`, `py_ruc`, `sl_tin`, `sv_nit`, `uy_ruc`, `vg_cn`, and `za_tin` on enums `V2.Core.Account.identity.business_details.id_numbers[].type`, `V2.Core.AccountCreateParams.identity.business_details.id_numbers[].type`, `V2.Core.AccountTokenCreateParams.identity.business_details.id_numbers[].type`, and `V2.Core.AccountUpdateParams.identity.business_details.id_numbers[].type`\n  * ⚠️ Add support for new values `bm_pp`, `bo_ci`, `bt_cid`, `eg_tin`, `gh_pin`, `gy_tin`, `hn_rtn`, `jm_trn`, `jo_pin`, `ky_pp`, `lk_nic`, `mo_bir`, `mt_nic`, `mv_tin`, `pa_ruc`, `ph_tin`, `py_ruc`, `si_pin`, `sv_nit`, and `vg_pp` on enums `V2.Core.Account.identity.individual.id_numbers[].type`, `V2.Core.AccountCreateParams.identity.individual.id_numbers[].type`, `V2.Core.AccountPerson.id_numbers[].type`, `V2.Core.AccountPersonCreateParams.id_numbers[].type`, `V2.Core.AccountPersonTokenCreateParams.id_numbers[].type`, `V2.Core.AccountPersonUpdateParams.id_numbers[].type`, `V2.Core.AccountTokenCreateParams.identity.individual.id_numbers[].type`, and `V2.Core.AccountUpdateParams.identity.individual.id_numbers[].type`\n  * ⚠️ Change type of `V2.Core.Event.reason.request.client.stripe_action` from `map(string: dynamic)` to `an object`\n  * ⚠️ Change type of `V2.MoneyManagement.InboundTransfer.transfer_history[].bank_debit_processing` from `map(string: dynamic)` to `an object`\n  * ⚠️ Change type of `V2.MoneyManagement.InboundTransfer.transfer_history[].bank_debit_queued` from `map(string: dynamic)` to `an object`\n  * ⚠️ Change type of `V2.MoneyManagement.InboundTransfer.transfer_history[].bank_debit_succeeded` from `map(string: dynamic)` to `an object`\n  * ⚠️ Add support for new values `paper_check_attachment_too_large`, `paper_check_expired`, and `paper_check_undeliverable` on enum `V2.MoneyManagement.OutboundPayment.status_details.failed.reason`\n  * ⚠️ Remove support for `town` on `V2.MoneyManagement.OutboundPayment.tracking_details.paper_check.mailing_address`\n  * Change `V2.MoneyManagement.OutboundPayment.delivery_options.paper_check.memo` to be required\n  * ⚠️ Add support for new value `payout_method_amount_limit_exceeded` on enum `V2.MoneyManagement.OutboundTransfer.status_details.failed.reason`\n  * Add support for `application_fee_amount_requested` on `V2.Payments.OffSessionPayment`\n  * ⚠️ Remove support for `compartment_id` on `V2.Payments.OffSessionPayment`\n  * ⚠️ Add support for new value `exceeded_retry_window` on enum `V2.Payments.OffSessionPayment.failure_reason`\n  * Add support for `retry_until` on `V2.Payments.OffSessionPayment.retry_details`\n  * ⚠️ Add support for new value `paused` on enum `V2.Payments.OffSessionPayment.status`\n  * ⚠️ Change `V2.Reporting.ReportRun.result.file` to be optional\n  * Add support for `application_fee_amount` on `V2.Payments.OffSessionPaymentCaptureParams` and `V2.Payments.OffSessionPaymentCreateParams`\n  * ⚠️ Add support for new value `paper_checks` on enum `EventsV2CoreAccountIncludingConfigurationRecipientCapabilityStatusUpdatedEvent.updated_capability`\n  * ⚠️ Add support for new value `outbound_payments.paper_checks` on enum `EventsV2CoreAccountIncludingConfigurationStorerCapabilityStatusUpdatedEvent.updated_capability`\n  * Add support for `alert_id` on `EventsV2CoreHealthApiErrorResolvedEvent`, `EventsV2CoreHealthApiLatencyResolvedEvent`, `EventsV2CoreHealthAuthorizationRateDropResolvedEvent`, `EventsV2CoreHealthIssuingAuthorizationRequestErrorsFiringEvent`, `EventsV2CoreHealthIssuingAuthorizationRequestErrorsResolvedEvent`, `EventsV2CoreHealthIssuingAuthorizationRequestTimeoutResolvedEvent`, `EventsV2CoreHealthPaymentMethodErrorResolvedEvent`, `EventsV2CoreHealthSepaDebitDelayedFiringEvent`, `EventsV2CoreHealthSepaDebitDelayedResolvedEvent`, `EventsV2CoreHealthTrafficVolumeDropResolvedEvent`, and `EventsV2CoreHealthWebhookLatencyResolvedEvent`\n  * Add support for `api_key` on `EventsV2IamApiKeyCreatedEvent`, `EventsV2IamApiKeyDefaultSecretRevealedEvent`, `EventsV2IamApiKeyExpiredEvent`, `EventsV2IamApiKeyPermissionsUpdatedEvent`, `EventsV2IamApiKeyRotatedEvent`, and `EventsV2IamApiKeyUpdatedEvent`\n  * Add support for `stripe_access_grant` on `EventsV2IamStripeAccessGrantApprovedEvent`, `EventsV2IamStripeAccessGrantCanceledEvent`, `EventsV2IamStripeAccessGrantDeniedEvent`, `EventsV2IamStripeAccessGrantRemovedEvent`, `EventsV2IamStripeAccessGrantRequestedEvent`, and `EventsV2IamStripeAccessGrantUpdatedEvent`\n  * Add support for event notifications `V2DataReportingQueryRunCreatedEvent`, `V2DataReportingQueryRunFailedEvent`, `V2DataReportingQueryRunSucceededEvent`, and `V2DataReportingQueryRunUpdatedEvent` with related object `V2.Data.Reporting.QueryRun`\n  * Add support for event notifications `V2PaymentsOffSessionPaymentPausedEvent` and `V2PaymentsOffSessionPaymentResumedEvent` with related object `V2.Payments.OffSessionPayment`\n* [#2647](https://github.com/stripe/stripe-node/pull/2647) Merge to private-preview\n* [#2641](https://github.com/stripe/stripe-node/pull/2641) Merge to private-preview\n* [#2636](https://github.com/stripe/stripe-node/pull/2636) Prathmesh/merge node private preview\n\nSee [the changelog for more details](https://github.com/stripe/stripe-node/blob/v22.1.0-alpha.1/CHANGELOG.md).\n","https://github.com/stripe/stripe-node/releases/tag/v22.1.0-alpha.1","2026-04-03T00:09:45Z",[],[],"2026-04-03T06:23:55.771Z",{"id":14584,"packageName":14187,"packageRepo":14188,"packagePriority":14182,"tagName":14585,"name":14585,"body":14586,"htmlUrl":14587,"publishedAt":14588,"isPrerelease":14166,"summary":14216,"breakingChanges":14589,"newFeatures":14590,"importance":14219,"relevanceScore":14220,"processedAt":14591,"model":14222},"stripe/stripe-node:304903162","v22.1.0-beta.1","Please review the [changelog for 22.0.0](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md#2200---2026-04-02) for more information about changes in this release.\n\nSee [the changelog for more details](https://github.com/stripe/stripe-node/blob/v22.1.0-beta.1/CHANGELOG.md).\n","https://github.com/stripe/stripe-node/releases/tag/v22.1.0-beta.1","2026-04-03T00:06:27Z",[],[],"2026-04-03T06:23:55.152Z",{"id":14593,"packageName":14187,"packageRepo":14188,"packagePriority":14182,"tagName":14594,"name":14594,"body":14595,"htmlUrl":14596,"publishedAt":14597,"isPrerelease":27,"summary":14216,"breakingChanges":14598,"newFeatures":14599,"importance":14219,"relevanceScore":14220,"processedAt":14600,"model":14222},"stripe/stripe-node:304902870","v22.0.0","* [#2642](https://github.com/stripe/stripe-node/pull/2642) Update README.md\n* [#2645](https://github.com/stripe/stripe-node/pull/2645) ⚠️ Remove `stripeMethod` and standardize how function args are handled (including removing callback support)\n  \r\n  - ⚠️ Refactor how incoming method arguments are parsed. Type signatures for API methods should be _much_ more accurate and reliable now\r\n    - ⚠️ Remove support for providing callbacks to API methods. Use `async / await` instead\r\n    - ⚠️ Remove support for passing a plain API key as a function arg. If supplied on a per-request basis, it should be in the `RequestOptions` under the `apiKey` property\r\n    - ⚠️ Keys from `params` and `options` objects are no longer mixed. If present on a method, `RequestParams` must always come first and `RequestOptions` must always come second. To supply options without params, pass `undefined` as the first argument explicitly\r\n    - ⚠️ Removed methods from `StripeResource`: `createFullPath`, `createResourcePathWithSymbols`, `extend`, `method` and `_joinUrlParts`. These were mostly intended for internal use and we no longer need them\r\n  \r\n  As a result, the following call patterns are no longer supported:\r\n  \r\n  ```ts\r\n  stripe.customers.retrieve('cus_123', 'sk_test_123')\r\n  stripe.customers.create({name: 'david', host: 'example.com'}, 'sk_test_123')\r\n  stripe.customers.create({apiKey: 'sk_test_123'})\r\n  stripe.customers.list(customers => {\r\n    // do something with customers\r\n  })\r\n  ```\r\n  \r\n  If those look familiar, head over to the [migration guide](https://github.com/stripe/stripe-node/wiki/Migration-guide-for-v22) to update your code.\n* [#2643](https://github.com/stripe/stripe-node/pull/2643) ⚠️ remove support for overriding host per-request\n  \r\n  - ⚠️ Removed per-request host override. To use a custom host, set it in the client configuration. All requests from that client will use that host.\r\n  \r\n  Before:\r\n  ```ts\r\n  import Stripe from 'stripe';\r\n  const stripe = new Stripe('sk_test_...');\r\n  \r\n  const customer = await stripe.customers.create({\r\n    email: 'customer@example.com',\r\n  }, {host: 'example.com'});\r\n  ```\r\n  \r\n  After:\r\n  ```ts\r\n  import Stripe from 'stripe';\r\n  const stripe = new Stripe('sk_test_...', {host: 'example.com'});\r\n  \r\n  // goes to example.com\r\n  const customer = await stripe.customers.create({\r\n    email: 'customer@example.com',\r\n  });\r\n  ```\n* [#2619](https://github.com/stripe/stripe-node/pull/2619) Improved TypeScript support in the Node SDK\n* [#2638](https://github.com/stripe/stripe-node/pull/2638) Converted V2/Amount.ts to V2/V2Amount.ts\n* [#2635](https://github.com/stripe/stripe-node/pull/2635) Updated stripe.spec.ts test and constructEvent.tolerance type\n\nSee [the changelog for more details](https://github.com/stripe/stripe-node/blob/v22.0.0/CHANGELOG.md).\n","https://github.com/stripe/stripe-node/releases/tag/v22.0.0","2026-04-03T00:04:32Z",[],[],"2026-04-03T06:23:56.380Z",{"id":14602,"packageName":14206,"packageRepo":14207,"packagePriority":14195,"tagName":14603,"name":14603,"body":14604,"htmlUrl":14605,"publishedAt":14606,"isPrerelease":27,"summary":14216,"breakingChanges":14607,"newFeatures":14608,"importance":14219,"relevanceScore":14220,"processedAt":14609,"model":14222},"nitrojs/nitro:304580807","v2.13.3","[compare changes](https://github.com/nitrojs/nitro/compare/v2.13.2...v2.13.3)\r\n\r\n### 📦 Dependency Updates\r\n\r\n| Package | From | To |\r\n| --- | --- | --- |\r\n| `httpxy` | ^0.3.1 | **^0.5.0** |\r\n| `h3` | ^1.15.9 | ^1.15.10 |\r\n| `esbuild` | ^0.27.4 | ^0.27.5 |\r\n| `rollup` | ^4.59.0 | ^4.60.1 |\r\n| `@vercel/nft` | ^1.4.0 | ^1.5.0 |\r\n| `c12` | ^3.3.3 | ^3.3.4 |\r\n| `citty` | ^0.2.1 | ^0.2.2 |\r\n| `defu` | ^6.1.4 | ^6.1.6 |\r\n| `globby` | ^16.1.1 | ^16.2.0 |\r\n| `listhen` | ^1.9.0 | ^1.9.1 |\r\n| `unstorage` | ^1.17.4 | ^1.17.5 |\r\n| `cookie-es` | ^2.0.0 | ^2.0.1 |\r\n| `youch` | ^4.1.0 | ^4.1.1 |\r\n","https://github.com/nitrojs/nitro/releases/tag/v2.13.3","2026-04-02T09:13:28Z",[],[],"2026-04-03T06:24:13.358Z",{"id":14611,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14612,"name":14612,"body":14613,"htmlUrl":14614,"publishedAt":14615,"isPrerelease":27,"summary":14216,"breakingChanges":14616,"newFeatures":14617,"importance":14219,"relevanceScore":14220,"processedAt":14618,"model":14222},"vercel/ai:304454626","@ai-sdk/anthropic@2.0.72","### Patch Changes\n\n-   98bd0e2: feat(provider/anthropic): support new code_execution tool\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/anthropic%402.0.72","2026-04-01T23:50:31Z",[],[],"2026-04-02T06:24:15.655Z",{"id":14620,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14621,"name":14621,"body":14622,"htmlUrl":14623,"publishedAt":14624,"isPrerelease":27,"summary":14216,"breakingChanges":14625,"newFeatures":14626,"importance":14219,"relevanceScore":14220,"processedAt":14627,"model":14222},"vercel/ai:304454619","@ai-sdk/google-vertex@3.0.123","### Patch Changes\n\n-   Updated dependencies [98bd0e2]\n    -   @ai-sdk/anthropic@2.0.72\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/google-vertex%403.0.123","2026-04-01T23:50:28Z",[],[],"2026-04-02T06:24:15.079Z",{"id":14629,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14630,"name":14630,"body":14631,"htmlUrl":14632,"publishedAt":14633,"isPrerelease":27,"summary":14216,"breakingChanges":14634,"newFeatures":14635,"importance":14219,"relevanceScore":14220,"processedAt":14636,"model":14222},"vercel/ai:304413502","@ai-sdk/groq@3.0.32","### Patch Changes\n\n-   cb3ca8f: feat: Groq support for performance service tier\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/groq%403.0.32","2026-04-01T21:17:14Z",[],[],"2026-04-02T06:24:14.535Z",{"id":14638,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14639,"name":14639,"body":14640,"htmlUrl":14641,"publishedAt":14642,"isPrerelease":14166,"summary":14216,"breakingChanges":14643,"newFeatures":14644,"importance":14219,"relevanceScore":14220,"processedAt":14645,"model":14222},"vercel/ai:304031648","@ai-sdk/amazon-bedrock@5.0.0-beta.15","### Patch Changes\n\n-   4b20a5d: fix(provider/amazon-bedrock): transform bedrock/anthropic error responses to anthropic format\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/amazon-bedrock%405.0.0-beta.15","2026-04-01T06:31:14Z",[],[],"2026-04-01T06:32:48.473Z",{"id":14647,"packageName":14184,"packageRepo":14185,"packagePriority":14182,"tagName":14648,"name":14648,"body":14649,"htmlUrl":14650,"publishedAt":14651,"isPrerelease":27,"summary":14216,"breakingChanges":14652,"newFeatures":14653,"importance":14219,"relevanceScore":14220,"processedAt":14654,"model":14222},"vercel/ai:303962364","@ai-sdk/google@3.0.55","### Patch Changes\n\n-   bdde9d4: feat(provider/google): support combining built-in tools with function calling on Gemini 3\n","https://github.com/vercel/ai/releases/tag/%40ai-sdk/google%403.0.55","2026-04-01T00:41:09Z",[],[],"2026-04-01T06:32:45.963Z",100,"2026-04-10T06:34:59.072Z",{"limit":14220,"offset":14658},0,1775838808358]