snice 7.2.0 → 7.4.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 (232) hide show
  1. package/.agents/skills/snice/SKILL.md +36 -13
  2. package/README.md +11 -6
  3. package/adapters/react/SniceProvider.js +1 -1
  4. package/adapters/react/SniceRouter.d.ts +2 -0
  5. package/adapters/react/SniceRouter.js +3 -2
  6. package/adapters/react/SniceRouter.js.map +1 -1
  7. package/adapters/react/matchRoute.d.ts +4 -1
  8. package/adapters/react/matchRoute.js +13 -3
  9. package/adapters/react/matchRoute.js.map +1 -1
  10. package/adapters/react/useRequestHandler.js +1 -1
  11. package/bin/analyzer-contracts.js +4 -1
  12. package/bin/project-analyzer.js +521 -20
  13. package/bin/snice.js +77 -13
  14. package/bin/templates/AI_GUIDANCE.md +6 -1
  15. package/bin/templates/default/README.md +1 -1
  16. package/bin/templates/default/src/components/app-header.ts +11 -16
  17. package/bin/templates/default/src/components/notification-badge.ts +5 -14
  18. package/bin/templates/default/src/components/search-bar.ts +21 -65
  19. package/bin/templates/default/src/pages/dashboard.ts +15 -11
  20. package/bin/templates/default/src/pages/data.ts +10 -13
  21. package/bin/templates/default/src/pages/notifications.ts +4 -9
  22. package/bin/templates/default/src/pages/profile.ts +2 -8
  23. package/bin/templates/default/src/pages/settings.ts +16 -16
  24. package/dist/app-context.d.ts +10 -10
  25. package/dist/cdn/accordion/snice-accordion.js +1 -1
  26. package/dist/cdn/action-bar/snice-action-bar.js +1 -1
  27. package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
  28. package/dist/cdn/alert/snice-alert.js +1 -1
  29. package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
  30. package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
  31. package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
  32. package/dist/cdn/availability/snice-availability.js +1 -1
  33. package/dist/cdn/avatar/snice-avatar.js +1 -1
  34. package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
  35. package/dist/cdn/badge/snice-badge.js +1 -1
  36. package/dist/cdn/banner/snice-banner.js +1 -1
  37. package/dist/cdn/binpack/snice-binpack.js +1 -1
  38. package/dist/cdn/book/snice-book.js +1 -1
  39. package/dist/cdn/booking/snice-booking.js +1 -1
  40. package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
  41. package/dist/cdn/button/snice-button.js +1 -1
  42. package/dist/cdn/calendar/snice-calendar.js +1 -1
  43. package/dist/cdn/camera/snice-camera.js +1 -1
  44. package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
  45. package/dist/cdn/candlestick/snice-candlestick.js +1 -1
  46. package/dist/cdn/card/snice-card.js +1 -1
  47. package/dist/cdn/carousel/snice-carousel.js +1 -1
  48. package/dist/cdn/cart/snice-cart.js +1 -1
  49. package/dist/cdn/chart/snice-chart.js +1 -1
  50. package/dist/cdn/chat/snice-chat.js +1 -1
  51. package/dist/cdn/checkbox/snice-checkbox.js +1 -1
  52. package/dist/cdn/chip/snice-chip.js +1 -1
  53. package/dist/cdn/code-block/snice-code-block.js +1 -1
  54. package/dist/cdn/color-display/snice-color-display.js +1 -1
  55. package/dist/cdn/color-picker/snice-color-picker.js +1 -1
  56. package/dist/cdn/command-palette/snice-command-palette.js +1 -1
  57. package/dist/cdn/comments/snice-comments.js +1 -1
  58. package/dist/cdn/countdown/snice-countdown.js +1 -1
  59. package/dist/cdn/cropper/snice-cropper.js +1 -1
  60. package/dist/cdn/data-card/snice-data-card.js +1 -1
  61. package/dist/cdn/date-picker/snice-date-picker.js +1 -1
  62. package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
  63. package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
  64. package/dist/cdn/diff/snice-diff.js +1 -1
  65. package/dist/cdn/divider/snice-divider.js +1 -1
  66. package/dist/cdn/doc/snice-doc.js +1 -1
  67. package/dist/cdn/draw/snice-draw.js +1 -1
  68. package/dist/cdn/drawer/snice-drawer.js +1 -1
  69. package/dist/cdn/empty-state/snice-empty-state.js +1 -1
  70. package/dist/cdn/estimate/snice-estimate.js +1 -1
  71. package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
  72. package/dist/cdn/file-upload/snice-file-upload.js +1 -1
  73. package/dist/cdn/flip-card/snice-flip-card.js +1 -1
  74. package/dist/cdn/flow/snice-flow.js +1 -1
  75. package/dist/cdn/form-layout/snice-form-layout.js +1 -1
  76. package/dist/cdn/funnel/snice-funnel.js +1 -1
  77. package/dist/cdn/gantt/snice-gantt.js +1 -1
  78. package/dist/cdn/gauge/snice-gauge.js +1 -1
  79. package/dist/cdn/grid/snice-grid.js +1 -1
  80. package/dist/cdn/heatmap/snice-heatmap.js +1 -1
  81. package/dist/cdn/image/snice-image.js +1 -1
  82. package/dist/cdn/input/snice-input.js +1 -1
  83. package/dist/cdn/invoice/snice-invoice.js +1 -1
  84. package/dist/cdn/kanban/snice-kanban.js +1 -1
  85. package/dist/cdn/key-value/snice-key-value.js +1 -1
  86. package/dist/cdn/kpi/snice-kpi.js +1 -1
  87. package/dist/cdn/layout/snice-layout.js +1 -1
  88. package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
  89. package/dist/cdn/link/snice-link.js +1 -1
  90. package/dist/cdn/link-preview/snice-link-preview.js +1 -1
  91. package/dist/cdn/list/snice-list.js +1 -1
  92. package/dist/cdn/location/snice-location.js +1 -1
  93. package/dist/cdn/login/snice-login.js +1 -1
  94. package/dist/cdn/map/snice-map.js +1 -1
  95. package/dist/cdn/markdown/snice-markdown.js +1 -1
  96. package/dist/cdn/masonry/snice-masonry.js +1 -1
  97. package/dist/cdn/menu/snice-menu.js +1 -1
  98. package/dist/cdn/message-strip/snice-message-strip.js +1 -1
  99. package/dist/cdn/modal/snice-modal.js +1 -1
  100. package/dist/cdn/music-player/snice-music-player.js +1 -1
  101. package/dist/cdn/nav/snice-nav.js +1 -1
  102. package/dist/cdn/network-graph/snice-network-graph.js +1 -1
  103. package/dist/cdn/notification-center/snice-notification-center.js +1 -1
  104. package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
  105. package/dist/cdn/org-chart/snice-org-chart.js +1 -1
  106. package/dist/cdn/pagination/snice-pagination.js +1 -1
  107. package/dist/cdn/paint/snice-paint.js +1 -1
  108. package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
  109. package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
  110. package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
  111. package/dist/cdn/popover/snice-popover.js +1 -1
  112. package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
  113. package/dist/cdn/product-card/snice-product-card.js +1 -1
  114. package/dist/cdn/progress/snice-progress.js +1 -1
  115. package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
  116. package/dist/cdn/qr-code/snice-qr-code.js +1 -1
  117. package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
  118. package/dist/cdn/radio/snice-radio.js +1 -1
  119. package/dist/cdn/range-slider/snice-range-slider.js +1 -1
  120. package/dist/cdn/rating/snice-rating.js +1 -1
  121. package/dist/cdn/receipt/snice-receipt.js +1 -1
  122. package/dist/cdn/recipe/snice-recipe.js +1 -1
  123. package/dist/cdn/runtime/README.md +2 -2
  124. package/dist/cdn/runtime/snice-runtime.esm.js +333 -192
  125. package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
  126. package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
  127. package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
  128. package/dist/cdn/runtime/snice-runtime.js +333 -191
  129. package/dist/cdn/runtime/snice-runtime.js.map +1 -1
  130. package/dist/cdn/runtime/snice-runtime.min.js +1 -1
  131. package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
  132. package/dist/cdn/sankey/snice-sankey.js +1 -1
  133. package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
  134. package/dist/cdn/select/snice-select.js +1 -1
  135. package/dist/cdn/skeleton/snice-skeleton.js +1 -1
  136. package/dist/cdn/slider/snice-slider.js +1 -1
  137. package/dist/cdn/sortable/snice-sortable.js +1 -1
  138. package/dist/cdn/sparkline/snice-sparkline.js +1 -1
  139. package/dist/cdn/spinner/snice-spinner.js +1 -1
  140. package/dist/cdn/split-button/snice-split-button.js +1 -1
  141. package/dist/cdn/split-pane/snice-split-pane.js +1 -1
  142. package/dist/cdn/spotlight/snice-spotlight.js +1 -1
  143. package/dist/cdn/stat-group/snice-stat-group.js +1 -1
  144. package/dist/cdn/step-input/snice-step-input.js +1 -1
  145. package/dist/cdn/stepper/snice-stepper.js +1 -1
  146. package/dist/cdn/switch/snice-switch.js +1 -1
  147. package/dist/cdn/table/snice-table.esm.js +1 -1
  148. package/dist/cdn/table/snice-table.js +1 -1
  149. package/dist/cdn/tabs/snice-tabs.js +1 -1
  150. package/dist/cdn/tag/snice-tag.js +1 -1
  151. package/dist/cdn/tag-input/snice-tag-input.js +1 -1
  152. package/dist/cdn/terminal/snice-terminal.js +1 -1
  153. package/dist/cdn/testimonial/snice-testimonial.js +1 -1
  154. package/dist/cdn/textarea/snice-textarea.js +1 -1
  155. package/dist/cdn/time-picker/snice-time-picker.js +1 -1
  156. package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
  157. package/dist/cdn/timeline/snice-timeline.js +1 -1
  158. package/dist/cdn/timer/snice-timer.js +1 -1
  159. package/dist/cdn/toast/snice-toast.js +1 -1
  160. package/dist/cdn/tooltip/snice-tooltip.js +1 -1
  161. package/dist/cdn/tree/snice-tree.js +1 -1
  162. package/dist/cdn/treemap/snice-treemap.js +1 -1
  163. package/dist/cdn/user-card/snice-user-card.js +1 -1
  164. package/dist/cdn/video-player/snice-video-player.js +1 -1
  165. package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
  166. package/dist/cdn/waterfall/snice-waterfall.js +1 -1
  167. package/dist/cdn/weather/snice-weather.js +1 -1
  168. package/dist/cdn/work-order/snice-work-order.js +1 -1
  169. package/dist/components/pagination/snice-pagination.types.d.ts +5 -0
  170. package/dist/context-provider.d.ts +11 -1
  171. package/dist/context.d.ts +5 -6
  172. package/dist/index.cjs +330 -188
  173. package/dist/index.cjs.map +1 -1
  174. package/dist/index.d.ts +2 -1
  175. package/dist/index.esm.js +330 -189
  176. package/dist/index.esm.js.map +1 -1
  177. package/dist/index.iife.js +330 -188
  178. package/dist/index.iife.js.map +1 -1
  179. package/dist/react/SniceProvider.js +1 -1
  180. package/dist/react/SniceRouter.d.ts +2 -0
  181. package/dist/react/SniceRouter.js +3 -2
  182. package/dist/react/SniceRouter.js.map +1 -1
  183. package/dist/react/index.js +1 -1
  184. package/dist/react/matchRoute.d.ts +4 -1
  185. package/dist/react/matchRoute.js +13 -3
  186. package/dist/react/matchRoute.js.map +1 -1
  187. package/dist/react/useRequestHandler.js +1 -1
  188. package/dist/symbols.cjs +1 -1
  189. package/dist/symbols.esm.js +1 -1
  190. package/dist/testing-dom.cjs +48 -0
  191. package/dist/testing-dom.cjs.map +1 -0
  192. package/dist/testing-dom.d.ts +23 -0
  193. package/dist/testing-dom.esm.js +45 -0
  194. package/dist/testing-dom.esm.js.map +1 -0
  195. package/dist/transitions.cjs +1 -1
  196. package/dist/transitions.esm.js +1 -1
  197. package/dist/types/context.d.ts +8 -8
  198. package/dist/types/page-options.d.ts +15 -1
  199. package/dist/types/router-instance.d.ts +2 -1
  200. package/dist/utils.d.ts +3 -1
  201. package/docs/ai/README.md +6 -4
  202. package/docs/ai/api.md +13 -2
  203. package/docs/ai/architecture.md +28 -11
  204. package/docs/ai/bindings.md +1 -1
  205. package/docs/ai/cli.md +26 -1
  206. package/docs/ai/components/badge.md +4 -0
  207. package/docs/ai/components/pagination.md +4 -2
  208. package/docs/ai/components/segmented-control.md +5 -1
  209. package/docs/ai/controllers.md +41 -12
  210. package/docs/ai/decorators.md +3 -3
  211. package/docs/ai/events.md +8 -2
  212. package/docs/ai/fetcher.md +32 -1
  213. package/docs/ai/lifecycle.md +4 -1
  214. package/docs/ai/patterns.md +9 -4
  215. package/docs/ai/react-integration.md +1 -0
  216. package/docs/ai/request-response.md +20 -6
  217. package/docs/ai/routing.md +33 -4
  218. package/docs/ai/testing.md +71 -0
  219. package/docs/bindings.md +1 -1
  220. package/docs/cli.md +27 -2
  221. package/docs/components/badge.md +4 -0
  222. package/docs/components/pagination.md +8 -5
  223. package/docs/components/segmented-control.md +5 -1
  224. package/docs/controllers.md +46 -15
  225. package/docs/events.md +11 -2
  226. package/docs/fetcher.md +32 -1
  227. package/docs/lifecycle.md +5 -1
  228. package/docs/react-integration.md +1 -0
  229. package/docs/request-response.md +25 -7
  230. package/docs/routing.md +39 -3
  231. package/docs/testing.md +71 -0
  232. package/package.json +6 -1
package/bin/snice.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  import { execFile } from 'node:child_process';
15
15
  import { createRequire } from 'node:module';
16
16
  import { tmpdir } from 'node:os';
17
- import { basename, dirname, join, resolve } from 'node:path';
17
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
18
18
  import { fileURLToPath, pathToFileURL } from 'node:url';
19
19
  import { promisify } from 'node:util';
20
20
  import { analyzeProject, findImports, isTypeOnlyImport, maskComments } from './project-analyzer.js';
@@ -26,6 +26,7 @@ const packageRoot = join(__dirname, '..');
26
26
  const packageJson = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
27
27
  const argv = process.argv.slice(2);
28
28
  const command = argv[0];
29
+ const DIAGNOSTIC_IGNORE_FILENAME = '.sniceignore';
29
30
 
30
31
  function help() {
31
32
  console.log(`Snice CLI ${packageJson.version}
@@ -52,6 +53,9 @@ Commands:
52
53
  AI setup:
53
54
  npx snice init-ai Install version-matched guidance before an agent writes Snice code.
54
55
 
56
+ Diagnostics:
57
+ .sniceignore Suppress diagnostic codes globally or at an exact source location.
58
+
55
59
  Build options:
56
60
  --output=<dir> Output directory (default: ./dist/cdn)
57
61
  --format=iife,es Output formats (default: iife; table: iife,es)
@@ -207,21 +211,79 @@ function walkSource(root) {
207
211
  }
208
212
 
209
213
  function validateProject(targetDir, json = false) {
210
- const issues = collectValidationIssues(targetDir);
214
+ const ignored = readDiagnosticIgnores(targetDir);
215
+ const issues = collectValidationIssues(targetDir, ignored);
211
216
  renderValidation(issues, json);
212
217
  return issues;
213
218
  }
214
219
 
215
- function collectValidationIssues(targetDir) {
220
+ function collectValidationIssues(targetDir, ignored = readDiagnosticIgnores(targetDir)) {
216
221
  const sourceRoot = existsSync(join(targetDir, 'src')) ? join(targetDir, 'src') : targetDir;
217
222
  const projectFiles = ['package.json', 'tsconfig.json', 'index.html']
218
223
  .map(filename => join(targetDir, filename))
219
224
  .filter(existsSync);
220
225
  const files = [...new Set([...projectFiles, ...walkSource(sourceRoot)])].sort();
221
- return analyzeProject(files.map(file => ({
226
+ const issues = analyzeProject(files.map(file => ({
222
227
  filename: file,
223
228
  source: readFileSync(file, 'utf8')
224
229
  })));
230
+ return filterIgnoredDiagnostics(issues, ignored, targetDir);
231
+ }
232
+
233
+ function readDiagnosticIgnores(targetDir) {
234
+ const path = join(targetDir, DIAGNOSTIC_IGNORE_FILENAME);
235
+ if (!existsSync(path)) return [];
236
+
237
+ const entries = [];
238
+ for (const authoredLine of readFileSync(path, 'utf8').replace(/^\uFEFF/, '').split(/\r?\n/)) {
239
+ const line = authoredLine.replace(/\s+#.*$/, '').trim();
240
+ if (!line || line.startsWith('#')) continue;
241
+ const separator = line.search(/\s/);
242
+ const code = separator < 0 ? line : line.slice(0, separator);
243
+ const locator = separator < 0 ? '' : line.slice(separator).trim();
244
+ if (!locator) {
245
+ entries.push({ code });
246
+ continue;
247
+ }
248
+
249
+ let file = locator;
250
+ let lineNumber;
251
+ let column;
252
+ const exact = /^(.*):(\d+):(\d+)$/.exec(locator);
253
+ const lineOnly = exact ? null : /^(.*):(\d+)$/.exec(locator);
254
+ if (exact) {
255
+ file = exact[1];
256
+ lineNumber = Number(exact[2]);
257
+ column = Number(exact[3]);
258
+ } else if (lineOnly) {
259
+ file = lineOnly[1];
260
+ lineNumber = Number(lineOnly[2]);
261
+ }
262
+ if (!file) continue;
263
+ entries.push({ code, file: normalizeDiagnosticPath(file), line: lineNumber, column });
264
+ }
265
+ return entries;
266
+ }
267
+
268
+ function filterIgnoredDiagnostics(diagnostics, ignored, targetDir) {
269
+ if (!ignored.length) return diagnostics;
270
+ return diagnostics.filter(diagnostic => !ignored.some(entry => {
271
+ const code = diagnostic.code ?? diagnostic.ruleId;
272
+ if (entry.code !== code) return false;
273
+ if (!entry.file) return true;
274
+ if (!diagnostic.file) return false;
275
+ const absolute = isAbsolute(diagnostic.file)
276
+ ? diagnostic.file
277
+ : resolve(targetDir, diagnostic.file);
278
+ if (normalizeDiagnosticPath(relative(targetDir, absolute)) !== entry.file) return false;
279
+ if (entry.line !== undefined && diagnostic.line !== entry.line) return false;
280
+ if (entry.column !== undefined && diagnostic.column !== entry.column) return false;
281
+ return true;
282
+ }));
283
+ }
284
+
285
+ function normalizeDiagnosticPath(path) {
286
+ return path.replaceAll('\\', '/').replace(/^\.\//, '');
225
287
  }
226
288
 
227
289
  function renderValidation(issues, json = false) {
@@ -230,7 +292,7 @@ function renderValidation(issues, json = false) {
230
292
  else {
231
293
  for (const issue of issues) {
232
294
  console.log(
233
- `[${issue.severity.toUpperCase()}] ${issue.file ?? ''}:${issue.line}:${issue.column} ${issue.ruleId}\n` +
295
+ `[${issue.severity.toUpperCase()}] ${issue.file ?? ''}:${issue.line}:${issue.column} ${issue.code}\n` +
234
296
  ` ${issue.message}\n ${issue.fix}`
235
297
  );
236
298
  }
@@ -280,7 +342,8 @@ function resolveTsconfigChain(dir, entry = 'tsconfig.json', seen = new Set()) {
280
342
  }
281
343
 
282
344
  async function doctor(targetDir, json = false) {
283
- const findings = await collectDoctorFindings(targetDir);
345
+ const ignored = readDiagnosticIgnores(targetDir);
346
+ const findings = await collectDoctorFindings(targetDir, ignored);
284
347
  renderDoctor(findings, json);
285
348
  return findings;
286
349
  }
@@ -381,7 +444,7 @@ async function probeDecoratorTransform(targetDir, add, projectPackage) {
381
444
  }
382
445
  }
383
446
 
384
- async function collectDoctorFindings(targetDir) {
447
+ async function collectDoctorFindings(targetDir, ignored = readDiagnosticIgnores(targetDir)) {
385
448
  const findings = [];
386
449
  const add = (severity, code, message) => findings.push({ severity, code, message });
387
450
  const projectPackage = readJson(join(targetDir, 'package.json'));
@@ -443,7 +506,7 @@ async function collectDoctorFindings(targetDir) {
443
506
 
444
507
  await probeDecoratorTransform(targetDir, add, projectPackage);
445
508
 
446
- return findings;
509
+ return filterIgnoredDiagnostics(findings, ignored, targetDir);
447
510
  }
448
511
 
449
512
  function renderDoctor(findings, json = false) {
@@ -451,15 +514,16 @@ function renderDoctor(findings, json = false) {
451
514
  const warnings = findings.filter(item => item.severity === 'warning').length;
452
515
  if (json) console.log(JSON.stringify({ ok: errors === 0, findings }, null, 2));
453
516
  else {
454
- for (const finding of findings) console.log(`[${finding.severity.toUpperCase()}] ${finding.message}`);
517
+ for (const finding of findings) console.log(`[${finding.severity.toUpperCase()}] ${finding.code} ${finding.message}`);
455
518
  console.log(`Doctor finished with ${errors} error(s) and ${warnings} warning(s).`);
456
519
  }
457
520
  if (errors) process.exitCode = 1;
458
521
  }
459
522
 
460
523
  async function checkProject(targetDir, json = false) {
461
- const findings = await collectDoctorFindings(targetDir);
462
- const issues = collectValidationIssues(targetDir);
524
+ const ignored = readDiagnosticIgnores(targetDir);
525
+ const findings = await collectDoctorFindings(targetDir, ignored);
526
+ const issues = collectValidationIssues(targetDir, ignored);
463
527
  const ok = !findings.some(item => item.severity === 'error') &&
464
528
  !issues.some(item => item.severity === 'error');
465
529
 
@@ -467,12 +531,12 @@ async function checkProject(targetDir, json = false) {
467
531
  console.log(JSON.stringify({ ok, findings, issues }, null, 2));
468
532
  } else {
469
533
  console.log('Project configuration:');
470
- for (const finding of findings) console.log(`[${finding.severity.toUpperCase()}] ${finding.message}`);
534
+ for (const finding of findings) console.log(`[${finding.severity.toUpperCase()}] ${finding.code} ${finding.message}`);
471
535
  console.log('\nSnice source:');
472
536
  if (!issues.length) console.log('No Snice authoring issues found.');
473
537
  for (const issue of issues) {
474
538
  console.log(
475
- `[${issue.severity.toUpperCase()}] ${issue.file ?? ''}:${issue.line}:${issue.column} ${issue.ruleId}\n` +
539
+ `[${issue.severity.toUpperCase()}] ${issue.file ?? ''}:${issue.line}:${issue.column} ${issue.code}\n` +
476
540
  ` ${issue.message}\n ${issue.fix}`
477
541
  );
478
542
  }
@@ -7,6 +7,12 @@ reviewing Snice code. It routes you to the version-matched references under
7
7
  Do not infer Snice behavior from Lit or generic web-component conventions. Read
8
8
  only the topical reference and component documents needed for the task.
9
9
 
10
+ Visual behavior belongs in elements. Application behavior specific to a set of
11
+ elements belongs in a controller. Element orchestration belongs in pages. Keep
12
+ those ownership rules separate from the attachment mechanism: do not attach a
13
+ controller to a page host. A host-free reusable function may stay a plain module
14
+ wherever the project convention puts it; state plus lifecycle belongs in a daemon.
15
+
10
16
  Useful diagnostics:
11
17
 
12
18
  ```bash
@@ -15,4 +21,3 @@ npx snice check
15
21
 
16
22
  Use `doctor` or `validate` only to isolate the configuration or source half of
17
23
  a failed check.
18
-
@@ -61,7 +61,7 @@ src/
61
61
  notifications.ts # Live feed, filters, @watch, keyboard shortcuts
62
62
  settings.ts # Theme toggle, form bindings, @context, Ctrl+S save
63
63
  data.ts # Data table, search-bar, @observe('resize'), filters
64
- services/ # Business logic (auth, storage, jwt)
64
+ services/ # This project's host-free functions (auth, storage, jwt)
65
65
  middleware/ # Fetch middleware (auth, error, retry)
66
66
  daemons/ # Lifecycle-managed classes (notifications WebSocket)
67
67
  guards/ # Route guards (auth)
@@ -1,19 +1,17 @@
1
- import { element, property, render, styles, context, on, dispatch, dispose, html, css, watch, query } from 'snice';
1
+ import { element, render, styles, context, on, dispatch, dispose, html, css, state } from 'snice';
2
2
  import type { Context } from 'snice';
3
3
  import type { ApplicationContext } from '../context';
4
4
  import { logout } from '../services/auth';
5
5
 
6
6
  @element('app-header')
7
7
  export class AppHeader extends HTMLElement {
8
- @property() userName = '';
9
- @property() userAvatar = '';
10
- @property({ type: Boolean }) authenticated = false;
11
- @property({ type: Boolean }) menuOpen = false;
12
- @property({ type: Number }) notificationCount = 0;
8
+ @state() userName = '';
9
+ @state() userAvatar = '';
10
+ @state() authenticated = false;
11
+ @state() menuOpen = false;
12
+ @state() notificationCount = 0;
13
13
  private unsubscribeNotifications: (() => void) | null = null;
14
14
 
15
- @query('.user-menu') $menu!: HTMLElement;
16
-
17
15
  @context()
18
16
  handleContext(ctx: Context) {
19
17
  const { principal, notifications } = ctx.application as ApplicationContext;
@@ -37,13 +35,6 @@ export class AppHeader extends HTMLElement {
37
35
  }
38
36
  }
39
37
 
40
- @watch('menuOpen')
41
- onMenuToggle() {
42
- if (this.$menu) {
43
- this.$menu.style.display = this.menuOpen ? 'block' : 'none';
44
- }
45
- }
46
-
47
38
  toggleMenu() {
48
39
  this.menuOpen = !this.menuOpen;
49
40
  }
@@ -96,7 +87,7 @@ export class AppHeader extends HTMLElement {
96
87
  <span class="user-name">${this.userName}</span>
97
88
  </button>
98
89
 
99
- <div class="user-menu" style="display: none">
90
+ <div class="user-menu" ?hidden=${!this.menuOpen}>
100
91
  <a href="#/profile" @click=${() => this.menuOpen = false}>Profile</a>
101
92
  <a href="#/settings" @click=${() => this.menuOpen = false}>Settings</a>
102
93
  <snice-divider></snice-divider>
@@ -195,6 +186,10 @@ export class AppHeader extends HTMLElement {
195
186
  z-index: 100;
196
187
  }
197
188
 
189
+ .user-menu[hidden] {
190
+ display: none;
191
+ }
192
+
198
193
  .user-menu a,
199
194
  .user-menu button {
200
195
  display: block;
@@ -1,24 +1,15 @@
1
- import { element, property, render, styles, html, css, watch, query } from 'snice';
1
+ import { element, property, render, styles, html, css } from 'snice';
2
2
 
3
3
  @element('notification-badge')
4
4
  export class NotificationBadge extends HTMLElement {
5
5
  @property({ type: Number }) count = 0;
6
6
 
7
- @query('.badge') $badge!: HTMLElement;
8
-
9
- @watch('count')
10
- onCountChange() {
11
- if (this.$badge) {
12
- this.$badge.textContent = this.count > 99 ? '99+' : String(this.count);
13
- this.$badge.style.display = this.count > 0 ? 'flex' : 'none';
14
- }
15
- }
16
-
17
- @render({ once: true })
7
+ @render()
18
8
  renderContent() {
19
- const display = this.count > 0 ? 'flex' : 'none';
20
9
  return html`
21
- <span class="badge" style="display: ${display}">${this.count}</span>
10
+ <if ${this.count > 0}>
11
+ <span class="badge">${this.count > 99 ? '99+' : this.count}</span>
12
+ </if>
22
13
  `;
23
14
  }
24
15
 
@@ -1,20 +1,22 @@
1
- import { element, property, render, styles, on, dispatch, html, css } from 'snice';
1
+ import { debounce, dispatch, element, html, live, property, render, styles, css } from 'snice';
2
2
 
3
3
  @element('search-bar')
4
4
  export class SearchBar extends HTMLElement {
5
5
  @property() value = '';
6
6
  @property() placeholder = 'Search...';
7
7
 
8
- @on('input', 'input', { debounce: 300 })
9
- handleInput(e: Event) {
10
- this.value = (e.target as HTMLInputElement).value;
11
- this.dispatchSearch();
8
+ handleInput(e: CustomEvent<{ value: string }>) {
9
+ this.value = e.detail.value;
10
+ this.queueSearch();
12
11
  }
13
12
 
14
- @on('keydown:Escape', 'input')
15
- clearSearch(e: Event) {
13
+ clearSearch() {
16
14
  this.value = '';
17
- (e.target as HTMLInputElement).value = '';
15
+ this.dispatchSearch();
16
+ }
17
+
18
+ @debounce(300)
19
+ queueSearch() {
18
20
  this.dispatchSearch();
19
21
  }
20
22
 
@@ -26,17 +28,15 @@ export class SearchBar extends HTMLElement {
26
28
  @render()
27
29
  renderContent() {
28
30
  return html`
29
- <div class="search-container">
30
- <span class="icon">&#128269;</span>
31
- <input
32
- type="text"
33
- .value=${this.value}
34
- placeholder="${this.placeholder}"
35
- />
36
- <if ${this.value.length > 0}>
37
- <button class="clear" @click=${() => { this.value = ''; this.dispatchSearch(); }}>&#10005;</button>
38
- </if>
39
- </div>
31
+ <snice-input
32
+ type="search"
33
+ clearable
34
+ prefix-icon="🔍"
35
+ .value=${live(this.value)}
36
+ placeholder="${this.placeholder}"
37
+ @input-input=${this.handleInput}
38
+ @keydown.escape=${this.clearSearch}
39
+ ></snice-input>
40
40
  `;
41
41
  }
42
42
 
@@ -47,52 +47,8 @@ export class SearchBar extends HTMLElement {
47
47
  display: block;
48
48
  }
49
49
 
50
- .search-container {
51
- display: flex;
52
- align-items: center;
53
- gap: 0.5rem;
54
- padding: 0.5rem 0.75rem;
55
- background: var(--snice-color-surface);
56
- border: 1px solid var(--snice-color-border);
57
- border-radius: var(--snice-border-radius-lg);
58
- }
59
-
60
- .search-container:focus-within {
61
- border-color: var(--snice-color-primary);
62
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--snice-color-primary) 15%, transparent);
63
- }
64
-
65
- .icon {
66
- color: var(--snice-color-text-secondary);
67
- font-size: 0.875rem;
68
- }
69
-
70
- input {
71
- flex: 1;
72
- border: none;
73
- outline: none;
74
- background: transparent;
75
- font-size: 0.875rem;
76
- color: var(--snice-color-text);
77
- }
78
-
79
- input::placeholder {
80
- color: var(--snice-color-text-secondary);
81
- }
82
-
83
- .clear {
84
- background: none;
85
- border: none;
86
- cursor: pointer;
87
- color: var(--snice-color-text-secondary);
88
- font-size: 0.75rem;
89
- padding: 0.125rem 0.25rem;
90
- border-radius: var(--snice-border-radius-md);
91
- }
92
-
93
- .clear:hover {
94
- background: var(--snice-color-surface-container-low);
95
- color: var(--snice-color-text);
50
+ snice-input {
51
+ width: 100%;
96
52
  }
97
53
  `;
98
54
  }
@@ -1,5 +1,5 @@
1
1
  import { page } from '../router';
2
- import { render, styles, html, css, context, observe, dispose } from 'snice';
2
+ import { render, styles, html, css, context, observe, dispose, state } from 'snice';
3
3
  import type { Placard, Context } from 'snice';
4
4
  import type { ApplicationContext } from '../context';
5
5
  import { isAuthenticated } from '../guards/auth';
@@ -15,9 +15,9 @@ const placard: Placard = {
15
15
 
16
16
  @page({ tag: 'dashboard-page', routes: ['/', '/dashboard'], guards: [isAuthenticated], placard })
17
17
  export class DashboardPage extends HTMLElement {
18
- userName = '';
19
- notificationCount = 0;
20
- isCompact = false;
18
+ @state() userName = '';
19
+ @state() notificationCount = 0;
20
+ @state() isCompact = false;
21
21
  private unsubscribe: (() => void) | null = null;
22
22
 
23
23
  @context()
@@ -48,7 +48,7 @@ export class DashboardPage extends HTMLElement {
48
48
  @render()
49
49
  renderContent() {
50
50
  return html`
51
- <div class="container">
51
+ <div class="container" class:compact=${this.isCompact}>
52
52
  <div class="welcome">
53
53
  <h1>Welcome, ${this.userName}!</h1>
54
54
  <p class="subtitle">This is your dashboard</p>
@@ -87,12 +87,12 @@ export class DashboardPage extends HTMLElement {
87
87
  <snice-card>
88
88
  <h3>Architecture</h3>
89
89
  <ul>
90
- <li><strong>Pages:</strong> Route orchestrators</li>
91
- <li><strong>Components:</strong> Reusable UI</li>
92
- <li><strong>Controllers:</strong> Behavior modules</li>
93
- <li><strong>Services:</strong> Business logic</li>
90
+ <li><strong>Pages:</strong> Element orchestration</li>
91
+ <li><strong>Components:</strong> Reusable visual behavior</li>
92
+ <li><strong>Controllers:</strong> Application behavior for a set of elements</li>
93
+ <li><strong>Services:</strong> Host-free functions in this project</li>
94
94
  <li><strong>Middleware:</strong> Fetch interceptors</li>
95
- <li><strong>Daemons:</strong> Lifecycle classes</li>
95
+ <li><strong>Daemons:</strong> Explicit state and lifecycle</li>
96
96
  <li><strong>Guards:</strong> Route protection</li>
97
97
  </ul>
98
98
  </snice-card>
@@ -102,7 +102,7 @@ export class DashboardPage extends HTMLElement {
102
102
  <ul>
103
103
  <li><code>@page</code> - Route pages</li>
104
104
  <li><code>@element</code> - Custom elements</li>
105
- <li><code>@controller</code> - Behavior modules</li>
105
+ <li><code>@controller</code> - Element-specific application behavior</li>
106
106
  <li><code>@context</code> - Global state</li>
107
107
  <li><code>@observe</code> - DOM observers</li>
108
108
  <li><code>@watch</code> - Property watchers</li>
@@ -140,6 +140,10 @@ export class DashboardPage extends HTMLElement {
140
140
  margin: 0 auto;
141
141
  }
142
142
 
143
+ .container.compact {
144
+ padding: 1rem;
145
+ }
146
+
143
147
  .welcome {
144
148
  margin-bottom: 2rem;
145
149
  }
@@ -1,5 +1,5 @@
1
1
  import { page } from '../router';
2
- import { render, styles, observe, property, ready, html, css } from 'snice';
2
+ import { render, styles, observe, ready, html, css, state } from 'snice';
3
3
  import type { Placard } from 'snice';
4
4
  import { isAuthenticated } from '../guards/auth';
5
5
 
@@ -34,19 +34,16 @@ const MOCK_ITEMS: DataItem[] = [
34
34
 
35
35
  @page({ tag: 'data-page', routes: ['/data'], guards: [isAuthenticated], placard })
36
36
  export class DataPage extends HTMLElement {
37
- allItems: DataItem[] = [];
38
- filteredItems: DataItem[] = [];
39
- searchQuery = '';
40
- statusFilter: 'all' | 'active' | 'pending' | 'archived' = 'all';
41
- @property({ type: Boolean }) loading = true;
37
+ @state() allItems: DataItem[] = [];
38
+ @state() filteredItems: DataItem[] = [];
39
+ @state() searchQuery = '';
40
+ @state() statusFilter: 'all' | 'active' | 'pending' | 'archived' = 'all';
41
+ @state() loading = true;
42
+ @state() compact = false;
42
43
 
43
44
  @observe('resize')
44
45
  handleResize(entries: ResizeObserverEntry[]) {
45
- const width = entries[0].contentRect.width;
46
- const table = this.shadowRoot?.querySelector('.data-table');
47
- if (table) {
48
- table.classList.toggle('compact', width < 600);
49
- }
46
+ this.compact = entries[0].contentRect.width < 600;
50
47
  }
51
48
 
52
49
  @ready()
@@ -145,7 +142,7 @@ export class DataPage extends HTMLElement {
145
142
  ></snice-empty-state>
146
143
  </when>
147
144
  <default>
148
- <div class="data-table">
145
+ <div class="data-table" class:compact=${this.compact}>
149
146
  <div class="table-header">
150
147
  <span class="col-title">Title</span>
151
148
  <span class="col-status">Status</span>
@@ -158,7 +155,7 @@ export class DataPage extends HTMLElement {
158
155
  <span class="description">${item.description}</span>
159
156
  </div>
160
157
  <div class="col-status">
161
- <span class="status-dot" style="background: ${this.getStatusColor(item.status)}"></span>
158
+ <span class="status-dot" style:background=${this.getStatusColor(item.status)}></span>
162
159
  ${item.status}
163
160
  </div>
164
161
  <div class="col-date">${item.createdAt}</div>
@@ -1,5 +1,5 @@
1
1
  import { page } from '../router';
2
- import { render, styles, html, css, context, dispose, watch, on } from 'snice';
2
+ import { render, styles, html, css, context, dispose, on, state } from 'snice';
3
3
  import type { Placard, Context } from 'snice';
4
4
  import type { ApplicationContext } from '../context';
5
5
  import { isAuthenticated } from '../guards/auth';
@@ -16,8 +16,8 @@ const placard: Placard = {
16
16
 
17
17
  @page({ tag: 'notifications-page', routes: ['/notifications'], guards: [isAuthenticated], placard })
18
18
  export class NotificationsPage extends HTMLElement {
19
- notifications: Notification[] = [];
20
- filter: NotificationType | 'all' = 'all';
19
+ @state() notifications: Notification[] = [];
20
+ @state() filter: NotificationType | 'all' = 'all';
21
21
  private unsubscribe: (() => void) | null = null;
22
22
 
23
23
  @context()
@@ -38,11 +38,6 @@ export class NotificationsPage extends HTMLElement {
38
38
  }
39
39
  }
40
40
 
41
- @watch('filter')
42
- onFilterChange() {
43
- // Re-render triggers automatically via property change
44
- }
45
-
46
41
  get filteredNotifications(): Notification[] {
47
42
  if (this.filter === 'all') return this.notifications;
48
43
  return this.notifications.filter(n => n.type === this.filter);
@@ -124,7 +119,7 @@ export class NotificationsPage extends HTMLElement {
124
119
  key=${notification.id}
125
120
  .variant=${notification.type}
126
121
  dismissible
127
- @dismiss=${() => this.removeNotification(notification.id)}
122
+ @alert-dismiss=${() => this.removeNotification(notification.id)}
128
123
  >
129
124
  <strong>${notification.title}</strong>
130
125
  <p>${notification.message}</p>
@@ -1,5 +1,5 @@
1
1
  import { page } from '../router';
2
- import { render, styles, html, css, context, dispatch, observe } from 'snice';
2
+ import { render, styles, html, css, context, dispatch, state } from 'snice';
3
3
  import type { Placard, Context } from 'snice';
4
4
  import type { Principal, User } from '../types/auth';
5
5
  import { isAuthenticated } from '../guards/auth';
@@ -16,9 +16,8 @@ const placard: Placard = {
16
16
 
17
17
  @page({ tag: 'profile-page', routes: ['/profile'], guards: [isAuthenticated], placard })
18
18
  export class ProfilePage extends HTMLElement {
19
- user: User | null = null;
19
+ @state() user: User | null = null;
20
20
  private ctx?: Context;
21
- isMobile = false;
22
21
 
23
22
  @context()
24
23
  handleContext(ctx: Context) {
@@ -27,11 +26,6 @@ export class ProfilePage extends HTMLElement {
27
26
  this.user = principal?.user || null;
28
27
  }
29
28
 
30
- @observe('media:(max-width: 640px)')
31
- handleMediaChange(matches: boolean) {
32
- this.isMobile = matches;
33
- }
34
-
35
29
  @dispatch('user-logout')
36
30
  async handleLogout() {
37
31
  await logout();