This commit is contained in:
Félix Dorn 2025-06-19 12:27:59 +02:00
commit c3ee96429c
46 changed files with 6006 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>