@aurelia/storybook 2.0.0 → 2.2.1

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 (140) hide show
  1. package/.github/workflows/ci.yml +61 -0
  2. package/.github/workflows/publish.yml +82 -0
  3. package/.github/workflows/storybook-preview.yml +62 -0
  4. package/CHANGELOG.md +5 -0
  5. package/README.md +381 -148
  6. package/__tests__/create-aurelia-app.test.ts +94 -0
  7. package/__tests__/preset.test.ts +32 -3
  8. package/__tests__/preview.test.ts +9 -131
  9. package/__tests__/render.test.ts +15 -26
  10. package/apps/hello-world/.storybook/main.ts +0 -1
  11. package/apps/hello-world/package-lock.json +4585 -2609
  12. package/apps/hello-world/package.json +13 -23
  13. package/apps/hello-world/src/components/feedback-form.html +111 -0
  14. package/apps/hello-world/src/components/feedback-form.ts +45 -0
  15. package/apps/hello-world/src/components/notification-center.html +119 -0
  16. package/apps/hello-world/src/components/notification-center.ts +27 -0
  17. package/apps/hello-world/src/components/stat-card.html +107 -0
  18. package/apps/hello-world/src/components/stat-card.ts +41 -0
  19. package/apps/hello-world/src/components/weather-widget.html +89 -0
  20. package/apps/hello-world/src/components/weather-widget.ts +31 -0
  21. package/apps/hello-world/src/hello-world.html +44 -2
  22. package/apps/hello-world/src/services/weather-service.ts +15 -0
  23. package/apps/hello-world/src/stories/feedback-form.stories.ts +58 -0
  24. package/apps/hello-world/src/stories/hello-world.stories.ts +24 -14
  25. package/apps/hello-world/src/stories/notification-center.stories.ts +88 -0
  26. package/apps/hello-world/src/stories/stat-card.stories.ts +75 -0
  27. package/apps/hello-world/src/stories/weather-widget.stories.ts +62 -0
  28. package/apps/hello-world/tsconfig.json +4 -3
  29. package/apps/hello-world/vite.config.ts +0 -2
  30. package/apps/hello-world-rsbuild/.storybook/main.ts +16 -0
  31. package/apps/hello-world-rsbuild/.storybook/preview.ts +1 -0
  32. package/apps/hello-world-rsbuild/.stylelintrc.json +5 -0
  33. package/apps/hello-world-rsbuild/README.md +28 -0
  34. package/apps/hello-world-rsbuild/eslint.config.mjs +25 -0
  35. package/apps/hello-world-rsbuild/favicon.ico +0 -0
  36. package/apps/hello-world-rsbuild/index.html +17 -0
  37. package/apps/hello-world-rsbuild/package-lock.json +11131 -0
  38. package/apps/hello-world-rsbuild/package.json +56 -0
  39. package/apps/hello-world-rsbuild/src/components/feedback-form.html +111 -0
  40. package/apps/hello-world-rsbuild/src/components/feedback-form.ts +45 -0
  41. package/apps/hello-world-rsbuild/src/components/notification-center.html +119 -0
  42. package/apps/hello-world-rsbuild/src/components/notification-center.ts +27 -0
  43. package/apps/hello-world-rsbuild/src/components/stat-card.html +107 -0
  44. package/apps/hello-world-rsbuild/src/components/stat-card.ts +41 -0
  45. package/apps/hello-world-rsbuild/src/components/weather-widget.html +89 -0
  46. package/apps/hello-world-rsbuild/src/components/weather-widget.ts +31 -0
  47. package/apps/hello-world-rsbuild/src/hello-world.html +48 -0
  48. package/apps/hello-world-rsbuild/src/hello-world.ts +17 -0
  49. package/apps/hello-world-rsbuild/src/main.ts +6 -0
  50. package/apps/hello-world-rsbuild/src/my-app.html +1 -0
  51. package/apps/hello-world-rsbuild/src/my-app.ts +3 -0
  52. package/apps/hello-world-rsbuild/src/resource.d.ts +15 -0
  53. package/apps/hello-world-rsbuild/src/services/weather-service.ts +15 -0
  54. package/apps/hello-world-rsbuild/src/stories/feedback-form.stories.ts +58 -0
  55. package/apps/hello-world-rsbuild/src/stories/hello-world.stories.ts +64 -0
  56. package/apps/hello-world-rsbuild/src/stories/notification-center.stories.ts +88 -0
  57. package/apps/hello-world-rsbuild/src/stories/stat-card.stories.ts +75 -0
  58. package/apps/hello-world-rsbuild/src/stories/weather-widget.stories.ts +62 -0
  59. package/apps/hello-world-rsbuild/test/my-app.spec.ts +15 -0
  60. package/apps/hello-world-rsbuild/test/setup.ts +29 -0
  61. package/apps/hello-world-rsbuild/tsconfig.json +19 -0
  62. package/apps/hello-world-rsbuild/tsconfig.vitest.json +11 -0
  63. package/apps/hello-world-rsbuild/vite.config.ts +17 -0
  64. package/apps/hello-world-rsbuild/vitest.config.ts +15 -0
  65. package/apps/hello-world-webpack/.storybook/main.ts +0 -1
  66. package/apps/hello-world-webpack/package-lock.json +3553 -768
  67. package/apps/hello-world-webpack/package.json +8 -10
  68. package/apps/hello-world-webpack/src/components/feedback-form.html +111 -0
  69. package/apps/hello-world-webpack/src/components/feedback-form.ts +45 -0
  70. package/apps/hello-world-webpack/src/components/notification-center.html +119 -0
  71. package/apps/hello-world-webpack/src/components/notification-center.ts +27 -0
  72. package/apps/hello-world-webpack/src/components/stat-card.html +107 -0
  73. package/apps/hello-world-webpack/src/components/stat-card.ts +41 -0
  74. package/apps/hello-world-webpack/src/components/weather-widget.html +89 -0
  75. package/apps/hello-world-webpack/src/components/weather-widget.ts +31 -0
  76. package/apps/hello-world-webpack/src/hello-world.html +44 -2
  77. package/apps/hello-world-webpack/src/my-app.stories.ts +6 -4
  78. package/apps/hello-world-webpack/src/services/weather-service.ts +15 -0
  79. package/apps/hello-world-webpack/src/stories/feedback-form.stories.ts +58 -0
  80. package/apps/hello-world-webpack/src/stories/hello-world.stories.ts +25 -15
  81. package/apps/hello-world-webpack/src/stories/notification-center.stories.ts +88 -0
  82. package/apps/hello-world-webpack/src/stories/stat-card.stories.ts +75 -0
  83. package/apps/hello-world-webpack/src/stories/weather-widget.stories.ts +62 -0
  84. package/apps/hello-world-webpack/tsconfig.json +1 -1
  85. package/dist/index.d.ts +25 -0
  86. package/dist/index.js +68 -14
  87. package/dist/index.js.map +1 -1
  88. package/dist/preset.d.ts +21 -0
  89. package/dist/preset.js +46 -2
  90. package/dist/preset.js.map +1 -1
  91. package/dist/preview/helpers.d.ts +2 -0
  92. package/dist/preview/helpers.js +6 -0
  93. package/dist/preview/helpers.js.map +1 -0
  94. package/dist/preview/render.d.ts +7 -0
  95. package/dist/preview/render.js +66 -15
  96. package/dist/preview/render.js.map +1 -1
  97. package/dist/preview/storybook-types-runtime.d.ts +1 -0
  98. package/dist/preview/storybook-types-runtime.js +5 -0
  99. package/dist/preview/storybook-types-runtime.js.map +1 -0
  100. package/dist/preview/storybook-types.d.ts +27 -0
  101. package/dist/preview/types-runtime.d.ts +1 -0
  102. package/dist/preview/types-runtime.js +5 -0
  103. package/dist/preview/types-runtime.js.map +1 -0
  104. package/dist/preview/types.d.ts +44 -0
  105. package/dist/preview.d.ts +3 -0
  106. package/dist/preview.js +71 -16
  107. package/dist/preview.js.map +1 -1
  108. package/dist/webpack.d.ts +10 -0
  109. package/dist/webpack.js +19 -1
  110. package/dist/webpack.js.map +1 -1
  111. package/package.json +58 -13
  112. package/rollup.config.mjs +5 -3
  113. package/scripts/sync-versions.cjs +55 -0
  114. package/src/index.ts +11 -1
  115. package/src/preset.ts +32 -2
  116. package/src/preview/helpers.ts +7 -0
  117. package/src/preview/render.ts +98 -30
  118. package/src/preview/storybook-types-runtime.ts +2 -0
  119. package/src/preview/storybook-types.ts +34 -0
  120. package/src/preview/types-runtime.ts +2 -0
  121. package/src/preview/types.ts +57 -2
  122. package/src/preview.ts +11 -1
  123. package/src/webpack.ts +19 -0
  124. package/apps/hello-world/.yarnrc.yml +0 -2
  125. package/apps/hello-world-webpack/.yarnrc.yml +0 -2
  126. package/dist/index.mjs +0 -132
  127. package/dist/index.mjs.map +0 -1
  128. package/dist/preset.mjs +0 -60
  129. package/dist/preset.mjs.map +0 -1
  130. package/dist/preview/render.mjs +0 -114
  131. package/dist/preview/render.mjs.map +0 -1
  132. package/dist/preview/types.js +0 -2
  133. package/dist/preview/types.js.map +0 -1
  134. package/dist/preview/types.mjs +0 -2
  135. package/dist/preview/types.mjs.map +0 -1
  136. package/dist/preview.mjs +0 -114
  137. package/dist/preview.mjs.map +0 -1
  138. package/dist/webpack.mjs +0 -21
  139. package/dist/webpack.mjs.map +0 -1
  140. /package/{jest.config.js → jest.config.cjs} +0 -0
@@ -1,26 +1,23 @@
1
1
  {
2
2
  "name": "apptasks-video",
3
3
  "description": "An Aurelia 2 client application.",
4
- "version": "0.1.0",
4
+ "version": "2.2.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "???"
8
8
  },
9
9
  "license": "UNLICENSED",
10
10
  "dependencies": {
11
- "@aurelia/router": "^2.0.0-beta.25",
12
- "@aurelia/validation": "^2.0.0-beta.25",
13
- "@aurelia/validation-html": "^2.0.0-beta.25",
14
- "aurelia": "^2.0.0-beta.25"
11
+ "@aurelia/router": "^2.0.0-rc.0",
12
+ "@aurelia/validation": "^2.0.0-rc.0",
13
+ "@aurelia/validation-html": "^2.0.0-rc.0",
14
+ "aurelia": "^2.0.0-rc.0"
15
15
  },
16
16
  "devDependencies": {
17
- "@aurelia/storybook": "^1.0.2",
18
- "@aurelia/testing": "^2.0.0-beta.25",
19
- "@aurelia/vite-plugin": "^2.0.0-beta.25",
20
- "@storybook/addon-actions": "^9.0.0",
21
- "@storybook/addon-links": "^9.0.0",
22
- "@storybook/builder-vite": "^9.0.0",
23
- "@storybook/test": "^9.0.0-alpha.2",
17
+ "@aurelia/storybook": "^2.2.1",
18
+ "@aurelia/testing": "^2.0.0-rc.0",
19
+ "@aurelia/vite-plugin": "^2.0.0-rc.0",
20
+ "@storybook/builder-vite": "^10.2.0",
24
21
  "@tailwindcss/vite": "^4.0.0",
25
22
  "@types/node": "^22.10.2",
26
23
  "@types/react": "^19.1.8",
@@ -32,7 +29,7 @@
32
29
  "react": "^19.1.0",
33
30
  "react-dom": "^19.1.0",
34
31
  "sass": "^1.83.4",
35
- "storybook": "^9.0.0",
32
+ "storybook": "^10.2.0",
36
33
  "stylelint": "^16.12.0",
37
34
  "stylelint-config-standard": "^36.0.1",
38
35
  "stylus": "^0.64.0",
@@ -40,9 +37,8 @@
40
37
  "tslib": "^2.8.1",
41
38
  "typescript": "^5.7.2",
42
39
  "typescript-eslint": "^8.18.1",
43
- "vite": "^6.0.3",
44
- "vite-plugin-node-polyfills": "^0.22.0",
45
- "vitest": "^2.1.8"
40
+ "vite": "^7.0.0",
41
+ "vitest": "^4.0.0"
46
42
  },
47
43
  "scripts": {
48
44
  "lint:js": "eslint src test",
@@ -55,11 +51,5 @@
55
51
  "storybook": "storybook dev -p 6006",
56
52
  "build-storybook": "storybook build"
57
53
  },
58
- "type": "module",
59
- "overrides": {
60
- "vite-plugin-node-polyfills": {
61
- "vite": "^6.0.0"
62
- }
63
- },
64
- "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
54
+ "type": "module"
65
55
  }
@@ -0,0 +1,111 @@
1
+ <style>
2
+ .feedback-form {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 18px;
6
+ padding: 28px;
7
+ border-radius: 18px;
8
+ background: #fff;
9
+ border: 1px solid rgba(15, 23, 42, 0.08);
10
+ box-shadow: 0 15px 40px rgba(15, 23, 42, 0.08);
11
+ max-width: 420px;
12
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
13
+ }
14
+
15
+ .feedback-form label {
16
+ display: flex;
17
+ flex-direction: column;
18
+ gap: 8px;
19
+ font-size: 14px;
20
+ color: #0f172a;
21
+ font-weight: 600;
22
+ }
23
+
24
+ .feedback-form input,
25
+ .feedback-form select,
26
+ .feedback-form textarea {
27
+ border-radius: 12px;
28
+ border: 1px solid rgba(15, 23, 42, 0.15);
29
+ padding: 12px 14px;
30
+ font-size: 15px;
31
+ font-weight: 500;
32
+ color: #0f172a;
33
+ background: #f8fafc;
34
+ transition: border 0.2s ease, box-shadow 0.2s ease;
35
+ }
36
+
37
+ .feedback-form input:focus,
38
+ .feedback-form select:focus,
39
+ .feedback-form textarea:focus {
40
+ outline: none;
41
+ border-color: #2563eb;
42
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
43
+ background: #fff;
44
+ }
45
+
46
+ .feedback-form__actions {
47
+ display: flex;
48
+ gap: 12px;
49
+ }
50
+
51
+ .feedback-form__actions button {
52
+ flex: 1;
53
+ border: none;
54
+ border-radius: 12px;
55
+ padding: 12px 16px;
56
+ font-size: 15px;
57
+ font-weight: 600;
58
+ cursor: pointer;
59
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
60
+ }
61
+
62
+ .feedback-form__actions button:first-child {
63
+ background: linear-gradient(135deg, #2563eb, #4f46e5);
64
+ color: #fff;
65
+ box-shadow: 0 15px 30px rgba(79, 70, 229, 0.3);
66
+ }
67
+
68
+ .feedback-form__actions button:first-child:disabled {
69
+ opacity: 0.65;
70
+ box-shadow: none;
71
+ cursor: not-allowed;
72
+ }
73
+
74
+ .feedback-form__actions button:last-child {
75
+ background: #e2e8f0;
76
+ color: #0f172a;
77
+ }
78
+
79
+ .feedback-form__success {
80
+ margin: 0;
81
+ font-size: 14px;
82
+ font-weight: 600;
83
+ color: #059669;
84
+ text-align: center;
85
+ }
86
+ </style>
87
+ <form class="feedback-form" submit.trigger="submit($event)" aria-live="polite">
88
+ <label>
89
+ Name
90
+ <input type="text" value.two-way="form.name" placeholder="Ada Lovelace" required />
91
+ </label>
92
+ <label>
93
+ Email
94
+ <input type="email" value.two-way="form.email" placeholder="ada@example.com" required />
95
+ </label>
96
+ <label>
97
+ Topic
98
+ <select value.two-way="form.topic">
99
+ <option repeat.for="topic of topics" value.bind="topic">${topic}</option>
100
+ </select>
101
+ </label>
102
+ <label>
103
+ Message
104
+ <textarea rows="4" value.two-way="form.message"></textarea>
105
+ </label>
106
+ <div class="feedback-form__actions">
107
+ <button type="submit" disabled.bind="submitting">${submitting ? 'Sending...' : 'Send feedback'}</button>
108
+ <button type="button" click.trigger="reset()" disabled.bind="submitting">Reset</button>
109
+ </div>
110
+ <p if.bind="submitted" class="feedback-form__success">Thank you for the feedback!</p>
111
+ </form>
@@ -0,0 +1,45 @@
1
+ import { bindable } from 'aurelia';
2
+
3
+ export interface FeedbackPayload {
4
+ name: string;
5
+ email: string;
6
+ topic: string;
7
+ message: string;
8
+ }
9
+
10
+ export class FeedbackForm {
11
+ @bindable() topics: string[] = ['Bug report', 'Feature idea', 'General praise'];
12
+ @bindable() submitting = false;
13
+ @bindable() onSubmit?: (payload: FeedbackPayload) => Promise<void> | void;
14
+
15
+ form: FeedbackPayload = {
16
+ name: '',
17
+ email: '',
18
+ topic: 'Bug report',
19
+ message: '',
20
+ };
21
+
22
+ submitted = false;
23
+
24
+ async submit(event?: Event) {
25
+ event?.preventDefault();
26
+ if (this.submitting) {
27
+ return;
28
+ }
29
+
30
+ this.submitting = true;
31
+ await this.onSubmit?.({ ...this.form });
32
+ this.submitting = false;
33
+ this.submitted = true;
34
+ }
35
+
36
+ reset() {
37
+ this.form = {
38
+ name: '',
39
+ email: '',
40
+ topic: this.topics[0] ?? '',
41
+ message: '',
42
+ };
43
+ this.submitted = false;
44
+ }
45
+ }
@@ -0,0 +1,119 @@
1
+ <style>
2
+ .notification-center {
3
+ width: 420px;
4
+ border-radius: 20px;
5
+ background: #0f172a;
6
+ color: #f8fafc;
7
+ padding: 20px 24px;
8
+ box-shadow: 0 25px 50px rgba(15, 23, 42, 0.45);
9
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
10
+ }
11
+
12
+ .notification-center__header {
13
+ display: flex;
14
+ justify-content: space-between;
15
+ margin-bottom: 18px;
16
+ }
17
+
18
+ .notification-center__header h2 {
19
+ margin: 0;
20
+ font-size: 20px;
21
+ }
22
+
23
+ .notification-center__count {
24
+ font-size: 13px;
25
+ opacity: 0.7;
26
+ align-self: center;
27
+ }
28
+
29
+ .notification-center__list {
30
+ list-style: none;
31
+ margin: 0;
32
+ padding: 0;
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: 12px;
36
+ }
37
+
38
+ .notification-center__item {
39
+ display: flex;
40
+ justify-content: space-between;
41
+ padding: 14px 16px;
42
+ border-radius: 14px;
43
+ background: rgba(148, 163, 184, 0.12);
44
+ border: 1px solid rgba(148, 163, 184, 0.2);
45
+ gap: 12px;
46
+ }
47
+
48
+ .notification-center__item strong {
49
+ display: block;
50
+ font-size: 15px;
51
+ margin-bottom: 4px;
52
+ }
53
+
54
+ .notification-center__item p {
55
+ margin: 0 0 6px;
56
+ font-size: 14px;
57
+ opacity: 0.9;
58
+ }
59
+
60
+ .notification-center__item small {
61
+ font-size: 12px;
62
+ opacity: 0.6;
63
+ }
64
+
65
+ .notification-center__item button {
66
+ background: transparent;
67
+ border: 1px solid rgba(255, 255, 255, 0.4);
68
+ color: inherit;
69
+ border-radius: 999px;
70
+ padding: 4px 12px;
71
+ height: fit-content;
72
+ cursor: pointer;
73
+ transition: background 0.2s ease;
74
+ }
75
+
76
+ .notification-center__item button:hover,
77
+ .notification-center__item button:focus-visible {
78
+ background: rgba(255, 255, 255, 0.1);
79
+ }
80
+
81
+ .notification-center__item.success {
82
+ border-color: rgba(45, 212, 191, 0.4);
83
+ }
84
+
85
+ .notification-center__item.warning {
86
+ border-color: rgba(249, 115, 22, 0.4);
87
+ }
88
+
89
+ .notification-center__item.error {
90
+ border-color: rgba(239, 68, 68, 0.4);
91
+ }
92
+
93
+ .notification-center__empty {
94
+ text-align: center;
95
+ padding: 16px;
96
+ background: rgba(148, 163, 184, 0.1);
97
+ border-radius: 12px;
98
+ font-size: 14px;
99
+ }
100
+ </style>
101
+ <section class="notification-center">
102
+ <header class="notification-center__header">
103
+ <h2>Notifications</h2>
104
+ <span class="notification-center__count">${notifications.length} total</span>
105
+ </header>
106
+ <ul class="notification-center__list">
107
+ <li repeat.for="note of visibleNotifications" class="notification-center__item ${note.level}">
108
+ <div>
109
+ <strong>${note.title}</strong>
110
+ <p>${note.message}</p>
111
+ <small if.bind="showTimestamp">${note.timestamp}</small>
112
+ </div>
113
+ <button type="button" click.trigger="dismiss(note)">Dismiss</button>
114
+ </li>
115
+ <li if.bind="visibleNotifications.length === 0" class="notification-center__empty">
116
+ You're all caught up!
117
+ </li>
118
+ </ul>
119
+ </section>
@@ -0,0 +1,27 @@
1
+ import { bindable } from 'aurelia';
2
+
3
+ export type NotificationLevel = 'info' | 'success' | 'warning' | 'error';
4
+
5
+ export interface NotificationItem {
6
+ id: number;
7
+ title: string;
8
+ message: string;
9
+ level: NotificationLevel;
10
+ timestamp?: string;
11
+ }
12
+
13
+ export class NotificationCenter {
14
+ @bindable() notifications: NotificationItem[] = [];
15
+ @bindable() maxVisible = 4;
16
+ @bindable() showTimestamp = true;
17
+ @bindable() onDismiss?: (notification: NotificationItem) => void;
18
+
19
+ get visibleNotifications() {
20
+ return (this.notifications ?? []).slice(0, this.maxVisible);
21
+ }
22
+
23
+ dismiss(notification: NotificationItem) {
24
+ this.notifications = (this.notifications ?? []).filter((note) => note.id !== notification.id);
25
+ this.onDismiss?.(notification);
26
+ }
27
+ }
@@ -0,0 +1,107 @@
1
+ <style>
2
+ .stat-card {
3
+ border-radius: 16px;
4
+ padding: 20px 24px;
5
+ background: linear-gradient(135deg, #1f3d8f, #3c74ff);
6
+ color: #fff;
7
+ box-shadow: 0 20px 45px rgba(13, 32, 94, 0.35);
8
+ max-width: 360px;
9
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
10
+ }
11
+
12
+ .stat-card__header {
13
+ display: flex;
14
+ justify-content: space-between;
15
+ gap: 12px;
16
+ align-items: flex-start;
17
+ }
18
+
19
+ .stat-card__label {
20
+ text-transform: uppercase;
21
+ letter-spacing: 0.08em;
22
+ font-size: 12px;
23
+ opacity: 0.8;
24
+ margin: 0 0 6px;
25
+ }
26
+
27
+ .stat-card__value {
28
+ font-size: 44px;
29
+ margin: 0;
30
+ font-weight: 600;
31
+ line-height: 1;
32
+ }
33
+
34
+ .stat-card__unit {
35
+ font-size: 20px;
36
+ margin-left: 6px;
37
+ opacity: 0.85;
38
+ }
39
+
40
+ .stat-card__refresh {
41
+ border: 1px solid rgba(255, 255, 255, 0.4);
42
+ background: rgba(255, 255, 255, 0.12);
43
+ color: #fff;
44
+ padding: 6px 12px;
45
+ border-radius: 999px;
46
+ font-size: 12px;
47
+ letter-spacing: 0.05em;
48
+ text-transform: uppercase;
49
+ cursor: pointer;
50
+ transition: background 0.2s ease, border 0.2s ease;
51
+ }
52
+
53
+ .stat-card__refresh:focus-visible,
54
+ .stat-card__refresh:hover {
55
+ background: rgba(255, 255, 255, 0.2);
56
+ border-color: rgba(255, 255, 255, 0.7);
57
+ }
58
+
59
+ .stat-card__description {
60
+ margin: 18px 0 12px;
61
+ font-size: 15px;
62
+ opacity: 0.9;
63
+ }
64
+
65
+ .stat-card__footer {
66
+ display: inline-flex;
67
+ align-items: center;
68
+ gap: 8px;
69
+ border-radius: 999px;
70
+ padding: 6px 14px;
71
+ font-size: 14px;
72
+ font-weight: 500;
73
+ }
74
+
75
+ .stat-card__footer.positive {
76
+ background: rgba(20, 235, 178, 0.2);
77
+ color: #14ebb2;
78
+ }
79
+
80
+ .stat-card__footer.negative {
81
+ background: rgba(255, 107, 107, 0.2);
82
+ color: #ff6b6b;
83
+ }
84
+
85
+ .stat-card__footer.neutral {
86
+ background: rgba(255, 255, 255, 0.2);
87
+ color: #fff;
88
+ }
89
+ </style>
90
+ <section class="stat-card" aria-live="polite">
91
+ <header class="stat-card__header">
92
+ <div>
93
+ <p class="stat-card__label">${title}</p>
94
+ <h2 class="stat-card__value">
95
+ ${value}<span if.bind="unit" class="stat-card__unit">${unit}</span>
96
+ </h2>
97
+ </div>
98
+ <button type="button" class="stat-card__refresh" click.trigger="refresh()" title="Refresh metric" aria-label="Refresh metric">
99
+ Refresh
100
+ </button>
101
+ </header>
102
+ <p class="stat-card__description">${description}</p>
103
+ <footer class="stat-card__footer ${changeState}">
104
+ <strong>${changeLabel}</strong>
105
+ <span>${changeCopy}</span>
106
+ </footer>
107
+ </section>
@@ -0,0 +1,41 @@
1
+ import { bindable } from 'aurelia';
2
+
3
+ type TrendState = 'positive' | 'negative' | 'neutral';
4
+
5
+ export class StatCard {
6
+ @bindable() title = 'Active users';
7
+ @bindable() value: number | string = 0;
8
+ @bindable() unit = '';
9
+ @bindable() change = 0; // percent delta
10
+ @bindable() description = '';
11
+ @bindable() changeCopy = 'vs last week';
12
+ @bindable() onRefresh?: () => void;
13
+
14
+ refresh() {
15
+ this.onRefresh?.();
16
+ }
17
+
18
+ get changeLabel() {
19
+ const numeric = typeof this.change === 'number' ? this.change : Number(this.change);
20
+ if (!Number.isFinite(numeric)) {
21
+ return '0%';
22
+ }
23
+ const rounded = numeric.toFixed(1).replace(/\.0$/, '');
24
+ const sign = numeric > 0 ? '+' : '';
25
+ return `${sign}${rounded}%`;
26
+ }
27
+
28
+ get changeState(): TrendState {
29
+ const numeric = typeof this.change === 'number' ? this.change : Number(this.change);
30
+ if (!Number.isFinite(numeric)) {
31
+ return 'neutral';
32
+ }
33
+ if (numeric > 0) {
34
+ return 'positive';
35
+ }
36
+ if (numeric < 0) {
37
+ return 'negative';
38
+ }
39
+ return 'neutral';
40
+ }
41
+ }
@@ -0,0 +1,89 @@
1
+ <style>
2
+ .weather-widget {
3
+ width: 320px;
4
+ border-radius: 24px;
5
+ padding: 22px;
6
+ background: linear-gradient(160deg, #fef3c7, #fcd34d);
7
+ color: #1f2937;
8
+ box-shadow: 0 20px 45px rgba(244, 114, 10, 0.25);
9
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
10
+ }
11
+
12
+ .weather-widget header {
13
+ display: flex;
14
+ justify-content: space-between;
15
+ align-items: center;
16
+ margin-bottom: 14px;
17
+ }
18
+
19
+ .weather-widget header h2 {
20
+ margin: 0;
21
+ font-size: 20px;
22
+ font-weight: 600;
23
+ }
24
+
25
+ .weather-widget button {
26
+ border: none;
27
+ background: rgba(255, 255, 255, 0.4);
28
+ color: #92400e;
29
+ border-radius: 12px;
30
+ padding: 6px 12px;
31
+ font-weight: 600;
32
+ cursor: pointer;
33
+ transition: background 0.2s ease;
34
+ }
35
+
36
+ .weather-widget button:disabled {
37
+ opacity: 0.6;
38
+ cursor: not-allowed;
39
+ }
40
+
41
+ .weather-widget button:not(:disabled):hover,
42
+ .weather-widget button:not(:disabled):focus-visible {
43
+ background: rgba(255, 255, 255, 0.6);
44
+ }
45
+
46
+ .weather-widget__state {
47
+ padding: 16px;
48
+ border-radius: 16px;
49
+ background: rgba(255, 255, 255, 0.6);
50
+ font-weight: 600;
51
+ text-align: center;
52
+ }
53
+
54
+ .weather-widget__state--error {
55
+ background: rgba(248, 113, 113, 0.3);
56
+ color: #991b1b;
57
+ }
58
+
59
+ .weather-widget__body {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 6px;
63
+ font-weight: 600;
64
+ }
65
+
66
+ .weather-widget__temp {
67
+ font-size: 64px;
68
+ margin: 0;
69
+ font-weight: 700;
70
+ }
71
+
72
+ .weather-widget__body small {
73
+ font-size: 14px;
74
+ opacity: 0.8;
75
+ }
76
+ </style>
77
+ <section class="weather-widget" aria-live="polite">
78
+ <header>
79
+ <h2>${location}</h2>
80
+ <button type="button" click.trigger="refresh()" disabled.bind="state === 'loading'">Refresh</button>
81
+ </header>
82
+ <div if.bind="state === 'loading'" class="weather-widget__state">Loading latest data...</div>
83
+ <div if.bind="state === 'error'" class="weather-widget__state weather-widget__state--error">${error}</div>
84
+ <div if.bind="state === 'ready'" class="weather-widget__body">
85
+ <p class="weather-widget__temp">${report.temperature}&deg;</p>
86
+ <p>${report.condition}</p>
87
+ <small>High ${report.high}&deg; - Low ${report.low}&deg;</small>
88
+ </div>
89
+ </section>
@@ -0,0 +1,31 @@
1
+ import { bindable } from 'aurelia';
2
+ import { IWeatherService, WeatherSummary, WeatherService } from '../services/weather-service';
3
+
4
+ export class WeatherWidget {
5
+ static inject = [IWeatherService];
6
+
7
+ constructor(private readonly service: WeatherService) {}
8
+
9
+ @bindable() location = 'Seattle, WA';
10
+
11
+ report: WeatherSummary | null = null;
12
+ state: 'idle' | 'loading' | 'ready' | 'error' = 'idle';
13
+ error = '';
14
+
15
+ binding() {
16
+ void this.refresh();
17
+ }
18
+
19
+ async refresh() {
20
+ try {
21
+ this.state = 'loading';
22
+ this.error = '';
23
+ const location = this.location ?? 'Seattle, WA';
24
+ this.report = await this.service.getWeather(location);
25
+ this.state = 'ready';
26
+ } catch (err) {
27
+ this.state = 'error';
28
+ this.error = err instanceof Error ? err.message : 'Unable to load weather data.';
29
+ }
30
+ }
31
+ }
@@ -1,6 +1,48 @@
1
- <div style="border: 1px solid #ccc; padding: 16px; border-radius: 4px;">
1
+ <style>
2
+ .hello-card {
3
+ max-width: 360px;
4
+ padding: 32px;
5
+ border-radius: 24px;
6
+ background: linear-gradient(135deg, #ec4899, #a855f7);
7
+ color: #fff;
8
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
9
+ box-shadow: 0 25px 45px rgba(168, 85, 247, 0.35);
10
+ }
11
+
12
+ .hello-card h1 {
13
+ margin: 0 0 12px;
14
+ font-size: 28px;
15
+ font-weight: 600;
16
+ }
17
+
18
+ .hello-card p {
19
+ margin: 0 0 18px;
20
+ font-size: 16px;
21
+ opacity: 0.95;
22
+ }
23
+
24
+ .hello-card button {
25
+ border: none;
26
+ border-radius: 16px;
27
+ padding: 12px 18px;
28
+ font-size: 15px;
29
+ font-weight: 600;
30
+ background: #fff;
31
+ color: #a855f7;
32
+ cursor: pointer;
33
+ box-shadow: 0 15px 30px rgba(255, 255, 255, 0.25);
34
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
35
+ }
36
+
37
+ .hello-card button:hover,
38
+ .hello-card button:focus-visible {
39
+ transform: translateY(-2px);
40
+ box-shadow: 0 18px 32px rgba(255, 255, 255, 0.35);
41
+ }
42
+ </style>
43
+ <div class="hello-card">
2
44
  <h1>${message}</h1>
3
45
  <p>Counter: ${counter}</p>
4
46
  <button click.trigger="increment()">Increment</button>
5
47
  <au-slot></au-slot>
6
- </div>
48
+ </div>