@logictan/dsh-config-manager 0.1.60
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/LICENSE +21 -0
- package/README.md +105 -0
- package/cordis.patch.yml +21 -0
- package/lib/adapters/agent-instructions.d.ts +13 -0
- package/lib/adapters/agent-instructions.js +46 -0
- package/lib/adapters/agent-presets.d.ts +13 -0
- package/lib/adapters/agent-presets.js +14 -0
- package/lib/adapters/credentials.d.ts +37 -0
- package/lib/adapters/credentials.js +122 -0
- package/lib/adapters/file-collection.d.ts +15 -0
- package/lib/adapters/file-collection.js +127 -0
- package/lib/adapters/index.d.ts +57 -0
- package/lib/adapters/index.js +55 -0
- package/lib/adapters/link-report.d.ts +16 -0
- package/lib/adapters/link-report.js +47 -0
- package/lib/adapters/mcp.d.ts +28 -0
- package/lib/adapters/mcp.js +178 -0
- package/lib/adapters/plugin-files.d.ts +18 -0
- package/lib/adapters/plugin-files.js +142 -0
- package/lib/adapters/plugins.d.ts +41 -0
- package/lib/adapters/plugins.js +603 -0
- package/lib/adapters/pnpm-workspace.d.ts +26 -0
- package/lib/adapters/pnpm-workspace.js +147 -0
- package/lib/adapters/prompts.d.ts +30 -0
- package/lib/adapters/prompts.js +231 -0
- package/lib/adapters/providers.d.ts +34 -0
- package/lib/adapters/providers.js +155 -0
- package/lib/adapters/self.d.ts +17 -0
- package/lib/adapters/self.js +71 -0
- package/lib/adapters/sessions.d.ts +14 -0
- package/lib/adapters/sessions.js +15 -0
- package/lib/adapters/settings.d.ts +28 -0
- package/lib/adapters/settings.js +165 -0
- package/lib/adapters/skills.d.ts +13 -0
- package/lib/adapters/skills.js +14 -0
- package/lib/adapters/test-helpers.d.ts +125 -0
- package/lib/adapters/test-helpers.js +236 -0
- package/lib/adapters/ui.d.ts +27 -0
- package/lib/adapters/ui.js +89 -0
- package/lib/adapters/workspaces.d.ts +13 -0
- package/lib/adapters/workspaces.js +106 -0
- package/lib/client.d.ts +1026 -0
- package/lib/client.js +10047 -0
- package/lib/core/analyzer.d.ts +99 -0
- package/lib/core/analyzer.js +934 -0
- package/lib/core/backup.d.ts +138 -0
- package/lib/core/backup.js +513 -0
- package/lib/core/boot-rescue.d.ts +146 -0
- package/lib/core/boot-rescue.js +618 -0
- package/lib/core/cache-cleaner.d.ts +31 -0
- package/lib/core/cache-cleaner.js +95 -0
- package/lib/core/config-lifecycle.d.ts +180 -0
- package/lib/core/config-lifecycle.js +481 -0
- package/lib/core/config-snapshot.d.ts +135 -0
- package/lib/core/config-snapshot.js +424 -0
- package/lib/core/config-state.d.ts +70 -0
- package/lib/core/config-state.js +177 -0
- package/lib/core/consult-source.d.ts +58 -0
- package/lib/core/consult-source.js +260 -0
- package/lib/core/crash-report.d.ts +84 -0
- package/lib/core/crash-report.js +317 -0
- package/lib/core/exporter.d.ts +68 -0
- package/lib/core/exporter.js +375 -0
- package/lib/core/ghost-sweep.d.ts +19 -0
- package/lib/core/ghost-sweep.js +52 -0
- package/lib/core/importer.d.ts +60 -0
- package/lib/core/importer.js +29 -0
- package/lib/core/index.d.ts +14 -0
- package/lib/core/index.js +14 -0
- package/lib/core/journal.d.ts +197 -0
- package/lib/core/journal.js +418 -0
- package/lib/core/local-plugin-host.d.ts +35 -0
- package/lib/core/local-plugin-host.js +120 -0
- package/lib/core/local-plugin-pack.d.ts +135 -0
- package/lib/core/local-plugin-pack.js +278 -0
- package/lib/core/messages.d.ts +310 -0
- package/lib/core/messages.js +621 -0
- package/lib/core/migration-consult.d.ts +117 -0
- package/lib/core/migration-consult.js +363 -0
- package/lib/core/migration-history.d.ts +153 -0
- package/lib/core/migration-history.js +411 -0
- package/lib/core/model-tools.d.ts +50 -0
- package/lib/core/model-tools.js +148 -0
- package/lib/core/msg-types.d.ts +8 -0
- package/lib/core/msg-types.js +6 -0
- package/lib/core/phase3-child-crash.d.ts +1 -0
- package/lib/core/phase3-child-crash.js +87 -0
- package/lib/core/phase3-host.d.ts +138 -0
- package/lib/core/phase3-host.js +359 -0
- package/lib/core/phase3-prod-child.d.ts +1 -0
- package/lib/core/phase3-prod-child.js +35 -0
- package/lib/core/phase4-crash-child.d.ts +1 -0
- package/lib/core/phase4-crash-child.js +103 -0
- package/lib/core/plugin-cli.d.ts +150 -0
- package/lib/core/plugin-cli.js +497 -0
- package/lib/core/reconcile.d.ts +120 -0
- package/lib/core/reconcile.js +389 -0
- package/lib/core/recovery-orchestrator.d.ts +94 -0
- package/lib/core/recovery-orchestrator.js +327 -0
- package/lib/core/restore.d.ts +162 -0
- package/lib/core/restore.js +703 -0
- package/lib/core/rollback.d.ts +12 -0
- package/lib/core/rollback.js +145 -0
- package/lib/core/run-registry.d.ts +85 -0
- package/lib/core/run-registry.js +157 -0
- package/lib/core/startup-barrier.d.ts +53 -0
- package/lib/core/startup-barrier.js +77 -0
- package/lib/core/types.d.ts +515 -0
- package/lib/core/types.js +27 -0
- package/lib/core/undo.d.ts +73 -0
- package/lib/core/undo.js +87 -0
- package/lib/core/validator.d.ts +18 -0
- package/lib/core/validator.js +77 -0
- package/lib/core/verify-recovery.d.ts +21 -0
- package/lib/core/verify-recovery.js +256 -0
- package/lib/core/watcher.d.ts +101 -0
- package/lib/core/watcher.js +219 -0
- package/lib/index.d.ts +238 -0
- package/lib/index.js +2764 -0
- package/lib/migrations/index.d.ts +29 -0
- package/lib/migrations/index.js +56 -0
- package/lib/migrations/v1-to-v2.d.ts +9 -0
- package/lib/migrations/v1-to-v2.js +10 -0
- package/lib/profiles/index.d.ts +4 -0
- package/lib/profiles/index.js +5 -0
- package/lib/profiles/profile-manager.d.ts +106 -0
- package/lib/profiles/profile-manager.js +543 -0
- package/lib/schema/config.d.ts +28 -0
- package/lib/schema/config.js +165 -0
- package/lib/schema/index.d.ts +24 -0
- package/lib/schema/index.js +26 -0
- package/lib/schema/manifest.d.ts +28 -0
- package/lib/schema/manifest.js +109 -0
- package/lib/schema/tombstones.d.ts +46 -0
- package/lib/schema/tombstones.js +64 -0
- package/lib/schema/types.d.ts +232 -0
- package/lib/schema/types.js +8 -0
- package/lib/schema/versions.d.ts +27 -0
- package/lib/schema/versions.js +50 -0
- package/lib/security/encryption.d.ts +85 -0
- package/lib/security/encryption.js +279 -0
- package/lib/security/index.d.ts +16 -0
- package/lib/security/index.js +17 -0
- package/lib/security/integrity.d.ts +30 -0
- package/lib/security/integrity.js +78 -0
- package/lib/security/redaction.d.ts +26 -0
- package/lib/security/redaction.js +95 -0
- package/lib/security/secret-scanner.d.ts +86 -0
- package/lib/security/secret-scanner.js +404 -0
- package/lib/security/vault.d.ts +59 -0
- package/lib/security/vault.js +151 -0
- package/lib/security/zip-security.d.ts +27 -0
- package/lib/security/zip-security.js +154 -0
- package/lib/sync/autosync-config.d.ts +39 -0
- package/lib/sync/autosync-config.js +151 -0
- package/lib/sync/autosync-scheduler.d.ts +139 -0
- package/lib/sync/autosync-scheduler.js +564 -0
- package/lib/sync/fs.d.ts +18 -0
- package/lib/sync/fs.js +49 -0
- package/lib/sync/git/git-transport.d.ts +100 -0
- package/lib/sync/git/git-transport.js +437 -0
- package/lib/sync/github-auth.d.ts +131 -0
- package/lib/sync/github-auth.js +264 -0
- package/lib/sync/layout.d.ts +43 -0
- package/lib/sync/layout.js +203 -0
- package/lib/sync/snapshot-json.d.ts +9 -0
- package/lib/sync/snapshot-json.js +107 -0
- package/lib/sync/sync-config.d.ts +81 -0
- package/lib/sync/sync-config.js +281 -0
- package/lib/sync/sync-engine.d.ts +269 -0
- package/lib/sync/sync-engine.js +591 -0
- package/lib/sync/sync-history.d.ts +42 -0
- package/lib/sync/sync-history.js +78 -0
- package/lib/sync/sync-selection.d.ts +29 -0
- package/lib/sync/sync-selection.js +127 -0
- package/lib/sync/sync-session.d.ts +40 -0
- package/lib/sync/sync-session.js +54 -0
- package/lib/sync/sync-state.d.ts +37 -0
- package/lib/sync/sync-state.js +108 -0
- package/lib/sync/transport.d.ts +75 -0
- package/lib/sync/transport.js +55 -0
- package/lib/sync/ui-prefs.d.ts +34 -0
- package/lib/sync/ui-prefs.js +109 -0
- package/lib/sync/webdav/webdav-transport.d.ts +77 -0
- package/lib/sync/webdav/webdav-transport.js +451 -0
- package/lib/ui/errors.d.ts +19 -0
- package/lib/ui/errors.js +81 -0
- package/lib/ui/export-flow.d.ts +82 -0
- package/lib/ui/export-flow.js +98 -0
- package/lib/ui/i18n.d.ts +289 -0
- package/lib/ui/i18n.js +581 -0
- package/lib/ui/migration-consult-view.d.ts +91 -0
- package/lib/ui/migration-consult-view.js +115 -0
- package/lib/ui/progress.d.ts +38 -0
- package/lib/ui/progress.js +87 -0
- package/lib/ui/report.d.ts +23 -0
- package/lib/ui/report.js +153 -0
- package/lib/ui/test-helpers.d.ts +69 -0
- package/lib/ui/test-helpers.js +154 -0
- package/lib/ui/types.d.ts +257 -0
- package/lib/ui/types.js +21 -0
- package/lib/utils/atomic-write.d.ts +88 -0
- package/lib/utils/atomic-write.js +399 -0
- package/lib/utils/env-lock.d.ts +393 -0
- package/lib/utils/env-lock.js +930 -0
- package/lib/utils/hashing.d.ts +17 -0
- package/lib/utils/hashing.js +41 -0
- package/lib/utils/json.d.ts +24 -0
- package/lib/utils/json.js +76 -0
- package/lib/utils/logger.d.ts +39 -0
- package/lib/utils/logger.js +82 -0
- package/lib/utils/paths.d.ts +42 -0
- package/lib/utils/paths.js +189 -0
- package/lib/utils/proxy.d.ts +69 -0
- package/lib/utils/proxy.js +455 -0
- package/lib/utils/recursive-walk.d.ts +24 -0
- package/lib/utils/recursive-walk.js +134 -0
- package/lib/utils/zip.d.ts +54 -0
- package/lib/utils/zip.js +285 -0
- package/package.json +188 -0
- package/src/adapters/agent-instructions.ts +48 -0
- package/src/adapters/agent-presets.ts +14 -0
- package/src/adapters/credentials.test.ts +90 -0
- package/src/adapters/credentials.ts +143 -0
- package/src/adapters/file-collection.ts +137 -0
- package/src/adapters/files.test.ts +353 -0
- package/src/adapters/index.ts +94 -0
- package/src/adapters/link-report.test.ts +65 -0
- package/src/adapters/link-report.ts +61 -0
- package/src/adapters/mcp.test.ts +96 -0
- package/src/adapters/mcp.ts +185 -0
- package/src/adapters/plugin-files.ts +146 -0
- package/src/adapters/plugins.test.ts +326 -0
- package/src/adapters/plugins.ts +615 -0
- package/src/adapters/pnpm-workspace.test.ts +87 -0
- package/src/adapters/pnpm-workspace.ts +150 -0
- package/src/adapters/prompts.test.ts +110 -0
- package/src/adapters/prompts.ts +230 -0
- package/src/adapters/providers.test.ts +126 -0
- package/src/adapters/providers.ts +173 -0
- package/src/adapters/roundtrip.test.ts +190 -0
- package/src/adapters/self.test.ts +92 -0
- package/src/adapters/self.ts +75 -0
- package/src/adapters/sessions.ts +15 -0
- package/src/adapters/settings.test.ts +100 -0
- package/src/adapters/settings.ts +187 -0
- package/src/adapters/skills.ts +14 -0
- package/src/adapters/test-helpers.ts +248 -0
- package/src/adapters/ui.test.ts +57 -0
- package/src/adapters/ui.ts +108 -0
- package/src/adapters/workspaces.test.ts +106 -0
- package/src/adapters/workspaces.ts +109 -0
- package/src/client/ConfigManagerSection.tsx +90 -0
- package/src/client/api.ts +66 -0
- package/src/client/client-types.ts +18 -0
- package/src/client/common/ErrorBanner.tsx +73 -0
- package/src/client/common/Icon.tsx +129 -0
- package/src/client/common/Modal.tsx +173 -0
- package/src/client/common/ToastViewport.tsx +73 -0
- package/src/client/common/toast-store.test.ts +192 -0
- package/src/client/common/toast-store.ts +181 -0
- package/src/client/common/toast-types.ts +7 -0
- package/src/client/common/ui.tsx +342 -0
- package/src/client/config-manager.module.css +2971 -0
- package/src/client/css-modules.d.ts +8 -0
- package/src/client/index.ts +65 -0
- package/src/client/locales.ts +56 -0
- package/src/client/lucide-icons.d.ts +38 -0
- package/src/client/run-store.test.ts +223 -0
- package/src/client/run-store.ts +372 -0
- package/src/client/sync/SyncConfirmView.tsx +379 -0
- package/src/client/sync/SyncHistoryView.test.ts +40 -0
- package/src/client/sync/SyncHistoryView.tsx +220 -0
- package/src/client/sync/SyncSettingsView.tsx +1535 -0
- package/src/client/sync/history-model.test.ts +194 -0
- package/src/client/sync/history-model.ts +195 -0
- package/src/client/sync/sync-api.test.ts +585 -0
- package/src/client/sync/sync-api.ts +543 -0
- package/src/client/sync/sync-locales.ts +416 -0
- package/src/client/sync/sync-push-preview.test.ts +52 -0
- package/src/client/sync/sync-view-v2.test.ts +213 -0
- package/src/client/sync/sync-view.test.ts +487 -0
- package/src/client/sync/sync-view.ts +759 -0
- package/src/core/analyzer.ts +1068 -0
- package/src/core/backup.ts +605 -0
- package/src/core/boot-rescue.test.ts +847 -0
- package/src/core/boot-rescue.ts +737 -0
- package/src/core/cache-cleaner.test.ts +169 -0
- package/src/core/cache-cleaner.ts +121 -0
- package/src/core/config-lifecycle.test.ts +727 -0
- package/src/core/config-lifecycle.ts +573 -0
- package/src/core/config-snapshot.test.ts +369 -0
- package/src/core/config-snapshot.ts +522 -0
- package/src/core/config-state.test.ts +381 -0
- package/src/core/config-state.ts +207 -0
- package/src/core/consult-source.test.ts +199 -0
- package/src/core/consult-source.ts +293 -0
- package/src/core/crash-report.test.ts +380 -0
- package/src/core/crash-report.ts +369 -0
- package/src/core/exporter.ts +417 -0
- package/src/core/ghost-sweep.test.ts +94 -0
- package/src/core/ghost-sweep.ts +67 -0
- package/src/core/importer.ts +83 -0
- package/src/core/index.ts +56 -0
- package/src/core/journal.test.ts +257 -0
- package/src/core/journal.ts +518 -0
- package/src/core/local-plugin-host.ts +152 -0
- package/src/core/local-plugin-pack.test.ts +359 -0
- package/src/core/local-plugin-pack.ts +363 -0
- package/src/core/messages.test.ts +54 -0
- package/src/core/messages.ts +664 -0
- package/src/core/migration-consult.test.ts +315 -0
- package/src/core/migration-consult.ts +494 -0
- package/src/core/migration-history.test.ts +319 -0
- package/src/core/migration-history.ts +484 -0
- package/src/core/model-tools.test.ts +190 -0
- package/src/core/model-tools.ts +208 -0
- package/src/core/msg-types.ts +10 -0
- package/src/core/phase3-child-crash.ts +95 -0
- package/src/core/phase3-host.ts +411 -0
- package/src/core/phase3-p1.test.ts +212 -0
- package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
- package/src/core/phase3-prod-child.ts +36 -0
- package/src/core/phase3-production-integration.test.ts +286 -0
- package/src/core/phase4-crash-child.ts +101 -0
- package/src/core/phase4-crash-injection.test.ts +166 -0
- package/src/core/plugin-cli.fs.test.ts +184 -0
- package/src/core/plugin-cli.test.ts +137 -0
- package/src/core/plugin-cli.ts +563 -0
- package/src/core/reconcile.test.ts +392 -0
- package/src/core/reconcile.ts +498 -0
- package/src/core/recovery-orchestrator.test.ts +145 -0
- package/src/core/recovery-orchestrator.ts +366 -0
- package/src/core/restore-manage.test.ts +94 -0
- package/src/core/restore.ts +849 -0
- package/src/core/rollback.ts +151 -0
- package/src/core/run-progress.test.ts +237 -0
- package/src/core/run-registry.test.ts +232 -0
- package/src/core/run-registry.ts +209 -0
- package/src/core/smoke.test.ts +842 -0
- package/src/core/startup-barrier.ts +99 -0
- package/src/core/types.ts +542 -0
- package/src/core/undo.ts +116 -0
- package/src/core/validator.ts +84 -0
- package/src/core/verify-recovery.ts +265 -0
- package/src/core/watcher.test.ts +326 -0
- package/src/core/watcher.ts +263 -0
- package/src/index.facade.test.ts +265 -0
- package/src/index.sync.test.ts +217 -0
- package/src/index.ts +2975 -0
- package/src/migrations/index.ts +71 -0
- package/src/migrations/v1-to-v2.ts +17 -0
- package/src/profiles/index.ts +8 -0
- package/src/profiles/profile-manager.test.ts +282 -0
- package/src/profiles/profile-manager.ts +630 -0
- package/src/schema/config.ts +165 -0
- package/src/schema/index.ts +71 -0
- package/src/schema/manifest-schema.test.ts +519 -0
- package/src/schema/manifest.ts +124 -0
- package/src/schema/tombstones.test.ts +131 -0
- package/src/schema/tombstones.ts +89 -0
- package/src/schema/types.ts +245 -0
- package/src/schema/versions.ts +55 -0
- package/src/security/encryption.ts +335 -0
- package/src/security/index.ts +16 -0
- package/src/security/integrity.ts +102 -0
- package/src/security/redaction.ts +101 -0
- package/src/security/secret-scanner.ts +445 -0
- package/src/security/security.test.ts +1215 -0
- package/src/security/vault.test.ts +164 -0
- package/src/security/vault.ts +198 -0
- package/src/security/zip-security.ts +180 -0
- package/src/sync/autosync-config.test.ts +147 -0
- package/src/sync/autosync-config.ts +179 -0
- package/src/sync/autosync-scheduler.test.ts +490 -0
- package/src/sync/autosync-scheduler.ts +646 -0
- package/src/sync/fs.ts +48 -0
- package/src/sync/git/git-transport.test.ts +673 -0
- package/src/sync/git/git-transport.ts +491 -0
- package/src/sync/github-auth.test.ts +280 -0
- package/src/sync/github-auth.ts +356 -0
- package/src/sync/layout.test.ts +270 -0
- package/src/sync/layout.ts +222 -0
- package/src/sync/snapshot-json.test.ts +119 -0
- package/src/sync/snapshot-json.ts +128 -0
- package/src/sync/sync-config.test.ts +310 -0
- package/src/sync/sync-config.ts +299 -0
- package/src/sync/sync-engine.test.ts +1074 -0
- package/src/sync/sync-engine.ts +754 -0
- package/src/sync/sync-history.test.ts +116 -0
- package/src/sync/sync-history.ts +120 -0
- package/src/sync/sync-selection.test.ts +186 -0
- package/src/sync/sync-selection.ts +143 -0
- package/src/sync/sync-session.test.ts +143 -0
- package/src/sync/sync-session.ts +77 -0
- package/src/sync/sync-state.test.ts +164 -0
- package/src/sync/sync-state.ts +131 -0
- package/src/sync/transport.test.ts +111 -0
- package/src/sync/transport.ts +118 -0
- package/src/sync/ui-prefs.test.ts +152 -0
- package/src/sync/ui-prefs.ts +135 -0
- package/src/sync/webdav/webdav-transport.test.ts +871 -0
- package/src/sync/webdav/webdav-transport.ts +535 -0
- package/src/ui/errors.test.ts +47 -0
- package/src/ui/errors.ts +100 -0
- package/src/ui/export-flow.test.ts +87 -0
- package/src/ui/export-flow.ts +156 -0
- package/src/ui/i18n.ts +590 -0
- package/src/ui/migration-consult-view.test.ts +151 -0
- package/src/ui/migration-consult-view.ts +178 -0
- package/src/ui/progress.test.ts +47 -0
- package/src/ui/progress.ts +102 -0
- package/src/ui/report.test.ts +101 -0
- package/src/ui/report.ts +146 -0
- package/src/ui/test-helpers.ts +184 -0
- package/src/ui/types.ts +325 -0
- package/src/utils/atomic-write.test.ts +312 -0
- package/src/utils/atomic-write.ts +422 -0
- package/src/utils/env-lock.test.ts +999 -0
- package/src/utils/env-lock.ts +1086 -0
- package/src/utils/hashing.ts +53 -0
- package/src/utils/json.ts +77 -0
- package/src/utils/logger.ts +114 -0
- package/src/utils/paths.ts +176 -0
- package/src/utils/proxy.test.ts +465 -0
- package/src/utils/proxy.ts +493 -0
- package/src/utils/recursive-walk.test.ts +148 -0
- package/src/utils/recursive-walk.ts +145 -0
- package/src/utils/zip.ts +332 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图标原语 —— 基于 lucide-react 的统一图标层(Workbench Design System)。
|
|
3
|
+
*
|
|
4
|
+
* 取代散落的文本符号图标(▣ ⇥ ⇤ ⇅ ⟳ ◷ ⓘ ⭳ ⌕ ✕ ⧉ →),解决跨平台字形宽度/基线
|
|
5
|
+
* 漂移与视觉重量不一致问题。所有图标统一:
|
|
6
|
+
* - 尺寸由 `size` 控制(默认 14px,对齐正文行高);
|
|
7
|
+
* - 描边 strokeWidth 统一 1.75(克制、精致,匹配高密度开发者工具观感);
|
|
8
|
+
* - 颜色继承 currentColor(跟随父级文字色 / data-danger / hover 等语义);
|
|
9
|
+
* - vertical-align 居中,与相邻文字基线对齐。
|
|
10
|
+
*
|
|
11
|
+
* 用法:<Icon name="download" /> 或具名导出 <DownloadIcon />。
|
|
12
|
+
* 不引入第二套视觉体系:颜色走 currentColor → 父级 --dsw-* token。
|
|
13
|
+
*/
|
|
14
|
+
import type { CSSProperties } from 'react'
|
|
15
|
+
import type { LucideIcon } from 'lucide-react'
|
|
16
|
+
// 体积纪律:从各图标独立模块路径导入(非 `lucide-react` 桶导出),保证 rolldown 在 cjs
|
|
17
|
+
// 单文件打包下精确 tree-shake——只打入用到的 ~18 个图标,避免整库 1500+ 全量进 client.js。
|
|
18
|
+
import ArrowRight from 'lucide-react/dist/esm/icons/arrow-right.mjs'
|
|
19
|
+
import CheckCircle from 'lucide-react/dist/esm/icons/check-circle.mjs'
|
|
20
|
+
import ChevronDown from 'lucide-react/dist/esm/icons/chevron-down.mjs'
|
|
21
|
+
import ChevronRight from 'lucide-react/dist/esm/icons/chevron-right.mjs'
|
|
22
|
+
import Clock from 'lucide-react/dist/esm/icons/clock.mjs'
|
|
23
|
+
import CircleAlert from 'lucide-react/dist/esm/icons/circle-alert.mjs'
|
|
24
|
+
import Copy from 'lucide-react/dist/esm/icons/copy.mjs'
|
|
25
|
+
import DatabaseBackup from 'lucide-react/dist/esm/icons/database-backup.mjs'
|
|
26
|
+
import Download from 'lucide-react/dist/esm/icons/download.mjs'
|
|
27
|
+
import Eye from 'lucide-react/dist/esm/icons/eye.mjs'
|
|
28
|
+
import FolderInput from 'lucide-react/dist/esm/icons/folder-input.mjs'
|
|
29
|
+
import HardDriveDownload from 'lucide-react/dist/esm/icons/hard-drive-download.mjs'
|
|
30
|
+
import Info from 'lucide-react/dist/esm/icons/info.mjs'
|
|
31
|
+
import MessageSquare from 'lucide-react/dist/esm/icons/message-square.mjs'
|
|
32
|
+
import PackageCheck from 'lucide-react/dist/esm/icons/package-check.mjs'
|
|
33
|
+
import Pencil from 'lucide-react/dist/esm/icons/pencil.mjs'
|
|
34
|
+
import RefreshCw from 'lucide-react/dist/esm/icons/refresh-cw.mjs'
|
|
35
|
+
import RotateCw from 'lucide-react/dist/esm/icons/rotate-cw.mjs'
|
|
36
|
+
import Search from 'lucide-react/dist/esm/icons/search.mjs'
|
|
37
|
+
import Trash2 from 'lucide-react/dist/esm/icons/trash-2.mjs'
|
|
38
|
+
import TriangleAlert from 'lucide-react/dist/esm/icons/triangle-alert.mjs'
|
|
39
|
+
import Upload from 'lucide-react/dist/esm/icons/upload.mjs'
|
|
40
|
+
import X from 'lucide-react/dist/esm/icons/x.mjs'
|
|
41
|
+
|
|
42
|
+
/** 语义图标名 → Lucide 组件映射(单一事实来源)。 */
|
|
43
|
+
const ICONS = {
|
|
44
|
+
backup: HardDriveDownload,
|
|
45
|
+
export: Upload,
|
|
46
|
+
import: FolderInput,
|
|
47
|
+
sync: RefreshCw,
|
|
48
|
+
refresh: RotateCw,
|
|
49
|
+
activity: Clock,
|
|
50
|
+
clock: Clock,
|
|
51
|
+
about: Info,
|
|
52
|
+
info: Info,
|
|
53
|
+
message: MessageSquare,
|
|
54
|
+
pencil: Pencil,
|
|
55
|
+
close: X,
|
|
56
|
+
delete: Trash2,
|
|
57
|
+
download: Download,
|
|
58
|
+
inspect: Search,
|
|
59
|
+
view: Eye,
|
|
60
|
+
copy: Copy,
|
|
61
|
+
arrowRight: ArrowRight,
|
|
62
|
+
chevronRight: ChevronRight,
|
|
63
|
+
chevronDown: ChevronDown,
|
|
64
|
+
preview: Eye,
|
|
65
|
+
snapshot: DatabaseBackup,
|
|
66
|
+
ok: PackageCheck,
|
|
67
|
+
check: CheckCircle,
|
|
68
|
+
warn: TriangleAlert,
|
|
69
|
+
error: CircleAlert,
|
|
70
|
+
} as const satisfies Record<string, LucideIcon>
|
|
71
|
+
|
|
72
|
+
export type IconName = keyof typeof ICONS
|
|
73
|
+
|
|
74
|
+
export interface IconProps {
|
|
75
|
+
name: IconName
|
|
76
|
+
/** 像素尺寸(默认 14,对齐 12.5px 正文行高) */
|
|
77
|
+
size?: number
|
|
78
|
+
/** 描边粗细(默认 1.75) */
|
|
79
|
+
strokeWidth?: number
|
|
80
|
+
className?: string
|
|
81
|
+
style?: CSSProperties
|
|
82
|
+
/** 无障碍:装饰性图标默认 aria-hidden;需语义时由父级 aria-label 承担 */
|
|
83
|
+
decorative?: boolean
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 统一图标(lucide-react)。颜色继承 currentColor,尺寸/描边集中控制。
|
|
88
|
+
*/
|
|
89
|
+
export function Icon({ name, size = 14, strokeWidth = 1.75, className, style, decorative = true }: IconProps) {
|
|
90
|
+
const Cmp = ICONS[name]
|
|
91
|
+
return (
|
|
92
|
+
<Cmp
|
|
93
|
+
size={size}
|
|
94
|
+
strokeWidth={strokeWidth}
|
|
95
|
+
className={className}
|
|
96
|
+
style={{ verticalAlign: 'middle', flex: 'none', ...style }}
|
|
97
|
+
aria-hidden={decorative || undefined}
|
|
98
|
+
focusable={false}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* —— 具名导出(调用处可读性优先) —— */
|
|
104
|
+
export const BackupIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="backup" {...p} />
|
|
105
|
+
export const ExportIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="export" {...p} />
|
|
106
|
+
export const ImportIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="import" {...p} />
|
|
107
|
+
export const SyncIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="sync" {...p} />
|
|
108
|
+
export const RefreshIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="refresh" {...p} />
|
|
109
|
+
export const ActivityIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="activity" {...p} />
|
|
110
|
+
export const ClockIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="clock" {...p} />
|
|
111
|
+
export const AboutIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="about" {...p} />
|
|
112
|
+
export const MessageIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="message" {...p} />
|
|
113
|
+
export const PencilIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="pencil" {...p} />
|
|
114
|
+
export const CloseIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="close" {...p} />
|
|
115
|
+
export const DeleteIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="delete" {...p} />
|
|
116
|
+
export const DownloadIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="download" {...p} />
|
|
117
|
+
export const InspectIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="inspect" {...p} />
|
|
118
|
+
export const ViewIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="view" {...p} />
|
|
119
|
+
export const CopyIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="copy" {...p} />
|
|
120
|
+
export const ArrowRightIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="arrowRight" {...p} />
|
|
121
|
+
export const ChevronRightIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="chevronRight" {...p} />
|
|
122
|
+
export const ChevronDownIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="chevronDown" {...p} />
|
|
123
|
+
export const PreviewIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="preview" {...p} />
|
|
124
|
+
export const SnapshotIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="snapshot" {...p} />
|
|
125
|
+
export const OkIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="ok" {...p} />
|
|
126
|
+
export const CheckIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="check" {...p} />
|
|
127
|
+
export const WarnIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="warn" {...p} />
|
|
128
|
+
export const ErrorIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="error" {...p} />
|
|
129
|
+
export const InfoIcon = (p: Omit<IconProps, 'name'>): JSX.Element => <Icon name="info" {...p} />
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal —— 基于 @radix-ui/react-dialog 的统一弹窗原语(Workbench Design System)。
|
|
3
|
+
*
|
|
4
|
+
* 取代项目里散落的两套弹窗实现:
|
|
5
|
+
* 1. ConfirmDialog(手写 focus trap / Esc / 焦点还原);
|
|
6
|
+
* 2. 各页内联 dialogMask+dialogCard(无 focus trap、Esc 行为不一致)。
|
|
7
|
+
* 统一后获得 Radix 成熟的无障碍能力:
|
|
8
|
+
* - 自动 focus trap(Tab 循环限制在弹窗内,disabled/隐藏元素跳过);
|
|
9
|
+
* - Esc 关闭(可禁用)、遮罩点击关闭(可禁用)、初始焦点与关闭后焦点还原;
|
|
10
|
+
* - aria-modal / role=dialog、body 滚动锁定、Portal 渲染到 document.body(脱离宿主
|
|
11
|
+
* settings 弹窗的层叠上下文,z-index 由 Radix 内容层统一管理)。
|
|
12
|
+
*
|
|
13
|
+
* 视觉沿用既有 --dsw-* token 类(dialogMask/dialogCard/dialogHeader/dialogBody/...),
|
|
14
|
+
* 不引入第二套视觉体系;仅把「行为/a11y」交给 Radix,外观仍由 config-manager.module.css 控制。
|
|
15
|
+
*
|
|
16
|
+
* 用法:
|
|
17
|
+
* <Modal open={open} onClose={close} title="标题" wide>
|
|
18
|
+
* <Modal.Header onClose={close} /> // 可选:带关闭按钮的标题行
|
|
19
|
+
* <Modal.Body scroll>…内容…</Modal.Body>
|
|
20
|
+
* <Modal.Footer>…按钮…</Modal.Footer>
|
|
21
|
+
* </Modal>
|
|
22
|
+
*/
|
|
23
|
+
import { useEffect, useState } from 'react'
|
|
24
|
+
import type { CSSProperties, ReactNode, Ref } from 'react'
|
|
25
|
+
import * as Dialog from '@radix-ui/react-dialog'
|
|
26
|
+
import { CloseIcon } from './Icon.tsx'
|
|
27
|
+
import css from '../config-manager.module.css'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 插件根节点 id(渲染在 `ConfigManagerSection` 的最外层 div 上),同时是 Radix Portal 的挂载容器。
|
|
31
|
+
*
|
|
32
|
+
* 为什么不能挂 `document.body`(Radix 默认):宿主设置弹窗的 overlay 是
|
|
33
|
+
* `position: fixed; z-index: 1000`,插件弹窗若挂到 body 就成了它的**兄弟**,
|
|
34
|
+
* 自身 z-index 100/101 远低于 1000 → 弹窗被整个盖住、肉眼完全不可见;
|
|
35
|
+
* 而 Radix 在 modal 打开时已经把 `document.body` 置为 `pointer-events: none`,
|
|
36
|
+
* 于是表现为「打开备份与迁移后整页点不动,必须先在屏幕上点一下才行」——
|
|
37
|
+
* 那一下点击正是关掉这个"透明弹窗"的 outside-pointerdown。
|
|
38
|
+
*
|
|
39
|
+
* 把 Portal 容器指回插件根节点,弹窗就重新落在宿主弹窗自己的层叠上下文内
|
|
40
|
+
* (与 Radix 迁移前内联渲染 `dialogMask` 的层级语义一致),遮罩与卡片正常可见可点。
|
|
41
|
+
*/
|
|
42
|
+
export const MODAL_ROOT_ID = 'dsh-config-manager-root'
|
|
43
|
+
|
|
44
|
+
export interface ModalProps {
|
|
45
|
+
/** 是否打开(受控) */
|
|
46
|
+
open: boolean
|
|
47
|
+
/** 关闭回调(Esc / 遮罩点击 / 关闭按钮触发) */
|
|
48
|
+
onClose: () => void
|
|
49
|
+
/** 弹窗 accessible name(aria-label) */
|
|
50
|
+
title?: string
|
|
51
|
+
/** 宽变体(480px,用于计划预览/差异查看等密集内容) */
|
|
52
|
+
wide?: boolean
|
|
53
|
+
/** busy 时禁用一切关闭途径(防执行中误关) */
|
|
54
|
+
busy?: boolean
|
|
55
|
+
/** Radix 打开时的初始焦点重定向(如 ConfirmDialog 把焦点派发到取消按钮)。
|
|
56
|
+
* Radix 传入的是可 preventDefault 的 DOM Event。 */
|
|
57
|
+
onOpenAutoFocus?: (e: Event) => void
|
|
58
|
+
/** 卡片额外内联样式(如 ReleaseNotes 的自定义宽度/最大高度);常规布局仍走 CSS 类 */
|
|
59
|
+
cardStyle?: CSSProperties
|
|
60
|
+
children?: ReactNode
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 统一弹窗容器(Radix Dialog)。busy 时禁用 Esc 与遮罩关闭。
|
|
65
|
+
*/
|
|
66
|
+
export function Modal({ open, onClose, title, wide, busy, onOpenAutoFocus, cardStyle, children }: ModalProps) {
|
|
67
|
+
// Portal 容器在挂载后再解析一次:Modal 与根节点可能在同一次 commit 中渲染,
|
|
68
|
+
// 首次求值时根节点尚未进 DOM(届时回退 Radix 的 document.body)。
|
|
69
|
+
const [container, setContainer] = useState<HTMLElement | null>(null)
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
setContainer(document.getElementById(MODAL_ROOT_ID))
|
|
72
|
+
}, [])
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Dialog.Root
|
|
76
|
+
open={open}
|
|
77
|
+
onOpenChange={(next) => {
|
|
78
|
+
// busy 时拒绝关闭(Radix 通过 onOpenChange(false) 表达 Esc/遮罩关闭意图)
|
|
79
|
+
if (!next && busy === true) return
|
|
80
|
+
if (!next) onClose()
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<Dialog.Portal container={container ?? undefined}>
|
|
84
|
+
<Dialog.Overlay className={css.dialogMask} />
|
|
85
|
+
<Dialog.Content
|
|
86
|
+
className={`${css.dialogContentCenter} ${css.dialogCard}${wide === true ? ` ${css.dialogWide}` : ''}`}
|
|
87
|
+
style={cardStyle}
|
|
88
|
+
aria-label={title}
|
|
89
|
+
onOpenAutoFocus={onOpenAutoFocus}
|
|
90
|
+
// busy 时阻止 Radix 默认的 Esc/外部指针关闭(双保险,配合 onOpenChange 守卫)
|
|
91
|
+
onEscapeKeyDown={(e) => { if (busy === true) e.preventDefault() }}
|
|
92
|
+
onPointerDownOutside={(e) => { if (busy === true) e.preventDefault() }}
|
|
93
|
+
onInteractOutside={(e) => { if (busy === true) e.preventDefault() }}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</Dialog.Content>
|
|
97
|
+
</Dialog.Portal>
|
|
98
|
+
</Dialog.Root>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ---------------- 子部件(纯样式装配,无逻辑) ---------------- */
|
|
103
|
+
|
|
104
|
+
export interface ModalHeaderProps {
|
|
105
|
+
/** 标题文本(同时作为可视标题) */
|
|
106
|
+
title: string
|
|
107
|
+
/** 关闭按钮回调(缺省不渲染关闭按钮) */
|
|
108
|
+
onClose?: () => void
|
|
109
|
+
/** 关闭按钮 disabled(如 busy) */
|
|
110
|
+
closeDisabled?: boolean
|
|
111
|
+
/** 标题行右侧额外内容(如徽章/合计) */
|
|
112
|
+
trailing?: ReactNode
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 弹窗标题行(可选关闭按钮 + 右侧 trailing)。 */
|
|
116
|
+
function ModalHeader({ title, onClose, closeDisabled, trailing }: ModalHeaderProps) {
|
|
117
|
+
if (onClose === undefined && trailing === undefined) {
|
|
118
|
+
return <div className={css.dialogHeader}>{title}</div>
|
|
119
|
+
}
|
|
120
|
+
return (
|
|
121
|
+
<div className={css.dialogHeaderRow}>
|
|
122
|
+
<span className={css.dialogHeader}>{title}</span>
|
|
123
|
+
{trailing}
|
|
124
|
+
{onClose !== undefined && (
|
|
125
|
+
<Dialog.Close asChild>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
className={`${css.iconBtn} ${css.dialogClose}`}
|
|
129
|
+
aria-label="关闭"
|
|
130
|
+
disabled={closeDisabled === true}
|
|
131
|
+
>
|
|
132
|
+
<CloseIcon size={14} />
|
|
133
|
+
</button>
|
|
134
|
+
</Dialog.Close>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface ModalBodyProps {
|
|
141
|
+
children?: ReactNode
|
|
142
|
+
/** 限高内滚变体(长内容安全) */
|
|
143
|
+
scroll?: boolean
|
|
144
|
+
/** 正文容器 ref(如 ReleaseNotes 无限滚动需要监听滚动位置) */
|
|
145
|
+
innerRef?: Ref<HTMLDivElement>
|
|
146
|
+
/** 滚动回调(配合 innerRef 实现无限加载等) */
|
|
147
|
+
onScroll?: () => void
|
|
148
|
+
/** 正文额外内联样式(如自定义 maxHeight/gap);常规布局仍走 CSS 类 */
|
|
149
|
+
style?: CSSProperties
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 弹窗正文区。 */
|
|
153
|
+
function ModalBody({ children, scroll, innerRef, onScroll, style }: ModalBodyProps) {
|
|
154
|
+
return (
|
|
155
|
+
<div
|
|
156
|
+
ref={innerRef}
|
|
157
|
+
className={scroll === true ? `${css.dialogBody} ${css.dialogBodyScroll}` : css.dialogBody}
|
|
158
|
+
onScroll={onScroll}
|
|
159
|
+
style={style}
|
|
160
|
+
>
|
|
161
|
+
{children}
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 弹窗底部按钮区(actionRow 右对齐)。 */
|
|
167
|
+
function ModalFooter({ children }: { children?: ReactNode }) {
|
|
168
|
+
return <div className={`${css.actionRow} ${css.dialogFooter}`}>{children}</div>
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
Modal.Header = ModalHeader
|
|
172
|
+
Modal.Body = ModalBody
|
|
173
|
+
Modal.Footer = ModalFooter
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToastViewport —— 全局通知的渲染宿主(右下角堆叠)。
|
|
3
|
+
*
|
|
4
|
+
* 挂载在 `ConfigManagerSection` 根部(与 `.drawerPanel` 同级),而不是 Portal 到 body:
|
|
5
|
+
* 宿主设置弹窗 overlay 为 `position: fixed; z-index: 1000`(见 Modal.tsx 的
|
|
6
|
+
* MODAL_ROOT_ID 说明),任何挂到 body 的固定层都会被它盖住而"隐形"。
|
|
7
|
+
* 因此本组件用 `position: absolute` 贴合插件根节点(`.section` 已 position: relative),
|
|
8
|
+
* 天然落在宿主弹窗自己的层叠上下文内。
|
|
9
|
+
*
|
|
10
|
+
* 交互:
|
|
11
|
+
* - 每条可点 × 手动关闭;悬停整条时**暂停**自动消失(正在读的内容不会跑掉),
|
|
12
|
+
* 移开后按原时长重新计时。
|
|
13
|
+
* - 容器 `pointer-events: none`,仅卡片自身 `pointer-events: auto`:
|
|
14
|
+
* 通知不遮挡下方页面的点击(右下角常压着状态栏 / 列表)。
|
|
15
|
+
* - aria-live="polite" + role="status":屏幕阅读器播报但不打断。
|
|
16
|
+
*
|
|
17
|
+
* 视觉:走 Workbench Design System 既有 token(`--dsw-*`)+ Badge/Banner 的四态语义色,
|
|
18
|
+
* 不引入第二套配色。
|
|
19
|
+
*/
|
|
20
|
+
import { useSyncExternalStore } from 'react'
|
|
21
|
+
import type { TranslateNS } from '../client-types.ts'
|
|
22
|
+
import { CheckIcon, CloseIcon, ErrorIcon, InfoIcon, WarnIcon } from './Icon.tsx'
|
|
23
|
+
import { IconButton } from './ui.tsx'
|
|
24
|
+
import { toastStore } from './toast-store.ts'
|
|
25
|
+
import type { ToastKind } from './toast-types.ts'
|
|
26
|
+
import css from '../config-manager.module.css'
|
|
27
|
+
|
|
28
|
+
export interface ToastViewportProps {
|
|
29
|
+
/** 关闭按钮的无障碍文案(由 Shell 注入的 i18n)。 */
|
|
30
|
+
t: TranslateNS<'config-manager'>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** kind → 图标(与 Badge / Banner 四态同源语义)。 */
|
|
34
|
+
function ToastGlyph({ kind }: { kind: ToastKind }) {
|
|
35
|
+
switch (kind) {
|
|
36
|
+
case 'ok': return <CheckIcon size={14} />
|
|
37
|
+
case 'warn': return <WarnIcon size={14} />
|
|
38
|
+
case 'error': return <ErrorIcon size={14} />
|
|
39
|
+
case 'info': return <InfoIcon size={14} />
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 右下角通知视口。无通知时渲染 null(不进 DOM)。
|
|
45
|
+
*/
|
|
46
|
+
export function ToastViewport({ t }: ToastViewportProps) {
|
|
47
|
+
const snapshot = useSyncExternalStore(toastStore.subscribe, toastStore.getSnapshot)
|
|
48
|
+
|
|
49
|
+
if (snapshot.items.length === 0) return null
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={css.toastViewport} role="status" aria-live="polite">
|
|
53
|
+
{snapshot.items.map((item) => (
|
|
54
|
+
<div
|
|
55
|
+
key={item.id}
|
|
56
|
+
className={css.toast}
|
|
57
|
+
data-kind={item.kind}
|
|
58
|
+
// 悬停暂停自动消失(读完再走);移开按原时长重新计时
|
|
59
|
+
onMouseEnter={() => { toastStore.pauseTimer(item.id) }}
|
|
60
|
+
onMouseLeave={() => { toastStore.resetTimer(item.id) }}
|
|
61
|
+
>
|
|
62
|
+
<span className={css.toastGlyph} aria-hidden="true"><ToastGlyph kind={item.kind} /></span>
|
|
63
|
+
<span className={css.toastText}>{item.text}</span>
|
|
64
|
+
<IconButton
|
|
65
|
+
icon={<CloseIcon size={12} />}
|
|
66
|
+
label={t('common.close')}
|
|
67
|
+
onClick={() => { toastStore.dismiss(item.id) }}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* toast-store 单测:状态机行为(去重 / 上限 / 计时 / 手动关闭 / 空文案)。
|
|
3
|
+
*
|
|
4
|
+
* 计时器全部走注入的假实现(`schedule`/`cancel`),因此测试**零真实等待**、
|
|
5
|
+
* 结果确定(不受 CI 负载影响)。同时用 subscribe 计数验证「无变更不通知订阅者」——
|
|
6
|
+
* 该性质直接决定 React 端不会因悬停暂停而重渲染。
|
|
7
|
+
*/
|
|
8
|
+
import test from 'node:test'
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
import { ToastStore, DEFAULT_DURATION_MS } from './toast-store.ts'
|
|
11
|
+
|
|
12
|
+
/** 可控的假计时器:手动 `runDue()` 推进。 */
|
|
13
|
+
function makeClock() {
|
|
14
|
+
let now = 0
|
|
15
|
+
let seq = 0
|
|
16
|
+
const pending = new Map<number, { at: number, fn: () => void }>()
|
|
17
|
+
return {
|
|
18
|
+
schedule: (fn: () => void, ms: number): number => {
|
|
19
|
+
const id = ++seq
|
|
20
|
+
pending.set(id, { at: now + ms, fn })
|
|
21
|
+
return id
|
|
22
|
+
},
|
|
23
|
+
cancel: (handle: unknown): void => { pending.delete(handle as number) },
|
|
24
|
+
/** 推进 now 毫秒并触发所有到期任务。 */
|
|
25
|
+
runDue(ms: number): void {
|
|
26
|
+
now += ms
|
|
27
|
+
for (const [id, task] of [...pending]) {
|
|
28
|
+
if (task.at <= now) {
|
|
29
|
+
pending.delete(id)
|
|
30
|
+
task.fn()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
get pendingCount(): number { return pending.size },
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeStore(maxVisible?: number) {
|
|
39
|
+
const clock = makeClock()
|
|
40
|
+
const store = new ToastStore({ maxVisible, schedule: clock.schedule, cancel: clock.cancel })
|
|
41
|
+
return { store, clock }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test('toast: push 入队并通知订阅者,getSnapshot 返回该条', () => {
|
|
45
|
+
const { store } = makeStore()
|
|
46
|
+
let notified = 0
|
|
47
|
+
store.subscribe(() => { notified += 1 })
|
|
48
|
+
|
|
49
|
+
const id = store.push('ok', '已保存')
|
|
50
|
+
|
|
51
|
+
assert.ok(id > 0)
|
|
52
|
+
assert.equal(notified, 1)
|
|
53
|
+
assert.deepEqual(store.getSnapshot().items.map((t) => t.text), ['已保存'])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('toast: 空/纯空白文案被忽略(不入队、不通知)', () => {
|
|
57
|
+
const { store } = makeStore()
|
|
58
|
+
let notified = 0
|
|
59
|
+
store.subscribe(() => { notified += 1 })
|
|
60
|
+
|
|
61
|
+
assert.equal(store.push('ok', ''), 0)
|
|
62
|
+
assert.equal(store.push('ok', ' \n\t '), 0)
|
|
63
|
+
assert.equal(store.getSnapshot().items.length, 0)
|
|
64
|
+
assert.equal(notified, 0, '空文案不得触发订阅通知');
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('toast: 同 kind+text 重复 push 不新增条目(防连点刷屏)', () => {
|
|
68
|
+
const { store } = makeStore()
|
|
69
|
+
const first = store.push('ok', '已导出')
|
|
70
|
+
const second = store.push('ok', '已导出')
|
|
71
|
+
|
|
72
|
+
assert.equal(first, second, '应复用同一条目 id')
|
|
73
|
+
assert.equal(store.getSnapshot().items.length, 1)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('toast: 同文案不同 kind 视为两条(语义不同)', () => {
|
|
77
|
+
const { store } = makeStore()
|
|
78
|
+
store.push('ok', '完成')
|
|
79
|
+
store.push('error', '完成')
|
|
80
|
+
assert.equal(store.getSnapshot().items.length, 2)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('toast: 到期自动消失', () => {
|
|
84
|
+
const { store, clock } = makeStore()
|
|
85
|
+
store.push('ok', '临时提示')
|
|
86
|
+
assert.equal(store.getSnapshot().items.length, 1)
|
|
87
|
+
|
|
88
|
+
clock.runDue(DEFAULT_DURATION_MS.ok - 1)
|
|
89
|
+
assert.equal(store.getSnapshot().items.length, 1, '未到期不得消失')
|
|
90
|
+
|
|
91
|
+
clock.runDue(1)
|
|
92
|
+
assert.equal(store.getSnapshot().items.length, 0, '到期应自动移除')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('toast: durationMs=0 表示常驻,不自动消失', () => {
|
|
96
|
+
const { store, clock } = makeStore()
|
|
97
|
+
store.push('error', '需手动关闭', 0)
|
|
98
|
+
|
|
99
|
+
clock.runDue(10 * 60 * 1000)
|
|
100
|
+
assert.equal(store.getSnapshot().items.length, 1, '常驻通知不得被计时清掉')
|
|
101
|
+
assert.equal(clock.pendingCount, 0, '常驻通知不应占用计时器')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('toast: 超过同屏上限挤掉最旧一条,并释放其计时器', () => {
|
|
105
|
+
const { store, clock } = makeStore(3)
|
|
106
|
+
store.push('ok', 'a')
|
|
107
|
+
store.push('ok', 'b')
|
|
108
|
+
store.push('ok', 'c')
|
|
109
|
+
assert.equal(store.getSnapshot().items.length, 3)
|
|
110
|
+
|
|
111
|
+
store.push('ok', 'd')
|
|
112
|
+
|
|
113
|
+
assert.deepEqual(store.getSnapshot().items.map((t) => t.text), ['b', 'c', 'd'])
|
|
114
|
+
// 4 条各自排定过计时;a 被挤掉后其计时器必须被取消 → 只剩 3 个
|
|
115
|
+
assert.equal(clock.pendingCount, 3, '被挤掉的条目必须清掉计时器(防泄漏)')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('toast: dismiss 手动关闭并清掉计时器', () => {
|
|
119
|
+
const { store, clock } = makeStore()
|
|
120
|
+
const id = store.push('ok', '关闭我')
|
|
121
|
+
assert.equal(clock.pendingCount, 1)
|
|
122
|
+
|
|
123
|
+
store.dismiss(id)
|
|
124
|
+
|
|
125
|
+
assert.equal(store.getSnapshot().items.length, 0)
|
|
126
|
+
assert.equal(clock.pendingCount, 0)
|
|
127
|
+
assert.equal(store.dismiss(id), undefined, '重复 dismiss 应为安全 no-op')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('toast: pauseTimer 取消计时但保留条目,resetTimer 重新计时', () => {
|
|
131
|
+
const { store, clock } = makeStore()
|
|
132
|
+
const id = store.push('info', '悬停暂停')
|
|
133
|
+
assert.equal(clock.pendingCount, 1)
|
|
134
|
+
|
|
135
|
+
// 悬停:只停表,不动条目、不产生新快照
|
|
136
|
+
let notified = 0
|
|
137
|
+
store.subscribe(() => { notified += 1 })
|
|
138
|
+
store.pauseTimer(id)
|
|
139
|
+
assert.deepEqual(store.getSnapshot().items.map((t) => t.text), ['悬停暂停'])
|
|
140
|
+
assert.equal(clock.pendingCount, 0)
|
|
141
|
+
assert.equal(notified, 0, '暂停不应触发重渲染')
|
|
142
|
+
|
|
143
|
+
clock.runDue(10 * 60 * 1000)
|
|
144
|
+
assert.equal(store.getSnapshot().items.length, 1, '暂停期间不得消失')
|
|
145
|
+
|
|
146
|
+
// 移开:按原时长重新计时
|
|
147
|
+
store.resetTimer(id)
|
|
148
|
+
assert.equal(clock.pendingCount, 1)
|
|
149
|
+
clock.runDue(DEFAULT_DURATION_MS.info)
|
|
150
|
+
assert.equal(store.getSnapshot().items.length, 0)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('toast: clear 清空全部并释放全部计时器', () => {
|
|
154
|
+
const { store, clock } = makeStore()
|
|
155
|
+
store.push('ok', 'a')
|
|
156
|
+
store.push('warn', 'b')
|
|
157
|
+
assert.equal(clock.pendingCount, 2)
|
|
158
|
+
|
|
159
|
+
store.clear()
|
|
160
|
+
|
|
161
|
+
assert.equal(store.getSnapshot().items.length, 0)
|
|
162
|
+
assert.equal(clock.pendingCount, 0)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('toast: getSnapshot 引用稳定(相同内容不产生新对象,避免 React 死循环)', () => {
|
|
166
|
+
const { store } = makeStore()
|
|
167
|
+
const before = store.getSnapshot()
|
|
168
|
+
assert.equal(store.getSnapshot(), before, '无变更时快照引用必须一致')
|
|
169
|
+
|
|
170
|
+
store.push('ok', 'x')
|
|
171
|
+
const after = store.getSnapshot()
|
|
172
|
+
assert.notEqual(after, before, '有变更时应换新引用')
|
|
173
|
+
|
|
174
|
+
store.pauseTimer(store.getSnapshot().items[0]!.id)
|
|
175
|
+
assert.equal(store.getSnapshot(), after, '暂停计时不改变快照')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('toast: 订阅退订后不再收到通知', () => {
|
|
179
|
+
const { store } = makeStore()
|
|
180
|
+
let notified = 0
|
|
181
|
+
const off = store.subscribe(() => { notified += 1 })
|
|
182
|
+
store.push('ok', 'one')
|
|
183
|
+
off()
|
|
184
|
+
store.push('ok', 'two')
|
|
185
|
+
assert.equal(notified, 1)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('toast: 默认时长按语义分级(错误比成功停留更久)', () => {
|
|
189
|
+
assert.ok(DEFAULT_DURATION_MS.error > DEFAULT_DURATION_MS.ok)
|
|
190
|
+
assert.ok(DEFAULT_DURATION_MS.warn > DEFAULT_DURATION_MS.ok)
|
|
191
|
+
assert.equal(DEFAULT_DURATION_MS.ok > 0, true, '成功提示须自动消失')
|
|
192
|
+
})
|