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.
- package/.agents/skills/snice/SKILL.md +36 -13
- package/README.md +11 -6
- package/adapters/react/SniceProvider.js +1 -1
- package/adapters/react/SniceRouter.d.ts +2 -0
- package/adapters/react/SniceRouter.js +3 -2
- package/adapters/react/SniceRouter.js.map +1 -1
- package/adapters/react/matchRoute.d.ts +4 -1
- package/adapters/react/matchRoute.js +13 -3
- package/adapters/react/matchRoute.js.map +1 -1
- package/adapters/react/useRequestHandler.js +1 -1
- package/bin/analyzer-contracts.js +4 -1
- package/bin/project-analyzer.js +521 -20
- package/bin/snice.js +77 -13
- package/bin/templates/AI_GUIDANCE.md +6 -1
- package/bin/templates/default/README.md +1 -1
- package/bin/templates/default/src/components/app-header.ts +11 -16
- package/bin/templates/default/src/components/notification-badge.ts +5 -14
- package/bin/templates/default/src/components/search-bar.ts +21 -65
- package/bin/templates/default/src/pages/dashboard.ts +15 -11
- package/bin/templates/default/src/pages/data.ts +10 -13
- package/bin/templates/default/src/pages/notifications.ts +4 -9
- package/bin/templates/default/src/pages/profile.ts +2 -8
- package/bin/templates/default/src/pages/settings.ts +16 -16
- package/dist/app-context.d.ts +10 -10
- package/dist/cdn/accordion/snice-accordion.js +1 -1
- package/dist/cdn/action-bar/snice-action-bar.js +1 -1
- package/dist/cdn/activity-feed/snice-activity-feed.js +1 -1
- package/dist/cdn/alert/snice-alert.js +1 -1
- package/dist/cdn/app-tiles/snice-app-tiles.js +1 -1
- package/dist/cdn/approval-flow/snice-approval-flow.js +1 -1
- package/dist/cdn/audio-recorder/snice-audio-recorder.js +1 -1
- package/dist/cdn/availability/snice-availability.js +1 -1
- package/dist/cdn/avatar/snice-avatar.js +1 -1
- package/dist/cdn/avatar-group/snice-avatar-group.js +1 -1
- package/dist/cdn/badge/snice-badge.js +1 -1
- package/dist/cdn/banner/snice-banner.js +1 -1
- package/dist/cdn/binpack/snice-binpack.js +1 -1
- package/dist/cdn/book/snice-book.js +1 -1
- package/dist/cdn/booking/snice-booking.js +1 -1
- package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +1 -1
- package/dist/cdn/button/snice-button.js +1 -1
- package/dist/cdn/calendar/snice-calendar.js +1 -1
- package/dist/cdn/camera/snice-camera.js +1 -1
- package/dist/cdn/camera-annotate/snice-camera-annotate.js +1 -1
- package/dist/cdn/candlestick/snice-candlestick.js +1 -1
- package/dist/cdn/card/snice-card.js +1 -1
- package/dist/cdn/carousel/snice-carousel.js +1 -1
- package/dist/cdn/cart/snice-cart.js +1 -1
- package/dist/cdn/chart/snice-chart.js +1 -1
- package/dist/cdn/chat/snice-chat.js +1 -1
- package/dist/cdn/checkbox/snice-checkbox.js +1 -1
- package/dist/cdn/chip/snice-chip.js +1 -1
- package/dist/cdn/code-block/snice-code-block.js +1 -1
- package/dist/cdn/color-display/snice-color-display.js +1 -1
- package/dist/cdn/color-picker/snice-color-picker.js +1 -1
- package/dist/cdn/command-palette/snice-command-palette.js +1 -1
- package/dist/cdn/comments/snice-comments.js +1 -1
- package/dist/cdn/countdown/snice-countdown.js +1 -1
- package/dist/cdn/cropper/snice-cropper.js +1 -1
- package/dist/cdn/data-card/snice-data-card.js +1 -1
- package/dist/cdn/date-picker/snice-date-picker.js +1 -1
- package/dist/cdn/date-range-picker/snice-date-range-picker.js +1 -1
- package/dist/cdn/date-time-picker/snice-date-time-picker.js +1 -1
- package/dist/cdn/diff/snice-diff.js +1 -1
- package/dist/cdn/divider/snice-divider.js +1 -1
- package/dist/cdn/doc/snice-doc.js +1 -1
- package/dist/cdn/draw/snice-draw.js +1 -1
- package/dist/cdn/drawer/snice-drawer.js +1 -1
- package/dist/cdn/empty-state/snice-empty-state.js +1 -1
- package/dist/cdn/estimate/snice-estimate.js +1 -1
- package/dist/cdn/file-gallery/snice-file-gallery.js +1 -1
- package/dist/cdn/file-upload/snice-file-upload.js +1 -1
- package/dist/cdn/flip-card/snice-flip-card.js +1 -1
- package/dist/cdn/flow/snice-flow.js +1 -1
- package/dist/cdn/form-layout/snice-form-layout.js +1 -1
- package/dist/cdn/funnel/snice-funnel.js +1 -1
- package/dist/cdn/gantt/snice-gantt.js +1 -1
- package/dist/cdn/gauge/snice-gauge.js +1 -1
- package/dist/cdn/grid/snice-grid.js +1 -1
- package/dist/cdn/heatmap/snice-heatmap.js +1 -1
- package/dist/cdn/image/snice-image.js +1 -1
- package/dist/cdn/input/snice-input.js +1 -1
- package/dist/cdn/invoice/snice-invoice.js +1 -1
- package/dist/cdn/kanban/snice-kanban.js +1 -1
- package/dist/cdn/key-value/snice-key-value.js +1 -1
- package/dist/cdn/kpi/snice-kpi.js +1 -1
- package/dist/cdn/layout/snice-layout.js +1 -1
- package/dist/cdn/leaderboard/snice-leaderboard.js +1 -1
- package/dist/cdn/link/snice-link.js +1 -1
- package/dist/cdn/link-preview/snice-link-preview.js +1 -1
- package/dist/cdn/list/snice-list.js +1 -1
- package/dist/cdn/location/snice-location.js +1 -1
- package/dist/cdn/login/snice-login.js +1 -1
- package/dist/cdn/map/snice-map.js +1 -1
- package/dist/cdn/markdown/snice-markdown.js +1 -1
- package/dist/cdn/masonry/snice-masonry.js +1 -1
- package/dist/cdn/menu/snice-menu.js +1 -1
- package/dist/cdn/message-strip/snice-message-strip.js +1 -1
- package/dist/cdn/modal/snice-modal.js +1 -1
- package/dist/cdn/music-player/snice-music-player.js +1 -1
- package/dist/cdn/nav/snice-nav.js +1 -1
- package/dist/cdn/network-graph/snice-network-graph.js +1 -1
- package/dist/cdn/notification-center/snice-notification-center.js +1 -1
- package/dist/cdn/order-tracker/snice-order-tracker.js +1 -1
- package/dist/cdn/org-chart/snice-org-chart.js +1 -1
- package/dist/cdn/pagination/snice-pagination.js +1 -1
- package/dist/cdn/paint/snice-paint.js +1 -1
- package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +1 -1
- package/dist/cdn/permission-matrix/snice-permission-matrix.js +1 -1
- package/dist/cdn/podcast-player/snice-podcast-player.js +1 -1
- package/dist/cdn/popover/snice-popover.js +1 -1
- package/dist/cdn/pricing-table/snice-pricing-table.js +1 -1
- package/dist/cdn/product-card/snice-product-card.js +1 -1
- package/dist/cdn/progress/snice-progress.js +1 -1
- package/dist/cdn/progress-ring/snice-progress-ring.js +1 -1
- package/dist/cdn/qr-code/snice-qr-code.js +1 -1
- package/dist/cdn/qr-reader/snice-qr-reader.js +1 -1
- package/dist/cdn/radio/snice-radio.js +1 -1
- package/dist/cdn/range-slider/snice-range-slider.js +1 -1
- package/dist/cdn/rating/snice-rating.js +1 -1
- package/dist/cdn/receipt/snice-receipt.js +1 -1
- package/dist/cdn/recipe/snice-recipe.js +1 -1
- package/dist/cdn/runtime/README.md +2 -2
- package/dist/cdn/runtime/snice-runtime.esm.js +333 -192
- package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
- package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.js +333 -191
- package/dist/cdn/runtime/snice-runtime.js.map +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js +1 -1
- package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
- package/dist/cdn/sankey/snice-sankey.js +1 -1
- package/dist/cdn/segmented-control/snice-segmented-control.js +1 -1
- package/dist/cdn/select/snice-select.js +1 -1
- package/dist/cdn/skeleton/snice-skeleton.js +1 -1
- package/dist/cdn/slider/snice-slider.js +1 -1
- package/dist/cdn/sortable/snice-sortable.js +1 -1
- package/dist/cdn/sparkline/snice-sparkline.js +1 -1
- package/dist/cdn/spinner/snice-spinner.js +1 -1
- package/dist/cdn/split-button/snice-split-button.js +1 -1
- package/dist/cdn/split-pane/snice-split-pane.js +1 -1
- package/dist/cdn/spotlight/snice-spotlight.js +1 -1
- package/dist/cdn/stat-group/snice-stat-group.js +1 -1
- package/dist/cdn/step-input/snice-step-input.js +1 -1
- package/dist/cdn/stepper/snice-stepper.js +1 -1
- package/dist/cdn/switch/snice-switch.js +1 -1
- package/dist/cdn/table/snice-table.esm.js +1 -1
- package/dist/cdn/table/snice-table.js +1 -1
- package/dist/cdn/tabs/snice-tabs.js +1 -1
- package/dist/cdn/tag/snice-tag.js +1 -1
- package/dist/cdn/tag-input/snice-tag-input.js +1 -1
- package/dist/cdn/terminal/snice-terminal.js +1 -1
- package/dist/cdn/testimonial/snice-testimonial.js +1 -1
- package/dist/cdn/textarea/snice-textarea.js +1 -1
- package/dist/cdn/time-picker/snice-time-picker.js +1 -1
- package/dist/cdn/time-range-picker/snice-time-range-picker.js +1 -1
- package/dist/cdn/timeline/snice-timeline.js +1 -1
- package/dist/cdn/timer/snice-timer.js +1 -1
- package/dist/cdn/toast/snice-toast.js +1 -1
- package/dist/cdn/tooltip/snice-tooltip.js +1 -1
- package/dist/cdn/tree/snice-tree.js +1 -1
- package/dist/cdn/treemap/snice-treemap.js +1 -1
- package/dist/cdn/user-card/snice-user-card.js +1 -1
- package/dist/cdn/video-player/snice-video-player.js +1 -1
- package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +1 -1
- package/dist/cdn/waterfall/snice-waterfall.js +1 -1
- package/dist/cdn/weather/snice-weather.js +1 -1
- package/dist/cdn/work-order/snice-work-order.js +1 -1
- package/dist/components/pagination/snice-pagination.types.d.ts +5 -0
- package/dist/context-provider.d.ts +11 -1
- package/dist/context.d.ts +5 -6
- package/dist/index.cjs +330 -188
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.esm.js +330 -189
- package/dist/index.esm.js.map +1 -1
- package/dist/index.iife.js +330 -188
- package/dist/index.iife.js.map +1 -1
- package/dist/react/SniceProvider.js +1 -1
- package/dist/react/SniceRouter.d.ts +2 -0
- package/dist/react/SniceRouter.js +3 -2
- package/dist/react/SniceRouter.js.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/matchRoute.d.ts +4 -1
- package/dist/react/matchRoute.js +13 -3
- package/dist/react/matchRoute.js.map +1 -1
- package/dist/react/useRequestHandler.js +1 -1
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/testing-dom.cjs +48 -0
- package/dist/testing-dom.cjs.map +1 -0
- package/dist/testing-dom.d.ts +23 -0
- package/dist/testing-dom.esm.js +45 -0
- package/dist/testing-dom.esm.js.map +1 -0
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/dist/types/context.d.ts +8 -8
- package/dist/types/page-options.d.ts +15 -1
- package/dist/types/router-instance.d.ts +2 -1
- package/dist/utils.d.ts +3 -1
- package/docs/ai/README.md +6 -4
- package/docs/ai/api.md +13 -2
- package/docs/ai/architecture.md +28 -11
- package/docs/ai/bindings.md +1 -1
- package/docs/ai/cli.md +26 -1
- package/docs/ai/components/badge.md +4 -0
- package/docs/ai/components/pagination.md +4 -2
- package/docs/ai/components/segmented-control.md +5 -1
- package/docs/ai/controllers.md +41 -12
- package/docs/ai/decorators.md +3 -3
- package/docs/ai/events.md +8 -2
- package/docs/ai/fetcher.md +32 -1
- package/docs/ai/lifecycle.md +4 -1
- package/docs/ai/patterns.md +9 -4
- package/docs/ai/react-integration.md +1 -0
- package/docs/ai/request-response.md +20 -6
- package/docs/ai/routing.md +33 -4
- package/docs/ai/testing.md +71 -0
- package/docs/bindings.md +1 -1
- package/docs/cli.md +27 -2
- package/docs/components/badge.md +4 -0
- package/docs/components/pagination.md +8 -5
- package/docs/components/segmented-control.md +5 -1
- package/docs/controllers.md +46 -15
- package/docs/events.md +11 -2
- package/docs/fetcher.md +32 -1
- package/docs/lifecycle.md +5 -1
- package/docs/react-integration.md +1 -0
- package/docs/request-response.md +25 -7
- package/docs/routing.md +39 -3
- package/docs/testing.md +71 -0
- 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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
462
|
-
const
|
|
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.
|
|
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/ #
|
|
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,
|
|
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
|
-
@
|
|
9
|
-
@
|
|
10
|
-
@
|
|
11
|
-
@
|
|
12
|
-
@
|
|
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"
|
|
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
|
|
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
|
-
@
|
|
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
|
-
<
|
|
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 {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
this.
|
|
11
|
-
this.dispatchSearch();
|
|
8
|
+
handleInput(e: CustomEvent<{ value: string }>) {
|
|
9
|
+
this.value = e.detail.value;
|
|
10
|
+
this.queueSearch();
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
clearSearch(e: Event) {
|
|
13
|
+
clearSearch() {
|
|
16
14
|
this.value = '';
|
|
17
|
-
(
|
|
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
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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>
|
|
91
|
-
<li><strong>Components:</strong> Reusable
|
|
92
|
-
<li><strong>Controllers:</strong>
|
|
93
|
-
<li><strong>Services:</strong>
|
|
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>
|
|
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> -
|
|
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,
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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();
|