@nitrostack/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +131 -0
  2. package/dist/commands/build.d.ts +6 -0
  3. package/dist/commands/build.d.ts.map +1 -0
  4. package/dist/commands/build.js +185 -0
  5. package/dist/commands/dev.d.ts +7 -0
  6. package/dist/commands/dev.d.ts.map +1 -0
  7. package/dist/commands/dev.js +365 -0
  8. package/dist/commands/generate-types.d.ts +8 -0
  9. package/dist/commands/generate-types.d.ts.map +1 -0
  10. package/dist/commands/generate-types.js +219 -0
  11. package/dist/commands/generate.d.ts +12 -0
  12. package/dist/commands/generate.d.ts.map +1 -0
  13. package/dist/commands/generate.js +375 -0
  14. package/dist/commands/init.d.ts +7 -0
  15. package/dist/commands/init.d.ts.map +1 -0
  16. package/dist/commands/init.js +324 -0
  17. package/dist/commands/install.d.ts +10 -0
  18. package/dist/commands/install.d.ts.map +1 -0
  19. package/dist/commands/install.js +80 -0
  20. package/dist/commands/start.d.ts +6 -0
  21. package/dist/commands/start.d.ts.map +1 -0
  22. package/dist/commands/start.js +70 -0
  23. package/dist/commands/upgrade.d.ts +10 -0
  24. package/dist/commands/upgrade.d.ts.map +1 -0
  25. package/dist/commands/upgrade.js +214 -0
  26. package/dist/index.d.ts +11 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +94 -0
  29. package/dist/mcp-dev-wrapper.d.ts +15 -0
  30. package/dist/mcp-dev-wrapper.d.ts.map +1 -0
  31. package/dist/mcp-dev-wrapper.js +187 -0
  32. package/dist/ui/branding.d.ts +31 -0
  33. package/dist/ui/branding.d.ts.map +1 -0
  34. package/dist/ui/branding.js +136 -0
  35. package/package.json +69 -0
  36. package/templates/typescript-oauth/.env.example +27 -0
  37. package/templates/typescript-oauth/OAUTH_SETUP.md +592 -0
  38. package/templates/typescript-oauth/README.md +263 -0
  39. package/templates/typescript-oauth/package.json +29 -0
  40. package/templates/typescript-oauth/src/app.module.ts +92 -0
  41. package/templates/typescript-oauth/src/guards/oauth.guard.ts +126 -0
  42. package/templates/typescript-oauth/src/health/system.health.ts +55 -0
  43. package/templates/typescript-oauth/src/index.ts +63 -0
  44. package/templates/typescript-oauth/src/modules/flights/booking.tools.ts +323 -0
  45. package/templates/typescript-oauth/src/modules/flights/flights.module.ts +14 -0
  46. package/templates/typescript-oauth/src/modules/flights/flights.prompts.ts +228 -0
  47. package/templates/typescript-oauth/src/modules/flights/flights.resources.ts +215 -0
  48. package/templates/typescript-oauth/src/modules/flights/flights.tools.ts +457 -0
  49. package/templates/typescript-oauth/src/services/duffel.service.ts +285 -0
  50. package/templates/typescript-oauth/src/widgets/app/airport-search/page.tsx +270 -0
  51. package/templates/typescript-oauth/src/widgets/app/flight-details/page.tsx +261 -0
  52. package/templates/typescript-oauth/src/widgets/app/flight-search-results/page.tsx +378 -0
  53. package/templates/typescript-oauth/src/widgets/app/globals.css +167 -0
  54. package/templates/typescript-oauth/src/widgets/app/layout.tsx +18 -0
  55. package/templates/typescript-oauth/src/widgets/app/order-cancellation/page.tsx +207 -0
  56. package/templates/typescript-oauth/src/widgets/app/order-summary/page.tsx +245 -0
  57. package/templates/typescript-oauth/src/widgets/app/payment-confirmation/page.tsx +152 -0
  58. package/templates/typescript-oauth/src/widgets/app/seat-selection/page.tsx +486 -0
  59. package/templates/typescript-oauth/src/widgets/next-env.d.ts +5 -0
  60. package/templates/typescript-oauth/src/widgets/next.config.js +45 -0
  61. package/templates/typescript-oauth/src/widgets/package-lock.json +4493 -0
  62. package/templates/typescript-oauth/src/widgets/package.json +24 -0
  63. package/templates/typescript-oauth/src/widgets/tsconfig.json +28 -0
  64. package/templates/typescript-oauth/src/widgets/widget-manifest.json +395 -0
  65. package/templates/typescript-oauth/tsconfig.json +23 -0
  66. package/templates/typescript-pizzaz/README.md +252 -0
  67. package/templates/typescript-pizzaz/package.json +34 -0
  68. package/templates/typescript-pizzaz/src/app.module.ts +28 -0
  69. package/templates/typescript-pizzaz/src/index.ts +30 -0
  70. package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.data.ts +106 -0
  71. package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.module.ts +11 -0
  72. package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.service.ts +60 -0
  73. package/templates/typescript-pizzaz/src/modules/pizzaz/pizzaz.tools.ts +197 -0
  74. package/templates/typescript-pizzaz/src/widgets/app/layout.tsx +18 -0
  75. package/templates/typescript-pizzaz/src/widgets/app/pizza-list/page.tsx +272 -0
  76. package/templates/typescript-pizzaz/src/widgets/app/pizza-map/page.tsx +216 -0
  77. package/templates/typescript-pizzaz/src/widgets/app/pizza-shop/page.tsx +374 -0
  78. package/templates/typescript-pizzaz/src/widgets/components/CompactShopCard.tsx +144 -0
  79. package/templates/typescript-pizzaz/src/widgets/components/PizzaCard.tsx +191 -0
  80. package/templates/typescript-pizzaz/src/widgets/next.config.js +45 -0
  81. package/templates/typescript-pizzaz/src/widgets/package.json +30 -0
  82. package/templates/typescript-pizzaz/src/widgets/tsconfig.json +28 -0
  83. package/templates/typescript-pizzaz/src/widgets/widget-manifest.json +253 -0
  84. package/templates/typescript-pizzaz/tsconfig.json +30 -0
  85. package/templates/typescript-starter/README.md +320 -0
  86. package/templates/typescript-starter/package.json +25 -0
  87. package/templates/typescript-starter/src/app.module.ts +34 -0
  88. package/templates/typescript-starter/src/health/system.health.ts +55 -0
  89. package/templates/typescript-starter/src/index.ts +29 -0
  90. package/templates/typescript-starter/src/modules/calculator/calculator.module.ts +12 -0
  91. package/templates/typescript-starter/src/modules/calculator/calculator.prompts.ts +73 -0
  92. package/templates/typescript-starter/src/modules/calculator/calculator.resources.ts +59 -0
  93. package/templates/typescript-starter/src/modules/calculator/calculator.tools.ts +166 -0
  94. package/templates/typescript-starter/src/widgets/app/calculator-result/page.tsx +180 -0
  95. package/templates/typescript-starter/src/widgets/app/layout.tsx +18 -0
  96. package/templates/typescript-starter/src/widgets/next.config.js +45 -0
  97. package/templates/typescript-starter/src/widgets/package.json +24 -0
  98. package/templates/typescript-starter/src/widgets/tsconfig.json +28 -0
  99. package/templates/typescript-starter/src/widgets/widget-manifest.json +48 -0
  100. package/templates/typescript-starter/tsconfig.json +23 -0
@@ -0,0 +1,197 @@
1
+ import { ToolDecorator as Tool, Widget, ExecutionContext, Injectable, z } from 'nitrostack';
2
+ import { PizzazService } from './pizzaz.service.js';
3
+
4
+ const ShowMapSchema = z.object({
5
+ filter: z.enum(['open_now', 'top_rated', 'all']).optional().describe('Filter to apply'),
6
+ });
7
+
8
+ const ShowListSchema = z.object({
9
+ openNow: z.boolean().optional().describe('Show only shops that are currently open'),
10
+ minRating: z.number().min(1).max(5).optional().describe('Minimum rating (1-5)'),
11
+ maxPrice: z.number().min(1).max(3).optional().describe('Maximum price level (1-3)'),
12
+ });
13
+
14
+ const ShowShopSchema = z.object({
15
+ shopId: z.string().describe('ID of the pizza shop to display'),
16
+ });
17
+
18
+ @Injectable()
19
+ export class PizzazTools {
20
+ constructor(private readonly pizzazService: PizzazService) { }
21
+
22
+ @Tool({
23
+ name: 'show_pizza_map',
24
+ description: 'Display an interactive map of pizza shops in San Francisco',
25
+ inputSchema: ShowMapSchema,
26
+ examples: {
27
+ request: { filter: 'all' },
28
+ response: {
29
+ shops: [
30
+ {
31
+ id: 'tonys-pizza',
32
+ name: "Tony's New York Pizza",
33
+ description: "Authentic New York-style pizza with a crispy thin crust",
34
+ address: "123 Main St, San Francisco, CA 94102",
35
+ coords: [-122.4194, 37.7749],
36
+ rating: 4.5,
37
+ reviews: 342,
38
+ priceLevel: 2,
39
+ cuisine: ['Italian', 'Pizza'],
40
+ hours: { open: '11:00 AM', close: '10:00 PM' },
41
+ phone: '(415) 555-0123',
42
+ website: 'https://tonyspizza.example.com',
43
+ image: 'https://images.unsplash.com/photo-1513104890138-7c749659a591',
44
+ specialties: ['Margherita', 'Pepperoni'],
45
+ openNow: true
46
+ },
47
+ {
48
+ id: 'bella-napoli',
49
+ name: 'Bella Napoli',
50
+ description: "Traditional Neapolitan pizza baked in a wood-fired oven",
51
+ address: "456 Market St, San Francisco, CA 94103",
52
+ coords: [-122.4089, 37.7858],
53
+ rating: 4.8,
54
+ reviews: 521,
55
+ priceLevel: 3,
56
+ cuisine: ['Italian', 'Pizza'],
57
+ hours: { open: '12:00 PM', close: '11:00 PM' },
58
+ phone: '(415) 555-0456',
59
+ image: 'https://images.unsplash.com/photo-1574071318508-1cdbab80d002',
60
+ specialties: ['Marinara', 'Quattro Formaggi'],
61
+ openNow: true
62
+ }
63
+ ],
64
+ filter: 'all',
65
+ totalShops: 2
66
+ }
67
+ }
68
+ })
69
+ @Widget('pizza-map')
70
+ async showPizzaMap(args: z.infer<typeof ShowMapSchema>, ctx: ExecutionContext) {
71
+ let shops;
72
+
73
+ switch (args.filter) {
74
+ case 'open_now':
75
+ shops = this.pizzazService.getShopsFiltered({ openNow: true });
76
+ break;
77
+ case 'top_rated':
78
+ shops = this.pizzazService.getTopRatedShops();
79
+ break;
80
+ default:
81
+ shops = this.pizzazService.getAllShops();
82
+ }
83
+
84
+ ctx.logger.info('Showing pizza map', { filter: args.filter, totalShops: shops.length });
85
+
86
+ return {
87
+ shops,
88
+ filter: args.filter || 'all',
89
+ totalShops: shops.length,
90
+ };
91
+ }
92
+
93
+ @Tool({
94
+ name: 'show_pizza_list',
95
+ description: 'Display a list of pizza shops with filtering and sorting options',
96
+ inputSchema: ShowListSchema,
97
+ examples: {
98
+ request: { openNow: true },
99
+ response: {
100
+ shops: [
101
+ {
102
+ id: 'tonys-pizza',
103
+ name: "Tony's New York Pizza",
104
+ description: "Authentic New York-style pizza with a crispy thin crust",
105
+ address: "123 Main St, San Francisco, CA 94102",
106
+ coords: [-122.4194, 37.7749],
107
+ rating: 4.5,
108
+ reviews: 342,
109
+ priceLevel: 2,
110
+ cuisine: ['Italian', 'Pizza'],
111
+ hours: { open: '11:00 AM', close: '10:00 PM' },
112
+ phone: '(415) 555-0123',
113
+ website: 'https://tonyspizza.example.com',
114
+ image: 'https://images.unsplash.com/photo-1513104890138-7c749659a591',
115
+ specialties: ['Margherita', 'Pepperoni'],
116
+ openNow: true
117
+ }
118
+ ],
119
+ filters: { openNow: true },
120
+ totalShops: 1
121
+ }
122
+ }
123
+ })
124
+ @Widget('pizza-list')
125
+ async showPizzaList(args: z.infer<typeof ShowListSchema>, ctx: ExecutionContext) {
126
+ const shops = this.pizzazService.getShopsFiltered(args);
127
+
128
+ ctx.logger.info('Showing pizza list', { filters: args, totalShops: shops.length });
129
+
130
+ return {
131
+ shops,
132
+ filters: args,
133
+ totalShops: shops.length,
134
+ };
135
+ }
136
+
137
+ @Tool({
138
+ name: 'show_pizza_shop',
139
+ description: 'Display detailed information about a specific pizza shop',
140
+ inputSchema: ShowShopSchema,
141
+ examples: {
142
+ request: { shopId: 'tonys-pizza' },
143
+ response: {
144
+ shop: {
145
+ id: 'tonys-pizza',
146
+ name: "Tony's New York Pizza",
147
+ description: "Authentic New York-style pizza with a crispy thin crust and fresh toppings",
148
+ address: "123 Main St, San Francisco, CA 94102",
149
+ coords: [-122.4194, 37.7749],
150
+ rating: 4.5,
151
+ reviews: 342,
152
+ priceLevel: 2,
153
+ cuisine: ['Italian', 'Pizza', 'New York Style'],
154
+ hours: { open: '11:00 AM', close: '10:00 PM' },
155
+ phone: '(415) 555-0123',
156
+ website: 'https://tonyspizza.example.com',
157
+ image: 'https://images.unsplash.com/photo-1513104890138-7c749659a591',
158
+ specialties: ['Margherita', 'Pepperoni', 'White Pizza'],
159
+ openNow: true
160
+ },
161
+ relatedShops: [
162
+ {
163
+ id: 'bella-napoli',
164
+ name: 'Bella Napoli',
165
+ description: "Traditional Neapolitan pizza baked in a wood-fired oven",
166
+ address: "456 Market St, San Francisco, CA 94103",
167
+ coords: [-122.4089, 37.7858],
168
+ rating: 4.8,
169
+ reviews: 521,
170
+ priceLevel: 3,
171
+ cuisine: ['Italian', 'Pizza'],
172
+ hours: { open: '12:00 PM', close: '11:00 PM' },
173
+ phone: '(415) 555-0456',
174
+ image: 'https://images.unsplash.com/photo-1574071318508-1cdbab80d002',
175
+ specialties: ['Marinara', 'Quattro Formaggi'],
176
+ openNow: true
177
+ }
178
+ ]
179
+ }
180
+ }
181
+ })
182
+ @Widget('pizza-shop')
183
+ async showPizzaShop(args: z.infer<typeof ShowShopSchema>, ctx: ExecutionContext) {
184
+ const shop = this.pizzazService.getShopById(args.shopId);
185
+
186
+ if (!shop) {
187
+ throw new Error(`Pizza shop not found: ${args.shopId}`);
188
+ }
189
+
190
+ ctx.logger.info('Showing pizza shop', { shopId: args.shopId, shopName: shop.name });
191
+
192
+ return {
193
+ shop,
194
+ relatedShops: this.pizzazService.getTopRatedShops(3).filter(s => s.id !== shop.id),
195
+ };
196
+ }
197
+ }
@@ -0,0 +1,18 @@
1
+ 'use client';
2
+
3
+ import { WidgetLayout } from '@nitrostack/widgets';
4
+ import 'mapbox-gl/dist/mapbox-gl.css';
5
+
6
+ export default function RootLayout({
7
+ children,
8
+ }: {
9
+ children: React.ReactNode;
10
+ }) {
11
+ return (
12
+ <html lang="en">
13
+ <body style={{ margin: 0, padding: 0, fontFamily: 'system-ui, sans-serif' }}>
14
+ <WidgetLayout>{children}</WidgetLayout>
15
+ </body>
16
+ </html>
17
+ );
18
+ }
@@ -0,0 +1,272 @@
1
+ 'use client';
2
+
3
+ import { useTheme, useWidgetState, useMaxHeight, useWidgetSDK } from '@nitrostack/widgets';
4
+
5
+ // Disable static generation - this is a dynamic widget
6
+ export const dynamic = 'force-dynamic';
7
+ import { PizzaCard } from '../../components/PizzaCard';
8
+ import { SlidersHorizontal } from 'lucide-react';
9
+ import { useState } from 'react';
10
+
11
+ interface PizzaShop {
12
+ id: string;
13
+ name: string;
14
+ description: string;
15
+ address: string;
16
+ coords: [number, number];
17
+ rating: number;
18
+ reviews: number;
19
+ priceLevel: 1 | 2 | 3;
20
+ cuisine: string[];
21
+ hours: { open: string; close: string };
22
+ phone: string;
23
+ website?: string;
24
+ image: string;
25
+ specialties: string[];
26
+ openNow: boolean;
27
+ }
28
+
29
+ interface WidgetData {
30
+ shops: PizzaShop[];
31
+ filters: any;
32
+ totalShops: number;
33
+ }
34
+
35
+ export default function PizzaListWidget() {
36
+ const theme = useTheme();
37
+ const maxHeight = useMaxHeight();
38
+ const isDark = theme === 'dark';
39
+
40
+ const { isReady, getToolOutput, callTool } = useWidgetSDK();
41
+
42
+ // Access tool output
43
+ const data = getToolOutput<WidgetData>();
44
+
45
+ console.log('🍕 PizzaListWidget render:', { isReady, hasData: !!data, data });
46
+
47
+ // Persistent state for view mode and favorites
48
+ const [state, setState] = useWidgetState<{
49
+ viewMode: 'grid' | 'list';
50
+ favorites: string[];
51
+ sortBy: 'rating' | 'name' | 'price';
52
+ }>(() => ({
53
+ viewMode: 'grid',
54
+ favorites: [],
55
+ sortBy: 'rating',
56
+ }));
57
+
58
+ const [showFilters, setShowFilters] = useState(false);
59
+
60
+ if (!data) {
61
+ return (
62
+ <div style={{
63
+ padding: '40px',
64
+ textAlign: 'center',
65
+ color: isDark ? '#fff' : '#000',
66
+ }}>
67
+ Loading pizza shops... {isReady ? '(SDK ready but no data)' : '(waiting for SDK)'}
68
+ </div>
69
+ );
70
+ }
71
+
72
+ // Check if shops array exists
73
+ if (!data.shops || !Array.isArray(data.shops)) {
74
+ console.error('❌ Invalid data structure:', data);
75
+ return (
76
+ <div style={{
77
+ padding: '40px',
78
+ textAlign: 'center',
79
+ color: isDark ? '#fff' : '#000',
80
+ }}>
81
+ Error: Invalid data structure. Expected shops array.
82
+ <pre style={{ marginTop: '16px', fontSize: '12px', textAlign: 'left' }}>
83
+ {JSON.stringify(data, null, 2)}
84
+ </pre>
85
+ </div>
86
+ );
87
+ }
88
+
89
+ const toggleFavorite = (shopId: string) => {
90
+ const favorites = state?.favorites || [];
91
+ const newFavorites = favorites.includes(shopId)
92
+ ? favorites.filter(id => id !== shopId)
93
+ : [...favorites, shopId];
94
+
95
+ setState({ ...state, favorites: newFavorites });
96
+ };
97
+
98
+ const handleShopClick = async (shopId: string) => {
99
+ // Call the show_pizza_shop tool to show shop details
100
+ await callTool('show_pizza_shop', { shopId });
101
+ };
102
+
103
+ // Sort shops
104
+ let sortedShops = [...data.shops];
105
+ switch (state?.sortBy) {
106
+ case 'rating':
107
+ sortedShops.sort((a, b) => b.rating - a.rating);
108
+ break;
109
+ case 'name':
110
+ sortedShops.sort((a, b) => a.name.localeCompare(b.name));
111
+ break;
112
+ case 'price':
113
+ sortedShops.sort((a, b) => a.priceLevel - b.priceLevel);
114
+ break;
115
+ }
116
+
117
+ return (
118
+ <div style={{
119
+ background: isDark ? '#0a0a0a' : '#f9fafb',
120
+ minHeight: '400px',
121
+ maxHeight: maxHeight || '600px',
122
+ overflow: 'auto',
123
+ }}>
124
+ {/* Header */}
125
+ <div style={{
126
+ background: isDark ? '#1a1a1a' : '#ffffff',
127
+ borderBottom: `1px solid ${isDark ? '#333' : '#e5e7eb'}`,
128
+ padding: '12px 16px',
129
+ position: 'sticky',
130
+ top: 0,
131
+ zIndex: 10,
132
+ }}>
133
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
134
+ <div>
135
+ <h1 style={{
136
+ margin: '0 0 2px 0',
137
+ fontSize: '18px',
138
+ fontWeight: '700',
139
+ color: isDark ? '#fff' : '#111',
140
+ }}>
141
+ 🍕 Pizza Shops
142
+ </h1>
143
+ <p style={{
144
+ margin: 0,
145
+ fontSize: '12px',
146
+ color: isDark ? '#999' : '#666',
147
+ }}>
148
+ {data.totalShops} shops found
149
+ </p>
150
+ </div>
151
+
152
+ {/* Sort and Filter Controls */}
153
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
154
+ <select
155
+ value={state?.sortBy || 'rating'}
156
+ onChange={(e) => setState({ ...state, sortBy: e.target.value as any })}
157
+ style={{
158
+ padding: '6px 10px',
159
+ background: isDark ? '#1a1a1a' : '#fff',
160
+ border: `1px solid ${isDark ? '#444' : '#d1d5db'}`,
161
+ borderRadius: '6px',
162
+ color: isDark ? '#fff' : '#111',
163
+ fontSize: '12px',
164
+ cursor: 'pointer',
165
+ outline: 'none',
166
+ }}
167
+ >
168
+ <option value="rating">⭐ Rating</option>
169
+ <option value="name">🔤 Name</option>
170
+ <option value="price">💰 Price</option>
171
+ </select>
172
+
173
+ <button
174
+ onClick={() => setShowFilters(!showFilters)}
175
+ style={{
176
+ padding: '8px 12px',
177
+ background: showFilters
178
+ ? (isDark ? '#333' : '#f3f4f6')
179
+ : 'transparent',
180
+ border: `1px solid ${isDark ? '#444' : '#d1d5db'}`,
181
+ borderRadius: '8px',
182
+ cursor: 'pointer',
183
+ color: isDark ? '#fff' : '#111',
184
+ }}
185
+ >
186
+ <SlidersHorizontal size={18} />
187
+ </button>
188
+ </div>
189
+ </div>
190
+
191
+ {/* Filters */}
192
+ {showFilters && (
193
+ <div style={{
194
+ padding: '16px',
195
+ background: isDark ? '#111' : '#f9fafb',
196
+ borderRadius: '8px',
197
+ marginTop: '12px',
198
+ }}>
199
+ <p style={{
200
+ margin: '0 0 8px 0',
201
+ fontSize: '12px',
202
+ color: isDark ? '#999' : '#666',
203
+ }}>
204
+ Filters coming soon...
205
+ </p>
206
+ </div>
207
+ )}
208
+
209
+ {/* Favorites Count */}
210
+ {state?.favorites && state.favorites.length > 0 && (
211
+ <div style={{
212
+ marginTop: '12px',
213
+ padding: '8px 12px',
214
+ background: isDark ? '#1a1a1a' : '#fef3c7',
215
+ border: `1px solid ${isDark ? '#333' : '#fbbf24'}`,
216
+ borderRadius: '8px',
217
+ fontSize: '14px',
218
+ color: isDark ? '#fbbf24' : '#92400e',
219
+ }}>
220
+ ❤️ {state.favorites.length} favorite{state.favorites.length !== 1 ? 's' : ''}
221
+ </div>
222
+ )}
223
+ </div>
224
+
225
+ {/* Horizontal Scrolling Shop Cards */}
226
+ <div style={{
227
+ padding: '12px 16px',
228
+ overflowX: 'auto',
229
+ overflowY: 'hidden',
230
+ scrollSnapType: 'x mandatory',
231
+ WebkitOverflowScrolling: 'touch',
232
+ scrollbarWidth: 'thin',
233
+ }}>
234
+ <div style={{
235
+ display: 'flex',
236
+ gap: '12px',
237
+ paddingBottom: '4px',
238
+ }}>
239
+ {sortedShops.map(shop => (
240
+ <div
241
+ key={shop.id}
242
+ style={{
243
+ minWidth: '280px',
244
+ maxWidth: '280px',
245
+ scrollSnapAlign: 'start',
246
+ flexShrink: 0,
247
+ }}
248
+ >
249
+ <PizzaCard
250
+ shop={shop}
251
+ isFavorite={state?.favorites?.includes(shop.id)}
252
+ onToggleFavorite={toggleFavorite}
253
+ onSelect={() => handleShopClick(shop.id)}
254
+ />
255
+ </div>
256
+ ))}
257
+ </div>
258
+ </div>
259
+
260
+ {/* Footer */}
261
+ <div style={{
262
+ padding: '20px',
263
+ textAlign: 'center',
264
+ fontSize: '12px',
265
+ color: isDark ? '#666' : '#999',
266
+ borderTop: `1px solid ${isDark ? '#333' : '#e5e7eb'}`,
267
+ }}>
268
+ Powered by NitroStack • Theme: {theme || 'light'} • Scroll horizontally →
269
+ </div>
270
+ </div>
271
+ );
272
+ }