design/things
Things 3: Sztuka skupionej prostoty
“Wierzymy, że prostota jest trudna.” — Cultured Code
Things 3 jest często nazywany najpiękniejszym menedżerem zadań, jaki kiedykolwiek stworzono. Ta piękność wyłania się z bezwzględnego skupienia na tym, co ważne, i eliminacji wszystkiego innego.
Dlaczego Things 3 ma znaczenie
Things 3 pokazuje, że ograniczenia tworzą jasność. Podczas gdy konkurenci piętrzą funkcje (powtarzające się zadania z 47 opcjami, projekty z zależnościami i wykresami Gantta), Things pyta: czego naprawdę potrzebuje człowiek, żeby załatwiać sprawy?
Kluczowe osiągnięcia: - Osiągnięcie wizualnej prostoty bez poświęcania głębi - Nadanie znaczenia datom (Dziś, Wieczór, Kiedyś) - Stworzenie hierarchii odzwierciedlającej sposób myślenia ludzi o pracy - Udowodnienie, że priorytet klawiatury może być piękny - Wyznaczenie standardu dla natywnego designu aplikacji macOS/iOS
Kluczowe wnioski
- Rozdziel „kiedy to zrobię?" od „kiedy jest termin?" - Model podwójnej daty w Things (Kiedy + Termin) eliminuje niejednoznaczność, która nęka większość aplikacji do zadań; zaplanowana intencja i twarde terminy służą różnym celom
- Kiedyś to funkcja, nie stan porażki - Danie pomysłom domu wolnego od presji zapobiega lękowi przed skrzynką odbiorczą; zadania w Kiedyś nie zaśmiecają Dziś ani nie wywołują poczucia winy
- Obszary nigdy się nie kończą, Projekty tak - To rozróżnienie odzwierciedla ludzkie poznanie: „Praca" to trwająca domena życia, „Wydaj v2.0" ma linię mety; mieszanie ich tworzy zamieszanie
- Zarezerwuj kolor dla znaczenia - Interfejs Things jest w 95% neutralny (czarny, biały, szary); kolor pojawia się tylko dla informacji semantycznej (żółty=Dziś, czerwony=Termin, indygo=Wieczór)
- Ukończenie powinno być satysfakcjonujące - Animacja pola wyboru (wypełnienie → znacznik → dźwięk) trwa 500ms, ale dostarcza dopaminę; ukończenie zadań staje się wewnętrznie motywujące
Podstawowe zasady projektowania
1. Naturalna hierarchia
Things organizuje pracę w sposób, w jaki ludzie o niej myślą: od szerokiego (obszary życia) do szczegółowego (pojedyncze zadania).
HIERARCHIA THINGS:
Obszar (Praca, Osobiste, Zdrowie) ← Szerokie kategorie życiowe
│
└── Projekt (Wydaj aplikację) ← Skończony cel z zadaniami
│
└── Nagłówek (Design) ← Opcjonalne grupowanie
│
└── Zadanie ← Pojedyncza akcja do wykonania
│
└── Lista kontrolna ← Podkroki w zadaniu
Przykład:
┌─────────────────────────────────────────────────────────────┐
│ [A] PRACA (Obszar) │
│ ├── [P] Wydaj wersję 2.0 (Projekt) │
│ │ ├── [H] Design │
│ │ │ ├── [ ] Stwórz nowe ikony │
│ │ │ └── [ ] Zaktualizuj paletę kolorów │
│ │ └── [H] Rozwój │
│ │ ├── [ ] Zaimplementuj flow autoryzacji │
│ │ └── [ ] Dodaj analitykę │
│ └── [P] Redesign strony (Projekt) │
│ └── [ ] Napisz tekst na stronę główną │
└─────────────────────────────────────────────────────────────┘
Kluczowy wniosek: Obszary nigdy się nie kończą (to kategorie życiowe). Projekty się kończą (wydaj do piątku). To rozróżnienie eliminuje paraliż planowania.
Model danych:
// Hierarchia Things wyrażona w kodzie
struct Area: Identifiable {
let id: UUID
var title: String
var projects: [Project]
var tasks: [Task] // Luźne zadania nie w projektach
}
struct Project: Identifiable {
let id: UUID
var title: String
var notes: String?
var deadline: Date?
var headings: [Heading]
var tasks: [Task]
var isComplete: Bool
}
struct Heading: Identifiable {
let id: UUID
var title: String
var tasks: [Task]
}
struct Task: Identifiable {
let id: UUID
var title: String
var notes: String?
var when: TaskSchedule?
var deadline: Date?
var tags: [Tag]
var checklist: [ChecklistItem]
var isComplete: Bool
}
enum TaskSchedule {
case today
case evening
case specificDate(Date)
case someday
}
2. Kiedy, nie tylko termin
Things rozdziela „kiedy to zrobię?" (Dziś, Wieczór, Kiedyś) od „kiedy to musi być zrobione?" (Termin). Większość aplikacji miesza te pojęcia.
TRADYCYJNY MODEL DATY:
┌──────────────────────────────────────────────────────────────┐
│ Zadanie: Napisz raport │
│ Termin: 15 marca │
│ │
│ Problem: Czy 15 marca to kiedy to zrobię, czy kiedy jest │
│ termin? Jeśli to termin, kiedy powinienem zacząć? │
└──────────────────────────────────────────────────────────────┘
MODEL DATY W THINGS:
┌──────────────────────────────────────────────────────────────┐
│ Zadanie: Napisz raport │
│ Kiedy: Dziś (będę nad tym pracować dziś) │
│ Termin: 15 marca (musi być ukończone do tego dnia) │
│ │
│ Jasność: Dwa oddzielne pytania, dwie oddzielne odpowiedzi │
└──────────────────────────────────────────────────────────────┘
INTELIGENTNE LISTY (automatyczne filtrowanie):
┌───────────────────┬─────────────────────────────────────────┐
│ [I] SKRZYNKA │ Nieskategoryzowane wpisy │
├───────────────────┼─────────────────────────────────────────┤
│ [*] DZIŚ │ when == .today OR deadline == today │
├───────────────────┼─────────────────────────────────────────┤
│ [E] DZIŚ WIECZÓR │ when == .evening │
├───────────────────┼─────────────────────────────────────────┤
│ [D] NADCHODZĄCE │ when == .specificDate (przyszłość) │
├───────────────────┼─────────────────────────────────────────┤
│ [A] KIEDYKOLWIEK │ when == nil AND deadline == nil │
├───────────────────┼─────────────────────────────────────────┤
│ [S] KIEDYŚ │ when == .someday │
├───────────────────┼─────────────────────────────────────────┤
│ [L] DZIENNIK │ isComplete == true │
└───────────────────┴─────────────────────────────────────────┘
Kluczowy wniosek: „Kiedyś" to nie stan porażki — to zawór bezpieczeństwa. Pomysły mają swoje miejsce bez zaśmiecania Dziś.
Implementacja SwiftUI:
struct WhenPicker: View {
@Binding var schedule: TaskSchedule?
@Binding var deadline: Date?
@State private var showDatePicker = false
var body: some View {
VStack(spacing: 12) {
// Szybkie opcje
HStack(spacing: 8) {
WhenButton(
icon: "star.fill",
label: "Dziś",
color: .yellow,
isSelected: schedule == .today
) {
schedule = .today
}
WhenButton(
icon: "moon.fill",
label: "Wieczór",
color: .indigo,
isSelected: schedule == .evening
) {
schedule = .evening
}
WhenButton(
icon: "calendar",
label: "Wybierz datę",
color: .red,
isSelected: showDatePicker
) {
showDatePicker = true
}
WhenButton(
icon: "archivebox.fill",
label: "Kiedyś",
color: .brown,
isSelected: schedule == .someday
) {
schedule = .someday
}
}
// Termin (oddzielnie od "kiedy")
if let deadline = deadline {
HStack {
Image(systemName: "flag.fill")
.foregroundStyle(.red)
Text("Termin: \(deadline, style: .date)")
Spacer()
Button("Wyczyść") { self.deadline = nil }
}
.font(.caption)
}
}
}
}
struct WhenButton: View {
let icon: String
let label: String
let color: Color
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(spacing: 4) {
Image(systemName: icon)
.font(.title2)
Text(label)
.font(.caption2)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(isSelected ? color.opacity(0.2) : .clear)
.cornerRadius(8)
}
.buttonStyle(.plain)
.foregroundStyle(isSelected ? color : .secondary)
}
}
3. Najpierw klawiatura, przyjazność dla myszy
Things można obsługiwać całkowicie za pomocą klawiatury z efektywnością pamięci mięśniowej, a jednocześnie pozostaje intuicyjny dla użytkowników myszy.
SKRÓTY KLAWISZOWE (zapamiętywalne wzorce):
NAWIGACJA:
Cmd+1-6 Przeskocz do inteligentnych list (Skrzynka, Dziś, itp.)
Cmd+Up/Down Przesuń między sekcjami
Space Szybki podgląd (podgląd szczegółów zadania)
MANIPULACJA ZADANIAMI:
Cmd+K Ukończ zadanie
Cmd+S Zaplanuj (wybór Kiedy)
Cmd+Shift+D Ustaw termin
Cmd+Shift+M Przenieś do projektu
Cmd+Shift+T Dodaj tagi
TWORZENIE:
Space/Return Utwórz nowe zadanie (zależne od kontekstu)
Cmd+N Nowe zadanie w Skrzynce
Cmd+Opt+N Nowy projekt
Szybkie wprowadzanie (globalny skrót):
Ctrl+Space Otwiera pływające szybkie wprowadzanie z dowolnego miejsca
┌─────────────────────────────────────────────────┐
│ [ ] | │
│ Nowe zadanie pojawia się gdziekolwiek jesteś │
└─────────────────────────────────────────────────┘
Kluczowy wniosek: Skróty powinny być odkrywalne, ale nie wymagane. Interfejs podpowiada klawisze bez ich wymuszania.
Wzorzec CSS (podpowiedzi skrótów):
.action-button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
}
.shortcut-hint {
font-size: 11px;
font-family: var(--font-mono);
padding: 2px 6px;
background: var(--surface-subtle);
border-radius: 4px;
color: var(--text-tertiary);
opacity: 0;
transition: opacity 0.15s ease;
}
/* Pokaż przy najechaniu */
.action-button:hover .shortcut-hint {
opacity: 1;
}
/* Zawsze widoczne przy fokusie z klawiatury */
.action-button:focus-visible .shortcut-hint {
opacity: 1;
}
Szybkie wprowadzanie w SwiftUI:
struct QuickEntryPanel: View {
@Binding var isPresented: Bool
@State private var taskTitle = ""
@FocusState private var isFocused: Bool
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 12) {
// Pole wyboru (tylko wizualne)
Circle()
.strokeBorder(.tertiary, lineWidth: 1.5)
.frame(width: 20, height: 20)
// Pole wprowadzania zadania
TextField("Nowe zadanie", text: $taskTitle)
.textFieldStyle(.plain)
.font(.body)
.focused($isFocused)
.onSubmit {
createTask()
}
}
.padding()
.background(.ultraThinMaterial)
.cornerRadius(12)
.shadow(color: .black.opacity(0.2), radius: 20, y: 10)
}
.frame(width: 500)
.onAppear {
isFocused = true
}
.onExitCommand {
isPresented = false
}
}
private func createTask() {
guard !taskTitle.isEmpty else { return }
// Utwórz zadanie w Skrzynce
TaskStore.shared.createTask(title: taskTitle)
taskTitle = ""
}
}
4. Wizualna powściągliwość
Things używa koloru oszczędnie, rezerwując go wyłącznie dla znaczenia. Większość interfejsu jest neutralna, pozwalając kolorowym elementom się wyróżniać.
UŻYCIE KOLORU W THINGS:
Neutralne (95% interfejsu):
├── Tło: Czysta biel / Głęboka szarość
├── Tekst: Czarny / Biały
├── Obramowania: Bardzo subtelna szarość
└── Ikony: Monochromatyczne
Znaczący kolor (5%):
├── Żółty [*] = Dziś
├── Czerwony [!] = Termin / Zaległy
├── Niebieski [#] = Tagi (przypisane przez użytkownika)
├── Zielony [x] = Animacja ukończenia
└── Indygo [E] = Wieczór
PRZYKŁAD WIZUALNY:
┌─────────────────────────────────────────────────────────────┐
│ DZIŚ [*] (żółty) │
├─────────────────────────────────────────────────────────────┤
│ ( ) Napisz dokumentację │
│ Nazwa projektu - [!] Termin jutro (czerwony) │
│ │
│ ( ) Przejrzyj pull requesty │
│ [#] praca (niebieski tag) │
│ │
│ ( ) Zadzwoń do dentysty │
│ [E] Dziś wieczorem (indygo znacznik) │
└─────────────────────────────────────────────────────────────┘
Zauważ: Większość tekstu jest czarna. Kolor pojawia się tylko tam, gdzie niesie znaczenie.
Kluczowy wniosek: Gdy wszystko jest kolorowe, nic się nie wyróżnia. Zarezerwuj kolor dla informacji.
Wzorzec CSS:
:root {
/* Neutralna paleta (większość UI) */
--surface-primary: #ffffff;
--surface-secondary: #f5f5f7;
--text-primary: #1d1d1f;
--text-secondary: #86868b;
--border: rgba(0, 0, 0, 0.08);
/* Kolory semantyczne (używane oszczędnie) */
--color-today: #f5c518;
--color-deadline: #ff3b30;
--color-evening: #5856d6;
--color-complete: #34c759;
--color-tag: #007aff;
}
/* Wiersz zadania - głównie neutralny */
.task-row {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
background: var(--surface-primary);
color: var(--text-primary);
}
/* Kolor tylko dla znaczenia */
.deadline-badge {
font-size: 12px;
color: var(--color-deadline);
}
.deadline-badge.overdue {
font-weight: 600;
color: var(--color-deadline);
}
.tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
background: var(--color-tag);
color: white;
}
.evening-badge {
color: var(--color-evening);
}
5. Satysfakcjonujące ukończenie
Animacja ukończenia dostarcza nagrodę, nie tylko informację zwrotną. Satysfakcjonujący dźwięk „plink" i płynna animacja znacznika sprawiają, że ukończenie zadań jest przyjemne.
SEKWENCJA UKOŃCZENIA:
Klatka 1: ○ Tytuł zadania
Klatka 2-4: ◔ (okrąg wypełnia się zgodnie z ruchem wskazówek zegara)
Klatka 5-7: ● (całkowicie wypełniony, krótka pauza)
Klatka 8-10: ✓ (znacznik pojawia się, lekko się powiększa)
Klatka 11+: Wiersz odsuwa się, lista zamyka lukę
Dźwięk: Delikatny „plink" w klatce 7-8 (moment ukończenia)
Haptyka: Lekkie stuknięcie na iOS przy ukończeniu
Implementacja SwiftUI:
struct CompletableCheckbox: View {
@Binding var isComplete: Bool
@State private var animationPhase: AnimationPhase = .unchecked
enum AnimationPhase {
case unchecked, filling, checked, done
}
var body: some View {
Button {
completeWithAnimation()
} label: {
ZStack {
// Okrąg tła
Circle()
.strokeBorder(.tertiary, lineWidth: 1.5)
.frame(width: 22, height: 22)
// Animacja wypełnienia
Circle()
.trim(from: 0, to: fillAmount)
.stroke(.green, lineWidth: 1.5)
.frame(width: 22, height: 22)
.rotationEffect(.degrees(-90))
// Znacznik
Image(systemName: "checkmark")
.font(.system(size: 12, weight: .bold))
.foregroundStyle(.green)
.scaleEffect(checkScale)
.opacity(checkOpacity)
}
}
.buttonStyle(.plain)
.sensoryFeedback(.success, trigger: animationPhase == .checked)
}
private var fillAmount: CGFloat {
switch animationPhase {
case .unchecked: return 0
case .filling: return 1
case .checked, .done: return 1
}
}
private var checkScale: CGFloat {
animationPhase == .checked ? 1.2 : (animationPhase == .done ? 1.0 : 0)
}
private var checkOpacity: CGFloat {
animationPhase == .unchecked || animationPhase == .filling ? 0 : 1
}
private func completeWithAnimation() {
// Faza 1: Wypełnij okrąg
withAnimation(.easeInOut(duration: 0.2)) {
animationPhase = .filling
}
// Faza 2: Pokaż znacznik z odbiciem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
withAnimation(.spring(response: 0.3, dampingFraction: 0.5)) {
animationPhase = .checked
}
}
// Faza 3: Ustabilizuj i oznacz jako ukończone
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation(.easeOut(duration: 0.15)) {
animationPhase = .done
}
isComplete = true
}
}
}
Wzorce do przeniesienia
Wzorzec 1: Progresywne szczegóły zadania
Szczegóły zadania rozwijają się w miejscu, zamiast nawigować do oddzielnego widoku.
struct ExpandableTask: View {
let task: Task
@State private var isExpanded = false
var body: some View {
VStack(alignment: .leading, spacing: 0) {
// Zawsze widoczny wiersz
TaskRow(task: task, isExpanded: $isExpanded)
// Rozwijalne szczegóły
if isExpanded {
TaskDetails(task: task)
.padding(.leading, 44) // Wyrównaj z tytułem
.transition(.asymmetric(
insertion: .push(from: .top).combined(with: .opacity),
removal: .push(from: .bottom).combined(with: .opacity)
))
}
}
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: isExpanded)
}
}
Wzorzec 2: Wprowadzanie w języku naturalnym
Things parsuje język naturalny do ustawiania dat: „jutro", „w przyszłym tygodniu", „15 czerwca".
function parseNaturalDate(input: string): TaskSchedule | null {
const lower = input.toLowerCase();
// Skróty dla Dziś
if (['today', 'tod', 'now'].includes(lower)) {
return { type: 'today' };
}
// Wieczór
if (['tonight', 'evening', 'eve'].includes(lower)) {
return { type: 'evening' };
}
// Jutro
if (['tomorrow', 'tom', 'tmrw'].includes(lower)) {
const date = new Date();
date.setDate(date.getDate() + 1);
return { type: 'date', date };
}
// Kiedyś
if (['someday', 'later', 'eventually'].includes(lower)) {
return { type: 'someday' };
}
// Względne dni
const inMatch = lower.match(/in (\d+) days?/);
if (inMatch) {
const date = new Date();
date.setDate(date.getDate() + parseInt(inMatch[1]));
return { type: 'date', date };
}
// Nazwy dni
const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
const dayIndex = days.findIndex(d => lower.startsWith(d.slice(0, 3)));
if (dayIndex !== -1) {
const date = getNextDayOfWeek(dayIndex);
return { type: 'date', date };
}
return null;
}
Wzorzec 3: Przeciągnij i upuść na liście
Zmiana kolejności to bezpośrednia manipulacja z wyraźną informacją wizualną.
.task-row {
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.task-row.dragging {
transform: scale(1.02);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.15),
0 0 0 2px var(--color-focus);
z-index: 100;
}
.task-row.drop-above::before {
content: '';
position: absolute;
top: -2px;
left: 44px;
right: 16px;
height: 4px;
background: var(--color-focus);
border-radius: 2px;
}
.task-row.drop-below::after {
content: '';
position: absolute;
bottom: -2px;
left: 44px;
right: 16px;
height: 4px;
background: var(--color-focus);
border-radius: 2px;
}
Lekcje projektowania
- Rozdziel zagadnienia: „Kiedy to zrobię?" vs „Kiedy jest termin?" to różne pytania
- Powściągliwość z kolorem: Zarezerwuj kolor dla znaczenia; neutralność jest domyślna
- Ukończenie to nagroda: Spraw, by kończenie zadań było przyjemne
- Klawiatura przyspiesza, mysz wita: Projektuj dla obu bez kompromisów
- Naturalne hierarchie: Odzwierciedlaj sposób, w jaki ludzie już myślą o pracy (obszary → projekty → zadania)
- Kiedyś to funkcja: Daj pomysłom dom, który nie tworzy presji
Najczęściej zadawane pytania
Jaka jest różnica między „Kiedy" a „Terminem" w Things 3?
„Kiedy" odpowiada na pytanie „kiedy będę nad tym pracować?" (Dziś, Dziś wieczorem, Kiedyś lub konkretna data). „Termin" odpowiada na pytanie „kiedy to musi być ukończone?" Większość aplikacji do zadań miesza te pojęcia, ale służą one różnym celom. Zadanie może mieć termin na piątek (Termin), ale planujesz nad nim pracować w środę (Kiedy). To rozdzielenie pozwala planować dzień wokół intencji, jednocześnie śledząc twarde terminy.
Jak działa hierarchia w Things 3 (Obszary, Projekty, Zadania)?
Obszary to szerokie kategorie życiowe, które nigdy się nie kończą (Praca, Osobiste, Zdrowie). Projekty to skończone cele z jasnym stanem końcowym (Wydaj aplikację, Zaplanuj wakacje). Zadania to pojedyncze akcje do wykonania. Nagłówki opcjonalnie grupują zadania w projektach. To odzwierciedla sposób, w jaki ludzie naturalnie myślą o pracy: trwające obowiązki zawierają czasowo ograniczone cele, które zawierają dyskretne działania.
Dlaczego Things 3 używa tak mało koloru?
Things rezerwuje kolor wyłącznie dla znaczenia semantycznego. Interfejs jest w 95% neutralny (czarny, biały, szary), więc gdy pojawia się kolor, komunikuje: żółty oznacza Dziś, czerwony oznacza Termin/Zaległy, indygo oznacza Wieczór, niebieski oznacza tagi. Gdyby wszystko było kolorowe, nic by się nie wyróżniało. Powściągliwość sprawia, że znaczące kolory są niemożliwe do przeoczenia.
Czym jest „Kiedyś" w Things 3 i dlaczego jest wartościowe?
Kiedyś to dedykowane miejsce dla zadań, które chcesz zapamiętać, ale nie chcesz, by zaśmiecały aktywne listy. To nie jest stan porażki — to zawór bezpieczeństwa. Pomysły na „kiedyś" (naucz się hiszpańskiego, przeczytaj tę książkę) mają swój dom bez tworzenia codziennego poczucia winy. Możesz przeglądać Kiedyś co tydzień i promować zadania do Dziś, gdy będziesz gotowy.
Jak działa animacja ukończenia w Things 3?
Gdy stukniesz pole wyboru, okrąg wypełnia się zgodnie z ruchem wskazówek zegara (200ms), następnie pojawia się znacznik z lekkim odbiciem (animacja sprężynowa), czemu towarzyszy delikatny dźwięk „plink" i informacja haptyczna na iOS. Wiersz następnie odsuwa się (150ms). Cała sekwencja trwa około 500ms, ale ten celowy timing tworzy mały zastrzyk dopaminy, który sprawia, że ukończenie zadań jest satysfakcjonujące.
Zasoby
- Strona: culturedcode.com/things
- Filozofia projektowania: Wpisy na blogu Cultured Code o prostocie
- Skróty klawiszowe: Wbudowane menu Pomoc (Cmd+/)
- Integracja z GTD: Jak Things mapuje się na metodologię Getting Things Done