codexmate 0.0.43 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +2 -0
  2. package/README.zh.md +2 -0
  3. package/cli/claude-proxy.js +611 -14
  4. package/cli/update.js +77 -7
  5. package/cli.js +188 -21
  6. package/package.json +1 -1
  7. package/web-ui/app.js +36 -3
  8. package/web-ui/index.html +1 -0
  9. package/web-ui/logic.claude.mjs +65 -2
  10. package/web-ui/logic.runtime.mjs +0 -7
  11. package/web-ui/modules/app.computed.index.mjs +3 -1
  12. package/web-ui/modules/app.computed.main-tabs.mjs +3 -0
  13. package/web-ui/modules/app.computed.prompts.mjs +28 -0
  14. package/web-ui/modules/app.computed.session.mjs +23 -1
  15. package/web-ui/modules/app.methods.agents.mjs +50 -4
  16. package/web-ui/modules/app.methods.claude-config.mjs +28 -12
  17. package/web-ui/modules/app.methods.index.mjs +1 -1
  18. package/web-ui/modules/app.methods.install.mjs +129 -1
  19. package/web-ui/modules/app.methods.navigation.mjs +2 -1
  20. package/web-ui/modules/app.methods.session-actions.mjs +17 -2
  21. package/web-ui/modules/app.methods.session-timeline.mjs +0 -1
  22. package/web-ui/modules/app.methods.startup-claude.mjs +26 -3
  23. package/web-ui/modules/i18n/locales/en.mjs +42 -5
  24. package/web-ui/modules/i18n/locales/ja.mjs +42 -5
  25. package/web-ui/modules/i18n/locales/vi.mjs +51 -0
  26. package/web-ui/modules/i18n/locales/zh.mjs +42 -5
  27. package/web-ui/partials/index/layout-footer.html +1 -1
  28. package/web-ui/partials/index/layout-header.html +64 -0
  29. package/web-ui/partials/index/modal-config-template-agents.html +12 -13
  30. package/web-ui/partials/index/modals-basic.html +18 -1
  31. package/web-ui/partials/index/panel-config-claude.html +4 -7
  32. package/web-ui/partials/index/panel-config-codex.html +2 -6
  33. package/web-ui/partials/index/panel-prompts.html +100 -0
  34. package/web-ui/partials/index/panel-sessions.html +30 -10
  35. package/web-ui/partials/index/panel-usage.html +34 -18
  36. package/web-ui/res/web-ui-render.precompiled.js +579 -149
  37. package/web-ui/styles/controls-forms.css +5 -5
  38. package/web-ui/styles/layout-shell.css +145 -0
  39. package/web-ui/styles/modals-core.css +162 -0
  40. package/web-ui/styles/responsive.css +77 -5
  41. package/web-ui/styles/sessions-toolbar-trash.css +45 -10
  42. package/web-ui/styles/sessions-usage.css +31 -2
@@ -102,6 +102,18 @@
102
102
  :disabled="agentsLoading || agentsSaving || agentsDiffVisible">
103
103
  {{ t('common.paste') }}
104
104
  </button>
105
+ <span class="prompts-editor-actions-sep"></span>
106
+ <button class="btn-mini" @click="closeAgentsModal" :disabled="agentsSaving || agentsDiffLoading">{{ t('common.cancel') }}</button>
107
+ <button
108
+ v-if="agentsDiffVisible"
109
+ class="btn-mini"
110
+ @click="resetAgentsDiffState"
111
+ :disabled="agentsSaving || agentsDiffLoading">
112
+ {{ t('common.backToEdit') }}
113
+ </button>
114
+ <button class="btn-mini btn-confirm-mini" @click="applyAgentsContent" :disabled="agentsSaving || agentsLoading || agentsDiffLoading || (agentsDiffVisible && !agentsDiffHasChanges)">
115
+ {{ agentsSaving ? (agentsDiffVisible ? t('common.saving') : t('common.previewing')) : (agentsDiffVisible ? t('common.save') : t('common.preview')) }}
116
+ </button>
105
117
  </div>
106
118
  </div>
107
119
 
@@ -168,18 +180,5 @@
168
180
 
169
181
  </div>
170
182
 
171
- <div class="btn-group modal-editor-footer">
172
- <button class="btn btn-cancel" @click="closeAgentsModal" :disabled="agentsSaving || agentsDiffLoading">{{ t('common.cancel') }}</button>
173
- <button
174
- v-if="agentsDiffVisible"
175
- class="btn"
176
- @click="resetAgentsDiffState"
177
- :disabled="agentsSaving || agentsDiffLoading">
178
- {{ t('common.backToEdit') }}
179
- </button>
180
- <button class="btn btn-confirm" @click="applyAgentsContent" :disabled="agentsSaving || agentsLoading || agentsDiffLoading || (agentsDiffVisible && !agentsDiffHasChanges)">
181
- {{ agentsSaving ? (agentsDiffVisible ? t('common.applying') : t('common.confirming')) : (agentsDiffVisible ? t('common.apply') : t('common.confirm')) }}
182
- </button>
183
- </div>
184
183
  </div>
185
184
  </div>
@@ -165,6 +165,15 @@
165
165
  <input v-model="newClaudeConfig.model" :class="['form-input', { invalid: !!claudeConfigFieldError('add', 'model') }]" :placeholder="t('placeholder.modelExample')" autocomplete="off" spellcheck="false">
166
166
  <div v-if="claudeConfigFieldError('add', 'model')" class="form-hint form-error">{{ claudeConfigFieldError('add', 'model') }}</div>
167
167
  </div>
168
+ <div class="form-group">
169
+ <label class="form-label">{{ t('claude.targetApi.label') }}</label>
170
+ <select v-model="newClaudeConfig.targetApi" class="form-input">
171
+ <option value="responses">{{ t('claude.targetApi.responses') }}</option>
172
+ <option value="chat_completions">{{ t('claude.targetApi.chatCompletions') }}</option>
173
+ <option value="ollama">{{ t('claude.targetApi.ollama') }}</option>
174
+ </select>
175
+ <div class="form-hint">{{ t('claude.targetApi.hint') }}</div>
176
+ </div>
168
177
 
169
178
  <div class="btn-group">
170
179
  <button class="btn btn-cancel" @click="closeClaudeConfigModal">{{ t('common.cancel') }}</button>
@@ -204,6 +213,15 @@
204
213
  <input v-model="editingConfig.model" :class="['form-input', { invalid: !!claudeConfigFieldError('edit', 'model') }]" :placeholder="t('placeholder.modelExample')" autocomplete="off" spellcheck="false">
205
214
  <div v-if="claudeConfigFieldError('edit', 'model')" class="form-hint form-error">{{ claudeConfigFieldError('edit', 'model') }}</div>
206
215
  </div>
216
+ <div class="form-group">
217
+ <label class="form-label">{{ t('claude.targetApi.label') }}</label>
218
+ <select v-model="editingConfig.targetApi" class="form-input">
219
+ <option value="responses">{{ t('claude.targetApi.responses') }}</option>
220
+ <option value="chat_completions">{{ t('claude.targetApi.chatCompletions') }}</option>
221
+ <option value="ollama">{{ t('claude.targetApi.ollama') }}</option>
222
+ </select>
223
+ <div class="form-hint">{{ t('claude.targetApi.hint') }}</div>
224
+ </div>
207
225
 
208
226
  <div class="btn-group">
209
227
  <button class="btn btn-cancel" @click="closeEditConfigModal">{{ t('common.cancel') }}</button>
@@ -260,4 +278,3 @@
260
278
  </div>
261
279
  </div>
262
280
  </div>
263
-
@@ -34,6 +34,7 @@
34
34
  <label class="settings-toggle-row tool-config-write-toggle">
35
35
  <input
36
36
  type="checkbox"
37
+ autocomplete="off"
37
38
  :checked="isToolConfigWriteAllowed('claude')"
38
39
  :disabled="toolConfigPermissionSaving.claude"
39
40
  @change="setToolConfigPermission('claude', $event.target.checked)">
@@ -107,12 +108,6 @@
107
108
  <button class="btn-tool btn-template-editor" @click="openClaudeConfigTemplateEditor" :disabled="loading || !!initError">{{ t('config.template.openEditor') }}</button>
108
109
  </div>
109
110
 
110
- <div class="selector-section">
111
- <div class="selector-header"><span class="selector-title">CLAUDE.md</span></div>
112
- <button class="btn-tool" @click="openClaudeMdEditor" :disabled="loading || !!initError || agentsLoading">{{ agentsLoading ? t('config.modelLoading') : t('claude.md.open') }}</button>
113
- <div class="config-template-hint">{{ t('claude.md.hint') }}</div>
114
- </div>
115
-
116
111
  <div class="selector-section">
117
112
  <div class="selector-header"><span class="selector-title">{{ t('config.health.title') }}</span></div>
118
113
  <button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">{{ healthCheckLoading ? t('config.health.running') : t('config.health.run') }}</button>
@@ -145,10 +140,12 @@
145
140
  </div>
146
141
  <div v-for="(config, name) in claudeConfigs" :key="name" :class="['card', { active: currentClaudeConfig === name }]" @click="applyClaudeConfig(name)" @keydown.enter.self.prevent="applyClaudeConfig(name)" @keydown.space.self.prevent="applyClaudeConfig(name)" tabindex="0" role="button" :aria-current="currentClaudeConfig === name ? 'true' : null">
147
142
  <div class="card-leading">
148
- <div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
143
+ <div class="card-icon">{{ name.charAt(0).toUpperCase() }}<span v-if="config.targetApi === 'chat_completions' || config.targetApi === 'ollama'" class="card-icon-dot" :title="t('config.transformProvider.title')"></span></div>
149
144
  <div class="card-content">
150
145
  <div class="card-title">{{ name }}</div>
151
146
  <div class="card-subtitle card-subtitle-model">{{ config.model || t('claude.model.unset') }}</div>
147
+ <div class="card-subtitle" v-if="config.targetApi === 'chat_completions'">{{ t('claude.targetApi.chatCompletionsBadge') }}</div>
148
+ <div class="card-subtitle" v-else-if="config.targetApi === 'ollama'">{{ t('claude.targetApi.ollamaBadge') }}</div>
152
149
  <div class="card-subtitle card-subtitle-url" v-if="config.baseUrl">{{ config.baseUrl }}</div>
153
150
  </div>
154
151
  </div>
@@ -34,6 +34,7 @@
34
34
  <label class="settings-toggle-row tool-config-write-toggle">
35
35
  <input
36
36
  type="checkbox"
37
+ autocomplete="off"
37
38
  :checked="isToolConfigWriteAllowed('codex')"
38
39
  :disabled="toolConfigPermissionSaving.codex"
39
40
  @change="setToolConfigPermission('codex', $event.target.checked)">
@@ -60,11 +61,6 @@
60
61
  </div>
61
62
  </div>
62
63
 
63
- <div class="selector-section">
64
- <div class="selector-header"><span class="selector-title">AGENTS.md</span></div>
65
- <button class="btn-tool" @click="openAgentsEditor" :disabled="loading || !!initError || agentsLoading">{{ agentsLoading ? t('config.modelLoading') : t('config.agents.open') }}</button>
66
- </div>
67
-
68
64
  <div class="selector-section">
69
65
  <div class="selector-header">
70
66
  <span class="selector-title">{{ t('config.models') }}</span>
@@ -203,4 +199,4 @@
203
199
  </div>
204
200
  </div>
205
201
  </template>
206
- </div>
202
+ </div>
@@ -0,0 +1,100 @@
1
+ <!-- Prompts editor -->
2
+ <div
3
+ v-if="mainTab === 'prompts'"
4
+ class="mode-content mode-cards"
5
+ id="panel-prompts"
6
+ role="tabpanel"
7
+ aria-labelledby="tab-prompts">
8
+ <div class="segmented-control">
9
+ <button type="button" :class="['segment', { active: promptsSubTab === 'codex' }]" @click="switchPromptsSubTab('codex')">{{ t('prompts.subTab.codex') }}</button>
10
+ <button type="button" :class="['segment', { active: promptsSubTab === 'claude-md' }]" @click="switchPromptsSubTab('claude-md')">{{ t('prompts.subTab.claude') }}</button>
11
+ </div>
12
+
13
+ <div class="prompts-editor">
14
+ <div class="prompts-editor-toolbar">
15
+ <div class="form-hint">
16
+ {{ agentsPath || t('common.notLoaded') }}
17
+ <span v-if="agentsPath">
18
+ ({{ agentsExists ? t('common.exists') : t('common.notExistsWillCreateOnSave') }})
19
+ </span>
20
+ </div>
21
+ <div class="prompts-editor-actions">
22
+ <div class="prompts-editor-group prompts-editor-group--secondary">
23
+ <button
24
+ class="btn-mini"
25
+ @click="exportAgentsContent"
26
+ :disabled="agentsLoading">
27
+ {{ t('modal.agents.export') }}
28
+ </button>
29
+ <button
30
+ class="btn-mini"
31
+ @click="copyAgentsContent"
32
+ :disabled="agentsLoading">
33
+ {{ t('modal.agents.copy') }}
34
+ </button>
35
+ <button
36
+ class="btn-mini"
37
+ @click="pasteAgentsContent"
38
+ :disabled="agentsLoading || agentsSaving || agentsDiffVisible">
39
+ {{ t('common.paste') }}
40
+ </button>
41
+ </div>
42
+ <div class="prompts-editor-group prompts-editor-group--workflow">
43
+ <button class="btn-mini" @click="loadPromptsContent" :disabled="agentsSaving || agentsDiffLoading">{{ t('common.cancel') }}</button>
44
+ <button
45
+ v-if="agentsDiffVisible"
46
+ class="btn-mini"
47
+ @click="resetAgentsDiffState"
48
+ :disabled="agentsSaving || agentsDiffLoading">
49
+ {{ t('common.backToEdit') }}
50
+ </button>
51
+ <button class="btn-mini btn-confirm-mini" @click="applyAgentsContent" :disabled="agentsSaving || agentsLoading || agentsDiffLoading || (!agentsDiffVisible && !hasAgentsContentChanged()) || (agentsDiffVisible && !agentsDiffHasChanges)">
52
+ {{ agentsSaving ? (agentsDiffVisible ? t('common.saving') : t('common.previewing')) : (agentsDiffVisible ? t('common.save') : t('common.preview')) }}
53
+ </button>
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <div class="form-group">
59
+ <div v-if="agentsDiffVisible">
60
+ <div
61
+ v-if="!agentsDiffLoading && !agentsDiffError && !agentsDiffTruncated && (agentsDiffStats.added || agentsDiffStats.removed)"
62
+ class="agents-diff-summary">
63
+ <span class="agents-diff-stat add">+{{ agentsDiffStats.added }}</span>
64
+ <span class="agents-diff-stat del">-{{ agentsDiffStats.removed }}</span>
65
+ </div>
66
+ <div v-if="agentsDiffLoading" class="state-message">{{ t('diff.generating') }}</div>
67
+ <div v-else-if="agentsDiffError" class="state-message error">{{ agentsDiffError }}</div>
68
+ <div v-else-if="agentsDiffTruncated" class="agents-diff-empty">{{ t('diff.tooLargeSkip') }}</div>
69
+ <div v-else-if="!agentsDiffHasChanges" class="agents-diff-empty">{{ t('diff.noChanges') }}</div>
70
+ <div v-else class="agents-diff-view agents-diff-editor">
71
+ <div
72
+ v-for="(line, index) in agentsDiffLines"
73
+ :key="line.key || (line.type + '-' + index)"
74
+ :class="['agents-diff-line', line.type]">
75
+ <span class="agents-diff-line-sign">
76
+ {{ line.type === 'add' ? '+' : (line.type === 'del' ? '-' : ' ') }}
77
+ </span>
78
+ <span class="agents-diff-line-text">{{ line.value }}</span>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ <div :class="['editor-frame', { 'editor-frame--loading': agentsLoading }]">
83
+ <div v-if="agentsLoading" class="editor-skeleton">
84
+ <div class="skeleton-line" v-for="i in 6" :key="i"></div>
85
+ </div>
86
+ <textarea
87
+ v-model="agentsContent"
88
+ class="form-input template-editor"
89
+ spellcheck="false"
90
+ :readonly="agentsLoading || agentsSaving || agentsDiffVisible"
91
+ @input="onAgentsContentInput"
92
+ :placeholder="t(promptsSubTab === 'claude-md' ? 'modal.agents.placeholder.claudeMd' : 'modal.agents.placeholder')"></textarea>
93
+ </div>
94
+ <div v-if="promptsContextHint" :class="['prompts-context-hint', { 'prompts-context-hint--warn': promptsContextHint.warn }]">
95
+ {{ promptsContextHint.text }}
96
+ </div>
97
+ </div>
98
+
99
+ </div>
100
+ </div>
@@ -219,33 +219,53 @@
219
219
  </div>
220
220
  </div>
221
221
  <div class="session-actions">
222
- <button class="btn-session-refresh" @click="loadActiveSessionDetail" :disabled="sessionDetailLoading || !activeSession">
223
- {{ sessionDetailLoading ? t('sessions.preview.loading') : t('sessions.preview.refresh') }}
222
+ <button class="btn-session-refresh" @click="loadActiveSessionDetail" :disabled="sessionDetailLoading || !activeSession"
223
+ :title="sessionDetailLoading ? t('sessions.preview.loading') : t('sessions.preview.refresh')"
224
+ :aria-label="sessionDetailLoading ? t('sessions.preview.loading') : t('sessions.preview.refresh')">
225
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 8a5.5 5.5 0 0 1 9.4-3.8"/><path d="M13.5 8a5.5 5.5 0 0 1-9.4 3.8"/><polyline points="2.5 2 2.5 5 5.5 5"/><polyline points="13.5 14 13.5 11 10.5 11"/></svg>
224
226
  </button>
225
227
  <button
226
228
  v-if="isDeleteAvailable(activeSession)"
227
229
  class="btn-session-delete"
228
230
  @click="deleteSession(activeSession)"
229
- :disabled="!activeSession || sessionsLoading || sessionDeleting[getSessionExportKey(activeSession)]">
230
- {{ (activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? (sessionTrashEnabled === false ? t('sessions.preview.deleting') : t('sessions.preview.moving')) : (sessionTrashEnabled === false ? t('sessions.preview.deleteHard') : t('sessions.preview.moveToTrash')) }}
231
+ :disabled="!activeSession || sessionsLoading || sessionDeleting[getSessionExportKey(activeSession)]"
232
+ :title="(activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? (sessionTrashEnabled === false ? t('sessions.preview.deleting') : t('sessions.preview.moving')) : (sessionTrashEnabled === false ? t('sessions.preview.deleteHard') : t('sessions.preview.moveToTrash'))"
233
+ :aria-label="(activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? (sessionTrashEnabled === false ? t('sessions.preview.deleting') : t('sessions.preview.moving')) : (sessionTrashEnabled === false ? t('sessions.preview.deleteHard') : t('sessions.preview.moveToTrash'))">
234
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 4 4 4 13 4"/><path d="M5.5 4V2.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V4"/><path d="M12 4v9.5a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 4 13.5V4"/></svg>
231
235
  </button>
232
236
  <button
233
237
  class="btn-session-export"
234
238
  @click="exportSession(activeSession)"
235
- :disabled="!activeSession || sessionExporting[getSessionExportKey(activeSession)]">
236
- {{ (activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? t('sessions.preview.exporting') : t('sessions.preview.export') }}
239
+ :disabled="!activeSession || sessionExporting[getSessionExportKey(activeSession)]"
240
+ :title="(activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? t('sessions.preview.exporting') : t('sessions.preview.export')"
241
+ :aria-label="(activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? t('sessions.preview.exporting') : t('sessions.preview.export')">
242
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v8"/><polyline points="4 7 8 10.5 12 7"/><path d="M2.5 12v1.5a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V12"/></svg>
237
243
  </button>
244
+ <div class="session-link-group">
238
245
  <button
239
246
  class="btn-session-open"
240
247
  @click="copySessionLink(activeSession)"
241
- :disabled="!activeSession">
242
- {{ t('sessions.preview.copyLink') }}
248
+ :disabled="!activeSession || !canBuildStandaloneUrl(activeSession)"
249
+ :title="t('sessions.preview.copyLink')"
250
+ :aria-label="t('sessions.preview.copyLink')">
251
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="2.5" width="4" height="2" rx="1"/><path d="M4.5 4h7a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5"/><line x1="6" y1="7.5" x2="10" y2="7.5"/><line x1="6" y1="10" x2="10" y2="10"/></svg>
243
252
  </button>
253
+ <button
254
+ class="btn-session-open"
255
+ @click="openSessionLink(activeSession)"
256
+ :disabled="!activeSession || !canBuildStandaloneUrl(activeSession)"
257
+ :title="t('sessions.preview.openLink')"
258
+ :aria-label="t('sessions.preview.openLink')">
259
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2h-8.5A1.5 1.5 0 0 0 2 3.5v8A1.5 1.5 0 0 0 3.5 13h8a1.5 1.5 0 0 0 1.5-1.5v-3"/><path d="M13 2v4h-4"/><path d="M13 2L7 8"/></svg>
260
+ </button>
261
+ </div>
244
262
  <button
245
263
  class="btn-session-open"
246
264
  @click="copySessionPath(activeSession)"
247
- :disabled="!activeSession || !getSessionFilePath(activeSession)">
248
- {{ t('sessions.preview.copyPath') }}
265
+ :disabled="!activeSession || !getSessionFilePath(activeSession)"
266
+ :title="t('sessions.preview.copyPath')"
267
+ :aria-label="t('sessions.preview.copyPath')">
268
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="2.5" width="4" height="2" rx="1"/><path d="M4.5 4h7a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5"/><line x1="6" y1="7.5" x2="10" y2="7.5"/><line x1="6" y1="10" x2="10" y2="10"/></svg>
249
269
  </button>
250
270
  </div>
251
271
  </div>
@@ -75,26 +75,42 @@
75
75
  <section v-if="sessionUsageWave.points && sessionUsageWave.points.length" class="usage-wave-section">
76
76
  <div class="usage-card-title">{{ t('usage.daily.title') }}</div>
77
77
  <div class="usage-wave-container">
78
- <svg class="usage-wave-chart" viewBox="0 0 800 140" preserveAspectRatio="none">
79
- <defs>
80
- <linearGradient :id="'wave-gradient-' + sessionsUsageTimeRange" x1="0" y1="0" x2="0" y2="1">
81
- <stop offset="0%" :stop-color="'var(--color-brand)'" stop-opacity="0.35"/>
82
- <stop offset="100%" :stop-color="'var(--color-brand)'" stop-opacity="0"/>
83
- </linearGradient>
84
- </defs>
85
- <path :d="sessionUsageWave.areaPath" :fill="'url(#wave-gradient-' + sessionsUsageTimeRange + ')'" class="usage-wave-area"/>
86
- <path :d="sessionUsageWave.linePath" fill="none" :stroke="'var(--color-brand)'" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="usage-wave-line"/>
87
- <line v-if="sessionsUsageSelectedDay" x1="0" :x2="sessionUsageWave.width" :y1="sessionUsageWave.hoverY" :y2="sessionUsageWave.hoverY" stroke="currentColor" stroke-width="1" stroke-dasharray="4 4" opacity="0.5" class="usage-wave-hover-line"/>
88
- <circle v-if="sessionsUsageSelectedDay" :cx="sessionUsageWave.hoverX" :cy="sessionUsageWave.hoverY" r="5" :fill="'var(--color-surface)'" :stroke="'var(--color-brand)'" stroke-width="2.5" class="usage-wave-hover-point"/>
89
- </svg>
90
- <div class="usage-wave-labels">
91
- <span v-for="label in sessionUsageWave.labels" :key="label.key"
92
- class="usage-wave-label"
93
- :class="{ active: sessionsUsageSelectedDay === label.key }"
94
- @click="selectSessionsUsageDay(label.key)">
95
- {{ label.text }}
78
+ <div class="usage-wave-yaxis">
79
+ <span v-for="tick in sessionUsageWave.yTicks" :key="tick.value"
80
+ class="usage-wave-ytick"
81
+ :style="{ bottom: tick.percent + '%' }">
82
+ {{ tick.label }}
96
83
  </span>
97
84
  </div>
85
+ <div class="usage-wave-chart-area">
86
+ <svg class="usage-wave-chart" viewBox="0 0 800 140" preserveAspectRatio="none">
87
+ <defs>
88
+ <linearGradient :id="'wave-gradient-' + sessionsUsageTimeRange" x1="0" y1="0" x2="0" y2="1">
89
+ <stop offset="0%" :stop-color="'var(--color-brand)'" stop-opacity="0.35"/>
90
+ <stop offset="100%" :stop-color="'var(--color-brand)'" stop-opacity="0"/>
91
+ </linearGradient>
92
+ </defs>
93
+ <line v-for="tick in sessionUsageWave.yTicks" :key="'g-' + tick.value"
94
+ x1="0" :x2="sessionUsageWave.width"
95
+ :y1="tick.y" :y2="tick.y"
96
+ stroke="currentColor" stroke-width="0.5" opacity="0.12"
97
+ class="usage-wave-gridline"/>
98
+ <path :d="sessionUsageWave.areaPath" :fill="'url(#wave-gradient-' + sessionsUsageTimeRange + ')'" class="usage-wave-area"/>
99
+ <path :d="sessionUsageWave.linePath" fill="none" :stroke="'var(--color-brand)'" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="usage-wave-line"/>
100
+ <line v-if="sessionsUsageSelectedDay" x1="0" :x2="sessionUsageWave.width" :y1="sessionUsageWave.hoverY" :y2="sessionUsageWave.hoverY" stroke="currentColor" stroke-width="1" stroke-dasharray="4 4" opacity="0.5" class="usage-wave-hover-line"/>
101
+ <circle v-if="sessionsUsageSelectedDay" :cx="sessionUsageWave.hoverX" :cy="sessionUsageWave.hoverY" r="5" :fill="'var(--color-surface)'" :stroke="'var(--color-brand)'" stroke-width="2.5" class="usage-wave-hover-point"/>
102
+ </svg>
103
+ <div class="usage-wave-labels">
104
+ <button v-for="label in sessionUsageWave.labels" :key="label.key"
105
+ type="button"
106
+ class="usage-wave-label"
107
+ :class="{ active: sessionsUsageSelectedDay === label.key }"
108
+ :aria-pressed="sessionsUsageSelectedDay === label.key"
109
+ @click="selectSessionsUsageDay(label.key)">
110
+ {{ label.text }}
111
+ </button>
112
+ </div>
113
+ </div>
98
114
  </div>
99
115
  <div v-if="sessionsUsageSelectedDaySummary" class="usage-daydetail">
100
116
  <div class="usage-daydetail-header">