Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ Regras e atalhos para quem (agente ou humano) mexe neste repositório.
opções e a recomendada primeiro), não como texto solto.
- **Commits, títulos/corpos de PR e nomes de branch em inglês.** Identificadores
no código em inglês. Copy de UI em **pt-BR**.
- **Copy de UI nasce em i18n**: todo texto de interface (label, `aria-label`,
`title`, toast, placeholder) entra em `messages/pt-BR.json` **e**
`messages/en.json` no mesmo commit. Nada de string solta em `.svelte` — o
guard `src/lib/i18n/hardcoded.test.ts` roda no CI. Escreva pensando em
tradução: sem concatenar frases, use placeholders da própria mensagem. Ver
`docs/I18N.md`.
- Mensagem de commit: convencional (`feat:`, `fix:`, `docs:`), corpo em bullets
explicando o porquê — não descreva só o diff.

Expand Down
22 changes: 22 additions & 0 deletions docs/ANIM-NOTE-BUBBLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,25 @@ Framer Motion / Rive / After Effects e nos mandar o arquivo — a gente adapta o
desliza para baixo; o morph é só na entrada. Respeitar `prefers-reduced-motion`.
- A bolha da nota (preview do bloco a partir do chip `tempo`) continua passiva;
o contrato de âncoras e `z-index` da §3 segue valendo.

---

## 8. Ajustes de outubro/2026 (pós-S15)

- **Fixar não reanima.** Quando a bolha do planejador já está aberta, segurar
para fixar **não** repete a entrada (spring/cauda/stagger nem som): ela
reposiciona na hora e confirma com micro-pulso de escala (`1 → 1.03 → 1`,
~160 ms) + háptica. A entrada cheia só acontece na primeira abertura.
- **Material mais opaco.** Fundo ~92% de opacidade (antes 72%) mantendo blur e
tint da categoria; borda e realce interno levemente mais fortes.
- **Chip `tempo` é um chip só.** O `code` que embrulha a referência perde
fundo/padding quando contém `.tp-ref`; o texto do chip é
`user-select: none` (sem callout no iOS) para o **hold de 350 ms** abrir a
bolha do bloco sem selecionar texto. Tap continua alternando a bolha.
- **Blocos colados da mesma categoria** aparecem como uma entrada só no bubble
(`13:00–13:30` + `13:30–14:00` → `13:00–14:00`).
- **Insert de referência não abre teclado**: o texto entra no último caret
conhecido (ou no fim do corpo) sem `focus()` no textarea.
- **Janela de horas** do planejador é configurável no sheet de range
(preferência local, não sincroniza); o componente de calendário continua só
com datas.
2 changes: 2 additions & 0 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,10 @@ Ações em módulos sensíveis (notas, finanças) aparecem **bloqueadas** ("A IA

Selecionável nas configurações. Sempre dentro de um tile discreto "Invocar" e no drawer.

- **Atlas (padrão):** ainda sem arte; por enquanto aparece só o nome "Atlas".
- **Gato:** corpo grafite, olhos na cor de `--acc`. Estados: dormindo (respira, "z"), acordado (pisca, orelhas mexem), pensando (três pontos), feliz (olhos em arco). Os olhos seguem o ponteiro. Miado sintetizado ao invocar.
- **Ghost:** casca angular grafite com pontas e olho em lime. **Estado normal: placas encaixadas** (esfera quase toda coberta, só frestas escuras e o olho). Animações: varredura (radar), vista explodida, flutuar, pensar, celebrar, dormir.
- Trocar o mascote exige **Atlas Pro** no mobile; o padrão Atlas é livre. No desktop o plano é sempre Pro.
- Reações ligadas a eventos: aplicar sugestão = feliz + confete; erro/espera = pensando.

---
Expand Down
11 changes: 11 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
"settings_first_day_hint": "Affects weekly habits, goals and calendars.",
"settings_day_mon": "Mon",
"settings_day_sun": "Sun",
"settings_mascot_title": "Mascot",
"settings_mascot_hint": "Who shows up in Atlas. Soon.",
"settings_mascot_pro": "Changing requires Atlas Pro",
"settings_mascot_atlas": "Atlas",
"settings_mascot_cat": "Cat",
"settings_mascot_ghost": "Ghost",
"settings_diag_title": "Notification diagnostics",
"settings_diag_permission": "Permission: {state}",
"settings_diag_granted": "granted",
Expand Down Expand Up @@ -328,6 +334,10 @@
"planner_range_apply": "Apply",
"planner_range_hint": "Tap the first and the last day.",
"planner_range_max": "max. {days} days",
"time_window_title": "Time window",
"time_window_hint": "Hours shown in the grid",
"time_window_from": "Start",
"time_window_to": "End",
"planner_range_next_month": "Next month",
"planner_range_pick": "Select range",
"planner_range_prev_month": "Previous month",
Expand Down Expand Up @@ -694,6 +704,7 @@
"habit_value_done": "Done",
"habit_week_chip": "week",
"habit_week_streak": "Week streak",
"habit_pick_date": "Pick date",
"habit_xp_target": "+10 XP · Habit goal reached",
"shell_add": "Add",
"shell_collapse_menu": "Collapse menu",
Expand Down
11 changes: 11 additions & 0 deletions messages/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
"settings_first_day_hint": "Afeta hábito semanal, metas e calendários.",
"settings_day_mon": "Seg",
"settings_day_sun": "Dom",
"settings_mascot_title": "Mascote",
"settings_mascot_hint": "Quem aparece no Atlas. Em breve.",
"settings_mascot_pro": "Trocar exige Atlas Pro",
"settings_mascot_atlas": "Atlas",
"settings_mascot_cat": "Gato",
"settings_mascot_ghost": "Ghost",
"settings_diag_title": "Diagnóstico de notificações",
"settings_diag_permission": "Permissão: {state}",
"settings_diag_granted": "concedida",
Expand Down Expand Up @@ -328,6 +334,10 @@
"planner_range_apply": "Aplicar",
"planner_range_hint": "Toque no primeiro e no último dia.",
"planner_range_max": "máx. {days} dias",
"time_window_title": "Janela de horário",
"time_window_hint": "Horas visíveis na grade",
"time_window_from": "Início",
"time_window_to": "Fim",
"planner_range_next_month": "Próximo mês",
"planner_range_pick": "Selecionar período",
"planner_range_prev_month": "Mês anterior",
Expand Down Expand Up @@ -694,6 +704,7 @@
"habit_value_done": "Feito",
"habit_week_chip": "semana",
"habit_week_streak": "Sequência da semana",
"habit_pick_date": "Escolher data",
"habit_xp_target": "+10 XP · Meta do hábito alcançada",
"shell_add": "Adicionar",
"shell_collapse_menu": "Recolher menu",
Expand Down
38 changes: 31 additions & 7 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ h1 { margin-bottom: 8px; color: #e9ece5; font: 600 clamp(28px, 3vw, 39px)/1.14 M
.habit-prototype-ring strong { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 1px; color: var(--hp-text); font-size: 20px; font-weight: 800; }.habit-prototype-ring strong small { margin-left: 1px; color: var(--hp-muted); font-size: 12px; }.habit-prototype-hero h2 { margin: 0; color: var(--hp-text); font-size: 22px; font-weight: 800; letter-spacing: -.02em; }.habit-prototype-hero p { margin: 2px 0 0; color: var(--hp-muted); font-size: 13px; }
.habit-prototype-week { display: flex; flex-direction: column; gap: 8px; justify-content: space-between; padding: 14px; }.habit-prototype-day { display: flex; flex-direction: column; align-items: center; gap: 5px; padding: 0; border: 0; background: none; color: var(--hp-muted); font: inherit; font-size: 11px; cursor: pointer; }.habit-prototype-day:disabled { opacity: .35; cursor: default; }.habit-prototype-day span { width: 28px; height: 28px; display: grid; place-items: center; border-radius: 50%; background: var(--hp-s2); color: #141a0a; }.habit-prototype-day.done span { background: var(--hp-flame); }.habit-prototype-day.today span { box-shadow: 0 0 0 2px var(--hp-accent); }.habit-prototype-day.selected { color: var(--hp-flame); }.habit-prototype-day.selected span { box-shadow: 0 0 0 2px var(--hp-flame); }.habit-prototype-week.away { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--hp-flame) 45%, transparent); }
.habit-week-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; color: var(--hp-muted); font-size: 12px; font-weight: 600; }.habit-prototype-week.away .habit-week-head span { color: var(--hp-flame); }.habit-week-today { padding: 4px 10px; border-radius: 20px; background: color-mix(in srgb, var(--hp-flame) 18%, transparent); color: var(--hp-flame); font-size: 12px; font-weight: 700; }
.habit-week-open { min-width: 0; flex: 1; padding: 2px 0; border: 0; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }
.habit-week-open span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A área vazia do card também abre o seletor; os botões de dia ficam acima. */
.habit-prototype-week { position: relative; }
.habit-week-open-area { position: absolute; inset: 0; z-index: 0; border: 0; border-radius: inherit; background: none; cursor: pointer; }
.habit-week-head, .habit-week-days { position: relative; z-index: 1; pointer-events: none; }
.habit-week-head button, .habit-week-days button { pointer-events: auto; }
.habit-week-days { display: flex; justify-content: space-between; gap: 2px; }
.habit-prototype-hero:disabled { cursor: default; }.habit-prototype-hero.readonly h2 { color: var(--hp-flame); }
.habit-prototype-habits { margin-top: 6px; }.habit-prototype-habit { min-height: 116px; display: flex; flex-direction: column; justify-content: space-between; position: relative; overflow: hidden; padding: 14px; background: linear-gradient(to top, var(--hp-accent-dim) calc(var(--habit-progress,0) * 100%), var(--hp-s) 0); cursor: pointer; transition: background .35s, transform .15s; }.habit-prototype-habit:active { transform: scale(.97); }.habit-prototype-habit b { color: inherit; font-size: 16px; font-weight: 600; }.habit-prototype-value { display: inline-block; margin-top: 12px; color: var(--hp-accent); font-size: 30px; font-weight: 800; letter-spacing: -.04em; line-height: 1; }.habit-prototype-habit small { color: var(--hp-muted); font-size: 12px; }.habit-prototype-habit.done { background: var(--hp-accent); color: #141a0a; }.habit-prototype-habit.done .habit-prototype-value, .habit-prototype-habit.done small { color: #141a0a; }.habit-prototype-check { width: 26px; height: 26px; display: grid; place-items: center; position: absolute; top: 12px; right: 12px; border-radius: 50%; background: #141a0a; color: var(--hp-accent); }
Expand Down Expand Up @@ -505,6 +512,9 @@ h1 { margin-bottom: 8px; color: #e9ece5; font: 600 clamp(28px, 3vw, 39px)/1.14 M
.tp-ic { width: 38px; height: 38px; border-radius: 8px; background: var(--panel); font-size: 16px; text-align: center; color: var(--text); }
.tp-pal { display: flex; gap: 6px; overflow-x: auto; padding: 0 0 8px; scrollbar-width: none; }
.tp-pal::-webkit-scrollbar { display: none; }
/* Mobile: ações de range/resumo e o total abrem a linha de categorias. */
.tp-pal-actions { display: none; flex: none; align-items: center; gap: 6px; }
.tp-chip { flex: none; padding: 5px 10px; border-radius: 14px; background: var(--panel); color: var(--muted); font-size: 11px; font-weight: 600; white-space: nowrap; }
.tp-cat { flex: none; padding: 9px 13px; border-radius: 20px; background: var(--panel); font-weight: 600; font-size: 14px; display: flex; gap: 7px; align-items: center; transition: transform .12s; }
.tp-cat i { width: 9px; height: 9px; border-radius: 50%; background: var(--cc); }
.tp-cat small { color: var(--muted); font-size: 11px; font-weight: 600; }
Expand Down Expand Up @@ -615,6 +625,9 @@ h1 { margin-bottom: 8px; color: #e9ece5; font: 600 clamp(28px, 3vw, 39px)/1.14 M
.tp-prototype > .tp-pal {
flex: none;
}
/* O título já existe no header do shell; no mobile sobra só a grade. */
.tp-top { display: none; }
.tp-pal-actions { display: flex; }
.tp-scroll {
flex: 1 1 auto;
min-width: 0;
Expand Down Expand Up @@ -1001,6 +1014,17 @@ h1 { margin-bottom: 8px; color: #e9ece5; font: 600 clamp(28px, 3vw, 39px)/1.14 M
line-height: 1.5;
white-space: nowrap;
cursor: help;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
touch-action: manipulation;
}
/* A referência já é um chip: o `code` que a embrulha não pode virar outro chip. */
.nt-pv :where(code):has(> .tp-ref) {
background: none;
padding: 0;
border-radius: 0;
font-size: inherit;
}
.nt-pv .tp-ref.ms { background: #ffffff12; color: var(--nt-mu); }

Expand All @@ -1018,12 +1042,12 @@ h1 { margin-bottom: 8px; color: #e9ece5; font: 600 clamp(28px, 3vw, 39px)/1.14 M
.tbb.in { pointer-events: auto; }
.tbb-m {
position: relative;
background: color-mix(in srgb, var(--tint, #d4f25a) 13%, #1d211db8);
background: color-mix(in srgb, var(--tint, #d4f25a) 13%, #1d211deb);
backdrop-filter: blur(18px) saturate(1.5);
-webkit-backdrop-filter: blur(18px) saturate(1.5);
border: 1px solid #ffffff22;
border: 1px solid #ffffff2e;
border-radius: 12px;
box-shadow: 0 14px 34px #000a, inset 0 1px 0 #ffffff18;
box-shadow: 0 14px 34px #000a, inset 0 1px 0 #ffffff22;
padding: 10px 12px;
}
.tbb-t {
Expand All @@ -1032,13 +1056,13 @@ h1 { margin-bottom: 8px; color: #e9ece5; font: 600 clamp(28px, 3vw, 39px)/1.14 M
left: var(--tx, 50%);
width: 12px;
height: 12px;
background: color-mix(in srgb, var(--tint, #d4f25a) 13%, #222722);
border-right: 1px solid #ffffff22;
border-bottom: 1px solid #ffffff22;
background: color-mix(in srgb, var(--tint, #d4f25a) 13%, #222722f2);
border-right: 1px solid #ffffff2e;
border-bottom: 1px solid #ffffff2e;
border-radius: 0 0 3px 0;
transform: translateX(-50%) rotate(45deg);
}
.tbb-1 { font-size: 13px; font-weight: 700; color: var(--tx); }
.tbb-1 { font-size: 13px; font-weight: 700; color: var(--text); }
.tbb-2 { font-size: 12px; color: var(--mu); }
.tbb-lb { margin-bottom: 4px; color: var(--mu); font-size: 11px; }
.tbb-r {
Expand Down
20 changes: 20 additions & 0 deletions src/lib/atlas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
money,
normalizeDailyGoal,
normalizeFirstDayOfWeek,
normalizeMascot,
normalizeTimeWindow,
nextBooleanLog,
positiveValueOf,
remapWeeklyLogs,
Expand Down Expand Up @@ -369,6 +371,24 @@ describe('createEmptySnapshot', () => {
it('a semana começa na segunda por padrão', () => {
expect(createEmptySnapshot().preferences.firstDayOfWeek).toBe('mon');
});

it('janela de horas nasce em 07–22 e respeita o mínimo de 4h', () => {
const preferences = createEmptySnapshot().preferences;
expect(preferences.timeWindowStartHour).toBe(7);
expect(preferences.timeWindowEndHour).toBe(22);
expect(normalizeTimeWindow('9', '10')).toEqual({ startHour: 9, endHour: 12 });
expect(normalizeTimeWindow(-5, 30)).toEqual({ startHour: 0, endHour: 23 });
expect(normalizeTimeWindow(23, 23)).toEqual({ startHour: 20, endHour: 23 });
expect(normalizeTimeWindow(undefined, undefined)).toEqual({ startHour: 7, endHour: 22 });
});

it('mascote nasce em Atlas e só aceita as opções conhecidas', () => {
expect(createEmptySnapshot().preferences.mascot).toBe('atlas');
expect(normalizeMascot('cat')).toBe('cat');
expect(normalizeMascot('ghost')).toBe('ghost');
expect(normalizeMascot('dog')).toBe('atlas');
expect(normalizeMascot(undefined)).toBe('atlas');
});
});

describe('demoSnapshot (modo de teste)', () => {
Expand Down
44 changes: 43 additions & 1 deletion src/lib/atlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,40 @@ export const TIME_SLOT_START_HOUR = 7;
export const TIME_SLOT_COUNT = 32;
export const TIME_SLOT_MINUTES = 30;

/** Janela padrão do planejador: 07:00–22:00 (inclusiva nas duas pontas). */
export const TIME_WINDOW_DEFAULT_START_HOUR = 7;
export const TIME_WINDOW_DEFAULT_END_HOUR = 22;
/** Mínimo de horas visíveis para a grade continuar útil. */
export const TIME_WINDOW_MIN_HOURS = 4;

export interface TimeWindow {
startHour: number;
endHour: number;
}

/** Normaliza a janela de horas: inteiros 0–23, mínimo de `TIME_WINDOW_MIN_HOURS`. */
export function normalizeTimeWindow(start: unknown, end: unknown): TimeWindow {
const hour = (value: unknown, fallback: number) => {
const number = Number(value);
if (!Number.isFinite(number)) return fallback;
return Math.min(23, Math.max(0, Math.round(number)));
};
const startHour = Math.min(
23 - (TIME_WINDOW_MIN_HOURS - 1),
hour(start, TIME_WINDOW_DEFAULT_START_HOUR)
);
let endHour = hour(end, TIME_WINDOW_DEFAULT_END_HOUR);
if (endHour < startHour + TIME_WINDOW_MIN_HOURS - 1) endHour = startHour + TIME_WINDOW_MIN_HOURS - 1;
return { startHour, endHour };
}

/** Mascote do Atlas. `atlas` é o padrão; os outros exigem Atlas Pro no mobile. */
export type MascotChoice = 'atlas' | 'cat' | 'ghost';

export function normalizeMascot(value: unknown): MascotChoice {
return value === 'cat' || value === 'ghost' ? value : 'atlas';
}

export interface Transaction {
id: string;
type: TransactionType;
Expand Down Expand Up @@ -158,6 +192,11 @@ export interface AppPreferences {
backupEncryption: 'ask' | 'encrypted' | 'plain';
/** Primeiro dia da semana dos ciclos (hábito semanal, trackers, calendário). */
firstDayOfWeek: FirstDayOfWeek;
/** Mascote escolhido (só Atlas Pro troca no mobile). */
mascot: MascotChoice;
/** Janela de horas visível no módulo Tempo (inclusiva nas duas pontas). */
timeWindowStartHour: number;
timeWindowEndHour: number;
}

export interface AtlasSnapshot {
Expand Down Expand Up @@ -249,7 +288,10 @@ export function createEmptySnapshot(): AtlasSnapshot {
typingSoundEnabled: true,
hapticsEnabled: true,
backupEncryption: 'ask',
firstDayOfWeek: 'mon'
firstDayOfWeek: 'mon',
mascot: 'atlas',
timeWindowStartHour: TIME_WINDOW_DEFAULT_START_HOUR,
timeWindowEndHour: TIME_WINDOW_DEFAULT_END_HOUR
},
graph: createGraph()
};
Expand Down
23 changes: 23 additions & 0 deletions src/lib/bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,29 @@ export class TimeBubble {
this.html = html;
}

/**
* Fixa a bolha já aberta sem repetir a entrada: reposiciona na hora, troca o
* conteúdo e confirma com um pulso curto + haptic (long press no planejador).
*/
pin(anchor: HTMLElement, html: string, tint: string) {
clearTimeout(this.hideTimer);
this.el.style.setProperty('--tint', tint);
this.anchor = anchor;
this.html = html;
this.content.innerHTML = html;
this.el.style.display = 'block';
this.open = true;
this.el.getAnimations().forEach((animation) => animation.cancel());
this.body.getAnimations({ subtree: true }).forEach((animation) => animation.cancel());
this.apply(this.place(anchor));
this.vibrate(10);
if (this.reduced) return;
this.body.animate(
[{ transform: 'scale(1)' }, { transform: 'scale(1.03)' }, { transform: 'scale(1)' }],
{ duration: 160, easing: 'ease-out' }
);
}

private enter() {
const anchor = this.anchor;
if (!anchor) return;
Expand Down
Loading
Loading