@illli-studio/dsh-mory 0.0.0-stage → 0.1.2
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 +17 -0
- package/README.md +254 -2
- package/assets/memory-protocol.mjs +88 -0
- package/assets/mory-logo-premium.svg +51 -0
- package/assets/source.json +6 -0
- package/assets/website-i18n.tsx +106 -0
- package/assets/website-styles.css +7451 -0
- package/assets/website-theme.tsx +65 -0
- package/client.js +459 -0
- package/cordis.patch.yml +4 -0
- package/docs/source-assets.md +62 -0
- package/icon.svg +51 -0
- package/index.js +124 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +32 -4
- package/protocol.mjs +88 -0
- package/scripts/build-client.mjs +172 -0
- package/service.mjs +165 -0
- package/settings.mjs +294 -0
- package/src/client.js +458 -0
- package/test/service.mjs +395 -0
- package/test/settings.mjs +278 -0
package/src/client.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@illli-studio/dsh-mory',
|
|
3
|
+
factory(require) {
|
|
4
|
+
const React = require('react');
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
const css = __MORY_STYLES__;
|
|
7
|
+
const logo = __MORY_LOGO__;
|
|
8
|
+
const themes = __MORY_THEMES__;
|
|
9
|
+
const kinds = ['note', 'chat', 'bill', 'finance', 'task', 'waiting', 'bookmark', 'file', 'decision', 'log'];
|
|
10
|
+
const en = {
|
|
11
|
+
name: 'Mory', repository: 'Local Memory Repository', recordTypes: 'Record types', all: 'All memories',
|
|
12
|
+
add: 'New memory', refresh: 'Refresh', loading: 'Loading memories…', reading: 'Reading your local repository.',
|
|
13
|
+
empty: 'Your memory repository starts here', emptyHint: 'Save an idea, a useful detail, or a decision you want to keep.',
|
|
14
|
+
noMatch: 'No matching memories', clear: 'Clear filters', search: 'Search your memories…', allSources: 'All sources',
|
|
15
|
+
allTime: 'Any time', today: 'Today', week: 'This week', month: 'This month', topics: 'Topics', objects: 'memories',
|
|
16
|
+
allPrivacy: 'All visibility', public: 'Public', private: 'Private', tag: 'Filter by tag', project: 'Project ID',
|
|
17
|
+
filters: 'Filters', time: 'Time', entity: 'Source', content: 'Memory content', category: 'Category', lifecycle: 'Lifecycle',
|
|
18
|
+
active: 'Active', localDevice: 'Local device', local: 'Local', connected: 'Connected', offline: 'Not connected',
|
|
19
|
+
title: 'Title', tags: 'Tags (comma separated)', kind: 'Kind', save: 'Save memory', cancel: 'Cancel', close: 'Close',
|
|
20
|
+
edit: 'Edit', remove: 'Delete', detail: 'Memory details', revision: 'Revision', markPrivate: 'Mark as private',
|
|
21
|
+
privateHint: 'A display flag; connected AI tools can still read this memory.',
|
|
22
|
+
confirm: 'Delete this memory?', confirmHint: 'It will disappear from active memories. Mory keeps a deletion record.',
|
|
23
|
+
confirmDelete: 'Delete memory', saved: 'Memory saved.', deleted: 'Memory deleted.', unsaved: 'Save or cancel your changes before leaving.',
|
|
24
|
+
conflict: 'This memory changed elsewhere. Your draft is preserved. Cancel and reopen it to read the latest version.',
|
|
25
|
+
setup: 'Connect your local Mory', setupHint: 'Run these commands on the computer hosting DSH, then refresh.',
|
|
26
|
+
shared: 'The same memories, here and in your local Mory web console.', back: 'Back to conversation',
|
|
27
|
+
theme: 'Switch between the original light and midnight themes', failed: 'Mory request failed.',
|
|
28
|
+
previous: 'Previous page', next: 'Next page', pageSize: 'Per page', of: 'of', page: 'Page',
|
|
29
|
+
tooLarge: 'This record exceeds the editor limit. Please edit it in the Mory web console.',
|
|
30
|
+
catalogLimit: 'This project contains more than 10000 memories. Enter a project ID to narrow the repository.',
|
|
31
|
+
searchLimit: 'Showing up to 50 ranked search results.',
|
|
32
|
+
note: 'Note', chat: 'Chat', bill: 'Bill', finance: 'Finance', task: 'Task', waiting: 'Waiting', bookmark: 'Bookmark', file: 'File', decision: 'Decision', log: 'Log',
|
|
33
|
+
manual: 'Manual', dsh: 'DSH', browser: 'Browser', clipboard: 'Clipboard', github: 'GitHub', gitee: 'Gitee',
|
|
34
|
+
};
|
|
35
|
+
const zh = {
|
|
36
|
+
name: 'Mory', repository: '本地记忆仓库', recordTypes: '记录类型', all: '全部记忆',
|
|
37
|
+
add: '新增记忆', refresh: '刷新', loading: '正在加载记忆…', reading: '正在读取你的本地记忆仓库。',
|
|
38
|
+
empty: '从这里开始,留下你的记忆', emptyHint: '记录一个灵感、一段有用的信息,或一个值得保留的决定。',
|
|
39
|
+
noMatch: '没有找到匹配的记忆', clear: '清除筛选', search: '搜索你的记忆…', allSources: '全部来源',
|
|
40
|
+
allTime: '全部时间', today: '今天', week: '本周', month: '本月', topics: '个主题', objects: '条记忆',
|
|
41
|
+
allPrivacy: '全部可见性', public: '公开', private: '私密', tag: '筛选标签', project: '项目 ID',
|
|
42
|
+
filters: '筛选', time: '时间', entity: '来源', content: '记忆内容', category: '分类', lifecycle: '生命周期',
|
|
43
|
+
active: '活跃', localDevice: '本地设备', local: '本地', connected: '已连接', offline: '未连接',
|
|
44
|
+
title: '标题', tags: '标签(逗号分隔)', kind: '类型', save: '保存记忆', cancel: '取消', close: '关闭',
|
|
45
|
+
edit: '编辑', remove: '删除', detail: '记忆详情', revision: '版本', markPrivate: '标记为私密',
|
|
46
|
+
privateHint: '这是展示标记;已连接的 AI 工具仍可读取这条记忆。',
|
|
47
|
+
confirm: '删除这条记忆?', confirmHint: '它将从有效记忆中移除,Mory 会保留删除记录。',
|
|
48
|
+
confirmDelete: '确认删除', saved: '记忆已保存。', deleted: '记忆已删除。', unsaved: '请先保存或取消当前修改。',
|
|
49
|
+
conflict: '这条记忆已在其他位置修改。草稿已保留,请取消后重新打开以读取最新版本。',
|
|
50
|
+
setup: '连接你的本地 Mory', setupHint: '在运行 DSH 的电脑上执行以下命令,然后刷新。',
|
|
51
|
+
shared: '与本地 Mory 网页版,共用同一个记忆仓库。', back: '返回对话',
|
|
52
|
+
theme: '切换原站的浅色与午夜主题', failed: 'Mory 请求失败。',
|
|
53
|
+
previous: '上一页', next: '下一页', pageSize: '每页', of: '/', page: '页',
|
|
54
|
+
tooLarge: '这条记忆超过编辑器限制,请到 Mory 网页版编辑。',
|
|
55
|
+
catalogLimit: '此项目超过 10000 条记忆,请输入项目 ID 缩小仓库范围。',
|
|
56
|
+
searchLimit: '最多显示 50 条按相关度排序的搜索结果。',
|
|
57
|
+
note: '笔记', chat: '聊天', bill: '账单', finance: '财务', task: '任务', waiting: '等待', bookmark: '书签', file: '文件', decision: '决定', log: '日志',
|
|
58
|
+
manual: '手动', dsh: 'DSH', browser: '浏览器', clipboard: '剪贴板', github: 'GitHub', gitee: 'Gitee',
|
|
59
|
+
};
|
|
60
|
+
Object.assign(en, {
|
|
61
|
+
settings: 'Settings', settingsHint: 'Manage your repository, connections and preferences.', returnMemories: 'Back to memories',
|
|
62
|
+
syncSettings: 'Memory Sync', languageSettings: 'Language', themeSettings: 'Appearance', healthSettings: 'Health', connectionSettings: 'Mory connection',
|
|
63
|
+
languageHint: 'Choose a language for Mory. This does not change the rest of DSH.', followDSH: 'Follow DSH', appearanceHint: 'The original Mory themes, applied only to this panel.',
|
|
64
|
+
connectionHint: 'Connect to Mory on the DSH host, or to your own HTTPS service.', apiUrl: 'API address', accessToken: 'Access token', storedToken: 'Token saved — leave blank to keep it', tokenOptional: 'Leave blank to use the local Mory configuration', saveSettings: 'Save settings', settingsSaved: 'Settings saved.', checkConnection: 'Check connection', checking: 'Checking…', connectionOK: 'Connection successful', autoDetect: 'Use local auto-detection',
|
|
65
|
+
connectionPrecedence: 'Advanced DSH plugin configuration and environment variables take priority over these preferences.',
|
|
66
|
+
provider: 'Provider', repositoryUrl: 'Repository URL', branch: 'Branch', filePath: 'Snapshot path', deviceName: 'Device name',
|
|
67
|
+
syncHint: 'Use a private GitHub or Gitee repository to keep memories available across devices.', syncPrivacy: 'Sync includes private memories and deletion records. Use a private repository. Tokens stay on the DSH host and are never returned to this page.',
|
|
68
|
+
autoSync: 'Automatic backup', autoSyncHint: 'Merge and back up after future changes made through this plugin or its agent tool. Saving settings does not upload memories.',
|
|
69
|
+
mergePush: 'Merge & push', pushRemote: 'Push', pullRemote: 'Pull', syncing: 'Sync in progress…', syncDone: 'Sync completed.', lastSync: 'Last sync', neverSynced: 'No sync has run yet',
|
|
70
|
+
healthHint: 'Check the actual Mory service connection and repository status.', serviceAddress: 'Service address', totalMemories: 'Total memories', connectionStatus: 'Connection status',
|
|
71
|
+
settingsConflict: 'Settings changed elsewhere. Reload settings before saving again.', reloadSettings: 'Reload settings', unsavedSettings: 'Save your connection details before checking or syncing.',
|
|
72
|
+
});
|
|
73
|
+
Object.assign(zh, {
|
|
74
|
+
settings: '设置', settingsHint: '管理记忆仓库、服务连接和界面偏好。', returnMemories: '返回记忆库',
|
|
75
|
+
syncSettings: '记忆同步', languageSettings: '语言', themeSettings: '外观主题', healthSettings: '健康状态', connectionSettings: 'Mory 连接',
|
|
76
|
+
languageHint: '选择 Mory 的界面语言,不会改变 DSH 其他页面的语言。', followDSH: '跟随 DSH', appearanceHint: '沿用 Mory 原站的主题,仅应用于当前插件。',
|
|
77
|
+
connectionHint: '连接 DSH 所在电脑上的 Mory,或你自己的 HTTPS 服务。', apiUrl: 'API 地址', accessToken: '访问令牌', storedToken: '已保存令牌,留空保持不变', tokenOptional: '留空使用本地 Mory 配置', saveSettings: '保存设置', settingsSaved: '设置已保存。', checkConnection: '检测连接', checking: '检测中…', connectionOK: '连接成功', autoDetect: '自动检测本地服务',
|
|
78
|
+
connectionPrecedence: 'DSH 插件高级配置和环境变量的优先级高于这里保存的偏好。',
|
|
79
|
+
provider: '服务商', repositoryUrl: '仓库地址', branch: '分支', filePath: '快照路径', deviceName: '设备名称',
|
|
80
|
+
syncHint: '使用 GitHub 或 Gitee 私有仓库,在多台设备间保留记忆。', syncPrivacy: '同步会包含私密记忆和删除记录,请使用私有仓库。令牌保存在 DSH 后端,读取设置时不会返回到页面。',
|
|
81
|
+
autoSync: '自动备份', autoSyncHint: '之后通过此插件或 AI 工具修改记忆时,自动合并备份。保存设置本身不会上传记忆。',
|
|
82
|
+
mergePush: '合并并推送', pushRemote: '推送', pullRemote: '拉取', syncing: '正在同步…', syncDone: '同步完成。', lastSync: '最近同步', neverSynced: '尚未执行同步',
|
|
83
|
+
healthHint: '检测真实的 Mory 服务连接与记忆仓库状态。', serviceAddress: '服务地址', totalMemories: '记忆总数', connectionStatus: '连接状态',
|
|
84
|
+
settingsConflict: '设置已在其他位置修改,请重新读取后再保存。', reloadSettings: '重新读取设置', unsavedSettings: '请先保存连接信息,再检测或同步。',
|
|
85
|
+
});
|
|
86
|
+
for (const item of themes) {
|
|
87
|
+
en[`themeName.${item.id}`] = item.en.name; en[`themeHint.${item.id}`] = item.en.hint;
|
|
88
|
+
zh[`themeName.${item.id}`] = item.zh.name; zh[`themeHint.${item.id}`] = item.zh.hint;
|
|
89
|
+
}
|
|
90
|
+
const adaptation = `
|
|
91
|
+
@scope (.mory-app) {
|
|
92
|
+
:scope { min-height:0!important; height:100%; overflow:auto!important; color:var(--text); background:var(--bg); font-size:14px; line-height:1.5; }
|
|
93
|
+
*,*::before,*::after {box-sizing:border-box}
|
|
94
|
+
h1,h2,h3,p {margin:0} button,input,select,textarea {font:inherit} button {cursor:pointer} button:disabled {opacity:.45;cursor:default}
|
|
95
|
+
[data-slot=button] {display:inline-flex;align-items:center;justify-content:center;gap:8px;flex-shrink:0;min-height:40px;border:1px solid transparent;border-radius:var(--radius-ui);padding:8px 14px;font-size:13px;font-weight:500;white-space:nowrap;background:hsl(var(--primary));color:hsl(var(--primary-foreground));transition:background .15s,color .15s,border-color .15s}
|
|
96
|
+
[data-slot=button][data-variant=ghost] {background:transparent;color:var(--text)}
|
|
97
|
+
[data-slot=button][data-variant=outline] {background:var(--surface);color:var(--text);border-color:var(--border)}
|
|
98
|
+
[data-slot=button][data-variant=secondary] {background:var(--surface-muted);color:var(--text);border-color:var(--border-soft)}
|
|
99
|
+
[data-slot=button][data-variant=destructive] {background:var(--danger);color:var(--surface)}
|
|
100
|
+
[data-slot=button]:hover:not(:disabled) {filter:brightness(.96)}
|
|
101
|
+
:focus-visible {outline:2px solid var(--accent);outline-offset:3px}
|
|
102
|
+
[data-slot=input]:not([type=checkbox]),[data-slot=textarea],select {width:100%;min-width:0;min-height:42px;border:1px solid var(--border-soft);border-radius:12px;background:var(--surface);color:var(--text);padding:10px 12px;font-size:13px}
|
|
103
|
+
input::placeholder,textarea::placeholder {color:var(--muted)}
|
|
104
|
+
.product-shell {min-height:100%;padding:20px 24px 32px}
|
|
105
|
+
.product-header {width:100%;max-width:1560px;min-height:58px;margin:0 auto 18px;padding:0 4px;gap:16px}
|
|
106
|
+
.product-header .brand-lockup {opacity:1!important;pointer-events:auto!important;min-width:0}
|
|
107
|
+
.product-header .brand-lockup strong {font-size:19px;font-weight:700}
|
|
108
|
+
.product-header .brand-lockup span {font-size:12px;color:var(--muted)}
|
|
109
|
+
.product-header .brand-glyph.logo-mark {height:44px;width:44px;flex-shrink:0}
|
|
110
|
+
.mory-original-logo {width:44px;height:44px;display:block}
|
|
111
|
+
.header-actions {gap:8px;max-width:none}
|
|
112
|
+
.dsh-icon-button {width:38px!important;height:38px!important;min-height:38px!important;padding:0!important;border-radius:12px!important}
|
|
113
|
+
.status-chip {min-width:0;height:34px;font-weight:500;font-size:12px}
|
|
114
|
+
.dsh-status-dot {width:6px;height:6px;flex:none;border-radius:50%;background:currentColor}
|
|
115
|
+
.workspace-layout {display:grid!important;grid-template-columns:184px minmax(0,1fr);gap:18px;width:100%;max-width:1560px}
|
|
116
|
+
.workspace-nav {display:block!important;top:16px;border-radius:20px;padding:14px 10px;box-shadow:none}
|
|
117
|
+
.nav-section-label {font-size:11px;letter-spacing:.05em;margin:8px 10px 14px;color:var(--muted)}
|
|
118
|
+
.workspace-nav .record-nav-list {display:flex;flex-direction:column}
|
|
119
|
+
.workspace-nav .nav-section-label {display:block}
|
|
120
|
+
.record-nav-list button {display:flex;width:100%;min-height:40px;padding:8px 10px;gap:10px;justify-content:flex-start;font-weight:500}
|
|
121
|
+
.record-nav-list button span {flex:1;text-align:left}
|
|
122
|
+
.record-nav-list button strong {font-size:11px;min-width:22px;text-align:center;font-weight:500;opacity:.7}
|
|
123
|
+
.record-nav-list button.active {font-weight:600}
|
|
124
|
+
.sidebar-footer {font-size:11px;line-height:1.7;color:var(--muted);padding:14px 8px 2px}
|
|
125
|
+
.workspace-main {min-width:0}.feed-section {min-height:520px;border:1px solid var(--border);border-radius:20px;padding:20px;box-shadow:none;background:var(--surface)}
|
|
126
|
+
.section-heading {display:flex;gap:12px;align-items:center;margin:0 0 20px;flex-wrap:wrap}
|
|
127
|
+
.section-heading .capture-cta {margin:0;min-height:42px;box-shadow:none;border-radius:12px}
|
|
128
|
+
.feed-summary {margin-left:auto;font-size:12px;gap:14px;color:var(--muted);display:flex;align-items:center}
|
|
129
|
+
.feed-summary strong {font-size:14px;color:var(--text);font-weight:600}
|
|
130
|
+
.dsh-controls {display:grid;grid-template-columns:minmax(150px,1fr) 130px;gap:10px;margin-bottom:12px}
|
|
131
|
+
.dsh-controls .search-input {display:flex;align-items:center;min-width:0;gap:10px;background:var(--surface);border:1px solid var(--border-soft);border-radius:12px;padding:0 12px;box-shadow:0 2px 6px color-mix(in srgb,var(--text) 3%,transparent)}
|
|
132
|
+
.dsh-controls .search-input [data-slot=input] {height:42px;min-height:42px;padding:0!important;border:0!important;background:transparent!important;box-shadow:none!important;outline:0;border-radius:0!important}
|
|
133
|
+
.dsh-controls .search-input:focus-within {outline:2px solid var(--accent);outline-offset:2px}
|
|
134
|
+
.dsh-filter-row {display:flex;gap:8px;flex-wrap:wrap;margin-bottom:18px;align-items:center}
|
|
135
|
+
.dsh-filter-row select {width:auto;flex:1;min-width:110px;max-width:180px;min-height:36px;padding:7px 10px;border-radius:10px;font-size:12px}
|
|
136
|
+
.dsh-filter-row input {flex:1;min-width:100px;max-width:180px;min-height:36px!important;height:36px;padding:7px 10px!important;border-radius:10px!important;font-size:12px}
|
|
137
|
+
.dsh-filter-row .dsh-filter-label {display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);margin-right:2px}
|
|
138
|
+
.memory-list-header,.memory-card {grid-template-columns:70px 100px minmax(120px,1fr) 80px 60px!important;gap:12px;padding:12px 10px;min-width:0}
|
|
139
|
+
.memory-list-header {display:grid;font-weight:500;font-size:11px;letter-spacing:0;text-transform:none;min-height:40px}
|
|
140
|
+
.memory-card {min-height:82px;cursor:pointer;outline-offset:-2px}
|
|
141
|
+
.memory-title-button {display:block!important;width:100%;font-size:13px;font-weight:600;min-height:0!important;padding:0!important;white-space:nowrap;text-align:left;justify-content:flex-start;border:0!important;border-radius:0!important}
|
|
142
|
+
.memory-content-cell p {margin-top:5px;font-size:12px;font-weight:400;line-height:1.5}
|
|
143
|
+
.memory-time {font-size:11px;line-height:1.6}.memory-entity strong {font-size:12px;font-weight:500}.memory-entity span {font-size:11px}
|
|
144
|
+
.memory-category .dsh-kind {padding:4px 8px!important;min-height:0!important;font-size:11px!important;font-weight:500!important;border-radius:8px!important;max-width:100%;background:var(--surface-muted)!important}
|
|
145
|
+
.memory-category>span {font-size:10px}.memory-life [data-slot=badge] {display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:500;border-radius:6px;padding:4px 7px;white-space:nowrap}
|
|
146
|
+
.empty-feed {min-height:330px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:14px;padding:48px 24px;text-align:center;background:transparent;border:0;border-radius:0}
|
|
147
|
+
.empty-feed svg {color:var(--accent);opacity:.65}.empty-feed h3 {font-size:17px;font-weight:600;color:var(--text)}.empty-feed p {font-size:13px;line-height:1.8;color:var(--muted);max-width:340px}
|
|
148
|
+
.dsh-notice {display:flex;align-items:center;gap:10px;padding:12px 14px;border:1px solid var(--border-soft);border-radius:12px;font-size:12px;line-height:1.7;margin-bottom:14px;background:var(--surface-muted);overflow-wrap:anywhere}
|
|
149
|
+
.dsh-notice.error {color:var(--danger)}.dsh-notice.success {color:var(--accent)}
|
|
150
|
+
.dsh-setup {padding:24px;border:1px solid var(--border);border-radius:16px;margin-bottom:16px}.dsh-setup h2 {font-size:18px;margin-bottom:8px}.dsh-setup p {font-size:13px;color:var(--muted);line-height:1.7}.dsh-setup pre {padding:16px;border-radius:12px;background:var(--surface-muted);font-size:12px;line-height:1.8;white-space:pre-wrap}
|
|
151
|
+
.memory-pagination {padding-top:18px;gap:10px;font-size:11px;justify-content:space-between;flex-wrap:wrap}.page-size-options,.page-navigation {border:0;padding:0;margin:0;gap:6px}.page-size-options button,.page-navigation button {width:30px;min-width:30px;min-height:30px;height:30px;font-size:11px;padding:0}
|
|
152
|
+
.dsh-search-limit {font-size:11px;color:var(--muted);margin:10px 0}
|
|
153
|
+
.dsh-footer {margin:16px 4px 0;font-size:11px;color:var(--muted);line-height:1.7}
|
|
154
|
+
dialog.dsh-dialog {position:fixed;inset:0;margin:auto;transform:none;width:min(720px,calc(100vw - 40px));max-width:none;max-height:calc(100dvh - 64px);padding:0;border:1px solid var(--border);border-radius:20px;background:var(--surface);color:var(--text);box-shadow:var(--shadow-menu);overflow:auto}
|
|
155
|
+
dialog.dsh-dialog::backdrop {background:color-mix(in srgb,var(--text) 24%,transparent);backdrop-filter:blur(3px)}
|
|
156
|
+
.dsh-dialog-header {display:flex;justify-content:space-between;align-items:flex-start;gap:16px;padding:24px;border-bottom:1px solid var(--border-soft)}
|
|
157
|
+
.dsh-dialog-header .eyebrow {font-size:11px;letter-spacing:.06em;color:var(--accent);display:flex;align-items:center;gap:7px;margin-bottom:8px}.dsh-dialog-header h2 {font-size:20px;font-weight:600;line-height:1.5;overflow-wrap:anywhere}
|
|
158
|
+
.dsh-dialog-body {padding:24px;display:flex;flex-direction:column;gap:16px}.dsh-dialog-body label:not(.dsh-checkbox) {display:flex;flex-direction:column;gap:8px;font-size:12px;color:var(--muted)}
|
|
159
|
+
.dsh-edit-form {display:flex;flex-direction:column;gap:16px}
|
|
160
|
+
.dsh-form-row {display:grid;grid-template-columns:1fr 1fr;gap:14px}.dsh-dialog-body textarea {resize:vertical;min-height:220px;line-height:1.8;color:var(--text)}
|
|
161
|
+
.dsh-checkbox {display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text)}.dsh-checkbox input {accent-color:var(--accent)}
|
|
162
|
+
.dsh-help {font-size:11px;line-height:1.7;color:var(--muted)}.dsh-detail-text {font-size:14px;line-height:1.9;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
163
|
+
.dsh-meta {display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--muted)}.dsh-meta span {padding:4px 8px;background:var(--surface-muted);border-radius:6px}
|
|
164
|
+
.dsh-dialog-footer {display:flex;justify-content:flex-end;align-items:center;gap:8px;flex-wrap:wrap;padding:18px 24px;border-top:1px solid var(--border-soft)}.dsh-dialog-footer .dsh-delete-button {margin-right:auto;color:var(--danger)}
|
|
165
|
+
@container mory (max-width:1000px) {.workspace-layout{grid-template-columns:160px minmax(0,1fr)}.memory-list-header,.memory-card{grid-template-columns:64px minmax(130px,1fr) 74px 56px!important;gap:10px}.memory-entity,.dsh-entity-label{display:none!important}.feed-section{padding:16px}.product-shell{padding:18px}}
|
|
166
|
+
@container mory (min-width:761px) and (max-width:1000px) {.memory-card .memory-time{grid-column:1!important;grid-row:1!important;width:auto!important}.memory-card .memory-content-cell{grid-column:2!important;grid-row:1!important}.memory-card .memory-content-cell p{display:block!important}.memory-card .memory-category{grid-column:3!important;grid-row:1!important;display:grid!important}.memory-card .memory-life{grid-column:4!important;grid-row:1!important}}
|
|
167
|
+
@container mory (max-width:760px) {.workspace-layout{display:block!important}.workspace-nav{display:none!important}.dsh-mobile-kind{display:block!important}.product-shell{padding:14px}.product-header{margin-bottom:14px}.product-header .brand-lockup span{display:none}.feed-section{border-radius:16px}.header-actions{flex-wrap:nowrap;max-width:none!important}.header-actions .status-chip{display:none}.feed-summary{gap:8px}.dsh-controls{grid-template-columns:minmax(120px,1fr) 115px}.memory-list-header{display:none!important}.memory-card{grid-template-columns:minmax(110px,1fr) 62px 56px!important;min-height:70px;padding:12px 4px}.memory-card .memory-content-cell{grid-column:1!important;grid-row:1!important;display:block!important}.memory-card .memory-content-cell p{display:block!important}.memory-card .memory-time{grid-column:2!important;grid-row:1!important;width:auto!important}.memory-card .memory-life{grid-column:3!important;grid-row:1!important}.memory-category{display:none!important}.memory-pagination{justify-content:space-between!important;flex-wrap:wrap!important}.dsh-filter-row select,.dsh-filter-row input{max-width:none;flex:1 1 110px}.dsh-filter-label{display:none!important}.product-header .brand-lockup{gap:8px}.dsh-dialog-body,.dsh-dialog-header{padding:20px}.dsh-form-row{grid-template-columns:1fr}.dsh-dialog-footer{padding:16px 20px}}
|
|
168
|
+
.dsh-mobile-kind{display:none}
|
|
169
|
+
.feed-section[hidden] {display:none!important}
|
|
170
|
+
.dsh-settings-page {min-height:520px;border:1px solid var(--border);border-radius:20px;padding:24px;background:var(--surface);box-shadow:none}
|
|
171
|
+
.dsh-settings-page .settings-page-header {display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:24px}
|
|
172
|
+
.dsh-settings-page .settings-page-header h1 {font-size:24px;line-height:1.4;font-weight:600}.dsh-settings-page .settings-page-header p {font-size:13px;color:var(--muted);margin-top:8px;line-height:1.7}
|
|
173
|
+
.dsh-settings-page .settings-tabs {display:flex;flex-wrap:wrap;gap:6px;padding:0 0 16px;margin-bottom:24px;border-bottom:1px solid var(--border-soft);overflow:visible}
|
|
174
|
+
.dsh-settings-page .settings-tabs button {padding:9px 12px;min-height:38px;border-radius:10px;background:transparent;color:var(--muted);font-weight:500}
|
|
175
|
+
.dsh-settings-page .settings-tabs button.active {background:var(--surface-muted);color:var(--accent);border-color:var(--border-soft)}
|
|
176
|
+
.dsh-settings-panel {display:flex;flex-direction:column;gap:18px}.dsh-settings-panel>p {color:var(--muted);font-size:13px;line-height:1.8}
|
|
177
|
+
.dsh-settings-form {display:flex;flex-direction:column;gap:16px}.dsh-settings-form label:not(.dsh-checkbox) {display:flex;flex-direction:column;gap:8px;font-size:12px;color:var(--muted)}
|
|
178
|
+
.dsh-settings-form input,.dsh-settings-form select {color:var(--text)}.dsh-settings-actions {display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
179
|
+
.dsh-settings-form .dsh-checkbox {align-items:flex-start;padding:14px;border:1px solid var(--border-soft);border-radius:12px;background:var(--surface-muted)}.dsh-settings-form .dsh-checkbox input {margin-top:4px}.dsh-settings-form .dsh-checkbox strong,.dsh-settings-form .dsh-checkbox small {display:block}.dsh-settings-form .dsh-checkbox strong {font-weight:500}.dsh-settings-form .dsh-checkbox small {font-size:11px;color:var(--muted);line-height:1.7;margin-top:4px}
|
|
180
|
+
.dsh-settings-page .theme-options {display:grid!important;grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr))!important;gap:12px!important;max-height:none!important}
|
|
181
|
+
.dsh-settings-page .theme-option {min-height:96px;display:flex;align-items:center;text-align:left;gap:12px;padding:16px;border:1px solid var(--border-soft);border-radius:14px;background:var(--surface);color:var(--text)}
|
|
182
|
+
.dsh-settings-page .theme-option.selected {border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}.dsh-settings-page .theme-swatches {display:flex;gap:3px;flex-shrink:0}.dsh-settings-page .theme-swatches span {width:15px;height:36px;border-radius:5px;border:1px solid color-mix(in srgb,var(--text) 10%,transparent)}
|
|
183
|
+
.dsh-settings-page .theme-option-copy {min-width:0;flex:1;display:flex;flex-direction:column;gap:5px}.dsh-settings-page .theme-option-copy strong {font-size:13px;font-weight:500}.dsh-settings-page .theme-option-copy small {font-size:11px;line-height:1.6;color:var(--muted)}.dsh-settings-page .theme-check {width:16px;color:var(--accent)}
|
|
184
|
+
.dsh-health-grid {display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.dsh-health-metric {padding:20px 16px;background:var(--surface-muted);border:1px solid var(--border-soft);border-radius:14px}.dsh-health-metric strong {display:block;font-size:26px;font-weight:500;color:var(--text)}.dsh-health-metric span {font-size:12px;color:var(--muted)}
|
|
185
|
+
.dsh-health-details {display:grid;grid-template-columns:minmax(100px,.4fr) minmax(0,1fr);gap:10px 16px;font-size:12px;line-height:1.8}.dsh-health-details dt {color:var(--muted)}.dsh-health-details dd {margin:0;overflow-wrap:anywhere;color:var(--text)}
|
|
186
|
+
.dsh-settings-link {width:100%;justify-content:flex-start!important;gap:8px}.dsh-settings-link.active {color:var(--accent)!important;background:var(--surface-muted)!important}
|
|
187
|
+
@container mory (max-width:760px) {.dsh-settings-page{padding:18px 16px;border-radius:16px}.dsh-settings-page .settings-page-header{flex-direction:column;margin-bottom:18px}.dsh-settings-page .settings-tabs{gap:4px;margin-bottom:18px}.dsh-settings-page .settings-tabs button{padding:8px 10px;font-size:12px}.dsh-health-grid{gap:8px}.dsh-health-metric{padding:14px 10px}.dsh-health-metric strong{font-size:22px}.dsh-health-details{grid-template-columns:1fr;gap:4px}.dsh-health-details dd{margin-bottom:8px}}
|
|
188
|
+
@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
const paths = {
|
|
192
|
+
archive: ['M3 5h18v4H3z', 'M5 9v12h14V9', 'M10 13h4'], note: ['M5 3h14v18H5z', 'M9 8h6M9 12h6M9 16h4'],
|
|
193
|
+
chat: ['M21 11a8 8 0 0 1-8 8H6l-4 3V11a9 9 0 0 1 19 0Z'], bill: ['M6 3h12v18l-3-2-3 2-3-2-3 2V3Z', 'M9 8h6M9 12h4'],
|
|
194
|
+
finance: ['M3 9h18L12 3 3 9Z', 'M5 10v8M10 10v8M15 10v8M20 10v8M3 21h18'], task: ['M9 3H5v18h14V7', 'm9 8 3 3 8-8'],
|
|
195
|
+
waiting: ['M6 3h12M6 21h12M8 3v5l4 4 4-4V3M8 21v-5l4-4 4 4v5'], bookmark: ['M6 3h12v18l-6-4-6 4V3Z'], file: ['M5 3h9l5 5v13H5V3Z', 'M14 3v6h5'],
|
|
196
|
+
decision: ['M4 4h16v16H4z', 'm8 12 3 3 6-7'], log: ['M4 3h16v18H4z', 'M8 7h8M8 12h8M8 17h4'],
|
|
197
|
+
search: ['M19 19 15 15', 'M16 10a6 6 0 1 1-12 0 6 6 0 0 1 12 0Z'], refresh: ['M20 8a8 8 0 1 0 0 8', 'M20 3v5h-5'],
|
|
198
|
+
close: ['m6 6 12 12M18 6 6 18'], sun: ['M12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z', 'M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1 1M18 18l1 1M5 19l1-1M18 6l1-1'],
|
|
199
|
+
settings: ['M9 3h6l1 3 3 1 2 5-2 5-3 1-1 3H9l-1-3-3-1-2-5 2-5 3-1 1-3Z', 'M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z'], cloud: ['M6 18a4 4 0 0 1-1-8 7 7 0 0 1 13 1 4 4 0 0 1 0 8H6Z'], globe: ['M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z', 'M3 12h18M12 3a17 17 0 0 0 0 18M12 3a17 17 0 0 1 0 18'], health: ['M3 12h4l3-7 4 14 3-7h4'], connection: ['m8 12 8-8M5 8l5 5-5 5M16 12l3-3M13 5l6 6'], moon: ['M21 13A9 9 0 0 1 11 3a9 9 0 1 0 10 10Z'], back: ['m10 5-7 7 7 7', 'M4 12h17'], filter: ['M4 7h16M7 12h10M10 17h4'], plus: ['M12 5v14M5 12h14'],
|
|
200
|
+
};
|
|
201
|
+
function Icon({ name = 'archive', size = 18 }) { return h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true }, ...(paths[name] || paths.note).map((d, i) => h('path', { d, key: i }))); }
|
|
202
|
+
function Modal({ title, eyebrow, closeLabel, children, footer, onClose, busy }) {
|
|
203
|
+
const ref = React.useRef(null);
|
|
204
|
+
const closeRef = React.useRef(onClose); closeRef.current = onClose;
|
|
205
|
+
React.useEffect(() => { const dialog = ref.current; dialog.showModal(); return () => dialog.close(); }, []);
|
|
206
|
+
return h('dialog', { className: 'dsh-dialog', ref, 'aria-label': title, onCancel: event => { event.preventDefault(); if (!busy) closeRef.current(); } },
|
|
207
|
+
h('header', { className: 'dsh-dialog-header' }, h('div', null, h('span', { className: 'eyebrow' }, h(Icon), eyebrow), h('h2', null, title)),
|
|
208
|
+
h('button', { type: 'button', 'data-slot': 'button', 'data-variant': 'ghost', className: 'dsh-icon-button', 'aria-label': closeLabel, disabled: busy, onClick: onClose }, h(Icon, { name: 'close' }))),
|
|
209
|
+
h('div', { className: 'dsh-dialog-body' }, children), footer ? h('footer', { className: 'dsh-dialog-footer' }, footer) : null);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
inject: ['slots', 'locale', 'layout'],
|
|
213
|
+
apply(ctx) {
|
|
214
|
+
for (const [locale, dict] of [['en', en], ['zh', zh], ['zh-CN', zh]]) {
|
|
215
|
+
ctx.effect(() => ctx.locale.register('mory', locale, dict));
|
|
216
|
+
ctx.effect(() => ctx.locale.register('mory.english', locale, en));
|
|
217
|
+
ctx.effect(() => ctx.locale.register('mory.chinese', locale, zh));
|
|
218
|
+
}
|
|
219
|
+
const translations = { auto: ctx.locale.bind('mory'), en: ctx.locale.bind('mory.english'), zh: ctx.locale.bind('mory.chinese') };
|
|
220
|
+
const t = translations.auto;
|
|
221
|
+
const subscribe = fn => ctx.locale.subscribe(fn), snapshot = () => ctx.locale.getSnapshot();
|
|
222
|
+
function Page() {
|
|
223
|
+
const locale = React.useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
224
|
+
const [language, setLanguage] = React.useState('auto');
|
|
225
|
+
const t = translations[language] || translations.auto;
|
|
226
|
+
const sourceLabel = value => Object.hasOwn(en, value) ? t(value) : value;
|
|
227
|
+
const [theme, setTheme] = React.useState(() => { try { const saved = localStorage.getItem('mory.dsh.theme'); return themes.some(item => item.id === saved) ? saved : 'default'; } catch { return 'default'; } });
|
|
228
|
+
const [catalog, setCatalog] = React.useState([]), [loading, setLoading] = React.useState(true), [connected, setConnected] = React.useState(false);
|
|
229
|
+
const [reload, setReload] = React.useState(0), [error, setError] = React.useState(null), [notice, setNotice] = React.useState('');
|
|
230
|
+
const [query, setQuery] = React.useState(''), [searchResults, setSearchResults] = React.useState(null), [searching, setSearching] = React.useState(false);
|
|
231
|
+
const [kind, setKind] = React.useState('all'), [source, setSource] = React.useState('all'), [privacy, setPrivacy] = React.useState('all'), [time, setTime] = React.useState('all');
|
|
232
|
+
const [tag, setTag] = React.useState(''), [projectDraft, setProjectDraft] = React.useState(''), [project, setProject] = React.useState('');
|
|
233
|
+
const [page, setPage] = React.useState(1), [pageSize, setPageSize] = React.useState(10);
|
|
234
|
+
const [selected, setSelected] = React.useState(null), [mode, setMode] = React.useState(null), [draft, setDraft] = React.useState({});
|
|
235
|
+
const [busy, setBusy] = React.useState(false), [confirmDelete, setConfirmDelete] = React.useState(false);
|
|
236
|
+
const [dialogError, setDialogError] = React.useState(null);
|
|
237
|
+
const [settingsTab, setSettingsTab] = React.useState(null), [settings, setSettings] = React.useState(null);
|
|
238
|
+
const [settingsBusy, setSettingsBusy] = React.useState(false), [settingsError, setSettingsError] = React.useState(null), [settingsNotice, setSettingsNotice] = React.useState('');
|
|
239
|
+
const [connectionDraft, setConnectionDraft] = React.useState({ apiUrl: '', token: '' });
|
|
240
|
+
const [syncDraft, setSyncDraft] = React.useState({ provider: 'github', repositoryUrl: '', branch: '', path: 'mory/memories.json', deviceName: '', token: '', autoSync: false });
|
|
241
|
+
const [health, setHealth] = React.useState(null);
|
|
242
|
+
const settingsRef = React.useRef(null);
|
|
243
|
+
const lifetime = React.useRef(null), detailController = React.useRef(null);
|
|
244
|
+
React.useEffect(() => { const controller = new AbortController(); lifetime.current = controller; return () => { controller.abort(); detailController.current?.abort(); }; }, []);
|
|
245
|
+
async function api(action, input = {}, signal) {
|
|
246
|
+
const response = await fetch('api/mory', { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action, ...input }), signal: signal || lifetime.current?.signal });
|
|
247
|
+
let result; try { result = await response.json(); } catch { throw new Error(t('failed')); }
|
|
248
|
+
if (!response.ok) { const failure = new Error(result.error?.message || t('failed')); failure.code = result.error?.code; throw failure; } return result;
|
|
249
|
+
}
|
|
250
|
+
function adoptSettings(value, resetDrafts = false) {
|
|
251
|
+
settingsRef.current = value; setSettings(value);
|
|
252
|
+
setTheme(value.appearance); setLanguage(value.language);
|
|
253
|
+
try { localStorage.setItem('mory.dsh.theme', value.appearance); } catch {}
|
|
254
|
+
if (resetDrafts) {
|
|
255
|
+
setConnectionDraft({ apiUrl: value.connection.apiUrl || '', token: '' });
|
|
256
|
+
setSyncDraft({ ...value.sync, token: '' });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function readSettings(resetDrafts = false) {
|
|
260
|
+
setSettingsBusy(true); setSettingsError(null);
|
|
261
|
+
try { const value = await api('settings.get'); if (!lifetime.current.signal.aborted) adoptSettings(value, resetDrafts); return value; }
|
|
262
|
+
catch (failure) { if (!lifetime.current.signal.aborted) setSettingsError(failure); return null; }
|
|
263
|
+
finally { if (!lifetime.current.signal.aborted) setSettingsBusy(false); }
|
|
264
|
+
}
|
|
265
|
+
async function saveSettings(patch, resetDrafts = false) {
|
|
266
|
+
if (!settingsRef.current) return null;
|
|
267
|
+
setSettingsBusy(true); setSettingsError(null); setSettingsNotice('');
|
|
268
|
+
try {
|
|
269
|
+
const value = await api('settings.update', { expectedRevision: settingsRef.current.revision, ...patch });
|
|
270
|
+
if (lifetime.current.signal.aborted) return null;
|
|
271
|
+
adoptSettings(value);
|
|
272
|
+
if (resetDrafts && patch.connection) setConnectionDraft({ apiUrl: value.connection.apiUrl || '', token: '' });
|
|
273
|
+
if (resetDrafts && patch.sync) setSyncDraft({ ...value.sync, token: '' });
|
|
274
|
+
setSettingsNotice(t('settingsSaved'));
|
|
275
|
+
if (patch.connection) setReload(value => value + 1);
|
|
276
|
+
return value;
|
|
277
|
+
} catch (failure) { if (!lifetime.current.signal.aborted) setSettingsError(failure); return null; }
|
|
278
|
+
finally { if (!lifetime.current.signal.aborted) setSettingsBusy(false); }
|
|
279
|
+
}
|
|
280
|
+
const connectionPatch = () => ({ apiUrl: connectionDraft.apiUrl.trim(), ...(connectionDraft.token.trim() ? { token: connectionDraft.token.trim() } : {}) });
|
|
281
|
+
const syncPatch = () => ({ provider: syncDraft.provider, repositoryUrl: syncDraft.repositoryUrl.trim(), branch: syncDraft.branch.trim(), path: syncDraft.path.trim(), deviceName: syncDraft.deviceName.trim(), autoSync: syncDraft.autoSync, ...(syncDraft.token.trim() ? { token: syncDraft.token.trim() } : {}) });
|
|
282
|
+
async function runSettings(action, input, patch) {
|
|
283
|
+
if (patch && !await saveSettings(patch, true)) return;
|
|
284
|
+
setSettingsBusy(true); setSettingsError(null); setSettingsNotice('');
|
|
285
|
+
if (action === 'health') setHealth(null);
|
|
286
|
+
try {
|
|
287
|
+
const result = await api(`settings.${action}`, input);
|
|
288
|
+
if (lifetime.current.signal.aborted) return;
|
|
289
|
+
if (action === 'health') setHealth(result);
|
|
290
|
+
if (action === 'check' && input.target === 'sync' && result.branch) setSyncDraft(previous => ({ ...previous, branch: previous.branch || result.branch }));
|
|
291
|
+
setSettingsNotice(t(action === 'sync' ? 'syncDone' : 'connectionOK') + (result.branch ? ` · ${result.branch}` : ''));
|
|
292
|
+
if (result.status === 'partial') { const failure = new Error(result.message || t('failed')); failure.code = result.code; setSettingsError(failure); setSettingsNotice(''); }
|
|
293
|
+
if (action === 'sync') setReload(value => value + 1);
|
|
294
|
+
if (action === 'sync' || action === 'health') { const current = await api('settings.get'); if (!lifetime.current.signal.aborted) adoptSettings(current); }
|
|
295
|
+
} catch (failure) { if (!lifetime.current.signal.aborted) setSettingsError(failure); }
|
|
296
|
+
finally { if (!lifetime.current.signal.aborted) setSettingsBusy(false); }
|
|
297
|
+
}
|
|
298
|
+
React.useEffect(() => { readSettings(true); }, []);
|
|
299
|
+
React.useEffect(() => { if (settingsTab === 'health') runSettings('health', {}); }, [settingsTab]);
|
|
300
|
+
React.useEffect(() => {
|
|
301
|
+
const controller = new AbortController(); setLoading(true); setError(null);
|
|
302
|
+
(async () => {
|
|
303
|
+
const records = []; let offset = 0;
|
|
304
|
+
do {
|
|
305
|
+
const result = await api('list', { limit: 50, offset, ...(project ? { projectId: project } : {}) }, controller.signal);
|
|
306
|
+
if (result.total > 10000) throw new Error(t('catalogLimit'));
|
|
307
|
+
records.push(...result.memories);
|
|
308
|
+
if (result.nextOffset != null && result.nextOffset <= offset) throw new Error(t('failed'));
|
|
309
|
+
offset = result.nextOffset;
|
|
310
|
+
} while (offset != null && !controller.signal.aborted);
|
|
311
|
+
if (!controller.signal.aborted) { setCatalog(records); setConnected(true); }
|
|
312
|
+
})().catch(failure => { if (!controller.signal.aborted) { setError(failure); setConnected(false); setCatalog([]); } }).finally(() => { if (!controller.signal.aborted) setLoading(false); });
|
|
313
|
+
return () => controller.abort();
|
|
314
|
+
}, [project, reload]);
|
|
315
|
+
React.useEffect(() => {
|
|
316
|
+
const controller = new AbortController();
|
|
317
|
+
if (!query.trim()) { setSearchResults(null); setSearching(false); return () => controller.abort(); }
|
|
318
|
+
setSearching(true); setSearchResults([]);
|
|
319
|
+
const timer = setTimeout(() => {
|
|
320
|
+
api('search', { query: query.trim(), limit: 50, ...(project ? { projectId: project } : {}) }, controller.signal)
|
|
321
|
+
.then(result => { if (!controller.signal.aborted) setSearchResults(result.results.map(item => item.memory)); })
|
|
322
|
+
.catch(failure => { if (!controller.signal.aborted) setError(failure); })
|
|
323
|
+
.finally(() => { if (!controller.signal.aborted) setSearching(false); });
|
|
324
|
+
}, 250);
|
|
325
|
+
return () => { clearTimeout(timer); controller.abort(); };
|
|
326
|
+
}, [query, project, reload]);
|
|
327
|
+
React.useEffect(() => { setPage(1); }, [query, project, kind, source, privacy, time, tag, pageSize]);
|
|
328
|
+
const filtered = (searchResults || catalog).filter(memory => {
|
|
329
|
+
const created = new Date(memory.capturedAt || memory.updatedAt);
|
|
330
|
+
const now = new Date(), age = now - created;
|
|
331
|
+
const dateMatches = time === 'all' || (time === 'today' ? created.toDateString() === now.toDateString() : age >= 0 && age <= (time === 'week' ? 7 : 30) * 86400000);
|
|
332
|
+
return (kind === 'all' || memory.kind === kind) && (source === 'all' || memory.source === source) && (privacy === 'all' || !!memory.isPrivate === (privacy === 'private')) && dateMatches && (!tag.trim() || (memory.tags || []).some(value => value.toLowerCase().includes(tag.trim().replace(/^#/, '').toLowerCase())));
|
|
333
|
+
});
|
|
334
|
+
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)), currentPage = Math.min(page, totalPages);
|
|
335
|
+
const visible = filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
|
336
|
+
const counts = Object.fromEntries(kinds.map(value => [value, catalog.filter(memory => memory.kind === value).length]));
|
|
337
|
+
const today = catalog.filter(memory => new Date(memory.capturedAt || memory.updatedAt).toDateString() === new Date().toDateString()).length;
|
|
338
|
+
const topics = new Set(catalog.flatMap(memory => memory.tags || [])).size;
|
|
339
|
+
const sources = [...new Set(catalog.map(memory => memory.source).filter(Boolean))];
|
|
340
|
+
const editing = mode === 'edit' || mode === 'create';
|
|
341
|
+
const editable = selected && selected.payload.content.length <= 200000 && selected.tags.length <= 50;
|
|
342
|
+
function close() { if (busy) return; if (editing) { setDialogError(new Error(t('unsaved'))); return; } setMode(null); setSelected(null); setConfirmDelete(false); setDialogError(null); }
|
|
343
|
+
function cancelEdit() { setMode(selected ? 'detail' : null); setDialogError(null); }
|
|
344
|
+
function create() { setSelected(null); setDraft({ title: '', text: '', tags: '', kind: kind === 'all' ? 'note' : kind, isPrivate: false }); setDialogError(null); setMode('create'); }
|
|
345
|
+
async function open(memory) {
|
|
346
|
+
if (busy) return; detailController.current?.abort(); const controller = new AbortController(); detailController.current = controller; setBusy(true); setError(null);
|
|
347
|
+
try { const result = await api('get', { memoryId: memory.id }, controller.signal); if (!controller.signal.aborted) { setSelected(result.memory); setMode('detail'); setDialogError(null); setConfirmDelete(false); } }
|
|
348
|
+
catch (failure) { if (!controller.signal.aborted) setError(failure); } finally { if (!controller.signal.aborted) setBusy(false); }
|
|
349
|
+
}
|
|
350
|
+
function edit() { if (!editable) return; setDraft({ title: selected.payload.title, text: selected.payload.content, tags: selected.tags.join(', '), kind: selected.kind, isPrivate: selected.isPrivate }); setMode('edit'); setDialogError(null); setConfirmDelete(false); }
|
|
351
|
+
async function mutate(action, input) {
|
|
352
|
+
setBusy(true); setDialogError(null); setNotice('');
|
|
353
|
+
try { const result = await api(action, input); if (lifetime.current.signal.aborted) return;
|
|
354
|
+
setSelected(action === 'forget' ? null : result.memory); setMode(action === 'forget' ? null : 'detail'); setConfirmDelete(false); setNotice(t(action === 'forget' ? 'deleted' : 'saved')); setReload(value => value + 1);
|
|
355
|
+
} catch (failure) { if (!lifetime.current.signal.aborted) setDialogError(failure); }
|
|
356
|
+
finally { if (!lifetime.current.signal.aborted) setBusy(false); }
|
|
357
|
+
}
|
|
358
|
+
function save(event) {
|
|
359
|
+
event.preventDefault(); if (busy) return;
|
|
360
|
+
const input = { ...draft, tags: draft.tags.split(/[,,]/u).map(value => value.trim()).filter(Boolean) };
|
|
361
|
+
if (mode === 'edit') { Object.assign(input, { memoryId: selected.id, expectedRevision: selected.revision }); if (draft.tags === selected.tags.join(', ')) delete input.tags; }
|
|
362
|
+
else if (project) input.projectId = project;
|
|
363
|
+
mutate(mode === 'edit' ? 'update' : 'create', input);
|
|
364
|
+
}
|
|
365
|
+
const button = (text, onClick, props = {}) => h('button', { type: 'button', 'data-slot': 'button', 'data-variant': 'outline', disabled: busy, onClick, ...props }, text);
|
|
366
|
+
const select = (value, onChange, items, label, props = {}) => h('select', { value, onChange: event => onChange(event.target.value), 'aria-label': label, disabled: busy, ...props }, ...items.map(([key, text]) => h('option', { key, value: key }, text)));
|
|
367
|
+
const field = (name, key, type = 'input', extra = {}) => h('label', { key }, t(name), h(type, { 'data-slot': type === 'textarea' ? 'textarea' : 'input', value: draft[key] || '', disabled: busy, onChange: event => setDraft(previous => ({ ...previous, [key]: event.target.value })), ...extra }));
|
|
368
|
+
const noticeView = failure => h('div', { className: 'dsh-notice error', role: 'alert' }, failure.code === 'REVISION_CONFLICT' ? t('conflict') : failure.message);
|
|
369
|
+
function formatTime(value) { const date = new Date(value); return Number.isNaN(date.getTime()) ? '—' : new Intl.DateTimeFormat(locale.active, { month: 'short', day: 'numeric' }).format(date); }
|
|
370
|
+
function chooseKind(value) { setSettingsTab(null); setKind(value); setQuery(''); }
|
|
371
|
+
function resetFilters() { setKind('all'); setSource('all'); setTime('all'); setPrivacy('all'); setTag(''); setQuery(''); }
|
|
372
|
+
const navButton = value => button([h(Icon, { name: value === 'all' ? 'archive' : value, size: 17, key: 'icon' }), h('span', { key: 'text' }, t(value)), h('strong', { key: 'count' }, value === 'all' ? catalog.length : counts[value])], () => chooseKind(value), { key: value, 'data-variant': 'ghost', className: !settingsTab && kind === value ? 'active' : '', 'aria-pressed': !settingsTab && kind === value });
|
|
373
|
+
const settingsTabs = [['sync', 'syncSettings', 'cloud'], ['language', 'languageSettings', 'globe'], ['theme', 'themeSettings', 'sun'], ['health', 'healthSettings', 'health'], ['connection', 'connectionSettings', 'connection']];
|
|
374
|
+
const settingsDisabled = settingsBusy || !settings;
|
|
375
|
+
const settingsButton = (label, onClick, props = {}) => button(label, onClick, { disabled: settingsDisabled, ...props });
|
|
376
|
+
const settingsField = (label, value, onChange, props = {}) => h('label', null, t(label), h('input', { 'data-slot': 'input', value: value || '', disabled: settingsBusy, onChange: event => onChange(event.target.value), ...props }));
|
|
377
|
+
const patchConnection = (key, value) => setConnectionDraft(previous => ({ ...previous, [key]: value }));
|
|
378
|
+
const patchSync = (key, value) => setSyncDraft(previous => ({ ...previous, [key]: value }));
|
|
379
|
+
const syncActions = h('div', { className: 'dsh-settings-actions' },
|
|
380
|
+
settingsButton(t('mergePush'), () => runSettings('sync', { mode: 'merge' }, { sync: syncPatch() }), { 'data-variant': 'default' }),
|
|
381
|
+
settingsButton(t('pushRemote'), () => runSettings('sync', { mode: 'push' }, { sync: syncPatch() })),
|
|
382
|
+
settingsButton(t('pullRemote'), () => runSettings('sync', { mode: 'pull' }, { sync: syncPatch() })));
|
|
383
|
+
const lastSync = settings?.lastSync;
|
|
384
|
+
const lastSyncMessage = lastSync ? lastSync.message || lastSync.error || t(lastSync.status === 'running' ? 'syncing' : lastSync.status === 'success' ? 'syncDone' : 'failed') : t('neverSynced');
|
|
385
|
+
const settingsContent = settingsTab === 'theme' ? h('section', { className: 'dsh-settings-panel' }, h('p', null, t('appearanceHint')),
|
|
386
|
+
h('div', { className: 'theme-options', role: 'group', 'aria-label': t('themeSettings') }, ...themes.map(item => h('button', { key: item.id, type: 'button', className: `theme-option ${theme === item.id ? 'selected' : ''}`, 'aria-pressed': theme === item.id, disabled: settingsDisabled, onClick: () => saveSettings({ appearance: item.id }) },
|
|
387
|
+
h('span', { className: 'theme-swatches', 'aria-hidden': true }, ...item.swatches.map(color => h('span', { key: color, style: { backgroundColor: color } }))), h('span', { className: 'theme-option-copy' }, h('strong', null, t(`themeName.${item.id}`)), h('small', null, t(`themeHint.${item.id}`))), h('span', { className: 'theme-check', 'aria-hidden': true }, theme === item.id ? '✓' : '')))))
|
|
388
|
+
: settingsTab === 'language' ? h('section', { className: 'dsh-settings-panel' }, h('p', null, t('languageHint')), h('div', { className: 'dsh-settings-form' }, h('label', null, t('languageSettings'), select(language, value => saveSettings({ language: value }), [['auto', t('followDSH')], ['en', 'English'], ['zh', '简体中文']], t('languageSettings'), { disabled: settingsDisabled }))))
|
|
389
|
+
: settingsTab === 'connection' ? h('section', { className: 'dsh-settings-panel' }, h('p', null, t('connectionHint')),
|
|
390
|
+
h('form', { className: 'dsh-settings-form', onSubmit: event => { event.preventDefault(); saveSettings({ connection: connectionPatch() }, true); } },
|
|
391
|
+
settingsField('apiUrl', connectionDraft.apiUrl, value => patchConnection('apiUrl', value), { type: 'url', placeholder: t('autoDetect'), maxLength: 2048 }),
|
|
392
|
+
settingsField('accessToken', connectionDraft.token, value => patchConnection('token', value), { type: 'password', autoComplete: 'new-password', placeholder: t(settings?.connection.tokenConfigured ? 'storedToken' : 'tokenOptional'), maxLength: 8192 }),
|
|
393
|
+
h('p', { className: 'dsh-help' }, t('connectionPrecedence')),
|
|
394
|
+
h('div', { className: 'dsh-settings-actions' }, settingsButton(t('saveSettings'), undefined, { type: 'submit', 'data-variant': 'default' }), settingsButton(t('checkConnection'), () => runSettings('check', { target: 'mory' }, { connection: connectionPatch() })), settingsButton(t('autoDetect'), () => saveSettings({ connection: { apiUrl: '', token: null } }, true)))))
|
|
395
|
+
: settingsTab === 'health' ? h('section', { className: 'dsh-settings-panel' }, h('p', null, t('healthHint')),
|
|
396
|
+
h('div', { className: 'dsh-health-grid' }, ...[[health?.total ?? catalog.length, 'totalMemories'], [today, 'today'], [topics, 'topics']].map(([value, key]) => h('div', { className: 'dsh-health-metric', key }, h('strong', null, value), h('span', null, t(key))))),
|
|
397
|
+
h('dl', { className: 'dsh-health-details' }, h('dt', null, t('connectionStatus')), h('dd', null, t(health?.connected ? 'connected' : 'offline')), h('dt', null, t('serviceAddress')), h('dd', null, health?.apiUrl || settings?.connection.apiUrl || t('autoDetect')), h('dt', null, t('lastSync')), h('dd', null, lastSyncMessage)),
|
|
398
|
+
h('div', { className: 'dsh-settings-actions' }, settingsButton(t('checkConnection'), () => runSettings('health', {}))))
|
|
399
|
+
: h('section', { className: 'dsh-settings-panel' }, h('p', null, t('syncHint')),
|
|
400
|
+
h('form', { className: 'dsh-settings-form', onSubmit: event => { event.preventDefault(); saveSettings({ sync: syncPatch() }, true); } },
|
|
401
|
+
h('div', { className: 'dsh-form-row' }, h('label', null, t('provider'), select(syncDraft.provider, value => patchSync('provider', value), [['github', 'GitHub'], ['gitee', 'Gitee']], t('provider'), { disabled: settingsBusy })), settingsField('repositoryUrl', syncDraft.repositoryUrl, value => patchSync('repositoryUrl', value), { type: 'url', placeholder: `https://${syncDraft.provider}.com/owner/repository`, maxLength: 2048 })),
|
|
402
|
+
h('div', { className: 'dsh-form-row' }, settingsField('accessToken', syncDraft.token, value => patchSync('token', value), { type: 'password', autoComplete: 'new-password', placeholder: t(settings?.sync.tokenConfigured ? 'storedToken' : 'accessToken'), maxLength: 8192 }), settingsField('deviceName', syncDraft.deviceName, value => patchSync('deviceName', value), { maxLength: 128 })),
|
|
403
|
+
h('div', { className: 'dsh-form-row' }, settingsField('branch', syncDraft.branch, value => patchSync('branch', value), { placeholder: 'main', maxLength: 256 }), settingsField('filePath', syncDraft.path, value => patchSync('path', value), { placeholder: 'mory/memories.json', maxLength: 1024 })),
|
|
404
|
+
h('label', { className: 'dsh-checkbox' }, h('input', { type: 'checkbox', checked: !!syncDraft.autoSync, disabled: settingsBusy, onChange: event => patchSync('autoSync', event.target.checked) }), h('span', null, h('strong', null, t('autoSync')), h('small', null, t('autoSyncHint')))),
|
|
405
|
+
h('div', { className: 'dsh-settings-actions' }, settingsButton(t('saveSettings'), undefined, { type: 'submit', 'data-variant': 'default' }), settingsButton(t('checkConnection'), () => runSettings('check', { target: 'sync' }, { sync: syncPatch() })))),
|
|
406
|
+
h('div', { className: 'dsh-notice' }, t('syncPrivacy')), syncActions,
|
|
407
|
+
h('p', { className: 'dsh-help' }, `${t('lastSync')}: ${lastSyncMessage}`));
|
|
408
|
+
const settingsView = settingsTab ? h('section', { className: 'settings-page dsh-settings-page', 'aria-label': t('settings') },
|
|
409
|
+
h('header', { className: 'settings-page-header' }, h('div', null, h('h1', null, t('settings')), h('p', null, t('settingsHint'))), button([h(Icon, { name: 'back', key: 'icon', size: 15 }), t('returnMemories')], () => setSettingsTab(null))),
|
|
410
|
+
h('nav', { className: 'settings-tabs', 'aria-label': t('settings') }, ...settingsTabs.map(([key, label, icon]) => button([h(Icon, { name: icon, key: 'icon', size: 15 }), t(label)], () => setSettingsTab(key), { key, className: settingsTab === key ? 'active' : '', 'aria-pressed': settingsTab === key }))),
|
|
411
|
+
settingsError ? h('div', { className: 'dsh-notice error', role: 'alert' }, h('span', null, ['REVISION_CONFLICT', 'SETTINGS_CONFLICT'].includes(settingsError.code) ? t('settingsConflict') : settingsError.message), button(t('reloadSettings'), () => readSettings(true), { disabled: settingsBusy })) : null,
|
|
412
|
+
settingsNotice ? h('div', { className: 'dsh-notice success', role: 'status' }, settingsNotice) : null,
|
|
413
|
+
settingsBusy ? h('p', { className: 'dsh-help', role: 'status' }, t('checking')) : null,
|
|
414
|
+
settingsContent) : null;
|
|
415
|
+
const details = mode && h(Modal, { title: mode === 'create' ? t('add') : editing ? t('edit') : selected.payload.title, eyebrow: t(mode === 'create' ? 'add' : 'detail'), closeLabel: t('close'), onClose: close, busy,
|
|
416
|
+
footer: editing ? [button(t('cancel'), cancelEdit, { key: 'cancel' }), button(t('save'), save, { key: 'save', 'data-variant': 'default', disabled: busy || (!draft.title.trim() && !draft.text.trim()) })] : [button(t('remove'), () => setConfirmDelete(true), { key: 'delete', 'data-variant': 'ghost', className: 'dsh-delete-button' }), button(t('close'), close, { key: 'close' }), button(t('edit'), edit, { key: 'edit', 'data-variant': 'default', disabled: busy || !editable })],
|
|
417
|
+
}, dialogError ? noticeView(dialogError) : null,
|
|
418
|
+
editing ? h('form', { onSubmit: save, className: 'dsh-edit-form' },
|
|
419
|
+
field('title', 'title', 'input', { maxLength: 4096, autoFocus: true }),
|
|
420
|
+
h('div', { className: 'dsh-form-row' }, h('label', null, t('kind'), select(draft.kind, value => setDraft(previous => ({ ...previous, kind: value })), kinds.map(value => [value, t(value)]), t('kind'))), field('tags', 'tags')),
|
|
421
|
+
field('content', 'text', 'textarea', { maxLength: 200000 }),
|
|
422
|
+
h('label', { className: 'dsh-checkbox' }, h('input', { type: 'checkbox', checked: !!draft.isPrivate, disabled: busy, onChange: event => setDraft(previous => ({ ...previous, isPrivate: event.target.checked })) }), t('markPrivate')),
|
|
423
|
+
h('p', { className: 'dsh-help' }, t('privateHint')))
|
|
424
|
+
: h(React.Fragment, null,
|
|
425
|
+
h('div', { className: 'dsh-meta' }, h('span', null, t(selected.kind)), h('span', null, sourceLabel(selected.source)), h('span', null, `${t('revision')} ${selected.revision}`), h('span', null, formatTime(selected.capturedAt)), ...selected.tags.map((value, index) => h('span', { key: index }, `#${value}`))),
|
|
426
|
+
h('div', { className: 'dsh-detail-text' }, selected.payload.content),
|
|
427
|
+
!editable ? h('p', { className: 'dsh-help' }, t('tooLarge')) : null,
|
|
428
|
+
selected.isPrivate ? h('p', { className: 'dsh-help' }, `${t('private')} · ${t('privateHint')}`) : null,
|
|
429
|
+
confirmDelete ? h('div', { className: 'dsh-notice error', role: 'group', 'aria-label': t('confirm') }, h('div', null, h('strong', null, t('confirm')), h('p', { className: 'dsh-help' }, t('confirmHint'))), button(t('cancel'), () => setConfirmDelete(false)), button(t('confirmDelete'), () => mutate('forget', { memoryId: selected.id, expectedRevision: selected.revision }), { 'data-variant': 'destructive' })) : null));
|
|
430
|
+
return h('div', { className: 'mory-app', 'data-theme': theme, 'data-mory-version': '0.1.2' }, h('style', null, css + adaptation),
|
|
431
|
+
h('main', { className: 'product-shell' },
|
|
432
|
+
h('header', { className: 'product-header' }, h('div', { className: 'brand-lockup' }, h('div', { className: 'brand-glyph logo-mark' }, h('img', { src: logo, alt: 'Mory', className: 'mory-original-logo' })), h('div', null, h('strong', null, 'Mory'), h('span', null, t('repository')))),
|
|
433
|
+
h('div', { className: 'header-actions' }, h('span', { className: `status-chip ${connected ? 'good' : 'error'}`, role: 'status' }, h('i', { className: 'dsh-status-dot' }), `${t(connected ? 'local' : 'offline')} · ${catalog.length} ${t('objects')}`),
|
|
434
|
+
button(h(Icon, { name: 'settings', size: 18 }), () => setSettingsTab(settingsTab ? null : 'sync'), { className: 'dsh-icon-button', title: t('settings'), 'aria-label': t('settings'), 'aria-pressed': !!settingsTab, 'data-variant': 'ghost' }),
|
|
435
|
+
button(h(Icon, { name: 'back', size: 17 }), () => ctx.layout.selectPanel(null), { className: 'dsh-icon-button', title: t('back'), 'aria-label': t('back'), 'data-variant': 'ghost' }))),
|
|
436
|
+
h('section', { className: 'workspace-layout' }, h('aside', { className: 'workspace-nav', 'aria-label': t('recordTypes') }, h('div', { className: 'nav-section-label' }, t('recordTypes')), h('div', { className: 'record-nav-list all-memory-list' }, navButton('all')), h('div', { className: 'record-nav-list all-types-list' }, ...kinds.map(navButton)), h('div', { className: 'sidebar-footer' }, button([h(Icon, { name: 'settings', key: 'icon', size: 16 }), t('settings')], () => setSettingsTab('sync'), { className: `dsh-settings-link ${settingsTab ? 'active' : ''}`, 'data-variant': 'ghost', 'aria-pressed': !!settingsTab }), h('p', { style: { marginTop: 12 } }, t('shared')))),
|
|
437
|
+
h('div', { className: 'workspace-main' }, settingsView, h('section', { className: 'feed-section', hidden: !!settingsTab },
|
|
438
|
+
h('div', { className: 'section-heading' }, button([h(Icon, { name: 'plus', key: 'icon', size: 16 }), t('add')], create, { className: 'capture-cta feed-capture-button', 'data-variant': 'default' }), h('div', { className: 'feed-summary' }, h('span', null, h('strong', null, today), ` ${t('today')}`), h('span', null, h('strong', null, topics), ` ${t('topics')}`)), button(h(Icon, { name: 'refresh', size: 17 }), () => setReload(value => value + 1), { className: 'dsh-icon-button', title: t('refresh'), 'aria-label': t('refresh'), disabled: busy || loading })),
|
|
439
|
+
h('div', { className: 'dsh-controls', role: 'search' }, h('label', { className: 'search-input' }, h(Icon, { name: 'search', size: 17 }), h('input', { 'data-slot': 'input', type: 'search', value: query, maxLength: 4096, placeholder: t('search'), 'aria-label': t('search'), disabled: busy, onChange: event => setQuery(event.target.value) })), select(source, setSource, [['all', t('allSources')], ...sources.map(value => [value, sourceLabel(value)])], t('entity'))),
|
|
440
|
+
h('div', { className: 'dsh-filter-row' }, h('span', { className: 'dsh-filter-label' }, h(Icon, { name: 'filter', size: 14 }), t('filters')), select(kind, chooseKind, [['all', t('all')], ...kinds.map(value => [value, t(value)])], t('recordTypes'), { className: 'dsh-mobile-kind' }), select(time, setTime, ['all', 'today', 'week', 'month'].map(value => [value, t(value === 'all' ? 'allTime' : value)]), t('time')), select(privacy, setPrivacy, ['all', 'public', 'private'].map(value => [value, t(value === 'all' ? 'allPrivacy' : value)]), t('allPrivacy')),
|
|
441
|
+
h('input', { 'data-slot': 'input', value: tag, placeholder: t('tag'), 'aria-label': t('tag'), onChange: event => setTag(event.target.value) }), h('form', { onSubmit: event => { event.preventDefault(); setProject(projectDraft.trim()); }, style: { display: 'contents' } }, h('input', { 'data-slot': 'input', value: projectDraft, maxLength: 256, placeholder: t('project'), 'aria-label': t('project'), onChange: event => setProjectDraft(event.target.value), onBlur: () => setProject(projectDraft.trim()) }))),
|
|
442
|
+
error ? noticeView(error) : null, notice ? h('div', { className: 'dsh-notice success', role: 'status' }, notice) : null,
|
|
443
|
+
!connected && !loading ? h('div', { className: 'dsh-setup' }, h('h2', null, t('setup')), h('p', null, t('setupHint')), h('pre', null, 'npm install -g @illli-studio/mory\nmory init\nmory start --background')) : null,
|
|
444
|
+
loading || searching ? h('div', { className: 'empty-feed', role: 'status' }, h(Icon, { name: 'waiting', size: 28 }), h('h3', null, t('loading')), h('p', null, t('reading')))
|
|
445
|
+
: !visible.length ? h('div', { className: 'empty-feed' }, h(Icon, { name: 'archive', size: 30 }), h('h3', null, t(catalog.length || query ? 'noMatch' : 'empty')), h('p', null, t('emptyHint')), catalog.length || query ? button(t('clear'), resetFilters) : button(t('add'), create, { 'data-variant': 'default' }))
|
|
446
|
+
: h(React.Fragment, null, h('div', { className: 'memory-list-header' }, h('span', null, t('time')), h('span', { className: 'dsh-entity-label' }, t('entity')), h('span', null, t('content')), h('span', null, t('category')), h('span', null, t('lifecycle'))),
|
|
447
|
+
h('div', { className: 'memory-grid' }, ...visible.map(memory => h('article', { key: memory.id, className: 'memory-card', role: 'button', tabIndex: busy ? -1 : 0, 'aria-label': memory.isPrivate ? t('private') : memory.title, 'aria-disabled': busy, onClick: event => { if (!event.target.closest('button')) open(memory); }, onKeyDown: event => { if (event.target === event.currentTarget && ['Enter', ' '].includes(event.key)) { event.preventDefault(); open(memory); } } },
|
|
448
|
+
h('div', { className: 'memory-time', title: memory.capturedAt || memory.updatedAt }, formatTime(memory.capturedAt || memory.updatedAt)), h('div', { className: 'memory-entity' }, h('strong', null, memory.origin?.deviceName || t('localDevice')), h('span', null, sourceLabel(memory.source))),
|
|
449
|
+
h('div', { className: 'memory-content-cell' }, button(memory.isPrivate ? t('private') : memory.title, () => open(memory), { className: 'memory-title-button', 'data-variant': 'ghost' }), h('p', null, memory.isPrivate ? t('private') : memory.preview)), h('div', { className: 'memory-category' }, button(t(memory.kind), () => chooseKind(memory.kind), { className: 'dsh-kind', 'data-variant': 'ghost' }), memory.tags[0] ? h('span', null, `#${memory.tags[0]}`) : null), h('div', { className: 'memory-life' }, h('span', { 'data-slot': 'badge' }, t('active')))))),
|
|
450
|
+
h('nav', { className: 'memory-pagination', 'aria-label': t('page') }, h('div', { className: 'page-size-options' }, h('span', null, t('pageSize')), ...[10, 20, 50].map(size => button(String(size), () => setPageSize(size), { key: size, 'data-variant': size === pageSize ? 'default' : 'outline', 'aria-pressed': size === pageSize }))), h('div', { className: 'page-navigation' }, button('‹', () => setPage(currentPage - 1), { disabled: busy || currentPage <= 1, 'aria-label': t('previous') }), h('span', null, `${currentPage} ${t('of')} ${totalPages}`), button('›', () => setPage(currentPage + 1), { disabled: busy || currentPage >= totalPages, 'aria-label': t('next') })))),
|
|
451
|
+
query.trim() ? h('p', { className: 'dsh-search-limit' }, t('searchLimit')) : null), h('footer', { className: 'dsh-footer' }, t('shared'))))), details);
|
|
452
|
+
}
|
|
453
|
+
ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({ name: 'sidebar.panellist', id: 'mory', order: 20, label: () => t('name') }, Icon));
|
|
454
|
+
ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: 'mory' }, Page));
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
},
|
|
458
|
+
});
|