[READ-ONLY] Mirror of https://github.com/lukebennett88/luke-ui. luke-ui.netlify.app/
0

Configure Feed

Select the types of activity you want to include in your feed.

Refactor code structure (#28)

authored by

Luke Bennett and committed by
GitHub
(May 3, 2026, 9:12 PM +1000) cf5f8b36 c153ad93

+1361 -763
+17
.oxlintrc.json
··· 9 9 "**/routeTree.gen.ts" 10 10 ], 11 11 "plugins": ["import", "jsx-a11y", "react", "typescript", "vitest"], 12 + "jsPlugins": [{ "name": "react-hooks-js", "specifier": "eslint-plugin-react-hooks" }], 12 13 "rules": { 13 14 "@typescript-eslint/array-type": ["error", { "default": "generic" }], 14 15 "@typescript-eslint/consistent-type-exports": [ ··· 29 30 "no-console": "warn", 30 31 "no-unused-vars": "error", 31 32 "react/no-array-index-key": "off", 33 + "react/exhaustive-deps": "warn", 34 + "react/rules-of-hooks": "error", 35 + "react-hooks-js/config": "error", 36 + "react-hooks-js/error-boundaries": "error", 37 + "react-hooks-js/gating": "error", 38 + "react-hooks-js/globals": "error", 39 + "react-hooks-js/immutability": "error", 40 + "react-hooks-js/preserve-manual-memoization": "error", 41 + "react-hooks-js/purity": "error", 42 + "react-hooks-js/refs": "error", 43 + "react-hooks-js/set-state-in-effect": "error", 44 + "react-hooks-js/set-state-in-render": "error", 45 + "react-hooks-js/static-components": "error", 46 + "react-hooks-js/unsupported-syntax": "warn", 47 + "react-hooks-js/use-memo": "error", 48 + "react-hooks-js/incompatible-library": "warn", 32 49 "sort-imports": "off" 33 50 } 34 51 }
+1
package.json
··· 33 33 "devDependencies": { 34 34 "@changesets/cli": "^2.31.0", 35 35 "@turbo/gen": "^2.9.6", 36 + "eslint-plugin-react-hooks": "^7.1.1", 36 37 "knip": "^6.6.3", 37 38 "oxfmt": "^0.46.0", 38 39 "oxlint": "^1.61.0",
+394
pnpm-lock.yaml
··· 83 83 '@turbo/gen': 84 84 specifier: ^2.9.6 85 85 version: 2.9.6(@types/node@25.6.0) 86 + eslint-plugin-react-hooks: 87 + specifier: ^7.1.1 88 + version: 7.1.1(eslint@10.3.0(jiti@2.6.1)) 86 89 knip: 87 90 specifier: ^6.6.3 88 91 version: 6.6.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) ··· 1016 1019 cpu: [x64] 1017 1020 os: [win32] 1018 1021 1022 + '@eslint-community/eslint-utils@4.9.1': 1023 + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} 1024 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 1025 + peerDependencies: 1026 + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 1027 + 1028 + '@eslint-community/regexpp@4.12.2': 1029 + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} 1030 + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 1031 + 1032 + '@eslint/config-array@0.23.5': 1033 + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} 1034 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 1035 + 1036 + '@eslint/config-helpers@0.5.5': 1037 + resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} 1038 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 1039 + 1040 + '@eslint/core@1.2.1': 1041 + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} 1042 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 1043 + 1044 + '@eslint/object-schema@3.0.5': 1045 + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} 1046 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 1047 + 1048 + '@eslint/plugin-kit@0.7.1': 1049 + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} 1050 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 1051 + 1019 1052 '@floating-ui/core@1.7.5': 1020 1053 resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} 1021 1054 ··· 1044 1077 peerDependenciesMeta: 1045 1078 tailwindcss: 1046 1079 optional: true 1080 + 1081 + '@humanfs/core@0.19.2': 1082 + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} 1083 + engines: {node: '>=18.18.0'} 1084 + 1085 + '@humanfs/node@0.16.8': 1086 + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} 1087 + engines: {node: '>=18.18.0'} 1088 + 1089 + '@humanfs/types@0.15.0': 1090 + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} 1091 + engines: {node: '>=18.18.0'} 1092 + 1093 + '@humanwhocodes/module-importer@1.0.1': 1094 + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 1095 + engines: {node: '>=12.22'} 1096 + 1097 + '@humanwhocodes/retry@0.4.3': 1098 + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} 1099 + engines: {node: '>=18.18'} 1047 1100 1048 1101 '@img/colour@1.1.0': 1049 1102 resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} ··· 3255 3308 '@types/doctrine@0.0.9': 3256 3309 resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} 3257 3310 3311 + '@types/esrecurse@4.3.1': 3312 + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} 3313 + 3258 3314 '@types/estree-jsx@1.0.5': 3259 3315 resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} 3260 3316 ··· 3266 3322 3267 3323 '@types/jsesc@2.5.1': 3268 3324 resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} 3325 + 3326 + '@types/json-schema@7.0.15': 3327 + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 3269 3328 3270 3329 '@types/mdast@4.0.4': 3271 3330 resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} ··· 3426 3485 resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} 3427 3486 engines: {node: '>=0.4.0'} 3428 3487 hasBin: true 3488 + 3489 + ajv@6.15.0: 3490 + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} 3429 3491 3430 3492 ajv@8.18.0: 3431 3493 resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} ··· 3827 3889 resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} 3828 3890 engines: {node: '>=4.0.0'} 3829 3891 3892 + deep-is@0.1.4: 3893 + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 3894 + 3830 3895 deep-object-diff@1.1.9: 3831 3896 resolution: {integrity: sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==} 3832 3897 ··· 3977 4042 resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 3978 4043 engines: {node: '>=6'} 3979 4044 4045 + escape-string-regexp@4.0.0: 4046 + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 4047 + engines: {node: '>=10'} 4048 + 3980 4049 escape-string-regexp@5.0.0: 3981 4050 resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} 3982 4051 engines: {node: '>=12'} 4052 + 4053 + eslint-plugin-react-hooks@7.1.1: 4054 + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} 4055 + engines: {node: '>=18'} 4056 + peerDependencies: 4057 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 4058 + 4059 + eslint-scope@9.1.2: 4060 + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} 4061 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 4062 + 4063 + eslint-visitor-keys@3.4.3: 4064 + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 4065 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 4066 + 4067 + eslint-visitor-keys@5.0.1: 4068 + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} 4069 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 4070 + 4071 + eslint@10.3.0: 4072 + resolution: {integrity: sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==} 4073 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 4074 + hasBin: true 4075 + peerDependencies: 4076 + jiti: '*' 4077 + peerDependenciesMeta: 4078 + jiti: 4079 + optional: true 4080 + 4081 + espree@11.2.0: 4082 + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} 4083 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 3983 4084 3984 4085 esprima@4.0.1: 3985 4086 resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} 3986 4087 engines: {node: '>=4'} 3987 4088 hasBin: true 4089 + 4090 + esquery@1.7.0: 4091 + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} 4092 + engines: {node: '>=0.10'} 4093 + 4094 + esrecurse@4.3.0: 4095 + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 4096 + engines: {node: '>=4.0'} 4097 + 4098 + estraverse@5.3.0: 4099 + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 4100 + engines: {node: '>=4.0'} 3988 4101 3989 4102 estree-util-attach-comments@3.0.0: 3990 4103 resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} ··· 4045 4158 resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 4046 4159 engines: {node: '>=8.6.0'} 4047 4160 4161 + fast-json-stable-stringify@2.1.0: 4162 + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 4163 + 4164 + fast-levenshtein@2.0.6: 4165 + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 4166 + 4048 4167 fast-uri@3.1.0: 4049 4168 resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} 4050 4169 ··· 4066 4185 fflate@0.8.2: 4067 4186 resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} 4068 4187 4188 + file-entry-cache@8.0.0: 4189 + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} 4190 + engines: {node: '>=16.0.0'} 4191 + 4069 4192 fill-range@7.1.1: 4070 4193 resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 4071 4194 engines: {node: '>=8'} ··· 4077 4200 find-up@5.0.0: 4078 4201 resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 4079 4202 engines: {node: '>=10'} 4203 + 4204 + flat-cache@4.0.1: 4205 + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} 4206 + engines: {node: '>=16'} 4080 4207 4081 4208 flatted@3.4.2: 4082 4209 resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} ··· 4251 4378 resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 4252 4379 engines: {node: '>= 6'} 4253 4380 4381 + glob-parent@6.0.2: 4382 + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 4383 + engines: {node: '>=10.13.0'} 4384 + 4254 4385 glob@13.0.6: 4255 4386 resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} 4256 4387 engines: {node: 18 || 20 || >=22} ··· 4326 4457 resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} 4327 4458 hasBin: true 4328 4459 4460 + hermes-estree@0.25.1: 4461 + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} 4462 + 4463 + hermes-parser@0.25.1: 4464 + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} 4465 + 4329 4466 hookable@6.1.1: 4330 4467 resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} 4331 4468 ··· 4366 4503 import-without-cache@0.3.3: 4367 4504 resolution: {integrity: sha512-bDxwDdF04gm550DfZHgffvlX+9kUlcz32UD0AeBTmVPFiWkrexF2XVmiuFFbDhiFuP8fQkrkvI2KdSNPYWAXkQ==} 4368 4505 engines: {node: '>=20.19.0'} 4506 + 4507 + imurmurhash@0.1.4: 4508 + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 4509 + engines: {node: '>=0.8.19'} 4369 4510 4370 4511 indent-string@4.0.0: 4371 4512 resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} ··· 4501 4642 engines: {node: '>=6'} 4502 4643 hasBin: true 4503 4644 4645 + json-buffer@3.0.1: 4646 + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 4647 + 4648 + json-schema-traverse@0.4.1: 4649 + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 4650 + 4504 4651 json-schema-traverse@1.0.0: 4505 4652 resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} 4653 + 4654 + json-stable-stringify-without-jsonify@1.0.1: 4655 + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 4506 4656 4507 4657 json5@2.2.3: 4508 4658 resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} ··· 4512 4662 jsonfile@4.0.0: 4513 4663 resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} 4514 4664 4665 + keyv@4.5.4: 4666 + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 4667 + 4515 4668 knip@6.6.3: 4516 4669 resolution: {integrity: sha512-7HSf5bLx6r66+sjXwSvSiDEE9RjRzHuAkrEFLE6XXHqaPDY97tdzNvyRVF9DeusbiV72kStAFiNnhj72rxJNGQ==} 4517 4670 engines: {node: ^20.19.0 || >=22.12.0} 4518 4671 hasBin: true 4672 + 4673 + levn@0.4.1: 4674 + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 4675 + engines: {node: '>= 0.8.0'} 4519 4676 4520 4677 lightningcss-android-arm64@1.32.0: 4521 4678 resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} ··· 4906 5063 engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 4907 5064 hasBin: true 4908 5065 5066 + natural-compare@1.4.0: 5067 + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 5068 + 4909 5069 negotiator@0.6.4: 4910 5070 resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} 4911 5071 engines: {node: '>= 0.6'} ··· 4995 5155 4996 5156 openapi-typescript-helpers@0.1.0: 4997 5157 resolution: {integrity: sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==} 5158 + 5159 + optionator@0.9.4: 5160 + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 5161 + engines: {node: '>= 0.8.0'} 4998 5162 4999 5163 outdent@0.5.0: 5000 5164 resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} ··· 5162 5326 resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} 5163 5327 engines: {node: ^10 || ^12 || >=14} 5164 5328 5329 + prelude-ls@1.2.1: 5330 + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 5331 + engines: {node: '>= 0.8.0'} 5332 + 5165 5333 prettier@2.8.8: 5166 5334 resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} 5167 5335 engines: {node: '>=10.13.0'} ··· 5183 5351 resolution: {integrity: sha512-JRJFeBTrfx4qLwEuGFPk+haJOJN97KnPuK01yj+4k/Wj5BgoOK5uNsivporiqBjk2JDaslg7qJOhGRnpltGeog==} 5184 5352 engines: {node: '>=18'} 5185 5353 hasBin: true 5354 + 5355 + punycode@2.3.1: 5356 + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 5357 + engines: {node: '>=6'} 5186 5358 5187 5359 quansync@0.2.11: 5188 5360 resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} ··· 5729 5901 resolution: {integrity: sha512-+v2QJey7ZUeUiuigkU+uFfklvNUyPI2VO2vBpMYJA+a1hKFLFiKtUYlRHdb3P9CrAvMzi0upbjI4WT+zKtqkBg==} 5730 5902 hasBin: true 5731 5903 5904 + type-check@0.4.0: 5905 + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 5906 + engines: {node: '>= 0.8.0'} 5907 + 5732 5908 type-fest@2.19.0: 5733 5909 resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} 5734 5910 engines: {node: '>=12.20'} ··· 5887 6063 5888 6064 update-check@1.5.4: 5889 6065 resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==} 6066 + 6067 + uri-js@4.4.1: 6068 + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 5890 6069 5891 6070 use-callback-ref@1.3.3: 5892 6071 resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} ··· 6108 6287 resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} 6109 6288 engines: {node: '>=12'} 6110 6289 6290 + word-wrap@1.2.5: 6291 + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} 6292 + engines: {node: '>=0.10.0'} 6293 + 6111 6294 wrap-ansi@6.2.0: 6112 6295 resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} 6113 6296 engines: {node: '>=8'} ··· 6171 6354 yoctocolors-cjs@2.1.3: 6172 6355 resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} 6173 6356 engines: {node: '>=18'} 6357 + 6358 + zod-validation-error@4.0.2: 6359 + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} 6360 + engines: {node: '>=18.0.0'} 6361 + peerDependencies: 6362 + zod: ^3.25.0 || ^4.0.0 6174 6363 6175 6364 zod@3.25.76: 6176 6365 resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} ··· 6822 7011 '@esbuild/win32-x64@0.28.0': 6823 7012 optional: true 6824 7013 7014 + '@eslint-community/eslint-utils@4.9.1(eslint@10.3.0(jiti@2.6.1))': 7015 + dependencies: 7016 + eslint: 10.3.0(jiti@2.6.1) 7017 + eslint-visitor-keys: 3.4.3 7018 + 7019 + '@eslint-community/regexpp@4.12.2': {} 7020 + 7021 + '@eslint/config-array@0.23.5': 7022 + dependencies: 7023 + '@eslint/object-schema': 3.0.5 7024 + debug: 4.4.3 7025 + minimatch: 10.2.4 7026 + transitivePeerDependencies: 7027 + - supports-color 7028 + 7029 + '@eslint/config-helpers@0.5.5': 7030 + dependencies: 7031 + '@eslint/core': 1.2.1 7032 + 7033 + '@eslint/core@1.2.1': 7034 + dependencies: 7035 + '@types/json-schema': 7.0.15 7036 + 7037 + '@eslint/object-schema@3.0.5': {} 7038 + 7039 + '@eslint/plugin-kit@0.7.1': 7040 + dependencies: 7041 + '@eslint/core': 1.2.1 7042 + levn: 0.4.1 7043 + 6825 7044 '@floating-ui/core@1.7.5': 6826 7045 dependencies: 6827 7046 '@floating-ui/utils': 0.2.11 ··· 6850 7069 postcss-selector-parser: 7.1.1 6851 7070 optionalDependencies: 6852 7071 tailwindcss: 4.2.4 7072 + 7073 + '@humanfs/core@0.19.2': 7074 + dependencies: 7075 + '@humanfs/types': 0.15.0 7076 + 7077 + '@humanfs/node@0.16.8': 7078 + dependencies: 7079 + '@humanfs/core': 0.19.2 7080 + '@humanfs/types': 0.15.0 7081 + '@humanwhocodes/retry': 0.4.3 7082 + 7083 + '@humanfs/types@0.15.0': {} 7084 + 7085 + '@humanwhocodes/module-importer@1.0.1': {} 7086 + 7087 + '@humanwhocodes/retry@0.4.3': {} 6853 7088 6854 7089 '@img/colour@1.1.0': {} 6855 7090 ··· 8658 8893 8659 8894 '@types/doctrine@0.0.9': {} 8660 8895 8896 + '@types/esrecurse@4.3.1': {} 8897 + 8661 8898 '@types/estree-jsx@1.0.5': 8662 8899 dependencies: 8663 8900 '@types/estree': 1.0.8 ··· 8669 8906 '@types/unist': 3.0.3 8670 8907 8671 8908 '@types/jsesc@2.5.1': {} 8909 + 8910 + '@types/json-schema@7.0.15': {} 8672 8911 8673 8912 '@types/mdast@4.0.4': 8674 8913 dependencies: ··· 8934 9173 acorn: 8.16.0 8935 9174 8936 9175 acorn@8.16.0: {} 9176 + 9177 + ajv@6.15.0: 9178 + dependencies: 9179 + fast-deep-equal: 3.1.3 9180 + fast-json-stable-stringify: 2.1.0 9181 + json-schema-traverse: 0.4.1 9182 + uri-js: 4.4.1 8937 9183 8938 9184 ajv@8.18.0: 8939 9185 dependencies: ··· 9285 9531 9286 9532 deep-extend@0.6.0: {} 9287 9533 9534 + deep-is@0.1.4: {} 9535 + 9288 9536 deep-object-diff@1.1.9: {} 9289 9537 9290 9538 deepmerge@4.3.1: {} ··· 9487 9735 9488 9736 escalade@3.2.0: {} 9489 9737 9738 + escape-string-regexp@4.0.0: {} 9739 + 9490 9740 escape-string-regexp@5.0.0: {} 9491 9741 9742 + eslint-plugin-react-hooks@7.1.1(eslint@10.3.0(jiti@2.6.1)): 9743 + dependencies: 9744 + '@babel/core': 7.29.0 9745 + '@babel/parser': 7.29.0 9746 + eslint: 10.3.0(jiti@2.6.1) 9747 + hermes-parser: 0.25.1 9748 + zod: 4.3.6 9749 + zod-validation-error: 4.0.2(zod@4.3.6) 9750 + transitivePeerDependencies: 9751 + - supports-color 9752 + 9753 + eslint-scope@9.1.2: 9754 + dependencies: 9755 + '@types/esrecurse': 4.3.1 9756 + '@types/estree': 1.0.8 9757 + esrecurse: 4.3.0 9758 + estraverse: 5.3.0 9759 + 9760 + eslint-visitor-keys@3.4.3: {} 9761 + 9762 + eslint-visitor-keys@5.0.1: {} 9763 + 9764 + eslint@10.3.0(jiti@2.6.1): 9765 + dependencies: 9766 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.6.1)) 9767 + '@eslint-community/regexpp': 4.12.2 9768 + '@eslint/config-array': 0.23.5 9769 + '@eslint/config-helpers': 0.5.5 9770 + '@eslint/core': 1.2.1 9771 + '@eslint/plugin-kit': 0.7.1 9772 + '@humanfs/node': 0.16.8 9773 + '@humanwhocodes/module-importer': 1.0.1 9774 + '@humanwhocodes/retry': 0.4.3 9775 + '@types/estree': 1.0.8 9776 + ajv: 6.15.0 9777 + cross-spawn: 7.0.6 9778 + debug: 4.4.3 9779 + escape-string-regexp: 4.0.0 9780 + eslint-scope: 9.1.2 9781 + eslint-visitor-keys: 5.0.1 9782 + espree: 11.2.0 9783 + esquery: 1.7.0 9784 + esutils: 2.0.3 9785 + fast-deep-equal: 3.1.3 9786 + file-entry-cache: 8.0.0 9787 + find-up: 5.0.0 9788 + glob-parent: 6.0.2 9789 + ignore: 5.3.2 9790 + imurmurhash: 0.1.4 9791 + is-glob: 4.0.3 9792 + json-stable-stringify-without-jsonify: 1.0.1 9793 + minimatch: 10.2.4 9794 + natural-compare: 1.4.0 9795 + optionator: 0.9.4 9796 + optionalDependencies: 9797 + jiti: 2.6.1 9798 + transitivePeerDependencies: 9799 + - supports-color 9800 + 9801 + espree@11.2.0: 9802 + dependencies: 9803 + acorn: 8.16.0 9804 + acorn-jsx: 5.3.2(acorn@8.16.0) 9805 + eslint-visitor-keys: 5.0.1 9806 + 9492 9807 esprima@4.0.1: {} 9808 + 9809 + esquery@1.7.0: 9810 + dependencies: 9811 + estraverse: 5.3.0 9812 + 9813 + esrecurse@4.3.0: 9814 + dependencies: 9815 + estraverse: 5.3.0 9816 + 9817 + estraverse@5.3.0: {} 9493 9818 9494 9819 estree-util-attach-comments@3.0.0: 9495 9820 dependencies: ··· 9567 9892 merge2: 1.4.1 9568 9893 micromatch: 4.0.8 9569 9894 9895 + fast-json-stable-stringify@2.1.0: {} 9896 + 9897 + fast-levenshtein@2.0.6: {} 9898 + 9570 9899 fast-uri@3.1.0: {} 9571 9900 9572 9901 fastq@1.20.1: ··· 9587 9916 9588 9917 fflate@0.8.2: {} 9589 9918 9919 + file-entry-cache@8.0.0: 9920 + dependencies: 9921 + flat-cache: 4.0.1 9922 + 9590 9923 fill-range@7.1.1: 9591 9924 dependencies: 9592 9925 to-regex-range: 5.0.1 ··· 9600 9933 dependencies: 9601 9934 locate-path: 6.0.0 9602 9935 path-exists: 4.0.0 9936 + 9937 + flat-cache@4.0.1: 9938 + dependencies: 9939 + flatted: 3.4.2 9940 + keyv: 4.5.4 9603 9941 9604 9942 flatted@3.4.2: {} 9605 9943 ··· 9759 10097 dependencies: 9760 10098 is-glob: 4.0.3 9761 10099 10100 + glob-parent@6.0.2: 10101 + dependencies: 10102 + is-glob: 4.0.3 10103 + 9762 10104 glob@13.0.6: 9763 10105 dependencies: 9764 10106 minimatch: 10.2.4 ··· 9912 10254 9913 10255 he@1.2.0: {} 9914 10256 10257 + hermes-estree@0.25.1: {} 10258 + 10259 + hermes-parser@0.25.1: 10260 + dependencies: 10261 + hermes-estree: 0.25.1 10262 + 9915 10263 hookable@6.1.1: {} 9916 10264 9917 10265 html-escaper@2.0.2: {} ··· 9942 10290 image-size@2.0.2: {} 9943 10291 9944 10292 import-without-cache@0.3.3: {} 10293 + 10294 + imurmurhash@0.1.4: {} 9945 10295 9946 10296 indent-string@4.0.0: {} 9947 10297 ··· 10042 10392 10043 10393 jsesc@3.1.0: {} 10044 10394 10395 + json-buffer@3.0.1: {} 10396 + 10397 + json-schema-traverse@0.4.1: {} 10398 + 10045 10399 json-schema-traverse@1.0.0: {} 10400 + 10401 + json-stable-stringify-without-jsonify@1.0.1: {} 10046 10402 10047 10403 json5@2.2.3: {} 10048 10404 10049 10405 jsonfile@4.0.0: 10050 10406 optionalDependencies: 10051 10407 graceful-fs: 4.2.11 10408 + 10409 + keyv@4.5.4: 10410 + dependencies: 10411 + json-buffer: 3.0.1 10052 10412 10053 10413 knip@6.6.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): 10054 10414 dependencies: ··· 10069 10429 transitivePeerDependencies: 10070 10430 - '@emnapi/core' 10071 10431 - '@emnapi/runtime' 10432 + 10433 + levn@0.4.1: 10434 + dependencies: 10435 + prelude-ls: 1.2.1 10436 + type-check: 0.4.0 10072 10437 10073 10438 lightningcss-android-arm64@1.32.0: 10074 10439 optional: true ··· 10674 11039 10675 11040 nanoid@3.3.11: {} 10676 11041 11042 + natural-compare@1.4.0: {} 11043 + 10677 11044 negotiator@0.6.4: {} 10678 11045 10679 11046 negotiator@1.0.0: {} ··· 10785 11152 openapi-typescript-helpers: 0.1.0 10786 11153 10787 11154 openapi-typescript-helpers@0.1.0: {} 11155 + 11156 + optionator@0.9.4: 11157 + dependencies: 11158 + deep-is: 0.1.4 11159 + fast-levenshtein: 2.0.6 11160 + levn: 0.4.1 11161 + prelude-ls: 1.2.1 11162 + type-check: 0.4.0 11163 + word-wrap: 1.2.5 10788 11164 10789 11165 outdent@0.5.0: {} 10790 11166 ··· 11060 11436 picocolors: 1.1.1 11061 11437 source-map-js: 1.2.1 11062 11438 11439 + prelude-ls@1.2.1: {} 11440 + 11063 11441 prettier@2.8.8: {} 11064 11442 11065 11443 prettier@3.8.1: {} ··· 11078 11456 package-manager-detector: 1.6.0 11079 11457 picocolors: 1.1.1 11080 11458 sade: 1.8.1 11459 + 11460 + punycode@2.3.1: {} 11081 11461 11082 11462 quansync@0.2.11: {} 11083 11463 ··· 11744 12124 '@turbo/windows-64': 2.9.6 11745 12125 '@turbo/windows-arm64': 2.9.6 11746 12126 12127 + type-check@0.4.0: 12128 + dependencies: 12129 + prelude-ls: 1.2.1 12130 + 11747 12131 type-fest@2.19.0: {} 11748 12132 11749 12133 typescript@5.6.1-rc: {} ··· 11839 12223 dependencies: 11840 12224 registry-auth-token: 3.3.2 11841 12225 registry-url: 3.1.0 12226 + 12227 + uri-js@4.4.1: 12228 + dependencies: 12229 + punycode: 2.3.1 11842 12230 11843 12231 use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.5): 11844 12232 dependencies: ··· 12001 12389 dependencies: 12002 12390 string-width: 5.1.2 12003 12391 12392 + word-wrap@1.2.5: {} 12393 + 12004 12394 wrap-ansi@6.2.0: 12005 12395 dependencies: 12006 12396 ansi-styles: 4.3.0 ··· 12055 12445 yocto-queue@0.1.0: {} 12056 12446 12057 12447 yoctocolors-cjs@2.1.3: {} 12448 + 12449 + zod-validation-error@4.0.2(zod@4.3.6): 12450 + dependencies: 12451 + zod: 4.3.6 12058 12452 12059 12453 zod@3.25.76: {} 12060 12454
+120 -55
packages/turbo-generators/config.ts
··· 5 5 6 6 const DOCS_GROUPS = ['actions', 'feedback', 'forms', 'typography', 'visuals'] as const; 7 7 8 + const COMPONENT_NAME_RE = /^[A-Za-z][A-Za-z0-9-]*$/; 9 + const CAMEL_BOUNDARY_RE = /([a-z0-9])([A-Z])/g; 10 + const NON_ALPHANUM_RE = /[^A-Za-z0-9-]/g; 11 + const TRAILING_WHITESPACE_RE = /\s+$/; 12 + const TRAILING_NEWLINES_RE = /\n*$/; 13 + 8 14 type GeneratorAnswers = { 9 15 group?: string; 10 16 name?: string; 11 17 }; 12 18 13 19 function validateComponentName(value: unknown): true | string { 14 - if (!value || typeof value !== 'string') { 20 + if (typeof value !== 'string') { 15 21 return 'Component name is required.'; 16 22 } 17 23 ··· 20 26 return 'Component name is required.'; 21 27 } 22 28 23 - if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(trimmed)) { 29 + if (!COMPONENT_NAME_RE.test(trimmed)) { 24 30 return 'Use letters/numbers/hyphens. Start with a letter.'; 25 31 } 26 32 ··· 30 36 function toKebabCase(value: string): string { 31 37 return value 32 38 .trim() 33 - .replaceAll(/([a-z0-9])([A-Z])/g, '$1-$2') 34 - .replaceAll(/[^A-Za-z0-9-]/g, '-') 39 + .replaceAll(CAMEL_BOUNDARY_RE, '$1-$2') 40 + .replaceAll(NON_ALPHANUM_RE, '-') 35 41 .toLowerCase(); 36 42 } 37 43 38 44 function toDisplayName(value: string): string { 39 - return toKebabCase(value) 40 - .split('-') 41 - .filter(Boolean) 42 - .map((part) => part[0]?.toUpperCase() + part.slice(1)) 43 - .join(' '); 45 + const kebab = toKebabCase(value); 46 + let result = ''; 47 + let capitalize = true; 48 + 49 + for (let i = 0; i < kebab.length; i += 1) { 50 + const ch = kebab.charAt(i); 51 + if (ch === '-') { 52 + result += ' '; 53 + capitalize = true; 54 + } else { 55 + result += capitalize ? ch.toUpperCase() : ch; 56 + capitalize = false; 57 + } 58 + } 59 + 60 + return result; 44 61 } 45 62 46 63 function toCamelCase(value: string): string { 47 - const parts = toKebabCase(value).split('-').filter(Boolean); 48 - if (parts.length === 0) { 49 - return ''; 64 + const kebab = toKebabCase(value); 65 + let result = ''; 66 + let capitalize = false; 67 + 68 + for (let i = 0; i < kebab.length; i += 1) { 69 + const ch = kebab.charAt(i); 70 + if (ch === '-') { 71 + capitalize = true; 72 + } else { 73 + result += capitalize ? ch.toUpperCase() : ch; 74 + capitalize = false; 75 + } 50 76 } 51 - return parts 52 - .map((part, index) => { 53 - return index === 0 ? part : (part[0]?.toUpperCase() ?? '') + part.slice(1); 54 - }) 55 - .join(''); 77 + 78 + return result; 56 79 } 57 80 58 81 function resolveGroup(answers: GeneratorAnswers): string { ··· 72 95 return toKebabCase(name); 73 96 } 74 97 98 + const DOCS_COMPONENTS_DIR = join(process.cwd(), 'apps/docs/content/docs/components'); 99 + const DOCS_CONTENT_DIR = join(process.cwd(), 'apps/docs/content/docs'); 100 + const DOCS_INDEX_PATH = join(DOCS_CONTENT_DIR, 'index.mdx'); 101 + const GETTING_STARTED_PATH = join(DOCS_CONTENT_DIR, 'getting-started.mdx'); 102 + const RECIPES_LAYERS_PATH = join( 103 + process.cwd(), 104 + 'packages/@luke-ui/react/src/recipes/layers.css.ts', 105 + ); 106 + const RECIPES_INDEX_PATH = join(process.cwd(), 'packages/@luke-ui/react/src/recipes/index.ts'); 107 + 75 108 function addComponentToDocsMeta(answers: GeneratorAnswers): string { 76 109 const group = resolveGroup(answers); 77 110 const componentDir = resolveComponentDir(answers); 78 - const groupDir = join(process.cwd(), 'apps/docs/content/docs/components', group); 111 + const groupDir = join(DOCS_COMPONENTS_DIR, group); 79 112 const groupMetaPath = join(groupDir, 'meta.json'); 80 113 81 114 mkdirSync(groupDir, { recursive: true }); ··· 93 126 return `Docs meta already contains ${componentDir}`; 94 127 } 95 128 96 - meta.pages = [...pages, componentDir].sort(); 129 + pages.push(componentDir); 130 + pages.sort(); 131 + meta.pages = pages; 97 132 writeFileSync(groupMetaPath, `${JSON.stringify(meta, null, '\t')}\n`); 98 133 99 - const parentMetaPath = join(process.cwd(), 'apps/docs/content/docs/components/meta.json'); 134 + const parentMetaPath = join(DOCS_CONTENT_DIR, 'meta.json'); 100 135 const parentContent = readFileSync(parentMetaPath, 'utf8'); 101 136 const parentMeta = JSON.parse(parentContent) as { 102 137 pages?: Array<string>; ··· 104 139 }; 105 140 const parentPages = Array.isArray(parentMeta.pages) ? parentMeta.pages : []; 106 141 if (!parentPages.includes(group)) { 107 - parentMeta.pages = [...parentPages, group].sort(); 142 + parentPages.push(group); 143 + parentPages.sort(); 144 + parentMeta.pages = parentPages; 108 145 writeFileSync(parentMetaPath, `${JSON.stringify(parentMeta, null, '\t')}\n`); 109 146 } 110 147 ··· 114 151 function addComponentToDocsIndex(answers: GeneratorAnswers): string { 115 152 const group = resolveGroup(answers); 116 153 const componentDir = resolveComponentDir(answers); 117 - const filePath = join(process.cwd(), 'apps/docs/content/docs/index.mdx'); 118 154 const href = `/docs/components/${group}/${componentDir}`; 119 155 const cardLine = `\t<Card title="${toDisplayName(componentDir)} component" href="${href}" />`; 120 156 121 - const content = readFileSync(filePath, 'utf8'); 157 + const content = readFileSync(DOCS_INDEX_PATH, 'utf8'); 122 158 if (content.includes(`href="${href}"`)) { 123 159 return `Docs index already links ${componentDir}`; 124 160 } 125 161 126 162 const marker = '</Cards>'; 127 163 if (!content.includes(marker)) { 128 - throw new Error(`Could not find "${marker}" in ${filePath}`); 164 + throw new Error(`Could not find "${marker}" in ${DOCS_INDEX_PATH}`); 129 165 } 130 166 131 167 const updatedContent = content.replace(marker, `${cardLine}\n${marker}`); 132 - writeFileSync(filePath, updatedContent); 168 + writeFileSync(DOCS_INDEX_PATH, updatedContent); 133 169 return `Added docs index card for ${componentDir}`; 134 170 } 135 171 136 172 function addComponentToGettingStarted(answers: GeneratorAnswers): string { 137 173 const group = resolveGroup(answers); 138 174 const componentDir = resolveComponentDir(answers); 139 - const filePath = join(process.cwd(), 'apps/docs/content/docs/getting-started.mdx'); 140 175 const displayName = toDisplayName(componentDir); 141 176 const nextStepLine = `- Read the [${displayName}](/docs/components/${group}/${componentDir}) docs.`; 142 177 143 - const content = readFileSync(filePath, 'utf8'); 178 + const content = readFileSync(GETTING_STARTED_PATH, 'utf8'); 144 179 if (content.includes(nextStepLine)) { 145 180 return `Getting Started already links ${componentDir}`; 146 181 } 147 182 148 183 const lines = content.split('\n'); 149 - const nextStepsIndex = lines.findIndex((line) => line.trim() === '## Next Steps'); 184 + let nextStepsIndex = -1; 185 + for (let i = 0; i < lines.length; i += 1) { 186 + if (lines[i]?.trim() === '## Next Steps') { 187 + nextStepsIndex = i; 188 + break; 189 + } 190 + } 150 191 151 192 if (nextStepsIndex === -1) { 152 - const appended = `${content.trimEnd()}\n\n## Next Steps\n\n${nextStepLine}\n`; 153 - writeFileSync(filePath, appended); 193 + const appended = `${content.replace(TRAILING_WHITESPACE_RE, '')}\n\n## Next Steps\n\n${nextStepLine}\n`; 194 + writeFileSync(GETTING_STARTED_PATH, appended); 154 195 return `Added Next Steps section with ${componentDir}`; 155 196 } 156 197 157 198 let insertAt = lines.length; 158 199 for (let i = nextStepsIndex + 1; i < lines.length; i += 1) { 159 - if (lines[i]?.startsWith('## ')) { 200 + const line = lines[i]; 201 + if (line && line.charAt(0) === '#' && line.charAt(1) === '#' && line.charAt(2) === ' ') { 160 202 insertAt = i; 161 203 break; 162 204 } 163 205 } 164 206 165 207 lines.splice(insertAt, 0, nextStepLine); 166 - writeFileSync(filePath, `${lines.join('\n').replace(/\n*$/, '\n')}`); 208 + writeFileSync(GETTING_STARTED_PATH, `${lines.join('\n').replace(TRAILING_NEWLINES_RE, '\n')}`); 167 209 return `Added Getting Started next step for ${componentDir}`; 168 210 } 169 211 170 - const STYLES_PRIMITIVES_PATH = join( 171 - process.cwd(), 172 - 'packages/@luke-ui/react/src/styles/primitives.css.ts', 173 - ); 174 - const RECIPES_INDEX_PATH = join(process.cwd(), 'packages/@luke-ui/react/src/recipes/index.ts'); 175 - 176 - /** Append new component CSS import to styles/primitives.css.ts and sort. */ 212 + /** Append new component CSS import to recipes/layers.css.ts and sort. */ 177 213 function addComponentToStylesIndex(answers: GeneratorAnswers): string { 178 214 const componentDir = resolveComponentDir(answers); 179 - const newImport = `import '../recipes/${componentDir}.css.js';`; 180 - const content = readFileSync(STYLES_PRIMITIVES_PATH, 'utf8'); 215 + const newImport = `import './${componentDir}.css.js';`; 216 + const content = readFileSync(RECIPES_LAYERS_PATH, 'utf8'); 181 217 if (content.includes(newImport)) { 182 - return `Styles primitives barrel already contains ${componentDir} CSS`; 218 + return `Recipes layers already contains ${componentDir} CSS`; 183 219 } 184 - const imports = Array.from( 185 - new Set( 186 - content 187 - .split('\n') 188 - .map((line) => line.trim()) 189 - .filter(Boolean) 190 - .concat(newImport), 191 - ), 192 - ); 220 + 221 + const seen = new Set<string>(); 222 + const imports: Array<string> = []; 223 + let start = 0; 224 + 225 + for (let i = 0; i <= content.length; i += 1) { 226 + if (i === content.length || content.charAt(i) === '\n') { 227 + const line = content.slice(start, i).trim(); 228 + if (line && !seen.has(line)) { 229 + seen.add(line); 230 + imports.push(line); 231 + } 232 + start = i + 1; 233 + } 234 + } 235 + 236 + if (!seen.has(newImport)) { 237 + imports.push(newImport); 238 + } 239 + 193 240 imports.sort((a, b) => a.localeCompare(b)); 194 - writeFileSync(STYLES_PRIMITIVES_PATH, `${imports.join('\n')}\n`); 195 - return `Added ${componentDir} CSS to styles primitives barrel`; 241 + writeFileSync(RECIPES_LAYERS_PATH, `${imports.join('\n')}\n`); 242 + return `Added ${componentDir} CSS to recipes layers`; 196 243 } 197 244 198 245 function addComponentToRecipesIndex(answers: GeneratorAnswers): string { ··· 206 253 if (content.includes(typeExport)) { 207 254 return `Recipes index already contains ${componentDir}`; 208 255 } 209 - const firstExportIndex = content.search(/^export\s/m); 256 + 257 + let firstExportIndex = -1; 258 + for (let i = 0; i < content.length - 6; i += 1) { 259 + if ( 260 + content.charAt(i) === 'e' && 261 + content.charAt(i + 1) === 'x' && 262 + content.charAt(i + 2) === 'p' && 263 + content.charAt(i + 3) === 'o' && 264 + content.charAt(i + 4) === 'r' && 265 + content.charAt(i + 5) === 't' && 266 + content.charAt(i + 6) === ' ' 267 + ) { 268 + if (i === 0 || content.charAt(i - 1) === '\n') { 269 + firstExportIndex = i; 270 + break; 271 + } 272 + } 273 + } 274 + 210 275 if (firstExportIndex === -1) { 211 - const separator = content.endsWith('\n') ? '' : '\n'; 276 + const separator = content.charAt(content.length - 1) === '\n' ? '' : '\n'; 212 277 writeFileSync(RECIPES_INDEX_PATH, `${content}${separator}${typeExport}\n${valueExport}\n`); 213 278 return `Added ${componentDir} to recipes index`; 214 279 }
+18
packages/turbo-generators/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "forceConsistentCasingInFileNames": true, 4 + "isolatedModules": true, 5 + "lib": ["ES2022"], 6 + "module": "NodeNext", 7 + "moduleDetection": "force", 8 + "moduleResolution": "NodeNext", 9 + "noEmit": true, 10 + "noUncheckedIndexedAccess": true, 11 + "resolveJsonModule": true, 12 + "skipLibCheck": true, 13 + "strict": true, 14 + "target": "ES2022", 15 + "types": ["node"] 16 + }, 17 + "include": ["**/*.ts"] 18 + }
+1
packages/@luke-ui/react/AGENTS.md
··· 3 3 - Do not hand-edit `.generated/entries.ts` or `package.json` exports; entries are generated, tsdown updates exports at build. 4 4 - When adding a component, use `pnpm generate:component` from repo root (not manual file creation) so the group barrel, styles index, and docs are updated correctly. 5 5 - Stories (`*.stories.tsx`) are the tests; there are no separate `*.test.tsx` files. 6 + - React Compiler is enabled — do not use `useCallback` or `useMemo`; the compiler auto-memoizes.
-3
packages/@luke-ui/react/package.json
··· 15 15 "./combobox-field": "./dist/combobox-field/index.js", 16 16 "./emoji": "./dist/emoji/index.js", 17 17 "./field": "./dist/field/index.js", 18 - "./field-description": "./dist/field-description/index.js", 19 - "./field-error": "./dist/field-error/index.js", 20 - "./field-label": "./dist/field-label/index.js", 21 18 "./field/primitive": "./dist/field/primitive.js", 22 19 "./heading": "./dist/heading/index.js", 23 20 "./heading-context": "./dist/heading-context/index.js",
+1 -1
packages/@luke-ui/react/src/field.docs.mdx
··· 6 6 Primitive building blocks: 7 7 8 8 ```ts 9 - import { Field, FieldDescription, FieldError, FieldLabel } from '@luke-ui/react/field/primitive'; 9 + import { Field, FieldDescription, FieldError, FieldLabel } from '@luke-ui/react/field'; 10 10 ``` 11 11 12 12 Opinionated composed component:
+1 -1
packages/turbo-generators/templates/component/component.tsx.hbs
··· 1 1 import type { ComponentProps, JSX } from 'react'; 2 - import type { DistributiveOmit } from '../types/index.js'; 2 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 3 3 import { cx } from '../utils/index.js'; 4 4 import * as styles from '../recipes/{{kebabCase name}}.css.js'; 5 5
+1 -1
packages/@luke-ui/react/src/button/index.tsx
··· 1 1 import type { JSX } from 'react'; 2 2 import type { ButtonProps as PrimitiveButtonProps } from '../button/primitive.js'; 3 3 import { Button as PrimitiveButton } from '../button/primitive.js'; 4 - import { BUTTON_FONT_SIZE, BUTTON_ICON_SIZE } from '../lib/button.js'; 5 4 import { LoadingSpinner } from '../loading-spinner/index.js'; 6 5 import * as styles from '../recipes/button-composed.css.js'; 7 6 import type * as primitiveStyles from '../recipes/button.css.js'; 7 + import { BUTTON_FONT_SIZE, BUTTON_ICON_SIZE } from '../sizing/button-sizing.js'; 8 8 import { Text } from '../text/index.js'; 9 9 10 10 interface ComposedButtonVariantProps extends NonNullable<styles.ButtonLabelVariants> {}
+1 -1
packages/@luke-ui/react/src/button/primitive.tsx
··· 3 3 import { Button as RacButton } from 'react-aria-components/Button'; 4 4 import { composeRenderProps } from 'react-aria-components/composeRenderProps'; 5 5 import { IconSizeProvider } from '../icon-size-context/index.js'; 6 - import { BUTTON_ICON_SIZE } from '../lib/button.js'; 7 6 import * as styles from '../recipes/button.css.js'; 7 + import { BUTTON_ICON_SIZE } from '../sizing/button-sizing.js'; 8 8 import { cx } from '../utils/index.js'; 9 9 10 10 interface ButtonVariantProps extends NonNullable<styles.ButtonVariants> {}
+15 -20
packages/@luke-ui/react/src/combobox-field/index.tsx
··· 1 1 import type { CSSProperties, JSX, ReactNode } from 'react'; 2 - import type { 3 - ComboboxInputProps, 4 - ComboboxListBoxProps, 5 - ComboboxLoadMoreItemProps, 6 - ComboboxPopoverProps, 7 - ComboboxSize, 8 - } from '../combobox/index.js'; 9 - import { 10 - ComboboxControl, 11 - ComboboxEmptyState, 12 - ComboboxInput, 13 - ComboboxListBox, 14 - ComboboxLoadMoreItem, 15 - ComboboxPopover, 16 - ComboboxTextInput, 17 - ComboboxTrigger, 18 - } from '../combobox/index.js'; 19 - import type { FieldErrorProps } from '../field-error/index.js'; 20 - import type { FieldNecessityIndicator } from '../field-label/index.js'; 2 + import { ComboboxControl } from '../combobox/control.js'; 3 + import { ComboboxEmptyState } from '../combobox/empty-state.js'; 4 + import { ComboboxTextInput } from '../combobox/input.js'; 5 + import { ComboboxLoadMoreItem } from '../combobox/item.js'; 6 + import type { ComboboxLoadMoreItemProps } from '../combobox/item.js'; 7 + import { ComboboxListBox } from '../combobox/listbox.js'; 8 + import type { ComboboxListBoxProps } from '../combobox/listbox.js'; 9 + import { ComboboxPopover } from '../combobox/popover.js'; 10 + import type { ComboboxPopoverProps } from '../combobox/popover.js'; 11 + import { ComboboxInput } from '../combobox/root.js'; 12 + import type { ComboboxInputProps, ComboboxSize } from '../combobox/root.js'; 13 + import { ComboboxTrigger } from '../combobox/trigger.js'; 14 + import type { FieldErrorProps } from '../field/error.js'; 21 15 import { Field } from '../field/index.js'; 16 + import type { FieldNecessityIndicator } from '../field/label.js'; 22 17 import { Icon } from '../icon/index.js'; 23 18 import { LoadingSpinner } from '../loading-spinner/index.js'; 24 - import type { DistributiveOmit } from '../types/index.js'; 19 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 25 20 26 21 type ComboboxLoadingState = 'error' | 'filtering' | 'idle' | 'loading' | 'loadingMore' | 'sorting'; 27 22
+34
packages/@luke-ui/react/src/combobox/control.tsx
··· 1 + import type { JSX } from 'react'; 2 + import type { GroupProps as RacGroupProps } from 'react-aria-components/Group'; 3 + import { Group as RacGroup } from 'react-aria-components/Group'; 4 + import { composeRenderProps } from 'react-aria-components/composeRenderProps'; 5 + import * as styles from '../recipes/combobox.css.js'; 6 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 7 + import { cx } from '../utils/index.js'; 8 + 9 + interface ComboboxVariantProps extends NonNullable<styles.ComboboxVariants> {} 10 + 11 + interface ComboboxStyleProps { 12 + /** Control size. */ 13 + size?: ComboboxVariantProps['size']; 14 + } 15 + 16 + /** Props for the styled combobox control group. */ 17 + export interface ComboboxControlProps 18 + extends DistributiveOmit<RacGroupProps, 'className'>, ComboboxStyleProps { 19 + className?: RacGroupProps['className']; 20 + } 21 + 22 + /** Control wrapper for combobox text input + trigger content. */ 23 + export function ComboboxControl(props: ComboboxControlProps): JSX.Element { 24 + const { size = 'medium', ...groupProps } = props; 25 + 26 + return ( 27 + <RacGroup 28 + {...groupProps} 29 + className={composeRenderProps(groupProps.className, (className) => { 30 + return cx(styles.comboboxControl({ size }), className); 31 + })} 32 + /> 33 + ); 34 + }
+14
packages/@luke-ui/react/src/combobox/empty-state.tsx
··· 1 + import type { JSX, ReactNode } from 'react'; 2 + import * as styles from '../recipes/combobox.css.js'; 3 + import { cx } from '../utils/index.js'; 4 + 5 + export interface ComboboxEmptyStateProps { 6 + children: ReactNode; 7 + className?: string; 8 + } 9 + 10 + export function ComboboxEmptyState(props: ComboboxEmptyStateProps): JSX.Element { 11 + const { children, className } = props; 12 + 13 + return <div className={cx(styles.comboboxEmptyState(), className)}>{children}</div>; 14 + }
+18 -312
packages/@luke-ui/react/src/combobox/index.tsx
··· 1 - import type { JSX, ReactNode } from 'react'; 2 - import { useCallback, useContext } from 'react'; 3 - import { Collection } from 'react-aria-components/Collection'; 4 - import type { 5 - ButtonProps as RacButtonProps, 6 - ComboBoxProps as RacComboBoxProps, 7 - InputProps as RacInputProps, 8 - Key, 9 - ListBoxItemProps as RacListBoxItemProps, 10 - ListBoxLoadMoreItemProps as RacListBoxLoadMoreItemProps, 11 - ListBoxProps as RacListBoxProps, 12 - ListBoxSectionProps as RacListBoxSectionProps, 13 - PopoverProps as RacPopoverProps, 14 - } from 'react-aria-components/ComboBox'; 15 - import { 16 - ComboBoxStateContext, 17 - Button as RacButton, 18 - ComboBox as RacComboBox, 19 - Input as RacInput, 20 - ListBox as RacListBox, 21 - ListBoxItem as RacListBoxItem, 22 - ListBoxLoadMoreItem as RacListBoxLoadMoreItem, 23 - ListBoxSection as RacListBoxSection, 24 - Popover as RacPopover, 25 - } from 'react-aria-components/ComboBox'; 26 - import type { GroupProps as RacGroupProps } from 'react-aria-components/Group'; 27 - import { Group as RacGroup } from 'react-aria-components/Group'; 28 - import { Header as RacHeader } from 'react-aria-components/Header'; 29 - import { ListBoxContext } from 'react-aria-components/ListBox'; 30 - import { composeRenderProps } from 'react-aria-components/composeRenderProps'; 31 - import { useSlottedContext } from 'react-aria-components/slots'; 32 - import * as styles from '../recipes/combobox.css.js'; 33 - import { themeRootClassName } from '../theme/index.js'; 34 - import type { DistributiveOmit } from '../types/index.js'; 35 - import { cx } from '../utils/index.js'; 36 - 37 - interface ComboboxVariantProps extends NonNullable<styles.ComboboxVariants> {} 38 - 39 - interface ComboboxStyleProps { 40 - /** Control size. */ 41 - size?: ComboboxVariantProps['size']; 42 - } 43 - 44 - /** Allowed `size` values for Combobox primitives. */ 45 - export type ComboboxSize = NonNullable<ComboboxVariantProps['size']>; 46 - 47 - /** Props for the `ComboboxInput` combobox root. */ 48 - export interface ComboboxInputProps<T extends object> extends DistributiveOmit< 49 - RacComboBoxProps<T, 'single'>, 50 - | 'defaultSelectedKey' 51 - | 'defaultValue' 52 - | 'onChange' 53 - | 'onOpenChange' 54 - | 'onSelectionChange' 55 - | 'selectedKey' 56 - | 'selectionMode' 57 - | 'value' 58 - > { 59 - /** The currently selected key (controlled). Pass `null` for no selection. */ 60 - value?: Key | null; 61 - 62 - /** The initially selected key (uncontrolled). */ 63 - defaultValue?: Key | null; 64 - 65 - /** Called when the selected value changes. */ 66 - onChange?: (value: Key | null) => void; 67 - 68 - /** Called when the open state changes. */ 69 - onOpenChange?: (isOpen: boolean) => void; 70 - 71 - /** 72 - * The interaction required to display the ComboBox menu. 73 - * @default 'focus' 74 - */ 75 - menuTrigger?: 'focus' | 'input' | 'manual'; 76 - } 77 - 78 - /** Spectrum-style combobox root primitive (single-select only). */ 79 - export function ComboboxInput<T extends object>(props: ComboboxInputProps<T>): JSX.Element { 80 - const { className, menuTrigger = 'focus', ...comboboxProps } = props; 81 - 82 - return ( 83 - <RacComboBox 84 - {...comboboxProps} 85 - menuTrigger={menuTrigger} 86 - className={composeRenderProps(className, (renderedClassName) => { 87 - return cx(styles.comboboxRoot, renderedClassName); 88 - })} 89 - /> 90 - ); 91 - } 92 - 93 - /** Props for the styled combobox control group. */ 94 - export interface ComboboxControlProps 95 - extends DistributiveOmit<RacGroupProps, 'className'>, ComboboxStyleProps { 96 - className?: RacGroupProps['className']; 97 - } 98 - 99 - /** Control wrapper for combobox text input + trigger content. */ 100 - export function ComboboxControl(props: ComboboxControlProps): JSX.Element { 101 - const { size = 'medium', ...groupProps } = props; 102 - 103 - return ( 104 - <RacGroup 105 - {...groupProps} 106 - className={composeRenderProps(groupProps.className, (className) => { 107 - return cx(styles.comboboxControl({ size }), className); 108 - })} 109 - /> 110 - ); 111 - } 112 - 113 - /** Props for the styled combobox text input. */ 114 - export interface ComboboxTextInputProps 115 - extends 116 - DistributiveOmit<RacInputProps, 'className' | keyof ComboboxStyleProps>, 117 - ComboboxStyleProps { 118 - className?: RacInputProps['className']; 119 - } 120 - 121 - /** Text input used within `ComboboxControl` for combobox behavior. */ 122 - export function ComboboxTextInput(props: ComboboxTextInputProps): JSX.Element { 123 - const { onClick, size = 'medium', ...inputProps } = props; 124 - const state = useContext(ComboBoxStateContext); 125 - 126 - const handleClick = useCallback( 127 - (event: React.MouseEvent<HTMLInputElement>) => { 128 - onClick?.(event); 129 - if (!state?.isOpen) { 130 - state?.open(); 131 - } 132 - }, 133 - [onClick, state], 134 - ); 135 - 136 - return ( 137 - <RacInput 138 - {...inputProps} 139 - onClick={handleClick} 140 - className={composeRenderProps(inputProps.className, (className) => { 141 - return cx(styles.comboboxTextInput({ size }), className); 142 - })} 143 - /> 144 - ); 145 - } 146 - 147 - /** Props for the combobox trigger button. */ 148 - export interface ComboboxTriggerProps 149 - extends DistributiveOmit<RacButtonProps, 'className'>, ComboboxStyleProps { 150 - className?: RacButtonProps['className']; 151 - } 152 - 153 - /** Trigger button used by combobox pattern. */ 154 - export function ComboboxTrigger(props: ComboboxTriggerProps): JSX.Element { 155 - const { size = 'medium', ...buttonProps } = props; 156 - 157 - return ( 158 - <RacButton 159 - {...buttonProps} 160 - className={composeRenderProps(buttonProps.className, (className) => { 161 - return cx(styles.comboboxTrigger({ size }), className); 162 - })} 163 - /> 164 - ); 165 - } 166 - 167 - /** Props for the styled combobox popover. */ 168 - export interface ComboboxPopoverProps extends DistributiveOmit< 169 - RacPopoverProps, 170 - 'UNSTABLE_portalContainer' 171 - > {} 172 - 173 - /** Popover surface used for listbox content. */ 174 - export function ComboboxPopover(props: ComboboxPopoverProps): JSX.Element { 175 - return ( 176 - <RacPopover 177 - {...props} 178 - className={composeRenderProps(props.className, (className) => { 179 - return cx(themeRootClassName, styles.comboboxPopover(), className); 180 - })} 181 - /> 182 - ); 183 - } 184 - 185 - /** Props for the styled listbox. */ 186 - export interface ComboboxListBoxProps<T extends object> extends DistributiveOmit< 187 - RacListBoxProps<T>, 188 - 'dependencies' | 'items' 189 - > { 190 - /** Item content for the listbox (render prop or static children). */ 191 - children?: RacListBoxProps<T>['children']; 192 - /** Values that should invalidate the dynamic item cache. */ 193 - dependencies?: ReadonlyArray<unknown>; 194 - /** Dynamic items rendered by the `children` render prop. */ 195 - items?: Iterable<T>; 196 - /** Optional content appended after the main collection, e.g. a load-more sentinel. */ 197 - loadMoreItem?: ReactNode; 198 - } 199 - 200 - /** Styled listbox for combobox options. */ 201 - export function ComboboxListBox<T extends object>(props: ComboboxListBoxProps<T>): JSX.Element { 202 - const { children, dependencies, items, loadMoreItem, ...listBoxProps } = props; 203 - const listBoxContext = useSlottedContext(ListBoxContext); 204 - const collectionItems = items ?? listBoxContext?.items; 205 - const listBoxChildren = 206 - typeof children === 'function' ? ( 207 - <Collection<T> dependencies={dependencies} items={collectionItems}> 208 - {children} 209 - </Collection> 210 - ) : ( 211 - children 212 - ); 213 - 214 - return ( 215 - <RacListBox 216 - className={composeRenderProps(listBoxProps.className, (className) => { 217 - return cx(styles.comboboxListBox(), className); 218 - })} 219 - {...listBoxProps} 220 - > 221 - {listBoxChildren} 222 - {loadMoreItem} 223 - </RacListBox> 224 - ); 225 - } 226 - 227 - /** Props for the combobox load-more sentinel row. */ 228 - export interface ComboboxLoadMoreItemProps 229 - extends DistributiveOmit<RacListBoxLoadMoreItemProps, 'className'>, ComboboxStyleProps { 230 - className?: RacListBoxLoadMoreItemProps['className']; 231 - } 232 - 233 - /** Styled load-more sentinel for async combobox collections. */ 234 - export function ComboboxLoadMoreItem(props: ComboboxLoadMoreItemProps): JSX.Element { 235 - const { size = 'medium', ...loadMoreItemProps } = props; 236 - 237 - return ( 238 - <RacListBoxLoadMoreItem 239 - {...loadMoreItemProps} 240 - className={cx(styles.comboboxLoadMoreItem({ size }), loadMoreItemProps.className)} 241 - /> 242 - ); 243 - } 244 - 245 - /** Props for a styled combobox option item. */ 246 - export interface ComboboxItemProps<T extends object> 247 - extends DistributiveOmit<RacListBoxItemProps<T>, 'className'>, ComboboxStyleProps { 248 - className?: RacListBoxItemProps<T>['className']; 249 - } 250 - 251 - /** Styled listbox option item. */ 252 - export function ComboboxItem<T extends object>(props: ComboboxItemProps<T>): JSX.Element { 253 - const { size = 'medium', ...itemProps } = props; 254 - 255 - return ( 256 - <RacListBoxItem 257 - {...itemProps} 258 - className={composeRenderProps(itemProps.className, (className) => { 259 - return cx(styles.comboboxItem({ size }), className); 260 - })} 261 - /> 262 - ); 263 - } 264 - 265 - /** Props for a styled section in the combobox listbox. */ 266 - export interface ComboboxSectionProps<T extends object> extends DistributiveOmit< 267 - RacListBoxSectionProps<T>, 268 - 'className' 269 - > { 270 - className?: RacListBoxSectionProps<T>['className']; 271 - /** Optional heading rendered above section items. */ 272 - title?: ReactNode; 273 - } 274 - 275 - /** Styled section wrapper for grouped options. */ 276 - export function ComboboxSection<T extends object>(props: ComboboxSectionProps<T>): JSX.Element { 277 - const { children, className, title, ...sectionProps } = props; 278 - 279 - const sectionClassName = cx(styles.comboboxSection(), className); 280 - 281 - if (typeof children === 'function') { 282 - return ( 283 - <RacListBoxSection {...sectionProps} className={sectionClassName}> 284 - {children} 285 - </RacListBoxSection> 286 - ); 287 - } 288 - 289 - return ( 290 - <RacListBoxSection {...sectionProps} className={sectionClassName}> 291 - {title != null ? ( 292 - <RacHeader className={styles.comboboxSectionHeading}>{title}</RacHeader> 293 - ) : null} 294 - {children} 295 - </RacListBoxSection> 296 - ); 297 - } 298 - 299 - /** Props for the combobox empty state container. */ 300 - export interface ComboboxEmptyStateProps { 301 - /** Content rendered inside the empty state container. */ 302 - children: ReactNode; 303 - /** Additional CSS class name. */ 304 - className?: string; 305 - } 306 - 307 - /** Styled container for combobox empty and loading states. */ 308 - export function ComboboxEmptyState(props: ComboboxEmptyStateProps): JSX.Element { 309 - const { children, className } = props; 310 - 311 - return <div className={cx(styles.comboboxEmptyState(), className)}>{children}</div>; 312 - } 1 + export { ComboboxControl } from './control.js'; 2 + export type { ComboboxControlProps } from './control.js'; 3 + export { ComboboxEmptyState } from './empty-state.js'; 4 + export type { ComboboxEmptyStateProps } from './empty-state.js'; 5 + export { ComboboxInput } from './root.js'; 6 + export type { ComboboxInputProps, ComboboxSize } from './root.js'; 7 + export { ComboboxItem, ComboboxLoadMoreItem } from './item.js'; 8 + export type { ComboboxItemProps, ComboboxLoadMoreItemProps } from './item.js'; 9 + export { ComboboxListBox } from './listbox.js'; 10 + export type { ComboboxListBoxProps } from './listbox.js'; 11 + export { ComboboxPopover } from './popover.js'; 12 + export type { ComboboxPopoverProps } from './popover.js'; 13 + export { ComboboxSection } from './section.js'; 14 + export type { ComboboxSectionProps } from './section.js'; 15 + export { ComboboxTextInput } from './input.js'; 16 + export type { ComboboxTextInputProps } from './input.js'; 17 + export { ComboboxTrigger } from './trigger.js'; 18 + export type { ComboboxTriggerProps } from './trigger.js';
+45
packages/@luke-ui/react/src/combobox/input.tsx
··· 1 + import type { JSX } from 'react'; 2 + import { useContext } from 'react'; 3 + import type { InputProps as RacInputProps } from 'react-aria-components/ComboBox'; 4 + import { ComboBoxStateContext, Input as RacInput } from 'react-aria-components/ComboBox'; 5 + import { composeRenderProps } from 'react-aria-components/composeRenderProps'; 6 + import * as styles from '../recipes/combobox.css.js'; 7 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 8 + import { cx } from '../utils/index.js'; 9 + 10 + interface ComboboxVariantProps extends NonNullable<styles.ComboboxVariants> {} 11 + 12 + interface ComboboxStyleProps { 13 + size?: ComboboxVariantProps['size']; 14 + } 15 + 16 + /** Props for the styled combobox text input. */ 17 + export interface ComboboxTextInputProps 18 + extends 19 + DistributiveOmit<RacInputProps, 'className' | keyof ComboboxStyleProps>, 20 + ComboboxStyleProps { 21 + className?: RacInputProps['className']; 22 + } 23 + 24 + /** Text input used within `ComboboxControl` for combobox behavior. */ 25 + export function ComboboxTextInput(props: ComboboxTextInputProps): JSX.Element { 26 + const { onClick, size = 'medium', ...inputProps } = props; 27 + const state = useContext(ComboBoxStateContext); 28 + 29 + const handleClick = (event: React.MouseEvent<HTMLInputElement>) => { 30 + onClick?.(event); 31 + if (!state?.isOpen) { 32 + state?.open(); 33 + } 34 + }; 35 + 36 + return ( 37 + <RacInput 38 + {...inputProps} 39 + onClick={handleClick} 40 + className={composeRenderProps(inputProps.className, (className) => { 41 + return cx(styles.comboboxTextInput({ size }), className); 42 + })} 43 + /> 44 + ); 45 + }
+51
packages/@luke-ui/react/src/combobox/item.tsx
··· 1 + import type { JSX } from 'react'; 2 + import type { 3 + ListBoxItemProps as RacListBoxItemProps, 4 + ListBoxLoadMoreItemProps as RacListBoxLoadMoreItemProps, 5 + } from 'react-aria-components/ComboBox'; 6 + import { 7 + ListBoxItem as RacListBoxItem, 8 + ListBoxLoadMoreItem as RacListBoxLoadMoreItem, 9 + } from 'react-aria-components/ComboBox'; 10 + import { composeRenderProps } from 'react-aria-components/composeRenderProps'; 11 + import * as styles from '../recipes/combobox.css.js'; 12 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 13 + import { cx } from '../utils/index.js'; 14 + 15 + interface ComboboxStyleProps { 16 + size?: 'small' | 'medium'; 17 + } 18 + 19 + export interface ComboboxItemProps<T extends object> 20 + extends DistributiveOmit<RacListBoxItemProps<T>, 'className'>, ComboboxStyleProps { 21 + className?: RacListBoxItemProps<T>['className']; 22 + } 23 + 24 + export function ComboboxItem<T extends object>(props: ComboboxItemProps<T>): JSX.Element { 25 + const { size = 'medium', ...itemProps } = props; 26 + 27 + return ( 28 + <RacListBoxItem 29 + {...itemProps} 30 + className={composeRenderProps(itemProps.className, (className) => { 31 + return cx(styles.comboboxItem({ size }), className); 32 + })} 33 + /> 34 + ); 35 + } 36 + 37 + export interface ComboboxLoadMoreItemProps 38 + extends DistributiveOmit<RacListBoxLoadMoreItemProps, 'className'>, ComboboxStyleProps { 39 + className?: RacListBoxLoadMoreItemProps['className']; 40 + } 41 + 42 + export function ComboboxLoadMoreItem(props: ComboboxLoadMoreItemProps): JSX.Element { 43 + const { size = 'medium', ...loadMoreItemProps } = props; 44 + 45 + return ( 46 + <RacListBoxLoadMoreItem 47 + {...loadMoreItemProps} 48 + className={cx(styles.comboboxLoadMoreItem({ size }), loadMoreItemProps.className)} 49 + /> 50 + ); 51 + }
+52
packages/@luke-ui/react/src/combobox/listbox.tsx
··· 1 + import type { JSX, ReactNode } from 'react'; 2 + import { Collection } from 'react-aria-components/Collection'; 3 + import type { ListBoxProps as RacListBoxProps } from 'react-aria-components/ComboBox'; 4 + import { ListBox as RacListBox } from 'react-aria-components/ComboBox'; 5 + import { ListBoxContext } from 'react-aria-components/ListBox'; 6 + import { composeRenderProps } from 'react-aria-components/composeRenderProps'; 7 + import { useSlottedContext } from 'react-aria-components/slots'; 8 + import * as styles from '../recipes/combobox.css.js'; 9 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 10 + import { cx } from '../utils/index.js'; 11 + 12 + /** Props for the styled listbox. */ 13 + export interface ComboboxListBoxProps<T extends object> extends DistributiveOmit< 14 + RacListBoxProps<T>, 15 + 'dependencies' | 'items' 16 + > { 17 + /** Item content for the listbox (render prop or static children). */ 18 + children?: RacListBoxProps<T>['children']; 19 + /** Values that should invalidate the dynamic item cache. */ 20 + dependencies?: ReadonlyArray<unknown>; 21 + /** Dynamic items rendered by the `children` render prop. */ 22 + items?: Iterable<T>; 23 + /** Optional content appended after the main collection, e.g. a load-more sentinel. */ 24 + loadMoreItem?: ReactNode; 25 + } 26 + 27 + /** Styled listbox for combobox options. */ 28 + export function ComboboxListBox<T extends object>(props: ComboboxListBoxProps<T>): JSX.Element { 29 + const { children, dependencies, items, loadMoreItem, ...listBoxProps } = props; 30 + const listBoxContext = useSlottedContext(ListBoxContext); 31 + const collectionItems = items ?? listBoxContext?.items; 32 + const listBoxChildren = 33 + typeof children === 'function' ? ( 34 + <Collection<T> dependencies={dependencies} items={collectionItems}> 35 + {children} 36 + </Collection> 37 + ) : ( 38 + children 39 + ); 40 + 41 + return ( 42 + <RacListBox 43 + {...listBoxProps} 44 + className={composeRenderProps(listBoxProps.className, (className) => { 45 + return cx(styles.comboboxListBox(), className); 46 + })} 47 + > 48 + {listBoxChildren} 49 + {loadMoreItem} 50 + </RacListBox> 51 + ); 52 + }
+22
packages/@luke-ui/react/src/combobox/popover.tsx
··· 1 + import type { JSX } from 'react'; 2 + import type { PopoverProps as RacPopoverProps } from 'react-aria-components/ComboBox'; 3 + import { Popover as RacPopover } from 'react-aria-components/ComboBox'; 4 + import { composeRenderProps } from 'react-aria-components/composeRenderProps'; 5 + import * as styles from '../recipes/combobox.css.js'; 6 + import { themeRootClassName } from '../theme/index.js'; 7 + import { cx } from '../utils/index.js'; 8 + 9 + /** Props for the styled combobox popover. */ 10 + export interface ComboboxPopoverProps extends Omit<RacPopoverProps, 'UNSTABLE_portalContainer'> {} 11 + 12 + /** Popover surface used for listbox content. */ 13 + export function ComboboxPopover(props: ComboboxPopoverProps): JSX.Element { 14 + return ( 15 + <RacPopover 16 + {...props} 17 + className={composeRenderProps(props.className, (className) => { 18 + return cx(themeRootClassName, styles.comboboxPopover(), className); 19 + })} 20 + /> 21 + ); 22 + }
+55
packages/@luke-ui/react/src/combobox/root.tsx
··· 1 + import type { JSX } from 'react'; 2 + import type { ComboBoxProps as RacComboBoxProps, Key } from 'react-aria-components/ComboBox'; 3 + import { ComboBox as RacComboBox } from 'react-aria-components/ComboBox'; 4 + import { composeRenderProps } from 'react-aria-components/composeRenderProps'; 5 + import * as styles from '../recipes/combobox.css.js'; 6 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 7 + import { cx } from '../utils/index.js'; 8 + 9 + interface ComboboxVariantProps extends NonNullable<styles.ComboboxVariants> {} 10 + 11 + export type ComboboxSize = NonNullable<ComboboxVariantProps['size']>; 12 + 13 + export interface ComboboxInputProps<T extends object> extends DistributiveOmit< 14 + RacComboBoxProps<T, 'single'>, 15 + | 'defaultSelectedKey' 16 + | 'defaultValue' 17 + | 'onChange' 18 + | 'onOpenChange' 19 + | 'onSelectionChange' 20 + | 'selectedKey' 21 + | 'selectionMode' 22 + | 'value' 23 + > { 24 + /** The currently selected key (controlled). Pass `null` for no selection. */ 25 + value?: Key | null; 26 + 27 + /** The initially selected key (uncontrolled). */ 28 + defaultValue?: Key | null; 29 + 30 + /** Called when the selected value changes. */ 31 + onChange?: (value: Key | null) => void; 32 + 33 + /** Called when the open state changes. */ 34 + onOpenChange?: (isOpen: boolean) => void; 35 + 36 + /** 37 + * The interaction required to display the ComboBox menu. 38 + * @default 'focus' 39 + */ 40 + menuTrigger?: 'focus' | 'input' | 'manual'; 41 + } 42 + 43 + export function ComboboxInput<T extends object>(props: ComboboxInputProps<T>): JSX.Element { 44 + const { className, menuTrigger = 'focus', ...comboboxProps } = props; 45 + 46 + return ( 47 + <RacComboBox 48 + {...comboboxProps} 49 + menuTrigger={menuTrigger} 50 + className={composeRenderProps(className, (renderedClassName) => { 51 + return cx(styles.comboboxRoot, renderedClassName); 52 + })} 53 + /> 54 + ); 55 + }
+38
packages/@luke-ui/react/src/combobox/section.tsx
··· 1 + import type { JSX, ReactNode } from 'react'; 2 + import type { ListBoxSectionProps as RacListBoxSectionProps } from 'react-aria-components/ComboBox'; 3 + import { ListBoxSection as RacListBoxSection } from 'react-aria-components/ComboBox'; 4 + import { Header as RacHeader } from 'react-aria-components/Header'; 5 + import * as styles from '../recipes/combobox.css.js'; 6 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 7 + import { cx } from '../utils/index.js'; 8 + 9 + export interface ComboboxSectionProps<T extends object> extends DistributiveOmit< 10 + RacListBoxSectionProps<T>, 11 + 'className' 12 + > { 13 + className?: RacListBoxSectionProps<T>['className']; 14 + title?: ReactNode; 15 + } 16 + 17 + export function ComboboxSection<T extends object>(props: ComboboxSectionProps<T>): JSX.Element { 18 + const { children, className, title, ...sectionProps } = props; 19 + 20 + const sectionClassName = cx(styles.comboboxSection(), className); 21 + 22 + if (typeof children === 'function') { 23 + return ( 24 + <RacListBoxSection {...sectionProps} className={sectionClassName}> 25 + {children} 26 + </RacListBoxSection> 27 + ); 28 + } 29 + 30 + return ( 31 + <RacListBoxSection {...sectionProps} className={sectionClassName}> 32 + {title != null ? ( 33 + <RacHeader className={styles.comboboxSectionHeading}>{title}</RacHeader> 34 + ) : null} 35 + {children} 36 + </RacListBoxSection> 37 + ); 38 + }
+33
packages/@luke-ui/react/src/combobox/trigger.tsx
··· 1 + import type { JSX } from 'react'; 2 + import type { ButtonProps as RacButtonProps } from 'react-aria-components/ComboBox'; 3 + import { Button as RacButton } from 'react-aria-components/ComboBox'; 4 + import { composeRenderProps } from 'react-aria-components/composeRenderProps'; 5 + import * as styles from '../recipes/combobox.css.js'; 6 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 7 + import { cx } from '../utils/index.js'; 8 + 9 + interface ComboboxVariantProps extends NonNullable<styles.ComboboxVariants> {} 10 + 11 + interface ComboboxStyleProps { 12 + size?: ComboboxVariantProps['size']; 13 + } 14 + 15 + /** Props for the combobox trigger button. */ 16 + export interface ComboboxTriggerProps 17 + extends DistributiveOmit<RacButtonProps, 'className'>, ComboboxStyleProps { 18 + className?: RacButtonProps['className']; 19 + } 20 + 21 + /** Trigger button used by combobox pattern. */ 22 + export function ComboboxTrigger(props: ComboboxTriggerProps): JSX.Element { 23 + const { size = 'medium', ...buttonProps } = props; 24 + 25 + return ( 26 + <RacButton 27 + {...buttonProps} 28 + className={composeRenderProps(buttonProps.className, (className) => { 29 + return cx(styles.comboboxTrigger({ size }), className); 30 + })} 31 + /> 32 + ); 33 + }
+1 -1
packages/@luke-ui/react/src/emoji/index.tsx
··· 1 1 import type { TextProps } from '../text/index.js'; 2 2 import { Text } from '../text/index.js'; 3 - import type { DistributiveOmit } from '../types/index.js'; 3 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 4 4 5 5 /** Props for `Emoji`. */ 6 6 export interface EmojiProps extends DistributiveOmit<TextProps, 'children' | 'elementType'> {
-24
packages/@luke-ui/react/src/field-description/index.tsx
··· 1 - import type { JSX } from 'react'; 2 - import type { TextProps as RacTextProps } from 'react-aria-components/Text'; 3 - import { Text as RacText } from 'react-aria-components/Text'; 4 - import * as styles from '../recipes/field.css.js'; 5 - import { cx } from '../utils/index.js'; 6 - 7 - interface FieldMessageVariantProps extends NonNullable<styles.FieldMessageVariants> {} 8 - 9 - /** Props for `FieldDescription`. */ 10 - export interface FieldDescriptionProps 11 - extends Omit<RacTextProps, 'slot'>, Omit<FieldMessageVariantProps, 'tone'> {} 12 - 13 - /** Styled helper text shown under a field. */ 14 - export function FieldDescription(props: FieldDescriptionProps): JSX.Element { 15 - const { className, ...restProps } = props; 16 - 17 - return ( 18 - <RacText 19 - {...restProps} 20 - className={cx(styles.fieldMessage({ tone: 'description' }), className)} 21 - slot="description" 22 - /> 23 - ); 24 - }
-24
packages/@luke-ui/react/src/field-error/index.tsx
··· 1 - import type { JSX } from 'react'; 2 - import type { FieldErrorProps as RacFieldErrorProps } from 'react-aria-components/FieldError'; 3 - import { FieldError as RacFieldError } from 'react-aria-components/FieldError'; 4 - import { composeRenderProps } from 'react-aria-components/composeRenderProps'; 5 - import * as styles from '../recipes/field.css.js'; 6 - import { cx } from '../utils/index.js'; 7 - 8 - interface FieldMessageVariantProps extends NonNullable<styles.FieldMessageVariants> {} 9 - 10 - /** Props for `FieldError`. */ 11 - export interface FieldErrorProps 12 - extends RacFieldErrorProps, Omit<FieldMessageVariantProps, 'tone'> {} 13 - 14 - /** Styled validation message for a field. */ 15 - export function FieldError(props: FieldErrorProps): JSX.Element { 16 - return ( 17 - <RacFieldError 18 - {...props} 19 - className={composeRenderProps(props.className, (className) => { 20 - return cx(styles.fieldMessage({ tone: 'error' }), className); 21 - })} 22 - /> 23 - ); 24 - }
-28
packages/@luke-ui/react/src/field-label/index.tsx
··· 1 - import type { JSX } from 'react'; 2 - import type { LabelProps as RacLabelProps } from 'react-aria-components/Label'; 3 - import { Label as RacLabel } from 'react-aria-components/Label'; 4 - import * as styles from '../recipes/field.css.js'; 5 - import { cx } from '../utils/index.js'; 6 - 7 - interface FieldLabelVariantProps extends NonNullable<styles.FieldLabelVariants> {} 8 - 9 - /** Allowed `necessityIndicator` values for `FieldLabel`. */ 10 - export type FieldNecessityIndicator = NonNullable<FieldLabelVariantProps['necessityIndicator']>; 11 - 12 - interface FieldLabelStyleProps { 13 - /** Shows how required fields are marked. */ 14 - necessityIndicator?: FieldNecessityIndicator; 15 - } 16 - 17 - /** Props for `FieldLabel`. */ 18 - export interface FieldLabelProps 19 - extends Omit<RacLabelProps, keyof FieldLabelStyleProps>, FieldLabelStyleProps {} 20 - 21 - /** Styled label for form fields. */ 22 - export function FieldLabel(props: FieldLabelProps): JSX.Element { 23 - const { className, necessityIndicator = 'icon', ...restProps } = props; 24 - 25 - return ( 26 - <RacLabel {...restProps} className={cx(styles.fieldLabel({ necessityIndicator }), className)} /> 27 - ); 28 - }
+24
packages/@luke-ui/react/src/field/description.tsx
··· 1 + import type { JSX } from 'react'; 2 + import type { TextProps as RacTextProps } from 'react-aria-components/Text'; 3 + import { Text as RacText } from 'react-aria-components/Text'; 4 + import * as styles from '../recipes/field.css.js'; 5 + import { cx } from '../utils/index.js'; 6 + 7 + interface FieldMessageVariantProps extends NonNullable<styles.FieldMessageVariants> {} 8 + 9 + /** Props for `FieldDescription`. */ 10 + export interface FieldDescriptionProps 11 + extends Omit<RacTextProps, 'slot'>, Omit<FieldMessageVariantProps, 'tone'> {} 12 + 13 + /** Styled helper text shown under a field. */ 14 + export function FieldDescription(props: FieldDescriptionProps): JSX.Element { 15 + const { className, ...restProps } = props; 16 + 17 + return ( 18 + <RacText 19 + {...restProps} 20 + className={cx(styles.fieldMessage({ tone: 'description' }), className)} 21 + slot="description" 22 + /> 23 + ); 24 + }
+24
packages/@luke-ui/react/src/field/error.tsx
··· 1 + import type { JSX } from 'react'; 2 + import type { FieldErrorProps as RacFieldErrorProps } from 'react-aria-components/FieldError'; 3 + import { FieldError as RacFieldError } from 'react-aria-components/FieldError'; 4 + import { composeRenderProps } from 'react-aria-components/composeRenderProps'; 5 + import * as styles from '../recipes/field.css.js'; 6 + import { cx } from '../utils/index.js'; 7 + 8 + interface FieldMessageVariantProps extends NonNullable<styles.FieldMessageVariants> {} 9 + 10 + /** Props for `FieldError`. */ 11 + export interface FieldErrorProps 12 + extends RacFieldErrorProps, Omit<FieldMessageVariantProps, 'tone'> {} 13 + 14 + /** Styled validation message for a field. */ 15 + export function FieldError(props: FieldErrorProps): JSX.Element { 16 + return ( 17 + <RacFieldError 18 + {...props} 19 + className={composeRenderProps(props.className, (className) => { 20 + return cx(styles.fieldMessage({ tone: 'error' }), className); 21 + })} 22 + /> 23 + ); 24 + }
+10 -6
packages/@luke-ui/react/src/field/index.tsx
··· 1 1 import type { ComponentProps, JSX, ReactNode } from 'react'; 2 - import { FieldDescription } from '../field-description/index.js'; 3 - import type { FieldErrorProps } from '../field-error/index.js'; 4 - import { FieldError } from '../field-error/index.js'; 5 - import type { FieldNecessityIndicator } from '../field-label/index.js'; 6 - import { FieldLabel } from '../field-label/index.js'; 7 - import { Field as PrimitiveField } from '../field/primitive.js'; 2 + import { FieldDescription } from './description.js'; 3 + import type { FieldDescriptionProps } from './description.js'; 4 + import type { FieldErrorProps } from './error.js'; 5 + import { FieldError } from './error.js'; 6 + import { FieldLabel } from './label.js'; 7 + import type { FieldLabelProps, FieldNecessityIndicator } from './label.js'; 8 + import { Field as PrimitiveField } from './primitive.js'; 9 + 10 + export { FieldDescription, FieldError, FieldLabel }; 11 + export type { FieldDescriptionProps, FieldErrorProps, FieldLabelProps, FieldNecessityIndicator }; 8 12 9 13 type PrimitiveFieldProps = ComponentProps<typeof PrimitiveField>; 10 14
+28
packages/@luke-ui/react/src/field/label.tsx
··· 1 + import type { JSX } from 'react'; 2 + import type { LabelProps as RacLabelProps } from 'react-aria-components/Label'; 3 + import { Label as RacLabel } from 'react-aria-components/Label'; 4 + import * as styles from '../recipes/field.css.js'; 5 + import { cx } from '../utils/index.js'; 6 + 7 + interface FieldLabelVariantProps extends NonNullable<styles.FieldLabelVariants> {} 8 + 9 + /** Allowed `necessityIndicator` values for `FieldLabel`. */ 10 + export type FieldNecessityIndicator = NonNullable<FieldLabelVariantProps['necessityIndicator']>; 11 + 12 + interface FieldLabelStyleProps { 13 + /** Shows how required fields are marked. */ 14 + necessityIndicator?: FieldNecessityIndicator; 15 + } 16 + 17 + /** Props for `FieldLabel`. */ 18 + export interface FieldLabelProps 19 + extends Omit<RacLabelProps, keyof FieldLabelStyleProps>, FieldLabelStyleProps {} 20 + 21 + /** Styled label for form fields. */ 22 + export function FieldLabel(props: FieldLabelProps): JSX.Element { 23 + const { className, necessityIndicator = 'icon', ...restProps } = props; 24 + 25 + return ( 26 + <RacLabel {...restProps} className={cx(styles.fieldLabel({ necessityIndicator }), className)} /> 27 + ); 28 + }
+1 -1
packages/@luke-ui/react/src/heading/index.tsx
··· 2 2 import { HeadingLevels, HeadingPresenceProvider } from '../heading-context/index.js'; 3 3 import type { TextProps } from '../text/index.js'; 4 4 import { Text } from '../text/index.js'; 5 - import type { DistributiveOmit } from '../types/index.js'; 5 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 6 6 7 7 export type { HeadingLevel } from '../heading-context/index.js'; 8 8 /** Valid heading tag name for Luke UI headings. */
+2 -2
packages/@luke-ui/react/src/icon/index.tsx
··· 2 2 import { createContext, useContext } from 'react'; 3 3 import { iconNames, iconViewBoxes } from '../../.generated/icon-data.js'; 4 4 import { useIconSizeContext } from '../icon-size-context/index.js'; 5 - import { ICON_VIEWBOX } from '../lib/icon.js'; 6 5 import * as styles from '../recipes/icon.css.js'; 7 - import type { DistributiveOmit } from '../types/index.js'; 6 + import { ICON_VIEWBOX } from '../sizing/icon-sizing.js'; 7 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 8 8 import { cx } from '../utils/index.js'; 9 9 10 10 export type { IconName } from '../../.generated/icon-data.js';
-13
packages/@luke-ui/react/src/lib/button.ts
··· 1 - import type { FontSizeToken, IconSizeToken } from '../tokens/index.js'; 2 - 3 - /** Button size → icon/spinner size so icons align with control size. */ 4 - export const BUTTON_ICON_SIZE = { 5 - medium: 'medium', 6 - small: 'xsmall', 7 - } as const satisfies Record<'medium' | 'small', IconSizeToken>; 8 - 9 - /** Button size → Text fontSize for composed button label. */ 10 - export const BUTTON_FONT_SIZE = { 11 - medium: 'standard', 12 - small: 'small', 13 - } as const satisfies Record<'medium' | 'small', FontSizeToken>;
-16
packages/@luke-ui/react/src/lib/icon.ts
··· 1 - /** Shared viewBox size for icon SVGs (icons and LoadingSpinner). */ 2 - export const ICON_VIEWBOX_SIZE = 24; 3 - 4 - /** Inset from viewBox edge to content; icons use 19×19 content in 24×24 box. */ 5 - const ICON_CONTENT_INSET = 2.5; 6 - 7 - /** Content area size (viewBox size minus insets). */ 8 - const ICON_CONTENT_SIZE = ICON_VIEWBOX_SIZE - 2 * ICON_CONTENT_INSET; 9 - 10 - /** Default viewBox string for icon-aligned SVGs. */ 11 - export const ICON_VIEWBOX = `0 0 ${ICON_VIEWBOX_SIZE} ${ICON_VIEWBOX_SIZE}`; 12 - 13 - export const SPINNER_STROKE_WIDTH = 2; 14 - 15 - /** Radius inset by half stroke so the stroke's outer edge aligns with icon content (19px). */ 16 - export const SPINNER_CIRCLE_RADIUS = (ICON_CONTENT_SIZE - SPINNER_STROKE_WIDTH) / 2;
+3 -3
packages/@luke-ui/react/src/loading-spinner/index.tsx
··· 1 1 import { clamp } from '@react-aria/utils'; 2 2 import type { ComponentProps } from 'react'; 3 + import * as styles from '../recipes/loading-spinner.css.js'; 3 4 import { 4 5 ICON_VIEWBOX, 5 6 ICON_VIEWBOX_SIZE, 6 7 SPINNER_CIRCLE_RADIUS, 7 8 SPINNER_STROKE_WIDTH, 8 - } from '../lib/icon.js'; 9 - import * as styles from '../recipes/loading-spinner.css.js'; 10 - import type { DistributiveOmit } from '../types/index.js'; 9 + } from '../sizing/icon-sizing.js'; 10 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 11 11 import { cx } from '../utils/index.js'; 12 12 13 13 interface LoadingSpinnerVariantProps extends NonNullable<styles.LoadingSpinnerVariants> {}
+10
packages/@luke-ui/react/src/recipes/layers.css.ts
··· 1 + import './button.css.js'; 2 + import './button-composed.css.js'; 3 + import './combobox.css.js'; 4 + import './field.css.js'; 5 + import './icon.css.js'; 6 + import './icon-button.css.js'; 7 + import './link.css.js'; 8 + import './loading-spinner.css.js'; 9 + import './text.css.js'; 10 + import './text-input.css.js';
+2 -1
packages/@luke-ui/react/src/recipes/loading-spinner.css.ts
··· 2 2 import type { RecipeVariants } from '@vanilla-extract/recipes'; 3 3 import { recipeInLayer, styleInLayer } from '../styles/layered-style.css.js'; 4 4 import { vars } from '../styles/vars.css.js'; 5 - import { tokenKeys, tokens } from '../tokens/index.js'; 5 + import { tokenKeys } from '../tokens/groups.js'; 6 + import { tokens } from '../tokens/index.js'; 6 7 import { iconSizeVariants } from './icon.css.js'; 7 8 8 9 const colorKeys = tokenKeys(tokens.foregroundColor);
+4 -3
packages/@luke-ui/react/src/recipes/text.css.ts
··· 1 1 import type { ComplexStyleRule } from '@vanilla-extract/css'; 2 2 import type { RecipeVariants } from '@vanilla-extract/recipes'; 3 - import { createPropertyVariants, createVariants } from '../style-helpers/index.js'; 3 + import { createPropertyVariants, createVariants } from '../style-helpers/create-variants.js'; 4 4 import { recipeInLayer, styleInLayer } from '../styles/layered-style.css.js'; 5 5 import { vars } from '../styles/vars.css.js'; 6 + import { tokenKeys } from '../tokens/groups.js'; 6 7 import type { 7 8 FontFamilyToken, 8 9 FontSizeToken, ··· 10 11 ForegroundColorToken, 11 12 LineHeightToken, 12 13 } from '../tokens/index.js'; 13 - import { tokenKeys, tokens } from '../tokens/index.js'; 14 - import { getTypographyClass } from '../typography/index.js'; 14 + import { tokens } from '../tokens/index.js'; 15 + import { getTypographyClass } from '../typography/get-typography-class.js'; 15 16 16 17 const colorKeys = tokenKeys(tokens.foregroundColor); 17 18 const fontFamilyKeys = tokenKeys(tokens.fontFamily);
+13
packages/@luke-ui/react/src/sizing/button-sizing.ts
··· 1 + import type { FontSizeToken, IconSizeToken } from '../tokens/index.js'; 2 + 3 + /** Maps button size to the appropriate icon/spinner size. */ 4 + export const BUTTON_ICON_SIZE: Record<'medium' | 'small', IconSizeToken> = { 5 + medium: 'medium', 6 + small: 'xsmall', 7 + }; 8 + 9 + /** Maps button size to the appropriate Text fontSize for composed button label. */ 10 + export const BUTTON_FONT_SIZE: Record<'medium' | 'small', FontSizeToken> = { 11 + medium: 'standard', 12 + small: 'small', 13 + };
+17
packages/@luke-ui/react/src/sizing/icon-sizing.ts
··· 1 + /** Shared viewBox size for icon SVGs. */ 2 + export const ICON_VIEWBOX_SIZE = 24; 3 + 4 + /** Inset from viewBox edge to content; icons use 19×19 content in 24×24 box. */ 5 + const ICON_CONTENT_INSET = 2.5; 6 + 7 + /** Content area size (viewBox size minus insets). */ 8 + const ICON_CONTENT_SIZE = ICON_VIEWBOX_SIZE - 2 * ICON_CONTENT_INSET; 9 + 10 + /** Default viewBox string for icon-aligned SVGs. */ 11 + export const ICON_VIEWBOX = `0 0 ${ICON_VIEWBOX_SIZE} ${ICON_VIEWBOX_SIZE}`; 12 + 13 + /** Stroke width for loading spinner. */ 14 + export const SPINNER_STROKE_WIDTH = 2; 15 + 16 + /** Radius inset by half stroke so the stroke's outer edge aligns with icon content (19px). */ 17 + export const SPINNER_CIRCLE_RADIUS = (ICON_CONTENT_SIZE - SPINNER_STROKE_WIDTH) / 2;
+22
packages/@luke-ui/react/src/style-helpers/create-variants.ts
··· 1 + type StylePrimitive = string | number; 2 + 3 + export function createVariants<Key extends string, Style extends Record<string, StylePrimitive>>( 4 + keys: ReadonlyArray<Key>, 5 + getStyle: (key: Key) => Style, 6 + ): Record<Key, Style> { 7 + const styles = {} as Record<Key, Style>; 8 + for (const key of keys) { 9 + styles[key] = getStyle(key); 10 + } 11 + return styles; 12 + } 13 + 14 + export function createPropertyVariants<Key extends string, Property extends string>( 15 + keys: ReadonlyArray<Key>, 16 + property: Property, 17 + values: Record<Key, string>, 18 + ): Record<Key, Record<Property, string>> { 19 + return createVariants(keys, (key) => ({ 20 + [property]: values[key], 21 + })) as Record<Key, Record<Property, string>>; 22 + }
-22
packages/@luke-ui/react/src/style-helpers/index.ts
··· 1 - type StylePrimitive = string | number; 2 - 3 - export function createVariants<Key extends string, Style extends Record<string, StylePrimitive>>( 4 - keys: ReadonlyArray<Key>, 5 - getStyle: (key: Key) => Style, 6 - ): Record<Key, Style> { 7 - const styles = {} as Record<Key, Style>; 8 - for (const key of keys) { 9 - styles[key] = getStyle(key); 10 - } 11 - return styles; 12 - } 13 - 14 - export function createPropertyVariants<Key extends string, Property extends string>( 15 - keys: ReadonlyArray<Key>, 16 - property: Property, 17 - values: Record<Key, string>, 18 - ): Record<Key, Record<Property, string>> { 19 - return createVariants(keys, (key) => ({ 20 - [property]: values[key], 21 - })) as Record<Key, Record<Property, string>>; 22 - }
-1
packages/@luke-ui/react/src/styles/composed.css.ts
··· 1 - import '../recipes/icon-button.css.js';
-1
packages/@luke-ui/react/src/styles/index.css.ts
··· 1 1 import './vars.css.js'; 2 2 import './reset.css.js'; 3 3 import './primitives.css.js'; 4 - import './composed.css.js';
+1 -1
packages/@luke-ui/react/src/styles/layered-style.css.ts
··· 1 1 import type { GlobalStyleRule, StyleRule } from '@vanilla-extract/css'; 2 2 import { globalStyle as vanillaGlobalStyle, style as vanillaStyle } from '@vanilla-extract/css'; 3 3 import { recipe as vanillaRecipe } from '@vanilla-extract/recipes'; 4 - import type { DistributiveOmit } from '../types/index.js'; 4 + import type { DistributiveOmit } from '../types/distributive-omit.js'; 5 5 import type { LayerName } from './layers.css.js'; 6 6 import { layers } from './layers.css.js'; 7 7
+1 -10
packages/@luke-ui/react/src/styles/primitives.css.ts
··· 1 - import '../recipes/button-composed.css.js'; 2 - import '../recipes/button.css.js'; 3 - import '../recipes/field.css.js'; 4 - import '../recipes/icon-button.css.js'; 5 - import '../recipes/icon.css.js'; 6 - import '../recipes/link.css.js'; 7 - import '../recipes/loading-spinner.css.js'; 8 - import '../recipes/combobox.css.js'; 9 - import '../recipes/text-input.css.js'; 10 - import '../recipes/text.css.js'; 1 + import '../recipes/layers.css.js';
+4 -4
packages/@luke-ui/react/src/styles/vars.css.ts
··· 1 1 import { createTheme, createThemeContract } from '@vanilla-extract/css'; 2 - import type { ColorTokenValue } from '../tokens/index.js'; 3 2 import { 4 3 colorToCssString, 5 4 cubicBezierToString, 6 5 dimensionToRemString, 7 6 durationToString, 8 - tokenKeys, 9 - tokens, 10 - } from '../tokens/index.js'; 7 + } from '../tokens/converters.js'; 8 + import { tokenKeys } from '../tokens/groups.js'; 9 + import type { ColorTokenValue } from '../tokens/index.js'; 10 + import { tokens } from '../tokens/index.js'; 11 11 import { classSelector, lukeUiClassNames } from './class-names.js'; 12 12 import { globalStyleInLayer } from './layered-style.css.js'; 13 13 import { layers } from './layers.css.js';
+2 -2
packages/@luke-ui/react/src/text-field/index.tsx
··· 4 4 TextFieldProps as RacTextFieldProps, 5 5 } from 'react-aria-components/TextField'; 6 6 import { TextField as RacTextField } from 'react-aria-components/TextField'; 7 - import type { FieldErrorProps } from '../field-error/index.js'; 8 - import type { FieldNecessityIndicator } from '../field-label/index.js'; 7 + import type { FieldErrorProps } from '../field/error.js'; 9 8 import { Field } from '../field/index.js'; 9 + import type { FieldNecessityIndicator } from '../field/label.js'; 10 10 import type { TextInputSize } from '../text-input/index.js'; 11 11 import { TextInput } from '../text-input/index.js'; 12 12
+50
packages/@luke-ui/react/src/tokens/converters.ts
··· 1 + import { pxToRem } from '../utils/index.js'; 2 + import type { 3 + ColorTokenValue, 4 + CubicBezierTokenValue, 5 + DimensionTokenValue, 6 + DurationTokenValue, 7 + } from './index.js'; 8 + 9 + export function dimensionToRemString(value: DimensionTokenValue, base: number = 16): string { 10 + return value.unit === 'rem' ? `${value.value}rem` : pxToRem(value.value, base); 11 + } 12 + 13 + export function dimensionToPxNumber(value: DimensionTokenValue, base: number = 16): number { 14 + return value.unit === 'px' ? value.value : value.value * base; 15 + } 16 + 17 + export function durationToString(value: DurationTokenValue): string { 18 + return `${value.value}${value.unit}`; 19 + } 20 + 21 + export function cubicBezierToString(value: CubicBezierTokenValue): string { 22 + return `cubic-bezier(${value[0]}, ${value[1]}, ${value[2]}, ${value[3]})`; 23 + } 24 + 25 + const functionLikeColorSpaces = new Set(['hsl', 'hwb', 'lab', 'lch', 'oklab', 'oklch']); 26 + 27 + export function colorToCssString(value: ColorTokenValue): string { 28 + if (value.components.length === 0) { 29 + throw new Error(`Color token "${value.colorSpace}" has no components`); 30 + } 31 + 32 + const components = value.components.map((component) => formatNumber(component)); 33 + const alphaSuffix = 34 + value.alpha === undefined || value.alpha === 1 ? '' : ` / ${formatNumber(value.alpha)}`; 35 + 36 + if (functionLikeColorSpaces.has(value.colorSpace)) { 37 + return `${value.colorSpace}(${components.join(' ')}${alphaSuffix})`; 38 + } 39 + 40 + return `color(${value.colorSpace} ${components.join(' ')}${alphaSuffix})`; 41 + } 42 + 43 + function formatNumber(value: number): string { 44 + if (!Number.isFinite(value)) { 45 + throw new Error(`Invalid numeric color component: ${value}`); 46 + } 47 + 48 + const normalized = Object.is(value, -0) ? 0 : value; 49 + return `${normalized}`; 50 + }
+24
packages/@luke-ui/react/src/tokens/groups.ts
··· 1 + import type { DesignTokenGroup, TokenName } from './index.js'; 2 + 3 + export function toTokenGroup<TType extends string, TValues extends Record<string, unknown>>( 4 + type: TType, 5 + values: TValues, 6 + ): DesignTokenGroup<TType, TValues> { 7 + const group = { $type: type } as DesignTokenGroup<TType, TValues>; 8 + 9 + for (const key in values) { 10 + (group as Record<string, unknown>)[key] = { 11 + $value: values[key], 12 + }; 13 + } 14 + 15 + return group; 16 + } 17 + 18 + export function tokenKeys<TGroup extends { $type: string }>( 19 + group: TGroup, 20 + ): Array<TokenName<TGroup>> { 21 + return (Object.keys(group) as Array<keyof TGroup>).filter( 22 + (key): key is TokenName<TGroup> => key !== '$type', 23 + ); 24 + }
+41 -178
packages/@luke-ui/react/src/tokens/index.ts
··· 4 4 foregroundColorTokenValues, 5 5 themeColorTokenValues, 6 6 } from '../../.generated/color-tokens.generated.js'; 7 - import { pxToRem, typedKeys } from '../utils/index.js'; 7 + import { toTokenGroup } from './groups.js'; 8 + import { minMediaQueries } from './media-queries.js'; 9 + import { 10 + borderRadiusValues, 11 + borderWidthValues, 12 + boxShadowValues, 13 + breakpointValues, 14 + controlSizeValues, 15 + fontFamilyValues, 16 + fontSizeValues, 17 + fontWeightValues, 18 + iconSizeValues, 19 + lineHeightValues, 20 + motionDurationValues, 21 + motionEasingValues, 22 + spaceValues, 23 + } from './values.js'; 8 24 9 25 export interface DimensionTokenValue { 10 26 unit: 'px' | 'rem'; ··· 39 55 string 40 56 >; 41 57 42 - function toTokenGroup<TType extends string, TValues extends Record<string, unknown>>( 43 - type: TType, 44 - values: TValues, 45 - ): DesignTokenGroup<TType, TValues> { 46 - const group = { $type: type } as DesignTokenGroup<TType, TValues>; 47 - 48 - for (const key in values) { 49 - (group as Record<string, unknown>)[key] = { 50 - $value: values[key], 51 - }; 52 - } 53 - 54 - return group; 55 - } 56 - 57 - export function tokenKeys<TGroup extends { $type: string }>( 58 - group: TGroup, 59 - ): Array<TokenName<TGroup>> { 60 - return (Object.keys(group) as Array<keyof TGroup>).filter( 61 - (key): key is TokenName<TGroup> => key !== '$type', 62 - ); 63 - } 64 - 65 - export function dimensionToRemString(value: DimensionTokenValue, base: number = 16): string { 66 - return value.unit === 'rem' ? `${value.value}rem` : pxToRem(value.value, base); 67 - } 68 - 69 - export function dimensionToPxNumber(value: DimensionTokenValue, base: number = 16): number { 70 - return value.unit === 'px' ? value.value : value.value * base; 71 - } 72 - 73 - export function durationToString(value: DurationTokenValue): string { 74 - return `${value.value}${value.unit}`; 75 - } 76 - 77 - export function cubicBezierToString(value: CubicBezierTokenValue): string { 78 - return `cubic-bezier(${value[0]}, ${value[1]}, ${value[2]}, ${value[3]})`; 79 - } 80 - 81 - function formatNumber(value: number): string { 82 - if (!Number.isFinite(value)) { 83 - throw new Error(`Invalid numeric color component: ${value}`); 84 - } 85 - 86 - const normalized = Object.is(value, -0) ? 0 : value; 87 - return `${normalized}`; 88 - } 89 - 90 - const functionLikeColorSpaces = new Set(['hsl', 'hwb', 'lab', 'lch', 'oklab', 'oklch']); 91 - 92 - export function colorToCssString(value: ColorTokenValue): string { 93 - if (value.components.length === 0) { 94 - throw new Error(`Color token "${value.colorSpace}" has no components`); 95 - } 96 - 97 - const components = value.components.map((component) => formatNumber(component)); 98 - const alphaSuffix = 99 - value.alpha === undefined || value.alpha === 1 ? '' : ` / ${formatNumber(value.alpha)}`; 100 - 101 - if (functionLikeColorSpaces.has(value.colorSpace)) { 102 - return `${value.colorSpace}(${components.join(' ')}${alphaSuffix})`; 103 - } 104 - 105 - return `color(${value.colorSpace} ${components.join(' ')}${alphaSuffix})`; 106 - } 107 - 108 - const breakpointValues = { 109 - xsmall: { unit: 'px', value: 0 }, 110 - small: { unit: 'px', value: 640 }, 111 - medium: { unit: 'px', value: 768 }, 112 - large: { unit: 'px', value: 1024 }, 113 - xlarge: { unit: 'px', value: 1280 }, 114 - xxlarge: { unit: 'px', value: 1536 }, 115 - } as const satisfies Record<string, DimensionTokenValue>; 116 - 117 - type Breakpoint = keyof typeof breakpointValues; 118 - 119 - const minMediaQueries = Object.fromEntries( 120 - typedKeys(breakpointValues).map((breakpoint) => [ 121 - breakpoint, 122 - `@media(width >= ${dimensionToRemString(breakpointValues[breakpoint])})`, 123 - ]), 124 - ) as Record<Breakpoint, string>; 125 - 126 - const borderRadiusValues = { 127 - none: { unit: 'px', value: 0 }, 128 - small: { unit: 'px', value: 2 }, 129 - medium: { unit: 'px', value: 4 }, 130 - large: { unit: 'px', value: 8 }, 131 - xlarge: { unit: 'px', value: 16 }, 132 - full: { unit: 'px', value: 9999 }, 133 - } as const satisfies Record<string, DimensionTokenValue>; 134 - 135 - const borderWidthValues = { 136 - thin: { unit: 'px', value: 1 }, 137 - thick: { unit: 'px', value: 2 }, 138 - } as const satisfies Record<string, DimensionTokenValue>; 139 - 140 - const boxShadowValues = { 141 - none: 'none', 142 - xsmall: '0px 1px 2px rgba(0, 0, 0, 0.2)', 143 - small: '0px 2px 4px rgba(0, 0, 0, 0.2)', 144 - medium: '0px 2px 8px rgba(0, 0, 0, 0.2)', 145 - large: '0px 4px 16px rgba(0, 0, 0, 0.2)', 146 - xlarge: '-8px 8px 32px rgba(0, 0, 0, 0.2)', 147 - } as const; 148 - 149 - const controlSizeValues = { 150 - small: { unit: 'px', value: 32 }, 151 - medium: { unit: 'px', value: 40 }, 152 - } as const satisfies Record<string, DimensionTokenValue>; 153 - 154 - const fontFamilyValues = { 155 - sans: "ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", 156 - mono: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", 157 - } as const; 158 - 159 - const fontSizeValues = { 160 - xxsmall: { unit: 'px', value: 10 }, 161 - xsmall: { unit: 'px', value: 12 }, 162 - small: { unit: 'px', value: 14 }, 163 - standard: { unit: 'px', value: 16 }, 164 - medium: { unit: 'px', value: 20 }, 165 - large: { unit: 'px', value: 40 }, 166 - xlarge: { unit: 'px', value: 48 }, 167 - xxlarge: { unit: 'px', value: 55 }, 168 - h1: { unit: 'px', value: 36 }, 169 - h2: { unit: 'px', value: 28 }, 170 - h3: { unit: 'px', value: 24 }, 171 - h4: { unit: 'px', value: 20 }, 172 - h5: { unit: 'px', value: 18 }, 173 - h6: { unit: 'px', value: 16 }, 174 - } as const satisfies Record<string, DimensionTokenValue>; 175 - 176 - const fontWeightValues = { 177 - regular: 400, 178 - medium: 500, 179 - bold: 700, 180 - } as const; 181 - 182 - const iconSizeValues = { 183 - xsmall: { unit: 'px', value: 16 }, 184 - small: { unit: 'px', value: 20 }, 185 - medium: { unit: 'px', value: 24 }, 186 - large: { unit: 'px', value: 32 }, 187 - } as const satisfies Record<string, DimensionTokenValue>; 188 - 189 - const lineHeightValues = { 190 - nospace: 1, 191 - tight: 1.25, 192 - loose: 1.5, 193 - } as const; 194 - 195 - const motionDurationValues = { 196 - quick: { unit: 'ms', value: 120 }, 197 - fast: { unit: 'ms', value: 150 }, 198 - slow: { unit: 's', value: 1.2 }, 199 - slower: { unit: 's', value: 2 }, 200 - } as const satisfies Record<string, DurationTokenValue>; 201 - 202 - const motionEasingValues = { 203 - emphasized: [0.42, 0, 0.58, 1], 204 - exit: [0, 0, 0.58, 1], 205 - linear: [0, 0, 1, 1], 206 - standard: [0.4, 0, 0.2, 1], 207 - } as const satisfies Record<string, CubicBezierTokenValue>; 208 - 209 - const spaceValues = { 210 - none: { unit: 'px', value: 0 }, 211 - xxsmall: { unit: 'px', value: 4 }, 212 - xsmall: { unit: 'px', value: 8 }, 213 - small: { unit: 'px', value: 12 }, 214 - medium: { unit: 'px', value: 16 }, 215 - large: { unit: 'px', value: 24 }, 216 - xlarge: { unit: 'px', value: 32 }, 217 - xxlarge: { unit: 'px', value: 40 }, 218 - } as const satisfies Record<string, DimensionTokenValue>; 58 + export { 59 + colorToCssString, 60 + cubicBezierToString, 61 + dimensionToRemString, 62 + durationToString, 63 + } from './converters.js'; 64 + export { tokenKeys, toTokenGroup } from './groups.js'; 65 + export { minMediaQueries } from './media-queries.js'; 66 + export type { Breakpoint } from './media-queries.js'; 67 + export { 68 + borderRadiusValues, 69 + borderWidthValues, 70 + boxShadowValues, 71 + breakpointValues, 72 + controlSizeValues, 73 + fontFamilyValues, 74 + fontSizeValues, 75 + fontWeightValues, 76 + iconSizeValues, 77 + lineHeightValues, 78 + motionDurationValues, 79 + motionEasingValues, 80 + spaceValues, 81 + } from './values.js'; 219 82 220 83 export const tokens = { 221 84 backgroundColor: toTokenGroup('color', backgroundColorTokenValues),
+12
packages/@luke-ui/react/src/tokens/media-queries.ts
··· 1 + import { typedKeys } from '../utils/index.js'; 2 + import { dimensionToRemString } from './converters.js'; 3 + import { breakpointValues } from './values.js'; 4 + 5 + export type Breakpoint = keyof typeof breakpointValues; 6 + 7 + export const minMediaQueries = Object.fromEntries( 8 + typedKeys(breakpointValues).map((breakpoint) => [ 9 + breakpoint, 10 + `@media(width >= ${dimensionToRemString(breakpointValues[breakpoint])})`, 11 + ]), 12 + ) as Record<Breakpoint, string>;
+104
packages/@luke-ui/react/src/tokens/values.ts
··· 1 + import type { DimensionTokenValue, DurationTokenValue, CubicBezierTokenValue } from './index.js'; 2 + 3 + export const breakpointValues = { 4 + xsmall: { unit: 'px', value: 0 }, 5 + small: { unit: 'px', value: 640 }, 6 + medium: { unit: 'px', value: 768 }, 7 + large: { unit: 'px', value: 1024 }, 8 + xlarge: { unit: 'px', value: 1280 }, 9 + xxlarge: { unit: 'px', value: 1536 }, 10 + } as const satisfies Record<string, DimensionTokenValue>; 11 + 12 + export const borderRadiusValues = { 13 + none: { unit: 'px', value: 0 }, 14 + small: { unit: 'px', value: 2 }, 15 + medium: { unit: 'px', value: 4 }, 16 + large: { unit: 'px', value: 8 }, 17 + xlarge: { unit: 'px', value: 16 }, 18 + full: { unit: 'px', value: 9999 }, 19 + } as const satisfies Record<string, DimensionTokenValue>; 20 + 21 + export const borderWidthValues = { 22 + thin: { unit: 'px', value: 1 }, 23 + thick: { unit: 'px', value: 2 }, 24 + } as const satisfies Record<string, DimensionTokenValue>; 25 + 26 + export const boxShadowValues = { 27 + none: 'none', 28 + xsmall: '0px 1px 2px rgba(0, 0, 0, 0.2)', 29 + small: '0px 2px 4px rgba(0, 0, 0, 0.2)', 30 + medium: '0px 2px 8px rgba(0, 0, 0, 0.2)', 31 + large: '0px 4px 16px rgba(0, 0, 0, 0.2)', 32 + xlarge: '-8px 8px 32px rgba(0, 0, 0, 0.2)', 33 + } as const; 34 + 35 + export const controlSizeValues = { 36 + small: { unit: 'px', value: 32 }, 37 + medium: { unit: 'px', value: 40 }, 38 + } as const satisfies Record<string, DimensionTokenValue>; 39 + 40 + export const fontFamilyValues = { 41 + sans: "ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", 42 + mono: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", 43 + } as const; 44 + 45 + export const fontSizeValues = { 46 + xxsmall: { unit: 'px', value: 10 }, 47 + xsmall: { unit: 'px', value: 12 }, 48 + small: { unit: 'px', value: 14 }, 49 + standard: { unit: 'px', value: 16 }, 50 + medium: { unit: 'px', value: 20 }, 51 + large: { unit: 'px', value: 40 }, 52 + xlarge: { unit: 'px', value: 48 }, 53 + xxlarge: { unit: 'px', value: 55 }, 54 + h1: { unit: 'px', value: 36 }, 55 + h2: { unit: 'px', value: 28 }, 56 + h3: { unit: 'px', value: 24 }, 57 + h4: { unit: 'px', value: 20 }, 58 + h5: { unit: 'px', value: 18 }, 59 + h6: { unit: 'px', value: 16 }, 60 + } as const satisfies Record<string, DimensionTokenValue>; 61 + 62 + export const fontWeightValues = { 63 + regular: 400, 64 + medium: 500, 65 + bold: 700, 66 + } as const; 67 + 68 + export const iconSizeValues = { 69 + xsmall: { unit: 'px', value: 16 }, 70 + small: { unit: 'px', value: 20 }, 71 + medium: { unit: 'px', value: 24 }, 72 + large: { unit: 'px', value: 32 }, 73 + } as const satisfies Record<string, DimensionTokenValue>; 74 + 75 + export const lineHeightValues = { 76 + nospace: 1, 77 + tight: 1.25, 78 + loose: 1.5, 79 + } as const; 80 + 81 + export const motionDurationValues = { 82 + quick: { unit: 'ms', value: 120 }, 83 + fast: { unit: 'ms', value: 150 }, 84 + slow: { unit: 's', value: 1.2 }, 85 + slower: { unit: 's', value: 2 }, 86 + } as const satisfies Record<string, DurationTokenValue>; 87 + 88 + export const motionEasingValues = { 89 + emphasized: [0.42, 0, 0.58, 1], 90 + exit: [0, 0, 0.58, 1], 91 + linear: [0, 0, 1, 1], 92 + standard: [0.4, 0, 0.2, 1], 93 + } as const satisfies Record<string, CubicBezierTokenValue>; 94 + 95 + export const spaceValues = { 96 + none: { unit: 'px', value: 0 }, 97 + xxsmall: { unit: 'px', value: 4 }, 98 + xsmall: { unit: 'px', value: 8 }, 99 + small: { unit: 'px', value: 12 }, 100 + medium: { unit: 'px', value: 16 }, 101 + large: { unit: 'px', value: 24 }, 102 + xlarge: { unit: 'px', value: 32 }, 103 + xxlarge: { unit: 'px', value: 40 }, 104 + } as const satisfies Record<string, DimensionTokenValue>;
+5
packages/@luke-ui/react/src/types/distributive-omit.ts
··· 1 + type KeysOfUnion<T> = T extends T ? keyof T : never; 2 + 3 + export type DistributiveOmit<T, K extends KeysOfUnion<T>> = T extends any 4 + ? Omit<T, Extract<K, keyof T>> 5 + : never;
-5
packages/@luke-ui/react/src/types/index.ts
··· 1 - type KeysOfUnion<T> = T extends T ? keyof T : never; 2 - 3 - export type DistributiveOmit<T, K extends KeysOfUnion<T>> = T extends any 4 - ? Omit<T, Extract<K, keyof T>> 5 - : never;
+24
packages/@luke-ui/react/src/typography/get-typography-class.ts
··· 1 + import fontMetrics from '@capsizecss/metrics/appleSystem'; 2 + import { createTextStyle } from '@capsizecss/vanilla-extract'; 3 + import { dimensionToPxNumber } from '../tokens/converters.js'; 4 + import type { FontSizeToken, LineHeightToken } from '../tokens/index.js'; 5 + import { tokens } from '../tokens/index.js'; 6 + 7 + interface GetTypographyInput { 8 + fontSize: FontSizeToken; 9 + lineHeight: LineHeightToken; 10 + } 11 + 12 + export function getTypographyClass(input: GetTypographyInput, debugId?: string) { 13 + const fontSize = dimensionToPxNumber(tokens.fontSize[input.fontSize].$value); 14 + const lineHeight = tokens.lineHeight[input.lineHeight].$value; 15 + 16 + return createTextStyle( 17 + { 18 + fontMetrics, 19 + fontSize, 20 + leading: fontSize * lineHeight, 21 + }, 22 + debugId, 23 + ); 24 + }
-23
packages/@luke-ui/react/src/typography/index.ts
··· 1 - import fontMetrics from '@capsizecss/metrics/appleSystem'; 2 - import { createTextStyle } from '@capsizecss/vanilla-extract'; 3 - import type { FontSizeToken, LineHeightToken } from '../tokens/index.js'; 4 - import { dimensionToPxNumber, tokens } from '../tokens/index.js'; 5 - 6 - interface GetTypographyInput { 7 - fontSize: FontSizeToken; 8 - lineHeight: LineHeightToken; 9 - } 10 - 11 - export function getTypographyClass(input: GetTypographyInput, debugId?: string) { 12 - const fontSize = dimensionToPxNumber(tokens.fontSize[input.fontSize].$value); 13 - const lineHeight = tokens.lineHeight[input.lineHeight].$value; 14 - 15 - return createTextStyle( 16 - { 17 - fontMetrics, 18 - fontSize, 19 - leading: fontSize * lineHeight, 20 - }, 21 - debugId, 22 - ); 23 - }