Skip to content

Commit 7f1b7cb

Browse files
committed
tactile animated button
1 parent 0770451 commit 7f1b7cb

5 files changed

Lines changed: 592 additions & 15 deletions

File tree

src/components/assessment/AnswerScale.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CSSProperties } from "react";
1+
import { useEffect, useState, type CSSProperties } from "react";
22
import type { AnswerValue, AssessmentSession } from "@/types/assessment";
33

44
interface Props {
@@ -124,7 +124,7 @@ function optionHint(session: AssessmentSession, n: AnswerValue) {
124124

125125
function hapticTap() {
126126
try {
127-
navigator.vibrate?.(12);
127+
navigator.vibrate?.([18, 10, 16]);
128128
} catch {
129129
// Browser may not support haptic vibration. The visual press effect still runs.
130130
}
@@ -134,9 +134,17 @@ export function AnswerScale({ value, onSelect, leftLabel, rightLabel, session }:
134134
const labels = getLabels(session);
135135
const selectedIndex = value ? value - 1 : -1;
136136
const selectedTone = value ? SCALE_TONES[value] : undefined;
137+
const [pressValue, setPressValue] = useState<AnswerValue | null>(null);
138+
139+
useEffect(() => {
140+
if (!pressValue) return;
141+
const timer = window.setTimeout(() => setPressValue(null), 560);
142+
return () => window.clearTimeout(timer);
143+
}, [pressValue]);
137144

138145
function handleSelect(n: AnswerValue) {
139146
hapticTap();
147+
setPressValue(n);
140148
onSelect(n);
141149
}
142150

@@ -186,7 +194,7 @@ export function AnswerScale({ value, onSelect, leftLabel, rightLabel, session }:
186194
aria-pressed={selected}
187195
className={`answer-pill-option group relative flex h-[54px] w-full items-center gap-3 rounded-full px-3 text-left outline-none transition-[background,box-shadow,transform,color] duration-150 ease-out focus-visible:ring-2 focus-visible:ring-primary/30 ${
188196
selected ? "is-selected text-foreground" : "text-foreground"
189-
}`}
197+
} ${pressValue === n ? "is-pressing" : ""}`}
190198
style={
191199
{
192200
"--scale-fill": tone.fill,

src/data/observableNarratives.ts

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import type { MicroRoleDefinition, MicroRoleId } from "@/data/microRoles";
2+
import type { MicroRoleScore } from "@/engine/patternSignature";
3+
4+
export interface MicroRoleNarrative {
5+
observable: string[];
6+
dailyHabits: string[];
7+
underPressure: string[];
8+
communicationOn: string[];
9+
communicationResistance: string[];
10+
analogy: string;
11+
blindSpot: string;
12+
}
13+
14+
const NARRATIVES: Partial<Record<MicroRoleId, MicroRoleNarrative>> = {
15+
information_collector: {
16+
observable: [
17+
"Sering merasa perlu mengumpulkan bahan, referensi, atau konteks sebelum merasa cukup yakin.",
18+
"Lebih mudah percaya pada penjelasan yang punya dasar, bukan hanya kesan umum.",
19+
],
20+
dailyHabits: [
21+
"Menyimpan catatan, link, foto, barang, atau informasi karena merasa suatu saat bisa berguna.",
22+
"Sering bertanya detail tambahan sebelum mengambil kesimpulan.",
23+
],
24+
underPressure: [
25+
"Saat terdesak, cenderung mencari data tambahan atau mengumpulkan konteks agar keputusan tidak terasa kosong.",
26+
],
27+
communicationOn: ["Ajak dengan pertanyaan berbasis data, contoh nyata, dan ruang untuk mengecek konteks."],
28+
communicationResistance: ["Jangan memaksa keputusan cepat sambil menutup akses pada informasi yang dianggap penting."],
29+
analogy: "Seperti perpustakaan berjalan: banyak bahan disimpan bukan untuk dipamerkan, tetapi agar bisa dipakai saat dibutuhkan.",
30+
blindSpot: "Bisa terlalu lama menunggu informasi terasa lengkap sebelum bergerak.",
31+
},
32+
fast_learner: {
33+
observable: [
34+
"Mudah tertarik pada hal baru yang membuat kapasitas diri naik.",
35+
"Cepat hidup ketika ada tantangan belajar atau kesempatan memahami sesuatu lebih dalam.",
36+
],
37+
dailyHabits: ["Mencari tutorial, penjelasan, kursus, atau referensi ketika menemukan bidang baru.", "Mudah bosan jika hanya diminta mengulang pola yang tidak memberi pembelajaran."],
38+
underPressure: ["Saat tertekan, cenderung mencoba memahami cepat apa yang belum diketahui agar bisa mengejar situasi."],
39+
communicationOn: ["Beri ruang belajar, alasan, dan kesempatan berkembang; bukan hanya instruksi kaku."],
40+
communicationResistance: ["Jangan meremehkan rasa ingin tahunya atau memaksa rutinitas tanpa makna belajar."],
41+
analogy: "Seperti mesin yang menyala ketika diberi bahan bakar pengetahuan baru.",
42+
blindSpot: "Bisa terlalu sering pindah minat jika tidak ada prioritas yang jelas.",
43+
},
44+
deep_thinker: {
45+
observable: ["Tidak selalu langsung bereaksi, tetapi sering membawa pemikiran yang lebih matang setelah diberi waktu.", "Mencari makna, filosofi, atau alasan lebih dalam di balik sebuah peristiwa."],
46+
dailyHabits: ["Merenung sendiri, menulis catatan, atau memutar ulang percakapan untuk memahami maknanya.", "Butuh jeda sebelum memberi jawaban penting."],
47+
underPressure: ["Saat tertekan, cenderung masuk ke dalam pikiran untuk menyusun makna sebelum merespons."],
48+
communicationOn: ["Beri waktu memproses; ajukan pertanyaan yang membuka refleksi, bukan hanya tuntutan respons cepat."],
49+
communicationResistance: ["Jangan menganggap diam sebagai tidak peduli; bisa jadi ia sedang menyusun pemahaman."],
50+
analogy: "Seperti sumur dalam: tidak selalu ramai di permukaan, tetapi menyimpan kedalaman yang bisa diambil ketika waktunya tepat.",
51+
blindSpot: "Bisa tampak jauh atau lambat merespons saat orang lain butuh keputusan segera.",
52+
},
53+
idea_translator: {
54+
observable: ["Mampu membuat hal rumit terdengar lebih sederhana dan bisa dipahami orang lain.", "Sering menjadi jembatan antara ide, bahasa, dan pemahaman orang."],
55+
dailyHabits: ["Menjelaskan ulang sesuatu dengan bahasa sendiri.", "Membuat analogi, contoh, atau alur penjelasan agar orang lain lebih cepat menangkap maksud."],
56+
underPressure: ["Saat tertekan, cenderung mencoba menjelaskan posisi, alasan, atau pola pikir agar situasi menjadi lebih jernih."],
57+
communicationOn: ["Tanya pendapatnya tentang cara menjelaskan, menyusun pesan, atau membuat orang lain mengerti."],
58+
communicationResistance: ["Jangan memotong penjelasannya terlalu cepat sebelum pola pikirnya selesai tersusun."],
59+
analogy: "Seperti penerjemah ruangan: mengambil ide yang masih berantakan lalu memberi bahasa yang bisa dimengerti bersama.",
60+
blindSpot: "Bisa terlalu banyak menjelaskan ketika orang lain sebenarnya hanya butuh keputusan singkat.",
61+
},
62+
strategy_designer: {
63+
observable: ["Cepat membaca beberapa kemungkinan jalan dan mencari rute yang paling masuk akal.", "Lebih tertarik pada arah, prioritas, dan kemungkinan daripada sekadar menjalankan rutinitas."],
64+
dailyHabits: ["Sering bertanya: opsi terbaiknya apa, risikonya apa, jalan lainnya apa?", "Mudah tertarik pada skenario, peta jalan, dan solusi alternatif."],
65+
underPressure: ["Saat terdesak, cenderung makin aktif mencari celah, strategi, dan jalan keluar."],
66+
communicationOn: ["Ajak bicara soal strategi, kemungkinan, trade-off, dan arah keputusan."],
67+
communicationResistance: ["Jangan langsung mengunci pilihan tanpa memberi ruang melihat opsi lain."],
68+
analogy: "Seperti navigator: bukan hanya melihat jalan di depan, tetapi mencari rute paling masuk akal menuju tujuan.",
69+
blindSpot: "Bisa tidak sabar pada proses yang terasa tanpa arah atau terlalu administratif.",
70+
},
71+
idea_synthesizer: {
72+
observable: ["Sering menghubungkan banyak ide menjadi bentuk baru yang lebih utuh.", "Mudah melihat hubungan antarhal yang bagi orang lain tampak terpisah."],
73+
dailyHabits: ["Menggabungkan referensi, pengalaman, dan ide menjadi konsep baru.", "Membuat rangkuman atau kerangka baru dari banyak informasi."],
74+
underPressure: ["Saat tertekan, cenderung merangkai ulang informasi untuk menemukan gambaran baru yang lebih masuk akal."],
75+
communicationOn: ["Beri ruang brainstorming, peta konsep, dan diskusi kemungkinan."],
76+
communicationResistance: ["Jangan terlalu cepat membunuh ide sebelum bentuknya sempat matang."],
77+
analogy: "Seperti dapur ide: bahan-bahan berbeda diolah menjadi menu baru yang lebih bermakna.",
78+
blindSpot: "Bisa membuka terlalu banyak kemungkinan sehingga keputusan tertunda.",
79+
},
80+
pattern_reader: {
81+
observable: ["Tidak mudah puas dengan jawaban permukaan; ingin melihat data, pola, dan hubungan sebab-akibat.", "Sering menemukan struktur di balik situasi yang tampak acak."],
82+
dailyHabits: ["Membandingkan angka, fakta, pola kejadian, atau bukti sebelum menerima kesimpulan.", "Menyukai penjelasan yang logis dan dapat ditelusuri."],
83+
underPressure: ["Saat terdesak, cenderung mengurai fakta dan mencari pola yang bisa dipercaya."],
84+
communicationOn: ["Beri data, alasan, dan struktur berpikir yang jelas."],
85+
communicationResistance: ["Jangan memakai alasan 'pokoknya' atau 'sudah biasa begitu' untuk keputusan penting."],
86+
analogy: "Seperti analis sinyal: mencari pola yang tersembunyi di balik noise.",
87+
blindSpot: "Bisa terlihat kritis atau sulit diyakinkan ketika bukti belum cukup.",
88+
},
89+
problem_restorer: {
90+
observable: ["Tertarik mencari penyebab masalah dan membuat sesuatu kembali berjalan.", "Lebih hidup ketika ada kerusakan, kekacauan, atau error yang bisa diperbaiki."],
91+
dailyHabits: ["Mencari akar masalah, mencoba solusi, lalu mengecek apakah sistem kembali normal.", "Sulit membiarkan masalah menggantung tanpa usaha perbaikan."],
92+
underPressure: ["Saat tertekan, cenderung masuk ke mode troubleshooting: apa penyebabnya, apa yang harus diperbaiki dulu?"],
93+
communicationOn: ["Ajak dengan problem statement yang jelas dan beri ruang menganalisis akar masalah."],
94+
communicationResistance: ["Jangan hanya menyuruh menerima kondisi rusak tanpa kesempatan memperbaiki."],
95+
analogy: "Seperti teknisi pemulih: tenang ketika bisa menemukan titik gangguan dan mengembalikan fungsi.",
96+
blindSpot: "Bisa terlalu fokus memperbaiki sesuatu yang sebenarnya tidak perlu diselamatkan.",
97+
},
98+
people_developer: {
99+
observable: ["Mudah melihat potensi orang lain dan senang ketika ada orang yang bertumbuh.", "Sering memberi dorongan, feedback, atau dukungan agar orang lebih mampu."],
100+
dailyHabits: ["Menyadari kemajuan kecil orang lain.", "Memberi semangat atau arahan agar orang tidak menyerah terlalu cepat."],
101+
underPressure: ["Saat tertekan, cenderung tetap memikirkan bagaimana orang lain bisa dibantu atau dinaikkan kapasitasnya."],
102+
communicationOn: ["Bicarakan perkembangan, proses belajar, dan dampak positif pada orang lain."],
103+
communicationResistance: ["Jangan membuatnya merasa usahanya membantu orang tidak dihargai."],
104+
analogy: "Seperti tukang kebun manusia: memperhatikan tunas kecil dan ingin melihatnya tumbuh.",
105+
blindSpot: "Bisa terlalu memikul proses tumbuh orang lain sampai lupa batas diri.",
106+
},
107+
system_organizer: {
108+
observable: ["Merasa lebih lega ketika ruang, barang, jadwal, atau proses menjadi rapi dan bisa diprediksi.", "Mudah melihat bagian yang berantakan dan ingin menatanya."],
109+
dailyHabits: ["Membuat daftar, merapikan file, mengatur ruang, atau membuat sistem agar mudah dicari.", "Merasa terganggu jika sesuatu sering berubah tanpa alasan jelas."],
110+
underPressure: ["Saat tertekan, cenderung mencari struktur: apa urutannya, siapa melakukan apa, kapan selesai?"],
111+
communicationOn: ["Berikan kejelasan langkah, jadwal, dan tanggung jawab."],
112+
communicationResistance: ["Jangan membuat perubahan mendadak tanpa konteks atau aturan baru yang jelas."],
113+
analogy: "Seperti penata ruang kontrol: semua indikator harus jelas agar sistem bisa dijalankan dengan tenang.",
114+
blindSpot: "Bisa tampak kaku saat situasi memang membutuhkan improvisasi.",
115+
},
116+
commitment_keeper: {
117+
observable: ["Serius memegang janji, tanggung jawab, dan kualitas hasil.", "Tidak nyaman jika pekerjaan dibuat asal selesai."],
118+
dailyHabits: ["Mengingat kewajiban, memperbaiki kesalahan, dan merasa perlu menuntaskan yang sudah dijanjikan.", "Sulit santai jika ada tanggung jawab yang belum beres."],
119+
underPressure: ["Saat tertekan, cenderung memikul lebih banyak karena merasa harus bertanggung jawab."],
120+
communicationOn: ["Bicarakan komitmen, kepercayaan, standar mutu, dan pembagian tanggung jawab yang adil."],
121+
communicationResistance: ["Jangan menggampangkan janji atau membuatnya merasa sendirian memikul beban."],
122+
analogy: "Seperti jangkar: menjaga agar komitmen tidak mudah hanyut oleh keadaan.",
123+
blindSpot: "Bisa merasa bersalah berlebihan atau sulit mendelegasikan." ,
124+
},
125+
social_connector: {
126+
observable: ["Mudah membuka percakapan dan membuat suasana lebih hidup.", "Kehadirannya dapat membuat kelompok terasa lebih ramai, cair, atau terbuka."],
127+
dailyHabits: ["Menyapa orang, membuka obrolan, atau menghubungkan satu orang dengan orang lain.", "Merasa penasaran dengan orang baru atau suasana sosial baru."],
128+
underPressure: ["Saat tertekan, bisa mencoba mencairkan suasana agar ketegangan tidak terlalu berat."],
129+
communicationOn: ["Mulai dengan sapaan hangat, cerita ringan, dan ruang interaksi yang manusiawi."],
130+
communicationResistance: ["Jangan membuat relasi terasa dingin, formal berlebihan, atau hanya transaksional."],
131+
analogy: "Seperti pemantik suasana: ruangan yang datar bisa terasa lebih hidup saat ia hadir.",
132+
blindSpot: "Bisa menghabiskan energi sosial jika terlalu banyak membuka koneksi tanpa batas." ,
133+
},
134+
harmony_keeper: {
135+
observable: ["Mencari titik temu dan menurunkan ketegangan agar hubungan tidak rusak.", "Lebih nyaman ketika perbedaan bisa dibicarakan dengan tenang."],
136+
dailyHabits: ["Menghindari kata-kata yang terlalu menusuk.", "Mencari kalimat tengah agar kedua pihak tidak merasa diserang."],
137+
underPressure: ["Saat tertekan, cenderung berusaha menenangkan konflik atau menunda ledakan emosi."],
138+
communicationOn: ["Gunakan nada tenang, akui sudut pandangnya, lalu ajak mencari jalan tengah."],
139+
communicationResistance: ["Jangan menyerang secara frontal atau mempermalukan di depan orang lain."],
140+
analogy: "Seperti peredam getaran: membantu sistem tetap stabil saat gesekan mulai naik.",
141+
blindSpot: "Bisa terlalu lama menghindari keputusan tegas demi menjaga suasana." ,
142+
},
143+
decision_director: {
144+
observable: ["Berani mengambil posisi, menghadapi percakapan sulit, dan memberi arah.", "Tidak nyaman jika keputusan penting terus digantung."],
145+
dailyHabits: ["Langsung bertanya inti masalah atau arah keputusan.", "Lebih suka kejelasan daripada terlalu banyak basa-basi."],
146+
underPressure: ["Saat terdesak, cenderung mengambil kontrol dan menentukan arah agar situasi tidak melebar."],
147+
communicationOn: ["Bicarakan tujuan, batas, keputusan, dan konsekuensi secara jelas."],
148+
communicationResistance: ["Jangan membuatnya merasa diputar-putar tanpa kejelasan posisi."],
149+
analogy: "Seperti kemudi darurat: ketika situasi kabur, ia ingin segera menentukan arah." ,
150+
blindSpot: "Bisa terasa terlalu keras jika ketegasan tidak diimbangi ruang mendengar." ,
151+
},
152+
quality_evaluator: {
153+
observable: ["Teliti melihat ketidaksesuaian, kekurangan, atau risiko mutu.", "Tidak mudah puas sebelum sesuatu terasa benar, aman, dan layak."],
154+
dailyHabits: ["Memeriksa ulang dokumen, angka, detail, atau prosedur.", "Mudah melihat bagian kecil yang dilewatkan orang lain."],
155+
underPressure: ["Saat tertekan, cenderung memperketat pemeriksaan agar kesalahan tidak lolos."],
156+
communicationOn: ["Beri kriteria kualitas, bukti, dan ruang untuk menguji hasil."],
157+
communicationResistance: ["Jangan menganggap pertanyaan detail sebagai serangan; sering kali itu cara menjaga mutu."],
158+
analogy: "Seperti quality gate: memastikan sesuatu layak lewat sebelum dipakai lebih luas.",
159+
blindSpot: "Bisa terlalu fokus pada yang belum benar sehingga apresiasi kurang terdengar." ,
160+
},
161+
creative_designer: {
162+
observable: ["Mudah membayangkan bentuk, cerita, visual, atau konsep baru.", "Sering membuat sesuatu terasa lebih menarik atau punya rasa."],
163+
dailyHabits: ["Menyukai referensi visual, desain, musik, cerita, atau bentuk presentasi yang punya karakter.", "Mencoba versi baru dari sesuatu yang biasa."],
164+
underPressure: ["Saat tertekan, bisa mencari cara kreatif agar situasi tidak buntu."],
165+
communicationOn: ["Beri ruang eksplorasi, referensi, dan izin membuat versi yang lebih menarik."],
166+
communicationResistance: ["Jangan terlalu cepat membatasi bentuk sebelum proses eksplorasinya terlihat."],
167+
analogy: "Seperti studio kecil di kepala: hal sederhana bisa diolah menjadi bentuk yang punya rasa." ,
168+
blindSpot: "Bisa kehilangan energi jika terlalu lama dipaksa mengikuti format kaku." ,
169+
},
170+
};
171+
172+
export function getMicroRoleNarrative(role: MicroRoleDefinition): MicroRoleNarrative {
173+
return (
174+
NARRATIVES[role.id] ?? {
175+
observable: [role.visible],
176+
dailyHabits: [role.tagline],
177+
underPressure: [`Saat tertekan, kecenderungan ${role.name.toLowerCase()} dapat muncul sebagai cara menjaga kontrol dan memahami situasi.`],
178+
communicationOn: [`Masuk lewat bahasa yang sesuai dengan ${role.name.toLowerCase()}: beri konteks, ruang, dan alasan yang jelas.`],
179+
communicationResistance: [role.risk],
180+
analogy: role.tagline,
181+
blindSpot: role.risk,
182+
}
183+
);
184+
}
185+
186+
function unique(items: string[], limit: number): string[] {
187+
const seen = new Set<string>();
188+
const output: string[] = [];
189+
for (const item of items) {
190+
if (!item || seen.has(item)) continue;
191+
seen.add(item);
192+
output.push(item);
193+
if (output.length >= limit) break;
194+
}
195+
return output;
196+
}
197+
198+
export function buildObservablePatterns(roles: MicroRoleScore[], limit = 5): string[] {
199+
return unique(roles.flatMap((role) => getMicroRoleNarrative(role).observable), limit);
200+
}
201+
202+
export function buildDailyHabits(roles: MicroRoleScore[], limit = 5): string[] {
203+
return unique(roles.flatMap((role) => getMicroRoleNarrative(role).dailyHabits), limit);
204+
}
205+
206+
export function buildPressurePatterns(roles: MicroRoleScore[], limit = 4): string[] {
207+
return unique(roles.flatMap((role) => getMicroRoleNarrative(role).underPressure), limit);
208+
}
209+
210+
export function buildCommunicationOn(roles: MicroRoleScore[], limit = 4): string[] {
211+
return unique(roles.flatMap((role) => getMicroRoleNarrative(role).communicationOn), limit);
212+
}
213+
214+
export function buildCommunicationResistance(roles: MicroRoleScore[], limit = 4): string[] {
215+
return unique(roles.flatMap((role) => getMicroRoleNarrative(role).communicationResistance), limit);
216+
}
217+
218+
export function buildAnalogies(roles: MicroRoleScore[], limit = 3): string[] {
219+
return unique(roles.map((role) => getMicroRoleNarrative(role).analogy), limit);
220+
}
221+
222+
export function buildBlindSpots(roles: MicroRoleScore[], limit = 4): string[] {
223+
return unique(roles.map((role) => getMicroRoleNarrative(role).blindSpot), limit);
224+
}

src/routes/assessment.$session.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ function AssessmentPage() {
6262
} else {
6363
setCurrentIndex(s, idx + 1);
6464
}
65-
}, 220);
65+
}, 430);
6666
},
6767
[idx, navigate, question, questions.length, s, setAnswer, setCurrentIndex],
6868
);
@@ -114,7 +114,6 @@ function AssessmentPage() {
114114
onSelect={handleSelect}
115115
leftLabel={LABELS[s].left}
116116
rightLabel={LABELS[s].right}
117-
session={s}
118117
/>
119118
</QuestionCard>
120119

0 commit comments

Comments
 (0)