jac-client 0.2.6__py3-none-any.whl → 0.2.8__py3-none-any.whl

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 (66) hide show
  1. jac_client/examples/all-in-one/app.jac +573 -0
  2. jac_client/examples/all-in-one/components/CategoryFilter.jac +35 -0
  3. jac_client/examples/all-in-one/components/Header.jac +13 -0
  4. jac_client/examples/all-in-one/components/ProfitOverview.jac +50 -0
  5. jac_client/examples/all-in-one/components/Summary.jac +53 -0
  6. jac_client/examples/all-in-one/components/TransactionForm.jac +158 -0
  7. jac_client/examples/all-in-one/components/TransactionItem.jac +55 -0
  8. jac_client/examples/all-in-one/components/TransactionList.jac +37 -0
  9. jac_client/examples/all-in-one/components/navigation.jac +132 -0
  10. jac_client/examples/all-in-one/constants/categories.jac +37 -0
  11. jac_client/examples/all-in-one/constants/clients.jac +13 -0
  12. jac_client/examples/all-in-one/context/BudgetContext.jac +28 -0
  13. jac_client/examples/all-in-one/hooks/useBudget.jac +116 -0
  14. jac_client/examples/all-in-one/hooks/useLocalStorage.jac +36 -0
  15. jac_client/examples/all-in-one/pages/BudgetPlanner.cl.jac +70 -0
  16. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +126 -0
  17. jac_client/examples/all-in-one/pages/FeaturesTest.cl.jac +552 -0
  18. jac_client/examples/all-in-one/pages/FeaturesTest.jac +126 -0
  19. jac_client/examples/all-in-one/pages/LandingPage.jac +101 -0
  20. jac_client/examples/all-in-one/pages/loginPage.jac +132 -0
  21. jac_client/examples/all-in-one/pages/nestedDemo.jac +61 -0
  22. jac_client/examples/all-in-one/pages/notFound.jac +24 -0
  23. jac_client/examples/all-in-one/pages/signupPage.jac +133 -0
  24. jac_client/examples/all-in-one/utils/formatters.jac +52 -0
  25. jac_client/examples/asset-serving/css-with-image/src/app.jac +3 -3
  26. jac_client/examples/asset-serving/image-asset/src/app.jac +3 -3
  27. jac_client/examples/asset-serving/import-alias/src/app.jac +3 -3
  28. jac_client/examples/basic/src/app.jac +3 -3
  29. jac_client/examples/basic-auth/src/app.jac +31 -37
  30. jac_client/examples/basic-auth-with-router/src/app.jac +16 -16
  31. jac_client/examples/basic-full-stack/src/app.jac +24 -30
  32. jac_client/examples/css-styling/js-styling/src/app.jac +5 -5
  33. jac_client/examples/css-styling/material-ui/src/app.jac +5 -5
  34. jac_client/examples/css-styling/pure-css/src/app.jac +5 -5
  35. jac_client/examples/css-styling/sass-example/src/app.jac +5 -5
  36. jac_client/examples/css-styling/styled-components/src/app.jac +5 -5
  37. jac_client/examples/css-styling/tailwind-example/src/app.jac +5 -5
  38. jac_client/examples/full-stack-with-auth/src/app.jac +16 -16
  39. jac_client/examples/ts-support/src/app.jac +4 -4
  40. jac_client/examples/with-router/src/app.jac +4 -4
  41. jac_client/plugin/cli.jac +160 -203
  42. jac_client/plugin/client.jac +8 -15
  43. jac_client/plugin/client_runtime.cl.jac +18 -14
  44. jac_client/plugin/impl/client.impl.jac +85 -26
  45. jac_client/plugin/impl/client_runtime.impl.jac +27 -9
  46. jac_client/plugin/plugin_config.jac +11 -11
  47. jac_client/plugin/src/compiler.jac +2 -1
  48. jac_client/plugin/src/impl/babel_processor.impl.jac +22 -17
  49. jac_client/plugin/src/impl/compiler.impl.jac +55 -18
  50. jac_client/plugin/src/impl/vite_bundler.impl.jac +215 -102
  51. jac_client/plugin/src/package_installer.jac +1 -1
  52. jac_client/plugin/src/vite_bundler.jac +9 -1
  53. jac_client/tests/conftest.py +10 -8
  54. jac_client/tests/fixtures/spawn_test/app.jac +15 -18
  55. jac_client/tests/fixtures/with-ts/app.jac +4 -4
  56. jac_client/tests/test_cli.py +105 -49
  57. jac_client/tests/test_it.py +297 -82
  58. {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/METADATA +16 -7
  59. jac_client-0.2.8.dist-info/RECORD +97 -0
  60. jac_client/examples/all-in-one/src/app.jac +0 -841
  61. jac_client-0.2.6.dist-info/RECORD +0 -74
  62. /jac_client/examples/all-in-one/{src/button.jac → button.jac} +0 -0
  63. /jac_client/examples/all-in-one/{src/components → components}/button.jac +0 -0
  64. {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/WHEEL +0 -0
  65. {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/entry_points.txt +0 -0
  66. {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,573 @@
1
+ import from datetime { datetime, timedelta }
2
+
3
+ #
4
+ # Basic backend walkers
5
+ #
6
+ node Todo {
7
+ has text: str,
8
+ done: bool = False;
9
+ }
10
+
11
+ walker create_todo {
12
+ has text: str;
13
+
14
+ can create with `root entry {
15
+ new_todo = here ++> Todo(text=self.text);
16
+ report new_todo ;
17
+ }
18
+ }
19
+
20
+ walker ping_server {
21
+ can ping with `root entry {
22
+ report "pong from backend!" ;
23
+ }
24
+ }
25
+
26
+ walker get_server_message {
27
+ can info with `root entry {
28
+ report "hello from a basic walker!" ;
29
+ }
30
+ }
31
+
32
+
33
+
34
+ # Features Test Page - Backend Logic
35
+ # This file is intentionally kept empty to demonstrate the separation of concerns
36
+ # where UI logic resides in .cl.jac files and backend walker logic in app.jac
37
+
38
+ # All walker implementations for this feature are in src/app.jac
39
+ # This demonstrates that you can have separate .jac files for different purposes
40
+ # Features Test - Backend Walkers
41
+ # This file demonstrates walker functionality for testing various JAC features
42
+
43
+
44
+ # Node definition for storing test data
45
+ node TestData {
46
+ has message: str;
47
+ has count: int = 0;
48
+ has created_at: str = "";
49
+ }
50
+
51
+ # Walker: Create test data
52
+ walker create_test_data {
53
+ has message: str;
54
+
55
+ can create with `root entry {
56
+ new_data = here ++> TestData(
57
+ message=self.message,
58
+ count=1,
59
+ created_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
60
+ );
61
+ report new_data;
62
+ }
63
+ }
64
+
65
+ # Walker: Read all test data
66
+ walker read_test_data {
67
+ can read with `root entry {
68
+ visit [-->(`?TestData)];
69
+ }
70
+
71
+ can report_data with exit {
72
+ report here;
73
+ }
74
+ }
75
+
76
+ # Walker: Update test data
77
+ walker update_test_data {
78
+ has new_message: str;
79
+
80
+ can update with TestData entry {
81
+ here.message = self.new_message;
82
+ here.count = here.count + 1;
83
+ report here;
84
+ }
85
+ }
86
+
87
+ # Walker: Delete test data
88
+ walker delete_test_data {
89
+ can delete with TestData entry {
90
+ del here;
91
+ report {"status": "deleted"};
92
+ }
93
+ }
94
+
95
+ # Walker: String manipulation test
96
+ walker test_string_methods {
97
+ has input_text: str;
98
+
99
+ can process with `root entry {
100
+ result = {
101
+ "original": self.input_text,
102
+ "uppercase": self.input_text.upper(),
103
+ "lowercase": self.input_text.lower(),
104
+ "capitalized": self.input_text.capitalize(),
105
+ "reversed": self.input_text[::-1],
106
+ "length": len(self.input_text),
107
+ "words": self.input_text.split(),
108
+ "trimmed": self.input_text.strip(),
109
+ "replaced": self.input_text.replace("test", "demo")
110
+ };
111
+ report result;
112
+ }
113
+ }
114
+
115
+ # Walker: Array/List operations test
116
+ walker test_list_operations {
117
+ has numbers: list;
118
+
119
+ can process with `root entry {
120
+ result = {
121
+ "original": self.numbers,
122
+ "sorted": sorted(self.numbers),
123
+ "reversed": list(reversed(self.numbers)),
124
+ "sum": sum(self.numbers),
125
+ "max": max(self.numbers) if len(self.numbers) > 0 else 0,
126
+ "min": min(self.numbers) if len(self.numbers) > 0 else 0,
127
+ "length": len(self.numbers),
128
+ "doubled": [x * 2 for x in self.numbers]
129
+ };
130
+ report result;
131
+ }
132
+ }
133
+
134
+ # Walker: Complex data processing
135
+ walker process_complex_data {
136
+ has items: list;
137
+
138
+ can process with `root entry {
139
+ processed = [];
140
+ for item in self.items {
141
+ processed.append({
142
+ "id": item.get("id", 0),
143
+ "name": item.get("name", "").upper(),
144
+ "value": item.get("value", 0) * 2,
145
+ "processed_at": datetime.now().strftime("%H:%M:%S")
146
+ });
147
+ }
148
+
149
+ result = {
150
+ "original_count": len(self.items),
151
+ "processed_count": len(processed),
152
+ "items": processed,
153
+ "total_value": sum([p["value"] for p in processed])
154
+ };
155
+
156
+ report result;
157
+ }
158
+ }
159
+
160
+
161
+ #
162
+ # Combined example: auth + routing + CSS styling + asset serving + nested folder imports
163
+ #
164
+ cl import from react { useEffect, useRef }
165
+ cl import from "@jac-client/utils" {
166
+ Router,
167
+ Routes,
168
+ Route,
169
+ Link,
170
+ useNavigate,
171
+ Navigate,
172
+ jacIsLoggedIn
173
+
174
+ }
175
+
176
+ # Pure CSS + asset-in-CSS example
177
+ cl import ".styles.css";
178
+
179
+ # Login Page
180
+ cl import from .pages.loginPage { LoginPage }
181
+
182
+ # Signup Page
183
+ cl import from .pages.signupPage { SignupPage }
184
+
185
+ # Simple 404 page
186
+ cl import from .pages.notFound { NotFound }
187
+
188
+ # Navigation component with active link styling and auth
189
+ cl import from .components.navigation { Navigation }
190
+
191
+ # Page showing nested imports from different folders
192
+ cl import from .pages.nestedDemo { NestedImportsDemo }
193
+
194
+ cl import from .pages.FeaturesTest { FeaturesTest }
195
+
196
+ cl import from .pages.LandingPage { LandingPage }
197
+
198
+ cl import from .pages.BudgetPlanner { BudgetPlanner }
199
+
200
+ # Context provider
201
+ cl import from .context.BudgetContext { BudgetProvider }
202
+
203
+
204
+
205
+ # TypeScript component import
206
+ cl import from ".components/Card.tsx" { Card }
207
+
208
+
209
+ # Main app wrapped in Router (same API as with-router/ example)
210
+ cl {
211
+ def:pub HomePage -> any {
212
+ # Check if user is logged in, redirect if not
213
+ if not jacIsLoggedIn() {
214
+ return <Navigate to="/login" />;
215
+ }
216
+ has count: int = 0;
217
+ has pingResult: str = "";
218
+ has serverMessage: str = "";
219
+ has lastTodoMessage: str = "";
220
+
221
+ useEffect(
222
+ lambda -> None{ console.log("Home count changed: ", count);} , [count]
223
+ );
224
+
225
+ # Call simple backend walkers
226
+ async def handlePing -> None {
227
+ result = root spawn ping_server();
228
+ if result.reports and result.reports.length > 0 {
229
+ pingResult = result.reports[0][0];
230
+ }
231
+ }
232
+
233
+ async def loadServerMessage -> None {
234
+ result = root spawn get_server_message();
235
+ if result.reports and result.reports.length > 0 {
236
+ serverMessage = result.reports[0][0];
237
+ }
238
+ }
239
+
240
+ # Create a sample Todo node in the graph with a hardcoded payload
241
+ async def handleCreateSampleTodo -> None {
242
+ result = root spawn create_todo(text="Sample todo from all-in-one app");
243
+ if result.reports and result.reports.length > 0 {
244
+ todo = result.reports[0][0];
245
+ lastTodoMessage = "Created Todo: " + todo.text;
246
+ console.log("Created Todo node:", todo);
247
+ }
248
+ }
249
+
250
+ useEffect(lambda -> None{ loadServerMessage();} , []);
251
+
252
+ # Initialize a Web Worker and handle message-based communication
253
+ workerRef = useRef(None);
254
+ has message: str = "";
255
+
256
+ useEffect(lambda -> None {
257
+ workerRef.current = Reflect.construct(Worker, ["/workers/worker.js"]);
258
+ workerRef.current.onmessage = lambda event: any -> None {
259
+ console.log("Message received from worker:", event.data);
260
+ message = event.data;
261
+ };
262
+ return (lambda -> None {
263
+ workerRef.current.terminate();
264
+ });
265
+ }, []);
266
+ handleClick = lambda -> None {
267
+ workerRef.current.postMessage("");
268
+ };
269
+
270
+ return <div
271
+ style={{
272
+ "padding": "2rem",
273
+ "fontFamily": "system-ui, -apple-system, sans-serif"
274
+ }}
275
+ >
276
+ <h1>
277
+ 🍔 Router + Styling + Assets Demo
278
+ </h1>
279
+ <p>
280
+ This home page combines
281
+ {" "}
282
+ <strong>
283
+ React Router,
284
+ </strong>
285
+ {" "}
286
+ <strong>
287
+ pure CSS styling,
288
+ </strong>
289
+ {" "}
290
+ <strong>
291
+ static assets
292
+ </strong>
293
+ {" "}
294
+ and
295
+ {" "}
296
+ <strong>
297
+ nested folder imports
298
+ </strong>
299
+ </p>
300
+ <div className="container">
301
+ <h2
302
+ style={{
303
+ "color": "white",
304
+ "textShadow": "2px 2px 4px rgba(0,0,0,0.6)"
305
+ }}
306
+ >
307
+ CSS Background Image
308
+ </h2>
309
+ <p
310
+ style={{
311
+ "color": "white",
312
+ "maxWidth": "480px",
313
+ "textShadow": "1px 1px 3px rgba(0,0,0,0.7)"
314
+ }}
315
+ >
316
+ This section uses the burger image as a background via CSS, just like the
317
+ {" "}
318
+ <code>
319
+ asset-serving/css-with-image
320
+ </code>
321
+ {" "}
322
+ example.
323
+ </p>
324
+ </div>
325
+ <Card
326
+ title="TypeScript Card Component"
327
+ description="This card is built with TypeScript and demonstrates type-safe component usage in Jac"
328
+ variant="highlighted"
329
+ >
330
+ <p
331
+ style={{"margin": "0.5rem 0", "color": "#374151"}}
332
+ >
333
+ This is a TypeScript component imported and used in Jac code!
334
+ </p>
335
+ </Card>
336
+ <div className="card">
337
+ <h3>
338
+ Direct &lt;img&gt; asset
339
+ </h3>
340
+ <img
341
+ src="/static/assets/burger.png"
342
+ alt="Burger asset served by Jac"
343
+ className="burgerImage"
344
+ />
345
+ <p
346
+ style={{"marginTop": "0.75rem", "color": "#555"}}
347
+ >
348
+ This image is served from the project
349
+ {" "}
350
+ <code>
351
+ assets/
352
+ </code>
353
+ {" "}
354
+ folder using the
355
+ {" "}
356
+ <code>
357
+ /static/assets/
358
+ </code>
359
+ {" "}
360
+ path.
361
+ </p>
362
+ </div>
363
+ <div
364
+ style={{"marginTop": "2rem"}}
365
+ >
366
+ <h3>
367
+ Counter with pure CSS classes
368
+ </h3>
369
+ <p>
370
+ You've clicked the burger
371
+ {" "}
372
+ <strong>
373
+ {count}
374
+ </strong>
375
+ {" "}
376
+ times.
377
+ </p>
378
+ <button
379
+ onClick={lambda e: any -> None{ count = count + 1;} }
380
+ style={{
381
+ "padding": "0.6rem 1.4rem",
382
+ "fontSize": "1rem",
383
+ "backgroundColor": "#ff6b35",
384
+ "color": "white",
385
+ "border": "none",
386
+ "borderRadius": "6px",
387
+ "cursor": "pointer",
388
+ "boxShadow": "0 2px 4px rgba(0,0,0,0.2)"
389
+ }}
390
+ >
391
+ Click the Burger! 🍔
392
+ </button>
393
+ </div>
394
+ <div
395
+ style={{"marginTop": "2rem"}}
396
+ >
397
+ <h3>
398
+ Backend Walkers
399
+ </h3>
400
+ <p>
401
+ Basic example walkers:
402
+ {" "}
403
+ <code>
404
+ ping_server
405
+ </code>
406
+ {" "}
407
+ and
408
+ {" "}
409
+ <code>
410
+ get_server_message
411
+ </code>
412
+ </p>
413
+ <button
414
+ onClick={lambda e: any -> None{ handlePing();} }
415
+ style={{
416
+ "padding": "0.5rem 1.2rem",
417
+ "marginRight": "0.75rem",
418
+ "backgroundColor": "#3b82f6",
419
+ "color": "white",
420
+ "border": "none",
421
+ "borderRadius": "6px",
422
+ "cursor": "pointer"
423
+ }}
424
+ >
425
+ Ping Backend
426
+ </button>
427
+ <button
428
+ onClick={lambda e: any -> None{ handleCreateSampleTodo();} }
429
+ style={{
430
+ "padding": "0.5rem 1.2rem",
431
+ "backgroundColor": "#10b981",
432
+ "color": "white",
433
+ "border": "none",
434
+ "borderRadius": "6px",
435
+ "cursor": "pointer"
436
+ }}
437
+ >
438
+ Create Sample Todo
439
+ </button>
440
+ {pingResult
441
+ and (
442
+ <span
443
+ style={{"marginLeft": "0.5rem", "color": "#374151"}}
444
+ >
445
+ Result:
446
+ {" "}
447
+ <code>
448
+ {pingResult}
449
+ </code>
450
+ </span>
451
+ )}
452
+ {serverMessage
453
+ and (
454
+ <p
455
+ style={{"marginTop": "0.75rem", "color": "#374151"}}
456
+ >
457
+ Message:
458
+ {" "}
459
+ <code>
460
+ {serverMessage}
461
+ </code>
462
+ </p>
463
+ )}
464
+ {lastTodoMessage
465
+ and (
466
+ <p
467
+ style={{"marginTop": "0.5rem", "color": "#111827"}}
468
+ >
469
+ {lastTodoMessage}
470
+ </p>
471
+ )}
472
+ </div>
473
+ <div
474
+ style={{"marginTop": "2rem"}}
475
+ >
476
+ <h3>
477
+ Web Worker
478
+ </h3>
479
+ <p>
480
+ This demonstrates how to communicate with a
481
+ {" "}
482
+ <strong>Python backend worker</strong>
483
+ {" "}
484
+ using Web Workers for asynchronous processing.
485
+ </p>
486
+ <p
487
+ style={{"fontSize": "0.9rem", "color": "#666", "marginTop": "0.5rem"}}
488
+ >
489
+ File:
490
+ {" "}
491
+ <code>
492
+ worker.py
493
+ </code>
494
+ {" "}
495
+ — Runs in a separate thread to avoid blocking the UI.
496
+ </p>
497
+ <button
498
+ onClick={lambda -> None { handleClick(); }}
499
+ style={{
500
+ "padding": "0.5rem 1.2rem",
501
+ "marginRight": "0.75rem",
502
+ "backgroundColor": "#d73bf6ff",
503
+ "color": "white",
504
+ "border": "none",
505
+ "borderRadius": "6px",
506
+ "cursor": "pointer"
507
+ }}
508
+ >
509
+ Call Python Worker
510
+ </button>
511
+ {message && (
512
+ <p style={{ marginTop: "1rem", fontWeight: "bold" }}>
513
+ {message}
514
+ </p>
515
+ )}
516
+ </div>
517
+ </div>;
518
+ }
519
+
520
+ def:pub app -> any {
521
+ return <Router>
522
+ <div
523
+ style={{"fontFamily": "system-ui, -apple-system, sans-serif"}}
524
+ >
525
+ <Navigation />
526
+ <div
527
+ style={{
528
+ "maxWidth": "960px",
529
+ "margin": "0 auto",
530
+ "padding": "0 1rem 3rem 1rem"
531
+ }}
532
+ >
533
+ <Routes>
534
+ <Route
535
+ path="/"
536
+ element={<HomePage />}
537
+ />
538
+ <Route
539
+ path="/login"
540
+ element={<LoginPage />}
541
+ />
542
+ <Route
543
+ path="/signup"
544
+ element={<SignupPage />}
545
+ />
546
+ <Route
547
+ path="/nested"
548
+ element={<NestedImportsDemo />}
549
+ />
550
+ <Route
551
+ path="/features-test"
552
+ element={<FeaturesTest />}
553
+ />
554
+ <Route
555
+ path="/landing"
556
+ element={<LandingPage />}
557
+ />
558
+ <Route
559
+ path="/budget-planner"
560
+ element={<BudgetProvider>
561
+ <BudgetPlanner />
562
+ </BudgetProvider>}
563
+ />
564
+ <Route
565
+ path="*"
566
+ element={<NotFound />}
567
+ />
568
+ </Routes>
569
+ </div>
570
+ </div>
571
+ </Router>;
572
+ }
573
+ }
@@ -0,0 +1,35 @@
1
+ # Category filter component
2
+ # Demonstrates: iteration with map, conditional classes, events
3
+
4
+ cl import from ..constants.categories { CATEGORIES, CATEGORY_LABELS, CATEGORY_COLORS }
5
+
6
+ cl {
7
+ # Filter buttons for categories
8
+ def:pub CategoryFilter(selectedCategory: str, onSelect: any) -> any {
9
+ # Add "ALL" to the beginning of categories
10
+ allCategories = ["ALL"].concat(CATEGORIES);
11
+
12
+ return <div className="category-filter">
13
+ <h3 className="filter-title">Filter by Category</h3>
14
+ <div className="filter-buttons">
15
+ {allCategories.map(lambda cat: str -> any {
16
+ isActive = selectedCategory == cat;
17
+ color = CATEGORY_COLORS[cat] if cat != "ALL" else "#374151";
18
+
19
+ return <button
20
+ key={cat}
21
+ className={("filter-btn active") if isActive else ("filter-btn")}
22
+ style={{
23
+ "borderColor": color,
24
+ "backgroundColor": (color) if isActive else ("transparent"),
25
+ "color": ("#fff") if isActive else (color)
26
+ }}
27
+ onClick={lambda: onSelect(cat)}
28
+ >
29
+ {(cat) if cat == "ALL" else (CATEGORY_LABELS[cat])}
30
+ </button>;
31
+ })}
32
+ </div>
33
+ </div>;
34
+ }
35
+ }
@@ -0,0 +1,13 @@
1
+ # Header component
2
+ # Demonstrates: simple component, CSS classes
3
+
4
+ cl {
5
+ def:pub Header() -> any {
6
+ return <header className="header">
7
+ <div className="header-content">
8
+ <h1 className="header-title">Budget Planner</h1>
9
+ <p className="header-subtitle">Track your income and expenses</p>
10
+ </div>
11
+ </header>;
12
+ }
13
+ }
@@ -0,0 +1,50 @@
1
+ # Profit Overview component - monthly profit snapshot
2
+ # Demonstrates: context usage, calculated displays, formatting
3
+
4
+ cl import from ..context.BudgetContext { useBudgetContext }
5
+ cl import from ..utils.formatters { formatCurrency }
6
+
7
+ cl {
8
+ def:pub ProfitOverview() -> any {
9
+ budget = useBudgetContext();
10
+
11
+ businessIncome = budget["businessIncome"];
12
+ businessExpenses = budget["businessExpenses"];
13
+ taxReserve = budget["taxReserve"];
14
+ netProfit = budget["netProfit"];
15
+
16
+ return <div className="profit-overview">
17
+ <h3 className="profit-title">Monthly Profit Snapshot</h3>
18
+
19
+ <div className="profit-breakdown">
20
+ <div className="profit-row income">
21
+ <span className="profit-label">Business Income</span>
22
+ <span className="profit-value positive">
23
+ +{formatCurrency(businessIncome)}
24
+ </span>
25
+ </div>
26
+
27
+ <div className="profit-row expense">
28
+ <span className="profit-label">Business Expenses</span>
29
+ <span className="profit-value negative">
30
+ -{formatCurrency(businessExpenses)}
31
+ </span>
32
+ </div>
33
+
34
+ <div className="profit-row tax">
35
+ <span className="profit-label">Tax Reserve (20%)</span>
36
+ <span className="profit-value negative">
37
+ -{formatCurrency(taxReserve)}
38
+ </span>
39
+ </div>
40
+
41
+ <div className="profit-row total">
42
+ <span className="profit-label">Net Profit</span>
43
+ <span className={("profit-value bold positive") if netProfit >= 0 else ("profit-value bold negative")}>
44
+ {("+") if netProfit > 0 else ("")}{formatCurrency(netProfit)}
45
+ </span>
46
+ </div>
47
+ </div>
48
+ </div>;
49
+ }
50
+ }