init
This commit is contained in:
commit
c3ee96429c
46 changed files with 6006 additions and 0 deletions
16
www/src/components/AppLogo.vue
Normal file
16
www/src/components/AppLogo.vue
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<span>Pucoti</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
font-family: var(--font-display);
|
||||
color: var(--color-light);
|
||||
background-color: var(--color-dark);
|
||||
padding: 0.25em 0.75em;
|
||||
font-style: italic;
|
||||
display: inline-flex;
|
||||
}
|
||||
</style>
|
51
www/src/components/Button.vue
Normal file
51
www/src/components/Button.vue
Normal file
|
@ -0,0 +1,51 @@
|
|||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
shortcut?: string
|
||||
label: string
|
||||
outline?: boolean
|
||||
}>()
|
||||
</script>
|
||||
<template>
|
||||
<button v-bind="$attrs" :class="{ outline: outline }">
|
||||
<span>{{ label }}</span>
|
||||
<kbd v-if="shortcut">{{ shortcut }}</kbd>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0 1vw;
|
||||
|
||||
padding: 0.5vw 0.8vw;
|
||||
|
||||
color: var(--color-light);
|
||||
border: 1px solid color-mix(in oklab, var(--color-light) 20%, transparent);
|
||||
background-image: linear-gradient(to top,
|
||||
transparent 0%,
|
||||
color-mix(in oklab, var(--color-light) 15%, var(--color-dark)) 90%);
|
||||
}
|
||||
|
||||
button.outline {
|
||||
border: none;
|
||||
background: var(--color-darker);
|
||||
padding: 0.3em 0.8em;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: monospace;
|
||||
width: clamp(1.2em, 2vw, 5em);
|
||||
height: clamp(1.2em, 2vw, 5em);
|
||||
background-color: var(--color-dark);
|
||||
border: 1px solid color-mix(in oklab, var(--color-light) 10%, transparent);
|
||||
}
|
||||
</style>
|
240
www/src/components/IntentionHistory.vue
Normal file
240
www/src/components/IntentionHistory.vue
Normal file
|
@ -0,0 +1,240 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import { usePucotiStore } from '@/stores/counter'
|
||||
import { fmtSeconds } from '@/utils'
|
||||
import Button from './Button.vue'
|
||||
import type { IntentionHistoryItem } from '@/types'
|
||||
|
||||
const store = usePucotiStore()
|
||||
|
||||
// Reactive state for editing
|
||||
const editingIndex = ref<number | null>(null)
|
||||
const editingValue = ref<string>('')
|
||||
|
||||
// Computed property for completed intentions with duration
|
||||
const completedIntentions = computed(() => {
|
||||
let history = store.intentionHistory.filter(
|
||||
(item) => item.intention != '' && item.end !== undefined,
|
||||
)
|
||||
return history
|
||||
.map((item: IntentionHistoryItem, index: number) => ({
|
||||
...item,
|
||||
index,
|
||||
duration: item.end ? item.end - item.start : 0,
|
||||
isCompleted: !!item.end,
|
||||
}))
|
||||
.reverse() // Show most recent first
|
||||
})
|
||||
|
||||
// Edit functionality
|
||||
const startEditing = (index: number, currentText: string) => {
|
||||
editingIndex.value = index
|
||||
editingValue.value = currentText
|
||||
nextTick(() => {
|
||||
const input = document.querySelector(`input[data-edit-index="${index}"]`) as HTMLInputElement
|
||||
if (input) {
|
||||
input.focus()
|
||||
input.select()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const saveEdit = (originalIndex: number) => {
|
||||
if (editingValue.value.trim()) {
|
||||
store.updateHistoricalIntention(originalIndex, editingValue.value.trim())
|
||||
}
|
||||
cancelEdit()
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
editingIndex.value = null
|
||||
editingValue.value = ''
|
||||
}
|
||||
|
||||
const handleEditKeydown = (event: KeyboardEvent, originalIndex: number) => {
|
||||
event.stopPropagation()
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
saveEdit(originalIndex)
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
cancelEdit()
|
||||
}
|
||||
}
|
||||
|
||||
// Continue functionality
|
||||
const continueFromHistory = (intention: string) => {
|
||||
store.continueFromHistory(intention)
|
||||
}
|
||||
|
||||
// Format duration helper
|
||||
const formatDuration = (milliseconds: number) => {
|
||||
const parts = fmtSeconds(milliseconds)
|
||||
return parts.join(':')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="history-section">
|
||||
<h2 class="history-title">History</h2>
|
||||
|
||||
<div v-if="completedIntentions.length === 0" class="empty-state">
|
||||
No intentions completed yet. Start working on something to see your history here.
|
||||
</div>
|
||||
|
||||
<div v-else class="history-list">
|
||||
<div
|
||||
v-for="item in completedIntentions"
|
||||
:key="`${item.index}-${item.start}`"
|
||||
class="history-item"
|
||||
>
|
||||
<div class="history-item-content">
|
||||
<div class="intention-text">
|
||||
<input
|
||||
v-if="editingIndex === item.index"
|
||||
:data-edit-index="item.index"
|
||||
v-model="editingValue"
|
||||
class="intention-edit-input"
|
||||
@keydown="handleEditKeydown($event, item.index)"
|
||||
@blur="cancelEdit"
|
||||
@click.stop
|
||||
/>
|
||||
<span v-else class="intention-display">{{ item.intention }}</span>
|
||||
</div>
|
||||
|
||||
<div class="history-actions">
|
||||
<button
|
||||
v-if="editingIndex !== item.index"
|
||||
class="edit-button"
|
||||
title="Edit intention"
|
||||
@click="startEditing(item.index, item.intention)"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
|
||||
<div class="duration">{{ formatDuration(item.duration) }}</div>
|
||||
|
||||
<Button label="Continue" @click="continueFromHistory(item.intention)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.history-section {
|
||||
margin-top: clamp(2em, 3vw, 100vw);
|
||||
}
|
||||
|
||||
.history-title {
|
||||
color: var(--color-acid);
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.5em, 2.5vw, 100vw);
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: color-mix(in oklab, var(--color-light) 80%, transparent);
|
||||
}
|
||||
|
||||
.history-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
background-color: color-mix(in oklab, var(--color-light) 5%, transparent);
|
||||
border: 1px solid color-mix(in oklab, var(--color-light) 15%, transparent);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5em;
|
||||
padding: 1em;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background-color: color-mix(in oklab, var(--color-light) 8%, transparent);
|
||||
}
|
||||
|
||||
.history-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.intention-text {
|
||||
flex: 1;
|
||||
min-width: 0; /* Allow text to shrink */
|
||||
}
|
||||
|
||||
.intention-display {
|
||||
color: var(--color-light);
|
||||
word-break: break-word;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.intention-edit-input {
|
||||
width: 100%;
|
||||
background: color-mix(in oklab, var(--color-light) 10%, transparent);
|
||||
border: 1px solid var(--color-acid);
|
||||
border-radius: 4px;
|
||||
color: var(--color-light);
|
||||
padding: 0.25em 0.5em;
|
||||
font-size: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.intention-edit-input:focus {
|
||||
border-color: var(--color-acid);
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--color-acid) 20%, transparent);
|
||||
}
|
||||
|
||||
.history-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.25em;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.edit-button:hover {
|
||||
background-color: color-mix(in oklab, var(--color-light) 15%, transparent);
|
||||
}
|
||||
|
||||
.duration {
|
||||
color: var(--color-acid);
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 0.9em;
|
||||
min-width: 4em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Scrollbar styling for webkit browsers */
|
||||
.history-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.history-list::-webkit-scrollbar-track {
|
||||
background: color-mix(in oklab, var(--color-light) 5%, transparent);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.history-list::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in oklab, var(--color-light) 20%, transparent);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.history-list::-webkit-scrollbar-thumb:hover {
|
||||
background: color-mix(in oklab, var(--color-light) 30%, transparent);
|
||||
}
|
||||
</style>
|
33
www/src/components/Timer.vue
Normal file
33
www/src/components/Timer.vue
Normal file
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Timer } from '@/types'
|
||||
import { useListenerFn } from '@/lib'
|
||||
import { timerToMs, timerToString } from '@/utils'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
timer: Timer
|
||||
color: string
|
||||
}>()
|
||||
|
||||
const displayTime = ref('00:00')
|
||||
const color = ref('')
|
||||
|
||||
function handleClockTick() {
|
||||
displayTime.value = timerToString(props.timer)
|
||||
color.value = timerToMs(props.timer) < 0 ? 'var(--timer-negative)' : props.color
|
||||
}
|
||||
|
||||
onMounted(handleClockTick)
|
||||
useListenerFn('clock-tick', handleClockTick)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :style="{ color: color }">{{ displayTime }}</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(2em, 12vw, 100em);
|
||||
}
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue