Compare commits
11 commits
main
...
custom-mod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a2f26684b | ||
|
|
a9f72b60af | ||
|
|
46e6014fea | ||
|
|
d6fba7e6e8 | ||
|
|
560a9442a7 | ||
|
|
93b578c3cf | ||
|
|
5c67b91302 | ||
|
|
962915829f | ||
|
|
497744d165 | ||
|
|
2ea386e557 | ||
|
|
820d31b3e5 |
|
|
@ -1,97 +0,0 @@
|
||||||
Young Serif
|
|
||||||
Copyright 2023 The Young Serif Project Authors (https://github.com/noirblancrouge/YoungSerif)
|
|
||||||
|
|
||||||
Atkinson Hyperlegible Next
|
|
||||||
Copyright 2020-2024 The Atkinson Hyperlegible Next Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next)
|
|
||||||
|
|
||||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
||||||
This license is copied below, and is also available with a FAQ at:
|
|
||||||
http://scripts.sil.org/OFL
|
|
||||||
|
|
||||||
|
|
||||||
-----------------------------------------------------------
|
|
||||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
||||||
-----------------------------------------------------------
|
|
||||||
|
|
||||||
PREAMBLE
|
|
||||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
||||||
development of collaborative font projects, to support the font creation
|
|
||||||
efforts of academic and linguistic communities, and to provide a free and
|
|
||||||
open framework in which fonts may be shared and improved in partnership
|
|
||||||
with others.
|
|
||||||
|
|
||||||
The OFL allows the licensed fonts to be used, studied, modified and
|
|
||||||
redistributed freely as long as they are not sold by themselves. The
|
|
||||||
fonts, including any derivative works, can be bundled, embedded,
|
|
||||||
redistributed and/or sold with any software provided that any reserved
|
|
||||||
names are not used by derivative works. The fonts and derivatives,
|
|
||||||
however, cannot be released under any other type of license. The
|
|
||||||
requirement for fonts to remain under this license does not apply
|
|
||||||
to any document created using the fonts or their derivatives.
|
|
||||||
|
|
||||||
DEFINITIONS
|
|
||||||
"Font Software" refers to the set of files released by the Copyright
|
|
||||||
Holder(s) under this license and clearly marked as such. This may
|
|
||||||
include source files, build scripts and documentation.
|
|
||||||
|
|
||||||
"Reserved Font Name" refers to any names specified as such after the
|
|
||||||
copyright statement(s).
|
|
||||||
|
|
||||||
"Original Version" refers to the collection of Font Software components as
|
|
||||||
distributed by the Copyright Holder(s).
|
|
||||||
|
|
||||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
||||||
or substituting -- in part or in whole -- any of the components of the
|
|
||||||
Original Version, by changing formats or by porting the Font Software to a
|
|
||||||
new environment.
|
|
||||||
|
|
||||||
"Author" refers to any designer, engineer, programmer, technical
|
|
||||||
writer or other person who contributed to the Font Software.
|
|
||||||
|
|
||||||
PERMISSION & CONDITIONS
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
||||||
redistribute, and sell modified and unmodified copies of the Font
|
|
||||||
Software, subject to the following conditions:
|
|
||||||
|
|
||||||
1) Neither the Font Software nor any of its individual components,
|
|
||||||
in Original or Modified Versions, may be sold by itself.
|
|
||||||
|
|
||||||
2) Original or Modified Versions of the Font Software may be bundled,
|
|
||||||
redistributed and/or sold with any software, provided that each copy
|
|
||||||
contains the above copyright notice and this license. These can be
|
|
||||||
included either as stand-alone text files, human-readable headers or
|
|
||||||
in the appropriate machine-readable metadata fields within text or
|
|
||||||
binary files as long as those fields can be easily viewed by the user.
|
|
||||||
|
|
||||||
3) No Modified Version of the Font Software may use the Reserved Font
|
|
||||||
Name(s) unless explicit written permission is granted by the corresponding
|
|
||||||
Copyright Holder. This restriction only applies to the primary font name as
|
|
||||||
presented to the users.
|
|
||||||
|
|
||||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
||||||
Software shall not be used to promote, endorse or advertise any
|
|
||||||
Modified Version, except to acknowledge the contribution(s) of the
|
|
||||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
||||||
permission.
|
|
||||||
|
|
||||||
5) The Font Software, modified or unmodified, in part or in whole,
|
|
||||||
must be distributed entirely under this license, and must not be
|
|
||||||
distributed under any other license. The requirement for fonts to
|
|
||||||
remain under this license does not apply to any document created
|
|
||||||
using the Font Software.
|
|
||||||
|
|
||||||
TERMINATION
|
|
||||||
This license becomes null and void if any of the above conditions are
|
|
||||||
not met.
|
|
||||||
|
|
||||||
DISCLAIMER
|
|
||||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
||||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
||||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
||||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
||||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M4 4 L7 24 Q7.5 28 11.5 28 L19.5 28 Q23.5 28 24 24 L27 4 Z" fill="#5F9B59" stroke="#2E2A26" stroke-width="2.5" stroke-linejoin="round"/><path d="M26.5 9 C32 9 32 20 25 21" fill="none" stroke="#2E2A26" stroke-width="3" stroke-linecap="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 318 B |
|
|
@ -1,48 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 214" role="img" aria-labelledby="t">
|
|
||||||
<title id="t">Trois tasses de couleur alignées sur la table, et une quatrième posée sur la Kluk, fumante.</title>
|
|
||||||
<style>
|
|
||||||
.table { fill: #D9BC94; }
|
|
||||||
.table-fil, .tasse-corps, .tasse-anse, .kluk-socle, .vapeur path {
|
|
||||||
stroke: #2E2A26; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
.tasse-anse { fill: none; stroke-width: 6; }
|
|
||||||
.tasse-corps { fill: currentColor; }
|
|
||||||
.tasse-reflet { fill: none; stroke: #fff; stroke-opacity: 0.45; stroke-width: 4; stroke-linecap: round; }
|
|
||||||
.kluk-socle { fill: #B48D5F; }
|
|
||||||
.kluk-voyant { fill: #F5F6F0; }
|
|
||||||
.vapeur path { fill: none; stroke-width: 2.5; stroke: #5A534C; }
|
|
||||||
.humeur-gris { color: #8D97A8; }
|
|
||||||
.humeur-orange { color: #EDA33D; }
|
|
||||||
.humeur-rouge { color: #D8573F; }
|
|
||||||
.humeur-vert { color: #5F9B59; }
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.vapeur path { animation: vapeur 5s ease-in-out infinite; }
|
|
||||||
.vapeur path + path { animation-delay: 2.5s; }
|
|
||||||
}
|
|
||||||
@keyframes vapeur {
|
|
||||||
0%, 100% { opacity: 0.25; transform: translateY(6px); }
|
|
||||||
50% { opacity: 0.9; transform: translateY(-6px); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<defs>
|
|
||||||
<g id="tasse">
|
|
||||||
<path class="tasse-anse" d="M63 22 C86 20 88 60 60 62" />
|
|
||||||
<path class="tasse-corps" d="M3 6 L10 78 Q11 86 20 86 L48 86 Q57 86 58 78 L65 6 Z" />
|
|
||||||
<path class="tasse-reflet" d="M13 18 L17 62" />
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
<rect class="table" x="0" y="200" width="580" height="14" rx="3" />
|
|
||||||
<path class="table-fil" d="M0 200 H580" />
|
|
||||||
<use href="#tasse" x="34" y="114" class="humeur-gris" />
|
|
||||||
<use href="#tasse" x="132" y="114" class="humeur-orange" />
|
|
||||||
<use href="#tasse" x="230" y="114" class="humeur-rouge" />
|
|
||||||
<g transform="translate(410 98)">
|
|
||||||
<g class="vapeur">
|
|
||||||
<path d="M22 -6 c-9 -11 9 -20 0 -31 c-9 -11 9 -20 0 -31" />
|
|
||||||
<path d="M44 -10 c-9 -11 9 -20 0 -31 c-9 -11 9 -20 0 -31" />
|
|
||||||
</g>
|
|
||||||
<use href="#tasse" x="0" y="0" class="humeur-vert" />
|
|
||||||
<rect class="kluk-socle" x="-22" y="86" width="112" height="16" rx="8" />
|
|
||||||
<circle class="kluk-voyant" cx="34" cy="94" r="2.6" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 44 36"><path d="M3 3 L6 25 Q6.5 29 10.5 29 L24.5 29 Q28.5 29 29 25 L32 3 Z M31.5 8 C39 8 39.5 21 30 22" fill="none" stroke="#B48D5F" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 262 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 95 95"><path d="M63 22 C86 20 88 60 60 62" fill="none" stroke="#2E2A26" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 6 L10 78 Q11 86 20 86 L48 86 Q57 86 58 78 L65 6 Z" fill="#8D97A8" stroke="#2E2A26" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 18 L17 62" fill="none" stroke="#fff" stroke-opacity="0.45" stroke-width="4" stroke-linecap="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 472 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 95 95"><path d="M63 22 C86 20 88 60 60 62" fill="none" stroke="#2E2A26" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 6 L10 78 Q11 86 20 86 L48 86 Q57 86 58 78 L65 6 Z" fill="#EDA33D" stroke="#2E2A26" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 18 L17 62" fill="none" stroke="#fff" stroke-opacity="0.45" stroke-width="4" stroke-linecap="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 472 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 95 95"><path d="M63 22 C86 20 88 60 60 62" fill="none" stroke="#2E2A26" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 6 L10 78 Q11 86 20 86 L48 86 Q57 86 58 78 L65 6 Z" fill="#D8573F" stroke="#2E2A26" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 18 L17 62" fill="none" stroke="#fff" stroke-opacity="0.45" stroke-width="4" stroke-linecap="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 472 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 95 95"><path d="M63 22 C86 20 88 60 60 62" fill="none" stroke="#2E2A26" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 6 L10 78 Q11 86 20 86 L48 86 Q57 86 58 78 L65 6 Z" fill="#5F9B59" stroke="#2E2A26" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 18 L17 62" fill="none" stroke="#fff" stroke-opacity="0.45" stroke-width="4" stroke-linecap="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 472 B |
|
|
@ -1,523 +0,0 @@
|
||||||
/*
|
|
||||||
* Styles propres à l'application, construits sur la charte (charte.css).
|
|
||||||
* La couleur d'un mode est choisie par l'utilisateur : elle arrive dans la variable
|
|
||||||
* --humeur, posée en style en ligne. Le reste de l'interface reste neutre
|
|
||||||
* (porcelaine, encre, chêne) pour que les humeurs soient les seules couleurs.
|
|
||||||
*
|
|
||||||
* Sommaire
|
|
||||||
* 1. Compléments à la charte (boutons, champs, navigation)
|
|
||||||
* 2. Messages flash
|
|
||||||
* 3. Carte d'humeur (illustration posée sur la Kluk)
|
|
||||||
* 4. Légende
|
|
||||||
* 5. Calendrier des humeurs
|
|
||||||
* 6. Tableau de bord
|
|
||||||
* 7. Page publique
|
|
||||||
* 8. Page d'un jour (journal)
|
|
||||||
* 9. Impression
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* ---------- 1. Compléments à la charte ---------- */
|
|
||||||
|
|
||||||
/* La charte ne prépare les <button class="bouton"> que dans un .formulaire */
|
|
||||||
button.bouton,
|
|
||||||
input.bouton {
|
|
||||||
border: 0;
|
|
||||||
font: inherit;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bouton d'action présenté comme un lien de la charte */
|
|
||||||
button.lien {
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
background: none;
|
|
||||||
color: var(--encre);
|
|
||||||
font: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration-line: underline;
|
|
||||||
text-decoration-color: var(--vert);
|
|
||||||
text-decoration-thickness: 0.12em;
|
|
||||||
text-underline-offset: 0.2em;
|
|
||||||
}
|
|
||||||
button.lien:hover { text-decoration-color: var(--encre); }
|
|
||||||
|
|
||||||
/* button_to enveloppe son bouton dans un formulaire : le garder en ligne */
|
|
||||||
.nav form.button_to { display: inline; }
|
|
||||||
|
|
||||||
.formulaire input[type="password"],
|
|
||||||
.formulaire input[type="text"] {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.7rem 0.85rem;
|
|
||||||
border: 2px solid var(--bordure-champ);
|
|
||||||
border-radius: var(--arrondi-petit);
|
|
||||||
background: var(--blanc);
|
|
||||||
color: var(--encre);
|
|
||||||
font: inherit;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
.formulaire input[type="password"]:focus-visible,
|
|
||||||
.formulaire input[type="text"]:focus-visible {
|
|
||||||
outline: 3px solid var(--vert);
|
|
||||||
outline-offset: 1px;
|
|
||||||
border-color: var(--encre);
|
|
||||||
}
|
|
||||||
.formulaire .formulaire-erreur { margin: 0 0 1.75rem; }
|
|
||||||
|
|
||||||
.texte-etroit { max-width: calc(30rem + 2 * var(--gouttiere)); }
|
|
||||||
.texte-etroit h1 { font-size: clamp(2.1rem, 1.7rem + 2vw, 2.75rem); }
|
|
||||||
.texte-etroit .formulaire { margin-top: 1.5rem; }
|
|
||||||
|
|
||||||
/* ---------- 2. Messages flash ---------- */
|
|
||||||
|
|
||||||
.flash {
|
|
||||||
max-width: calc(var(--colonne) + 2 * var(--gouttiere));
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0.5rem var(--gouttiere) 0;
|
|
||||||
}
|
|
||||||
.flash-message {
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-left: 4px solid var(--vert);
|
|
||||||
background: var(--surface);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.flash-alerte {
|
|
||||||
border-left-color: var(--rouge);
|
|
||||||
background: var(--erreur-fond);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 3. Carte d'humeur ---------- */
|
|
||||||
|
|
||||||
/* L'illustration du mode, dessinée à l'encre sur papier blanc, posée sur le socle
|
|
||||||
de la Kluk : une pilule à la couleur de l'humeur, avec son voyant. */
|
|
||||||
.carte-humeur {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.carte-humeur img {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
object-fit: contain;
|
|
||||||
background: var(--blanc);
|
|
||||||
border: 2.5px solid var(--encre);
|
|
||||||
border-radius: var(--arrondi);
|
|
||||||
}
|
|
||||||
.carte-humeur::after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
height: 1rem;
|
|
||||||
margin: 0.4rem 6% 0;
|
|
||||||
border: 2.5px solid var(--encre);
|
|
||||||
border-radius: var(--arrondi-pilule);
|
|
||||||
background:
|
|
||||||
radial-gradient(circle, var(--porcelaine) 0 2.5px, transparent 3px),
|
|
||||||
var(--humeur, var(--porcelaine-fonce));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 4. Légende ---------- */
|
|
||||||
|
|
||||||
.pastille {
|
|
||||||
flex: none;
|
|
||||||
display: inline-block;
|
|
||||||
width: 0.85rem;
|
|
||||||
height: 0.85rem;
|
|
||||||
border-radius: 3px;
|
|
||||||
background: var(--humeur);
|
|
||||||
}
|
|
||||||
.pastille-supposee {
|
|
||||||
background: color-mix(in srgb, var(--encre-douce) 35%, var(--porcelaine));
|
|
||||||
}
|
|
||||||
.pastille-inconnue {
|
|
||||||
background: var(--surface);
|
|
||||||
border: 1.5px dashed var(--bordure-champ);
|
|
||||||
}
|
|
||||||
.pastille-journal {
|
|
||||||
position: relative;
|
|
||||||
background: var(--porcelaine-fonce);
|
|
||||||
}
|
|
||||||
.pastille-journal::after,
|
|
||||||
.jour-journal::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
margin: auto;
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--blanc);
|
|
||||||
box-shadow: 0 0 0 1px var(--encre);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legende {
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
.legende ul {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.4rem 1.4rem;
|
|
||||||
}
|
|
||||||
.legende li {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.legende .legende-etats {
|
|
||||||
margin-top: 0.6rem;
|
|
||||||
color: var(--encre-douce);
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 5. Calendrier des humeurs ---------- */
|
|
||||||
|
|
||||||
/* Une colonne par semaine, une ligne par jour (du lundi au dimanche), comme sur GitHub. */
|
|
||||||
.calendrier-humeurs {
|
|
||||||
--case: 0.95rem;
|
|
||||||
--ecart: 3px;
|
|
||||||
}
|
|
||||||
@media (max-width: 30rem) {
|
|
||||||
.calendrier-humeurs { --case: 0.9rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.annee-titre {
|
|
||||||
margin: 2.25rem 0 0.75rem;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.annee-mois {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1.25rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mois-nom {
|
|
||||||
margin: 0 0 0.3rem;
|
|
||||||
font-family: var(--sans);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.4rem;
|
|
||||||
color: var(--encre-douce);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mois-jours,
|
|
||||||
.semaine-jours {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: repeat(7, var(--case));
|
|
||||||
grid-auto-flow: column;
|
|
||||||
grid-auto-columns: var(--case);
|
|
||||||
gap: var(--ecart);
|
|
||||||
}
|
|
||||||
|
|
||||||
.semaine-jours {
|
|
||||||
grid-auto-columns: auto;
|
|
||||||
padding-top: 1.7rem;
|
|
||||||
margin-right: 0.15rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: var(--case);
|
|
||||||
color: var(--encre-douce);
|
|
||||||
}
|
|
||||||
|
|
||||||
.jour {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
width: var(--case);
|
|
||||||
height: var(--case);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 3px;
|
|
||||||
background: transparent;
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
.jour-filled {
|
|
||||||
background: var(--humeur);
|
|
||||||
}
|
|
||||||
/* Mode supposé : la couleur de la veille, pâlie */
|
|
||||||
.jour-guessed {
|
|
||||||
background: color-mix(in srgb, var(--humeur) 35%, var(--porcelaine));
|
|
||||||
}
|
|
||||||
.jour-unknown {
|
|
||||||
background: var(--surface);
|
|
||||||
border: 1.5px dashed var(--bordure-champ);
|
|
||||||
}
|
|
||||||
.jour-empty {
|
|
||||||
background: var(--porcelaine-fonce);
|
|
||||||
}
|
|
||||||
.jour-future {
|
|
||||||
box-shadow: inset 0 0 0 1px var(--porcelaine-fonce);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.jour { cursor: pointer; }
|
|
||||||
button.jour[aria-pressed="true"] {
|
|
||||||
outline: 2.5px solid var(--encre);
|
|
||||||
outline-offset: 2px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
button.jour:focus-visible {
|
|
||||||
outline: 3px solid var(--encre);
|
|
||||||
outline-offset: 2px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
|
|
||||||
button.jour { transition: transform 0.12s ease; }
|
|
||||||
button.jour:hover { transform: scale(1.3); z-index: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 6. Tableau de bord ---------- */
|
|
||||||
|
|
||||||
.tableau {
|
|
||||||
max-width: var(--largeur-entete);
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0.5rem var(--gouttiere) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableau-titre {
|
|
||||||
margin: 0.5rem 0 1.25rem;
|
|
||||||
font-size: clamp(2.1rem, 1.7rem + 2vw, 3rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableau-calendrier { padding-bottom: 3rem; }
|
|
||||||
|
|
||||||
.detail-date {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.35rem;
|
|
||||||
}
|
|
||||||
.detail-date::first-letter { text-transform: uppercase; }
|
|
||||||
|
|
||||||
.detail-mode {
|
|
||||||
margin: 0.2rem 0 0;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
.detail-statut {
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--encre-douce);
|
|
||||||
}
|
|
||||||
.detail-statut:not(:empty)::before { content: "("; }
|
|
||||||
.detail-statut:not(:empty)::after { content: ")"; }
|
|
||||||
|
|
||||||
.detail-journal {
|
|
||||||
margin: 0.75rem 0 1.25rem;
|
|
||||||
white-space: pre-line;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
.detail-journal-vide {
|
|
||||||
color: var(--encre-douce);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sur mobile, le jour choisi reste en bas de l'écran pendant qu'on parcourt le calendrier */
|
|
||||||
@media (max-width: 59.99rem) {
|
|
||||||
.detail {
|
|
||||||
position: sticky;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 2;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 4.75rem minmax(0, 1fr);
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: start;
|
|
||||||
margin: 0 calc(-1 * var(--gouttiere));
|
|
||||||
padding: 0.9rem var(--gouttiere) 1rem;
|
|
||||||
background: var(--porcelaine);
|
|
||||||
border-top: 3px solid var(--encre);
|
|
||||||
}
|
|
||||||
.detail .carte-humeur img { border-width: 2px; border-radius: var(--arrondi-petit); }
|
|
||||||
.detail .carte-humeur::after { height: 0.6rem; margin-top: 0.25rem; border-width: 2px; background: var(--humeur, var(--porcelaine-fonce)); }
|
|
||||||
.detail-date { font-size: 1.1rem; }
|
|
||||||
.detail-mode { font-size: 1rem; }
|
|
||||||
.detail-journal {
|
|
||||||
margin: 0.35rem 0 0.6rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.45;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 60rem) {
|
|
||||||
.tableau {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 19rem;
|
|
||||||
gap: 4rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
.detail {
|
|
||||||
position: sticky;
|
|
||||||
top: 1.5rem;
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
}
|
|
||||||
.detail-texte { margin-top: 1.5rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 7. Page publique ---------- */
|
|
||||||
|
|
||||||
/* Le nom de la personne en titre. Sur ordinateur, tout tient dans le premier écran :
|
|
||||||
l'illustration à gauche, le jour choisi et les derniers mois à droite. */
|
|
||||||
.public {
|
|
||||||
max-width: var(--largeur-entete);
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 var(--gouttiere) 4rem;
|
|
||||||
display: grid;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-nom {
|
|
||||||
margin: 0;
|
|
||||||
font-size: clamp(2.6rem, 1.8rem + 3.4vw, 4rem);
|
|
||||||
line-height: 1.05;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.carte-humeur-grande {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 22rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-date {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
.public-date::first-letter { text-transform: uppercase; }
|
|
||||||
.public-mode {
|
|
||||||
margin: 0.15rem 0 0.5rem;
|
|
||||||
font-family: var(--serif);
|
|
||||||
font-size: clamp(2rem, 1.6rem + 1.6vw, 2.75rem);
|
|
||||||
line-height: 1.15;
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
.public-depuis {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-autre-jour .detail-statut:not(:empty) { margin-right: 0.75rem; }
|
|
||||||
|
|
||||||
.public-historique h2 {
|
|
||||||
margin: 2rem 0 0.75rem;
|
|
||||||
font-size: 1.35rem;
|
|
||||||
}
|
|
||||||
.public-historique .annee-mois { margin-top: 1rem; }
|
|
||||||
|
|
||||||
@media (min-width: 52rem) {
|
|
||||||
.public {
|
|
||||||
grid-template-columns: min(19rem, 42vh) minmax(0, 1fr);
|
|
||||||
column-gap: 3.5rem;
|
|
||||||
row-gap: 1.25rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
.public-nom { grid-column: 1 / -1; }
|
|
||||||
.carte-humeur-grande { max-width: none; margin-top: 0.4rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Changement de jour : bref fondu du détail */
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.apparition { animation: apparition 0.35s ease-out; }
|
|
||||||
}
|
|
||||||
@keyframes apparition {
|
|
||||||
from { opacity: 0.1; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 8. Page d'un jour (journal) ---------- */
|
|
||||||
|
|
||||||
.page-jour {
|
|
||||||
max-width: 56rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0.5rem var(--gouttiere) 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.retour {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-jour-titre {
|
|
||||||
margin: 0.5rem 0 1.5rem;
|
|
||||||
font-size: clamp(2rem, 1.6rem + 2vw, 2.75rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-jour .carte-humeur {
|
|
||||||
max-width: 11rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-jour .formulaire { margin-top: 1.75rem; }
|
|
||||||
|
|
||||||
.choix-modes {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0.25rem 0 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.6rem;
|
|
||||||
}
|
|
||||||
.choix-modes li { position: relative; }
|
|
||||||
.choix-modes input {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
}
|
|
||||||
.choix-modes label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.55rem;
|
|
||||||
padding: 0.5rem 1rem 0.5rem 0.85rem;
|
|
||||||
border: 2px solid var(--bordure-champ);
|
|
||||||
border-radius: var(--arrondi-pilule);
|
|
||||||
background: var(--blanc);
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.choix-modes label:hover { border-color: var(--encre); }
|
|
||||||
.choix-modes input:checked + label {
|
|
||||||
border-color: var(--encre);
|
|
||||||
box-shadow: inset 0 0 0 1.5px var(--encre);
|
|
||||||
background: var(--surface);
|
|
||||||
}
|
|
||||||
.choix-modes input:focus-visible + label {
|
|
||||||
outline: 3px solid var(--encre);
|
|
||||||
outline-offset: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 48rem) {
|
|
||||||
.page-jour {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 17rem minmax(0, 1fr);
|
|
||||||
grid-template-areas:
|
|
||||||
"carte entete"
|
|
||||||
"carte formulaire";
|
|
||||||
column-gap: 3.5rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
.page-jour-entete { grid-area: entete; }
|
|
||||||
.page-jour .carte-humeur {
|
|
||||||
grid-area: carte;
|
|
||||||
max-width: none;
|
|
||||||
position: sticky;
|
|
||||||
top: 1.5rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
.page-jour .formulaire { grid-area: formulaire; margin-top: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 9. Impression ---------- */
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.detail { position: static; border: 0; }
|
|
||||||
.page-jour .formulaire .actions { display: none; }
|
|
||||||
}
|
|
||||||
41
app/assets/stylesheets/application.bulma.scss
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
// @charset "utf-8";
|
||||||
|
|
||||||
|
// Import a Google Font
|
||||||
|
// @import url('https://fonts.googleapis.com/css?family=Nunito:400,700');
|
||||||
|
|
||||||
|
// Import only what you need from Bulma
|
||||||
|
// @import "bulma/sass/utilities/_all.sass";
|
||||||
|
// @import "bulma/sass/base/_all.sass";
|
||||||
|
// @import "bulma/sass/elements/button.sass";
|
||||||
|
// @import "bulma/sass/elements/container.sass";
|
||||||
|
// @import "bulma/sass/elements/title.sass";
|
||||||
|
// @import "bulma/sass/form/_all.sass";
|
||||||
|
// @import "bulma/sass/components/navbar.sass";
|
||||||
|
// @import "bulma/sass/layout/hero.sass";
|
||||||
|
// @import "bulma/sass/layout/section.sass";
|
||||||
|
|
||||||
|
@use 'bulma/bulma';
|
||||||
|
@use 'bulma/sass/utilities/_index.scss' as v;
|
||||||
|
@use './kluk';
|
||||||
|
|
||||||
|
// Set your brand colors
|
||||||
|
// $purple: #8A4D76;
|
||||||
|
// $pink: #FA7C91;
|
||||||
|
// $brown: #757763;
|
||||||
|
// $beige-light: #D0D1CD;
|
||||||
|
// $beige-lighter: #EFF0EB;
|
||||||
|
|
||||||
|
// Update Bulma's global variables
|
||||||
|
// $family-sans-serif: "Nunito", sans-serif;
|
||||||
|
// $grey-dark: $brown;
|
||||||
|
// $grey-light: $beige-light;
|
||||||
|
// $primary: $purple;
|
||||||
|
// $link: $pink;
|
||||||
|
// $widescreen-enabled: false;
|
||||||
|
// $fullhd-enabled: false;
|
||||||
|
|
||||||
|
// Update some of Bulma's component variables
|
||||||
|
// $body-background-color: $beige-lighter;
|
||||||
|
// $control-border-width: 2px;
|
||||||
|
// $input-border-color: transparent;
|
||||||
|
// $input-shadow: none;
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
// Point d'entrée des styles, compilé par cssbundling (yarn build:css).
|
|
||||||
// La charte d'abord (variables, base, composants), puis les styles propres à l'application.
|
|
||||||
@use 'charte';
|
|
||||||
@use 'app';
|
|
||||||
|
|
@ -1,531 +0,0 @@
|
||||||
/*
|
|
||||||
* Kluk — charte graphique, feuille de style réutilisable.
|
|
||||||
* Extraite de assets/css/style.scss (site kluk.fr), en CSS pur, sans Sass.
|
|
||||||
* Copie de charte/css/kluk.css : seuls les chemins des polices changent, ils
|
|
||||||
* pointent vers app/assets/fonts/ (Propshaft les réécrit avec l'empreinte).
|
|
||||||
* Ne pas modifier ici : les compléments propres à l’application sont dans app.css.
|
|
||||||
*
|
|
||||||
* Palette : la porcelaine du matin, l'encre du thé, le chêne de la table,
|
|
||||||
* et les quatre couleurs des tasses en accent.
|
|
||||||
*
|
|
||||||
* Sommaire
|
|
||||||
* 1. Polices
|
|
||||||
* 2. Variables (couleurs, typographies, mise en page)
|
|
||||||
* 3. Base
|
|
||||||
* 4. Liens, focus, lien d'évitement
|
|
||||||
* 5. Boutons
|
|
||||||
* 6. En-tête et navigation
|
|
||||||
* 7. Colonne de texte et titres
|
|
||||||
* 8. Illustration « tasses »
|
|
||||||
* 9. Calendrier d'humeurs
|
|
||||||
* 10. Photos
|
|
||||||
* 11. Formulaire
|
|
||||||
* 12. Pied de page
|
|
||||||
* 13. Impression
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* ---------- 1. Polices ---------- */
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Young Serif";
|
|
||||||
src: url("/young-serif-400.woff2") format("woff2");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
/* Fichier variable : couvre toutes les graisses de 200 à 800 */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Atkinson Hyperlegible Next";
|
|
||||||
src: url("/atkinson-hyperlegible-next-400.woff2") format("woff2");
|
|
||||||
font-weight: 200 800;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Atkinson Hyperlegible Next";
|
|
||||||
src: url("/atkinson-hyperlegible-next-400-italic.woff2") format("woff2");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 2. Variables ---------- */
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Fonds et texte */
|
|
||||||
--porcelaine: #F5F6F0;
|
|
||||||
--porcelaine-fonce: #E9ECE2;
|
|
||||||
--encre: #2E2A26;
|
|
||||||
--encre-douce: #5A534C;
|
|
||||||
--chene: #D9BC94;
|
|
||||||
--chene-fonce: #B48D5F;
|
|
||||||
|
|
||||||
/* Humeurs (les quatre tasses) */
|
|
||||||
--gris: #8D97A8;
|
|
||||||
--orange: #EDA33D;
|
|
||||||
--vert: #5F9B59;
|
|
||||||
--rouge: #D8573F;
|
|
||||||
--vert-lien: #3F7A3B;
|
|
||||||
|
|
||||||
/* Teintes utilitaires (écrites en dur dans le site d'origine) */
|
|
||||||
--encre-survol: #45403A;
|
|
||||||
--surface: #FBFCF8;
|
|
||||||
--bordure-champ: #8A8279;
|
|
||||||
--erreur-fond: #FBE9E5;
|
|
||||||
--blanc: #fff;
|
|
||||||
|
|
||||||
/* Typographies */
|
|
||||||
--serif: "Young Serif", Georgia, serif;
|
|
||||||
--sans: "Atkinson Hyperlegible Next", "Atkinson Hyperlegible", system-ui, sans-serif;
|
|
||||||
|
|
||||||
/* Mise en page */
|
|
||||||
--colonne: 36rem;
|
|
||||||
--gouttiere: 1.25rem;
|
|
||||||
--largeur-entete: 64rem;
|
|
||||||
|
|
||||||
/* Arrondis */
|
|
||||||
--arrondi-petit: 0.5rem;
|
|
||||||
--arrondi: 0.75rem;
|
|
||||||
--arrondi-pilule: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 3. Base ---------- */
|
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after { box-sizing: border-box; }
|
|
||||||
|
|
||||||
html {
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: var(--porcelaine);
|
|
||||||
color: var(--encre);
|
|
||||||
font-family: var(--sans);
|
|
||||||
font-size: 1.1875rem;
|
|
||||||
line-height: 1.65;
|
|
||||||
font-kerning: normal;
|
|
||||||
hyphens: manual;
|
|
||||||
}
|
|
||||||
|
|
||||||
main { flex: 1; outline: none; }
|
|
||||||
|
|
||||||
img, svg { display: block; max-width: 100%; }
|
|
||||||
|
|
||||||
/* ---------- 4. Liens, focus, lien d'évitement ---------- */
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--encre);
|
|
||||||
text-decoration-line: underline;
|
|
||||||
text-decoration-color: var(--vert);
|
|
||||||
text-decoration-thickness: 0.12em;
|
|
||||||
text-underline-offset: 0.2em;
|
|
||||||
}
|
|
||||||
a:hover { text-decoration-color: var(--encre); }
|
|
||||||
|
|
||||||
:focus-visible {
|
|
||||||
outline: 3px solid var(--encre);
|
|
||||||
outline-offset: 3px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.evitement {
|
|
||||||
position: absolute;
|
|
||||||
left: var(--gouttiere);
|
|
||||||
top: -4rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: var(--encre);
|
|
||||||
color: var(--porcelaine);
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
.evitement:focus { top: 0.75rem; }
|
|
||||||
|
|
||||||
/* ---------- 5. Boutons ---------- */
|
|
||||||
|
|
||||||
.bouton {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.8rem 1.6rem;
|
|
||||||
border-radius: var(--arrondi-pilule);
|
|
||||||
background: var(--encre);
|
|
||||||
color: var(--porcelaine);
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.3;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background-color 0.15s ease;
|
|
||||||
}
|
|
||||||
.bouton:hover { background: var(--encre-survol); color: var(--porcelaine); }
|
|
||||||
.bouton:focus-visible { outline-offset: 4px; border-radius: var(--arrondi-pilule); }
|
|
||||||
|
|
||||||
.bouton-petit {
|
|
||||||
padding: 0.45rem 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Un bouton seul dans son paragraphe respire davantage */
|
|
||||||
p:has(> .bouton:only-child) { margin: 2rem 0; }
|
|
||||||
|
|
||||||
/* ---------- 6. En-tête et navigation ---------- */
|
|
||||||
|
|
||||||
.entete {
|
|
||||||
max-width: var(--largeur-entete);
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 1.25rem var(--gouttiere);
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem 2rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.05rem 0.55rem 0.15rem;
|
|
||||||
border: 2.5px solid var(--encre);
|
|
||||||
border-radius: var(--arrondi-petit);
|
|
||||||
font-family: var(--sans);
|
|
||||||
font-weight: 800;
|
|
||||||
font-size: 1.35rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav ul {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem 1.35rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.nav a:not(.bouton) {
|
|
||||||
text-decoration-color: transparent;
|
|
||||||
}
|
|
||||||
.nav a:not(.bouton):hover,
|
|
||||||
.nav a[aria-current="page"]:not(.bouton) {
|
|
||||||
text-decoration-color: var(--vert);
|
|
||||||
}
|
|
||||||
.nav a.bouton[aria-current="page"] {
|
|
||||||
background: var(--porcelaine);
|
|
||||||
color: var(--encre);
|
|
||||||
box-shadow: inset 0 0 0 2px var(--encre);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 7. Colonne de texte et titres ---------- */
|
|
||||||
|
|
||||||
.texte {
|
|
||||||
max-width: calc(var(--colonne) + 2 * var(--gouttiere));
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem var(--gouttiere) 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3 {
|
|
||||||
font-family: var(--serif);
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.15;
|
|
||||||
letter-spacing: -0.005em;
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: clamp(2.35rem, 1.6rem + 3.6vw, 3.75rem);
|
|
||||||
margin: 1.5rem 0 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: clamp(1.6rem, 1.35rem + 1vw, 2rem);
|
|
||||||
margin: 4.5rem 0 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
p { margin: 0 0 1.15rem; }
|
|
||||||
|
|
||||||
/* Le chapeau : paragraphe entièrement en italique juste sous le titre */
|
|
||||||
h1 + p:has(> em:only-child) {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
line-height: 1.55;
|
|
||||||
color: var(--encre-douce);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note {
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
color: var(--encre-douce);
|
|
||||||
font-size: 1.0625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exergue {
|
|
||||||
margin: 2.25rem 0;
|
|
||||||
padding-left: 1.25rem;
|
|
||||||
border-left: 4px solid var(--orange);
|
|
||||||
font-family: var(--serif);
|
|
||||||
font-size: 1.45rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-size: 0.9em;
|
|
||||||
background: var(--porcelaine-fonce);
|
|
||||||
padding: 0.05em 0.3em;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 8. Illustration « tasses » ---------- */
|
|
||||||
|
|
||||||
.tasses {
|
|
||||||
margin: 1.5rem 0 0;
|
|
||||||
}
|
|
||||||
/* L'illustration déborde légèrement de la colonne sur grand écran */
|
|
||||||
@media (min-width: 48rem) {
|
|
||||||
.tasses { margin-inline: -4rem; }
|
|
||||||
}
|
|
||||||
.tasses svg { width: 100%; height: auto; overflow: visible; }
|
|
||||||
|
|
||||||
.table { fill: var(--chene); }
|
|
||||||
.table-fil,
|
|
||||||
.tasse-corps,
|
|
||||||
.tasse-anse,
|
|
||||||
.kluk-socle,
|
|
||||||
.vapeur path {
|
|
||||||
stroke: var(--encre);
|
|
||||||
stroke-width: 3;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
.tasse-anse { fill: none; stroke-width: 6; }
|
|
||||||
.tasse-corps { fill: currentColor; }
|
|
||||||
.tasse-reflet { fill: none; stroke: var(--blanc); stroke-opacity: 0.45; stroke-width: 4; stroke-linecap: round; }
|
|
||||||
.kluk-socle { fill: var(--chene-fonce); }
|
|
||||||
.kluk-voyant { fill: var(--porcelaine); }
|
|
||||||
.vapeur path { fill: none; stroke-width: 2.5; stroke: var(--encre-douce); }
|
|
||||||
|
|
||||||
.humeur-gris { color: var(--gris); }
|
|
||||||
.humeur-orange { color: var(--orange); }
|
|
||||||
.humeur-rouge { color: var(--rouge); }
|
|
||||||
.humeur-vert { color: var(--vert); }
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.vapeur path {
|
|
||||||
animation: vapeur 5s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
.vapeur path + path { animation-delay: 2.5s; }
|
|
||||||
}
|
|
||||||
@keyframes vapeur {
|
|
||||||
0%, 100% { opacity: 0.25; transform: translateY(6px); }
|
|
||||||
50% { opacity: 0.9; transform: translateY(-6px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 9. Calendrier d'humeurs (décoratif) ---------- */
|
|
||||||
|
|
||||||
.calendrier {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: repeat(7, auto);
|
|
||||||
grid-auto-flow: column;
|
|
||||||
grid-auto-columns: 1fr;
|
|
||||||
gap: 4px;
|
|
||||||
margin: 2.25rem 0 1rem;
|
|
||||||
padding: 1.25rem;
|
|
||||||
background: var(--surface);
|
|
||||||
border-radius: var(--arrondi);
|
|
||||||
box-shadow: inset 0 0 0 1px var(--porcelaine-fonce);
|
|
||||||
}
|
|
||||||
.jour {
|
|
||||||
aspect-ratio: 1;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
.jour-g { background: var(--gris); }
|
|
||||||
.jour-o { background: var(--orange); }
|
|
||||||
.jour-v { background: var(--vert); }
|
|
||||||
.jour-r { background: var(--rouge); }
|
|
||||||
|
|
||||||
@media (max-width: 30rem) {
|
|
||||||
.calendrier { gap: 3px; padding: 0.9rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 10. Photos ---------- */
|
|
||||||
|
|
||||||
.photo {
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
.photo img {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: var(--arrondi);
|
|
||||||
}
|
|
||||||
.photo figcaption {
|
|
||||||
margin-top: 0.6rem;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: var(--encre-douce);
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo-attente {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1.25rem;
|
|
||||||
aspect-ratio: 3 / 2;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border: 2px dashed var(--chene-fonce);
|
|
||||||
border-radius: var(--arrondi);
|
|
||||||
background: var(--porcelaine-fonce);
|
|
||||||
color: var(--encre-douce);
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
.photo-attente svg {
|
|
||||||
flex: none;
|
|
||||||
width: 2.75rem;
|
|
||||||
fill: none;
|
|
||||||
stroke: var(--chene-fonce);
|
|
||||||
stroke-width: 2.5;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
stroke-linecap: round;
|
|
||||||
}
|
|
||||||
.photo-attente p { margin: 0; }
|
|
||||||
.photo-attente strong { color: var(--encre); }
|
|
||||||
|
|
||||||
/* ---------- 11. Formulaire ---------- */
|
|
||||||
|
|
||||||
.formulaire {
|
|
||||||
margin: 2.5rem 0 1rem;
|
|
||||||
padding: 1.75rem 1.25rem;
|
|
||||||
background: var(--surface);
|
|
||||||
border-radius: var(--arrondi);
|
|
||||||
box-shadow: inset 0 0 0 1px var(--porcelaine-fonce);
|
|
||||||
}
|
|
||||||
@media (min-width: 40rem) {
|
|
||||||
.formulaire { padding: 2.25rem 2.5rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.champ {
|
|
||||||
margin: 0 0 1.75rem;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.champ > label,
|
|
||||||
.champ > legend {
|
|
||||||
display: block;
|
|
||||||
padding: 0;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.precision {
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--encre-douce);
|
|
||||||
}
|
|
||||||
|
|
||||||
.formulaire input[type="email"],
|
|
||||||
.formulaire textarea {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.7rem 0.85rem;
|
|
||||||
border: 2px solid var(--bordure-champ);
|
|
||||||
border-radius: var(--arrondi-petit);
|
|
||||||
background: var(--blanc);
|
|
||||||
color: var(--encre);
|
|
||||||
font: inherit;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
.formulaire textarea { resize: vertical; min-height: 8rem; }
|
|
||||||
|
|
||||||
.formulaire input[type="email"]:focus-visible,
|
|
||||||
.formulaire textarea:focus-visible {
|
|
||||||
outline: 3px solid var(--vert);
|
|
||||||
outline-offset: 1px;
|
|
||||||
border-color: var(--encre);
|
|
||||||
}
|
|
||||||
|
|
||||||
.choix {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.choix li {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.35rem 0;
|
|
||||||
}
|
|
||||||
.choix input {
|
|
||||||
flex: none;
|
|
||||||
width: 1.35rem;
|
|
||||||
height: 1.35rem;
|
|
||||||
margin: 0.2rem 0 0;
|
|
||||||
accent-color: var(--vert-lien);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.choix label { cursor: pointer; }
|
|
||||||
|
|
||||||
/* Champ piège anti-robots, invisible */
|
|
||||||
.piege {
|
|
||||||
position: absolute;
|
|
||||||
left: -9999px;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formulaire-erreur {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-left: 4px solid var(--rouge);
|
|
||||||
background: var(--erreur-fond);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formulaire .bouton {
|
|
||||||
border: 0;
|
|
||||||
font: inherit;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.formulaire .bouton:disabled { opacity: 0.6; cursor: progress; }
|
|
||||||
|
|
||||||
.formulaire-secours {
|
|
||||||
margin: 1rem 0 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--encre-douce);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 12. Pied de page : la table en bois ---------- */
|
|
||||||
|
|
||||||
.pied {
|
|
||||||
background: var(--chene);
|
|
||||||
border-top: 3px solid var(--encre);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.pied-interieur {
|
|
||||||
max-width: calc(var(--colonne) + 2 * var(--gouttiere));
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem var(--gouttiere) 2.5rem;
|
|
||||||
}
|
|
||||||
.pied-phrase {
|
|
||||||
font-family: var(--serif);
|
|
||||||
font-size: 1.15rem;
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
}
|
|
||||||
.pied-liens {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem 2rem;
|
|
||||||
}
|
|
||||||
.pied a { text-decoration-color: var(--encre); text-decoration-thickness: 1px; }
|
|
||||||
.pied a:hover { text-decoration-thickness: 2px; }
|
|
||||||
|
|
||||||
/* ---------- 13. Impression ---------- */
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.entete .nav, .pied, .tasses, .calendrier { display: none; }
|
|
||||||
body { background: var(--blanc); }
|
|
||||||
}
|
|
||||||
66
app/assets/stylesheets/kluk.css
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* This is a manifest file that'll be compiled into application.css.
|
||||||
|
*
|
||||||
|
* With Propshaft, assets are served efficiently without preprocessing steps. You can still include
|
||||||
|
* application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
|
||||||
|
* cascading order, meaning styles declared later in the document or manifest will override earlier ones,
|
||||||
|
* depending on specificity.
|
||||||
|
*
|
||||||
|
* Consider organizing styles into separate files for maintainability.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0px;
|
||||||
|
font-family: "Sour Gummy", sans-serif;
|
||||||
|
font-weight: 350;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navbar-top {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tracker {
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs {
|
||||||
|
height: 60vh;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-day {
|
||||||
|
background-color: white;
|
||||||
|
position: sticky;
|
||||||
|
top: 64px;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day {
|
||||||
|
border: 1px;
|
||||||
|
margin: 4px;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-day {
|
||||||
|
border: 2px double white;
|
||||||
|
margin: 4px;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
margin: 30px;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# Page de démonstration des composants de la charte graphique (développement seulement).
|
|
||||||
class CharteController < ApplicationController
|
|
||||||
allow_unauthenticated_access
|
|
||||||
|
|
||||||
def show
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -11,7 +11,7 @@ class DayLogsController < ApplicationController
|
||||||
|
|
||||||
if @day_log.save
|
if @day_log.save
|
||||||
handle_mood(@day_log.day, params[:day_log][:mode_id])
|
handle_mood(@day_log.day, params[:day_log][:mode_id])
|
||||||
redirect_to dashboard_path(day: @day_log.day)
|
redirect_to root_path
|
||||||
else
|
else
|
||||||
render :new, status: :unprocessable_content
|
render :new, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
@ -20,7 +20,7 @@ class DayLogsController < ApplicationController
|
||||||
def update
|
def update
|
||||||
if @day_log.update(day_log_params)
|
if @day_log.update(day_log_params)
|
||||||
handle_mood(@day_log.day, params[:day_log][:mode_id])
|
handle_mood(@day_log.day, params[:day_log][:mode_id])
|
||||||
redirect_to dashboard_path(day: @day_log.day)
|
redirect_to dashboard_path
|
||||||
else
|
else
|
||||||
render :edit, status: :unprocessable_content
|
render :edit, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ class HomeController < ApplicationController
|
||||||
def index
|
def index
|
||||||
@user = User.find_by(username: request.subdomain)
|
@user = User.find_by(username: request.subdomain)
|
||||||
if @user&.public?
|
if @user&.public?
|
||||||
render "moods/public"
|
render "moods/index"
|
||||||
else
|
else
|
||||||
redirect_to dashboard_path
|
redirect_to dashboard_path
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ class InvitationsController < ApplicationController
|
||||||
password_params = params.expect(user: [ :password, :password_confirmation ])
|
password_params = params.expect(user: [ :password, :password_confirmation ])
|
||||||
|
|
||||||
if @user.update(password_params.merge(invitation_accepted_at: Time.current))
|
if @user.update(password_params.merge(invitation_accepted_at: Time.current))
|
||||||
redirect_to root_path, notice: "Compte activé. Bienvenue !"
|
redirect_to root_path, notice: "Account activated!"
|
||||||
else
|
else
|
||||||
redirect_to invitation_path(params[:token]), alert: "Les deux mots de passe ne sont pas identiques."
|
redirect_to invitation_path(params[:token]), alert: "Passwords did not match."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -20,6 +20,6 @@ class InvitationsController < ApplicationController
|
||||||
def set_user_by_token
|
def set_user_by_token
|
||||||
@user = User.find_by_invitation_token(params[:token])
|
@user = User.find_by_invitation_token(params[:token])
|
||||||
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
||||||
redirect_to root_path, alert: "Ce lien d’invitation n’est plus valable."
|
redirect_to root_path, alert: "Invitation link is invalid or has expired."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
class MoodsController < ApplicationController
|
class MoodsController < ApplicationController
|
||||||
def index
|
def index
|
||||||
@user = Current.user
|
@user = Current.user
|
||||||
redirect_to dashboard_path(day: Date.today) unless params[:day]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ class PasswordsController < ApplicationController
|
||||||
PasswordsMailer.reset(user).deliver_later
|
PasswordsMailer.reset(user).deliver_later
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to new_session_path, notice: "Si un compte existe avec cette adresse, un lien pour changer de mot de passe vient de lui être envoyé."
|
redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
|
|
@ -18,9 +18,9 @@ class PasswordsController < ApplicationController
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if @user.update(params.permit(:password, :password_confirmation))
|
if @user.update(params.permit(:password, :password_confirmation))
|
||||||
redirect_to new_session_path, notice: "Mot de passe changé. Vous pouvez vous connecter."
|
redirect_to new_session_path, notice: "Password has been reset."
|
||||||
else
|
else
|
||||||
redirect_to edit_password_path(params[:token]), alert: "Les deux mots de passe ne sont pas identiques."
|
redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -28,6 +28,6 @@ class PasswordsController < ApplicationController
|
||||||
def set_user_by_token
|
def set_user_by_token
|
||||||
@user = User.find_by_password_reset_token!(params[:token])
|
@user = User.find_by_password_reset_token!(params[:token])
|
||||||
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
||||||
redirect_to new_password_path, alert: "Ce lien n’est plus valable. Demandez-en un nouveau."
|
redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
class SessionsController < ApplicationController
|
class SessionsController < ApplicationController
|
||||||
allow_unauthenticated_access only: %i[ new create ]
|
allow_unauthenticated_access only: %i[ new create ]
|
||||||
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Trop de tentatives. Réessayez dans quelques minutes." }
|
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." }
|
||||||
|
|
||||||
def new
|
def new
|
||||||
end
|
end
|
||||||
|
|
@ -10,7 +10,7 @@ class SessionsController < ApplicationController
|
||||||
start_new_session_for user
|
start_new_session_for user
|
||||||
redirect_to after_authentication_url
|
redirect_to after_authentication_url
|
||||||
else
|
else
|
||||||
redirect_to new_session_path, alert: "Adresse mail ou mot de passe incorrect."
|
redirect_to new_session_path, alert: "Try another email address or password."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,2 @@
|
||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
# Lien de navigation de l'en-tête : marqué aria-current="page" sur la page courante,
|
|
||||||
# ce qui déclenche le soulignement vert (ou l'inversion du bouton) de la charte.
|
|
||||||
def lien_nav(nom, chemin, **options)
|
|
||||||
options[:"aria-current"] = "page" if current_page?(chemin)
|
|
||||||
link_to nom, chemin, **options
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,86 +1,15 @@
|
||||||
module MoodsHelper
|
module MoodsHelper
|
||||||
# État d'un jour du calendrier :
|
|
||||||
# :filled une tasse a été posée ce jour-là
|
|
||||||
# :guessed pas de tasse posée, on suppose que le mode de la veille continue
|
|
||||||
# :unknown pas de tasse posée, et l'utilisateur ne veut pas de supposition
|
|
||||||
# :empty avant le premier enregistrement
|
|
||||||
# :future après aujourd'hui
|
|
||||||
def day_status(mood, user)
|
|
||||||
return :future if mood[:recorded_at].to_date > Date.current
|
|
||||||
return :filled if mood[:mode].present?
|
|
||||||
return :guessed if mood[:guess].present? && user.guess?
|
|
||||||
return :unknown if mood[:guess].present? && !user.guess?
|
|
||||||
:empty
|
|
||||||
end
|
|
||||||
|
|
||||||
def mode_for(mood, user)
|
def mode_for(mood, user)
|
||||||
case day_status(mood, user)
|
if user.guess?
|
||||||
when :filled then mood[:mode]
|
mood[:mode] || mood[:guess] || { label: "unknown", color: "white", image_url: "unknown.jpg" }
|
||||||
when :guessed then mood[:guess]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Les années du calendrier, la plus récente d'abord ; les mois dans l'ordre à l'intérieur.
|
|
||||||
def history_by_year(history)
|
|
||||||
history.group_by { |month| month[:month].year }.sort_by { |year, _| -year }
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_day(history, date)
|
|
||||||
history.each do |month|
|
|
||||||
month[:weeks].each do |week|
|
|
||||||
week.each { |mood| return mood if mood[:recorded_at].to_date == date }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def date_longue(date)
|
|
||||||
l(date, format: "%A %-d %B %Y")
|
|
||||||
end
|
|
||||||
|
|
||||||
# « de robi », mais « d’Anne » : élision devant une voyelle ou un h muet
|
|
||||||
def de_nom(nom)
|
|
||||||
nom.match?(/\A[aeiouyhàâéèêëîïôöùûü]/i) ? "d’#{nom}" : "de #{nom}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def libelle_statut(status)
|
|
||||||
case status
|
|
||||||
when :guessed then "supposé"
|
|
||||||
when :unknown then "pas de tasse posée"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Un jour du calendrier : un bouton si on peut le choisir, sinon une simple case.
|
|
||||||
# Le journal n'est exposé que s'il est demandé (jamais sur la page publique).
|
|
||||||
def case_jour(mood, user, interactive:, selected: false, journal: false)
|
|
||||||
date = mood[:recorded_at].to_date
|
|
||||||
status = day_status(mood, user)
|
|
||||||
mode = mode_for(mood, user)
|
|
||||||
day_log = mood[:day_log] if journal
|
|
||||||
|
|
||||||
description = [ date_longue(date), mode&.label, libelle_statut(status), ("journal écrit" if day_log) ].compact.join(", ")
|
|
||||||
classes = [ "jour", "jour-#{status}" ]
|
|
||||||
classes << "jour-journal" if day_log
|
|
||||||
classes << "jour-aujourdhui" if date == Date.current
|
|
||||||
options = { class: classes, style: ("--humeur: #{mode.color}" if mode), title: description }
|
|
||||||
|
|
||||||
if interactive && status != :future
|
|
||||||
tag.button(type: "button", **options,
|
|
||||||
tabindex: selected ? 0 : -1,
|
|
||||||
"aria-label": description,
|
|
||||||
"aria-pressed": selected.to_s,
|
|
||||||
data: {
|
|
||||||
day: date.iso8601,
|
|
||||||
date: date_longue(date),
|
|
||||||
mode: mode&.label,
|
|
||||||
statut: libelle_statut(status),
|
|
||||||
couleur: mode&.color,
|
|
||||||
image: (mode || Mode.new).image_url,
|
|
||||||
info: day_log&.info,
|
|
||||||
action: "calendrier#choisir keydown->calendrier#naviguer"
|
|
||||||
})
|
|
||||||
else
|
else
|
||||||
tag.span(**options, "aria-hidden": true)
|
mood[:mode] || { label: "unknown", color: "white", image_url: "unknown.jpg" }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def style_for_mode(mode)
|
||||||
|
style = "background-color: #{mode[:color]};"
|
||||||
|
style += " border: 2px double grey;" if mode[:label] == "unknown"
|
||||||
|
style
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
import { Controller } from "@hotwired/stimulus"
|
|
||||||
|
|
||||||
// Calendrier des humeurs : choisir un jour affiche son détail (illustration, mode, et
|
|
||||||
// sur le tableau de bord, le journal). Sert au tableau de bord comme à la page publique :
|
|
||||||
// chaque cible est facultative.
|
|
||||||
// Au clavier, les flèches se déplacent d'un jour (haut, bas) ou d'une semaine (gauche, droite).
|
|
||||||
export default class extends Controller {
|
|
||||||
static targets = [ "carte", "image", "date", "mode", "statut", "depuis", "retour", "journal", "lien", "anime" ]
|
|
||||||
static values = { aujourdhui: String, aujourdhuiDate: String, aujourdhuiMode: String, aujourdhuiImage: String, aujourdhuiCouleur: String }
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
const selectionne = this.element.querySelector(".jour[aria-pressed='true']")
|
|
||||||
if (selectionne && new URLSearchParams(window.location.search).has("day")) {
|
|
||||||
selectionne.scrollIntoView({ block: "nearest" })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
choisir(event) {
|
|
||||||
this.selectionner(event.currentTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
revenir() {
|
|
||||||
const aujourdhui = this.element.querySelector(`button.jour[data-day="${this.aujourdhuiValue}"]`)
|
|
||||||
if (aujourdhui) this.selectionner(aujourdhui)
|
|
||||||
}
|
|
||||||
|
|
||||||
naviguer(event) {
|
|
||||||
const decalages = { ArrowUp: -1, ArrowDown: 1, ArrowLeft: -7, ArrowRight: 7 }
|
|
||||||
const decalage = decalages[event.key]
|
|
||||||
if (!decalage) return
|
|
||||||
|
|
||||||
const date = new Date(`${event.currentTarget.dataset.day}T12:00:00`)
|
|
||||||
date.setDate(date.getDate() + decalage)
|
|
||||||
const cible = this.element.querySelector(`button.jour[data-day="${date.toISOString().slice(0, 10)}"]`)
|
|
||||||
if (!cible) return
|
|
||||||
|
|
||||||
event.preventDefault()
|
|
||||||
this.selectionner(cible)
|
|
||||||
cible.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionner(jour) {
|
|
||||||
const precedent = this.element.querySelector(".jour[aria-pressed='true']")
|
|
||||||
if (precedent) {
|
|
||||||
precedent.setAttribute("aria-pressed", "false")
|
|
||||||
precedent.tabIndex = -1
|
|
||||||
}
|
|
||||||
jour.setAttribute("aria-pressed", "true")
|
|
||||||
jour.tabIndex = 0
|
|
||||||
this.animer()
|
|
||||||
|
|
||||||
// Sur la page publique, aujourd'hui montre la dernière tasse posée et depuis quand
|
|
||||||
if (jour.dataset.day === this.aujourdhuiValue && this.hasDepuisTarget) {
|
|
||||||
this.afficherAujourdhui()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { day, date, mode, statut, couleur, image, info } = jour.dataset
|
|
||||||
this.carteTarget.style.setProperty("--humeur", couleur || "")
|
|
||||||
this.imageTarget.src = image
|
|
||||||
this.modeTarget.textContent = mode || "Pas de tasse posée"
|
|
||||||
this.statutTarget.textContent = mode ? (statut || "") : ""
|
|
||||||
|
|
||||||
this.dateTarget.textContent = date
|
|
||||||
if (this.hasDepuisTarget) this.depuisTarget.hidden = true
|
|
||||||
if (this.hasRetourTarget) this.retourTarget.hidden = false
|
|
||||||
|
|
||||||
if (this.hasJournalTarget) {
|
|
||||||
this.journalTarget.textContent = info || this.journalTarget.dataset.vide
|
|
||||||
this.journalTarget.classList.toggle("detail-journal-vide", !info)
|
|
||||||
}
|
|
||||||
if (this.hasLienTarget) {
|
|
||||||
this.lienTarget.href = `/day_logs/edit?day=${day}`
|
|
||||||
this.lienTarget.textContent = info ? "Modifier le journal" : "Écrire dans le journal"
|
|
||||||
}
|
|
||||||
|
|
||||||
history.replaceState({}, "", `?day=${day}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
afficherAujourdhui() {
|
|
||||||
this.carteTarget.style.setProperty("--humeur", this.aujourdhuiCouleurValue)
|
|
||||||
this.imageTarget.src = this.aujourdhuiImageValue
|
|
||||||
this.modeTarget.textContent = this.aujourdhuiModeValue
|
|
||||||
this.dateTarget.textContent = this.aujourdhuiDateValue
|
|
||||||
this.depuisTarget.hidden = false
|
|
||||||
this.retourTarget.hidden = true
|
|
||||||
history.replaceState({}, "", window.location.pathname)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Un bref fondu signale que le détail a changé, même quand le mode est le même
|
|
||||||
animer() {
|
|
||||||
this.animeTargets.forEach((element) => {
|
|
||||||
element.classList.remove("apparition")
|
|
||||||
void element.offsetWidth
|
|
||||||
element.classList.add("apparition")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { Controller } from "@hotwired/stimulus"
|
|
||||||
|
|
||||||
// Formulaire du journal : l'illustration suit le mode choisi.
|
|
||||||
export default class extends Controller {
|
|
||||||
static targets = [ "carte", "image" ]
|
|
||||||
|
|
||||||
changer(event) {
|
|
||||||
const { image, couleur } = event.currentTarget.dataset
|
|
||||||
this.imageTarget.src = image
|
|
||||||
this.carteTarget.style.setProperty("--humeur", couleur)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
app/javascript/controllers/menu_controller.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Controller } from '@hotwired/stimulus'
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [ 'burger', 'navbar' ];
|
||||||
|
|
||||||
|
toggle(event) {
|
||||||
|
[ this.burgerTarget, this.navbarTarget ].forEach((target) => {
|
||||||
|
target.classList.toggle("is-active");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/javascript/controllers/mood_controller.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Controller } from '@hotwired/stimulus'
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [ "image", "modeDay", "dayLogLink" ];
|
||||||
|
|
||||||
|
updateDayInfo(event) {
|
||||||
|
const image = this.imageTarget;
|
||||||
|
const modeDay = this.modeDayTarget;
|
||||||
|
const day = event.target.dataset.day;
|
||||||
|
const modeDayContent = day + ' : ' + event.target.dataset.mode;
|
||||||
|
image.src = event.target.dataset.image;
|
||||||
|
modeDay.textContent = modeDayContent;
|
||||||
|
event.target.className = "selected-day " + event.target.dataset.mode;
|
||||||
|
|
||||||
|
this.dayLogLinkTarget.href = `/day_logs/edit?day=${day}`
|
||||||
|
|
||||||
|
if (this.lastTarget) {
|
||||||
|
this.lastTarget.className = "day " + this.lastTarget.dataset.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastTarget = event.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.lastTarget = null;
|
||||||
|
window.location = "#end";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
class Mode < ApplicationRecord
|
class Mode < ApplicationRecord
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
has_one_attached :image
|
has_one_attached :image
|
||||||
|
|
||||||
def image_url
|
|
||||||
image.attached? ? Rails.application.routes.url_helpers.rails_blob_path(image, only_path: true) : ActionController::Base.helpers.asset_path("unknown.jpg")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
class RfidTag < ApplicationRecord
|
class RfidTag < ApplicationRecord
|
||||||
belongs_to :user, required: false
|
belongs_to :user, required: false
|
||||||
belongs_to :mode, required: false
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -33,25 +33,10 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def history
|
def history
|
||||||
MoodCalendarService.generate_calendar(self)
|
MoodCalendarService.generate_calendar(moods)
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_mode
|
def current_mode
|
||||||
self.moods.last&.mode || "croisiere"
|
self.moods.last&.mode || "croisiere"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Le jour où l'humeur actuelle a commencé : le plus ancien des derniers
|
|
||||||
# enregistrements consécutifs dans le mode actuel.
|
|
||||||
def current_mode_since
|
|
||||||
since = nil
|
|
||||||
moods.reorder(recorded_at: :desc).pluck(:mode_id, :recorded_at).each do |mode_id, recorded_at|
|
|
||||||
break if since && mode_id != since[:mode_id]
|
|
||||||
since = { mode_id: mode_id, day: recorded_at.to_date }
|
|
||||||
end
|
|
||||||
since&.dig(:day)
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_day_log
|
|
||||||
self.day_logs.find_by(day: Date.today)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,26 @@
|
||||||
|
# app/services/mood_calendar_service.rb
|
||||||
class MoodCalendarService
|
class MoodCalendarService
|
||||||
def self.generate_calendar(user, start_date: nil, end_date: nil)
|
def self.generate_calendar(moods, start_date: nil, end_date: Date.current)
|
||||||
data = user.moods.includes(:mode)
|
# Preload modes with their image attachments
|
||||||
.order(:recorded_at)
|
mode_ids = moods.joins(:mode).pluck("modes.id").uniq
|
||||||
.map { |mood| { mode: mood.mode, recorded_at: mood.recorded_at } }
|
modes_by_id = Mode.where(id: mode_ids)
|
||||||
|
.with_attached_image
|
||||||
|
.index_by(&:id)
|
||||||
|
|
||||||
day_logs_by_date = user.day_logs.index_by(&:day)
|
data = moods.joins(:mode)
|
||||||
|
.order(:recorded_at)
|
||||||
|
.pluck(:recorded_at, "modes.label", "modes.color", "modes.id")
|
||||||
|
.map do |recorded_at, label, color, mode_id|
|
||||||
|
mode = modes_by_id[mode_id]
|
||||||
|
{
|
||||||
|
mode: {
|
||||||
|
label: label,
|
||||||
|
color: color,
|
||||||
|
image_url: mode&.image&.attached? ? Rails.application.routes.url_helpers.rails_blob_url(mode.image, only_path: true) : nil
|
||||||
|
},
|
||||||
|
recorded_at: recorded_at
|
||||||
|
}
|
||||||
|
end # Convertir la relation ActiveRecord en tableau de hash
|
||||||
|
|
||||||
if data.empty?
|
if data.empty?
|
||||||
start_date = Date.current
|
start_date = Date.current
|
||||||
|
|
@ -12,58 +28,59 @@ class MoodCalendarService
|
||||||
start_date ||= data.first[:recorded_at].to_date
|
start_date ||= data.first[:recorded_at].to_date
|
||||||
end
|
end
|
||||||
|
|
||||||
start_date = start_date.to_date
|
if end_date < (start_date + 5.months)
|
||||||
end_date = end_date ? end_date.to_date : [ Date.current, start_date + 5.months ].max
|
|
||||||
=begin
|
|
||||||
if end_date.nil?
|
|
||||||
end_date = start_date + 5.months
|
end_date = start_date + 5.months
|
||||||
else
|
|
||||||
end_date = end_date.to_date
|
|
||||||
end
|
end
|
||||||
=end
|
|
||||||
|
|
||||||
|
# Convertir en Date si ce sont des DateTime ou Time
|
||||||
|
start_date = start_date.to_date
|
||||||
|
end_date = end_date&.to_date
|
||||||
|
|
||||||
|
# Grouper par jour et garder le plus récent pour chaque jour
|
||||||
data_by_date = data.group_by { |d| d[:recorded_at].to_date }
|
data_by_date = data.group_by { |d| d[:recorded_at].to_date }
|
||||||
.transform_values { |entries| entries.max_by { |e| e[:recorded_at] } }
|
.transform_values { |entries| entries.max_by { |e| e[:recorded_at] } }
|
||||||
|
|
||||||
|
# Trouver le dernier mood avant start_date pour initialiser le guess
|
||||||
last_mode = data.select { |d| d[:recorded_at].to_date < start_date }
|
last_mode = data.select { |d| d[:recorded_at].to_date < start_date }
|
||||||
.max_by { |d| d[:recorded_at] }
|
.max_by { |d| d[:recorded_at] }
|
||||||
&.[](:mode)
|
&.[](:mode)
|
||||||
|
|
||||||
|
# Générer le tableau complet avec tous les jours
|
||||||
complete_data = (start_date..end_date).map do |date|
|
complete_data = (start_date..end_date).map do |date|
|
||||||
day_log = day_logs_by_date[date]
|
|
||||||
if data_by_date[date]
|
if data_by_date[date]
|
||||||
last_mode = data_by_date[date][:mode]
|
last_mode = data_by_date[date][:mode]
|
||||||
data_by_date[date].merge(day_log: day_log)
|
data_by_date[date]
|
||||||
else
|
else
|
||||||
{ mode: nil, recorded_at: date.to_datetime, guess: last_mode, day_log: day_log }
|
{ mode: nil, recorded_at: date.to_datetime, guess: last_mode }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Regrouper par mois avec semaines commençant au premier lundi
|
||||||
complete_data.group_by { |d| d[:recorded_at].to_date.beginning_of_month }
|
complete_data.group_by { |d| d[:recorded_at].to_date.beginning_of_month }
|
||||||
.map do |month_start, _|
|
.map do |month_start, month_data|
|
||||||
first_monday = month_start
|
first_monday = month_start
|
||||||
first_monday = first_monday.next_occurring(:monday) unless first_monday.monday?
|
first_monday = first_monday.next_occurring(:monday) unless first_monday.monday?
|
||||||
|
|
||||||
next_month_start = month_start.next_month.beginning_of_month
|
next_month_start = month_start.next_month.beginning_of_month
|
||||||
next_first_monday = next_month_start
|
next_first_monday = next_month_start
|
||||||
next_first_monday = next_first_monday.next_occurring(:monday) unless next_first_monday.monday?
|
next_first_monday = next_first_monday.next_occurring(:monday) unless next_first_monday.monday?
|
||||||
|
|
||||||
month_end = next_first_monday - 1.day
|
month_end = next_first_monday - 1.day
|
||||||
|
|
||||||
data_hash = complete_data.index_by { |d| d[:recorded_at].to_date }
|
data_hash = complete_data.index_by { |d| d[:recorded_at].to_date }
|
||||||
|
|
||||||
all_days = (first_monday..month_end).map do |date|
|
all_days = (first_monday..month_end).map do |date|
|
||||||
data_hash[date] || { mode: nil, recorded_at: date.to_datetime, guess: nil, day_log: nil }
|
data_hash[date] || { mode: nil, recorded_at: date.to_datetime, guess: nil }
|
||||||
end
|
|
||||||
|
|
||||||
weeks = all_days.group_by { |d| d[:recorded_at].to_date.beginning_of_week(:monday) }
|
|
||||||
.sort_by { |week_start, _| week_start }
|
|
||||||
.map { |_, week_moods| week_moods }
|
|
||||||
|
|
||||||
{
|
|
||||||
month: month_start,
|
|
||||||
weeks: weeks
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
weeks = all_days.group_by { |d| d[:recorded_at].to_date.beginning_of_week(:monday) }
|
||||||
|
.sort_by { |week_start, _| week_start }
|
||||||
|
.map { |_, week_moods| week_moods }
|
||||||
|
|
||||||
|
{
|
||||||
|
month: month_start,
|
||||||
|
weeks: weeks
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end#
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<header class="entete">
|
|
||||||
<%= link_to "kluk", root_path, class: "logo", "aria-label": "Kluk, retour à l'accueil" %>
|
|
||||||
<nav class="nav" aria-label="Navigation principale">
|
|
||||||
<ul>
|
|
||||||
<% if authenticated? %>
|
|
||||||
<li><%= lien_nav "Mes humeurs", dashboard_path %></li>
|
|
||||||
<li><%= button_to "Se déconnecter", session_path, method: :delete, class: "bouton bouton-petit" %></li>
|
|
||||||
<% else %>
|
|
||||||
<li><%= lien_nav "Se connecter", new_session_path, class: "bouton bouton-petit" %></li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<% if flash[:alert].present? || flash[:notice].present? %>
|
|
||||||
<div class="flash">
|
|
||||||
<% if flash[:alert].present? %>
|
|
||||||
<p class="flash-message flash-alerte" role="alert"><%= flash[:alert] %></p>
|
|
||||||
<% end %>
|
|
||||||
<% if flash[:notice].present? %>
|
|
||||||
<p class="flash-message" role="status"><%= flash[:notice] %></p>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
27
app/views/application/_menu.html.erb
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<nav id="navbar-top" class="navbar has-background-primary" role="navigation" aria-label="main navigation" data-controller="menu">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<div class="navbar-item is-size-5">Comment ça </div>
|
||||||
|
<div class="navbar-item is-size-3"><strong>KLUK</strong></div>
|
||||||
|
<div class="navbar-item is-size-5">aujourd'hui ?</div>
|
||||||
|
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-menu-target="burger" data-action="menu#toggle">
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-menu" data-menu-target="navbar">
|
||||||
|
<div class="navbar-end">
|
||||||
|
<% if authenticated? %>
|
||||||
|
<div class="navbar-item">
|
||||||
|
<%= button_to "Se déconnecter", session_path, method: :delete, class: "button is-light" %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="navbar-item">
|
||||||
|
<%= link_to "Se connecter", new_session_path, class: "button is-light" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<footer class="pied">
|
|
||||||
<div class="pied-interieur">
|
|
||||||
<p class="pied-phrase">Comment ça Kluk aujourd’hui ?</p>
|
|
||||||
<ul class="pied-liens">
|
|
||||||
<li><%= link_to "Découvrir la Kluk", "https://www.kluk.fr/" %></li>
|
|
||||||
<li><%= link_to "Mentions légales", "https://www.kluk.fr/mentions-legales/" %></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<%# Bouton de la charte (pilule encre). Pour un bouton d'envoi : f.submit "…", class: "bouton".
|
|
||||||
render "charte/bouton", texte: "Je veux ma Kluk", href: dashboard_path
|
|
||||||
render "charte/bouton", texte: "Voir", href: "#", petit: true, data: { turbo_frame: "x" } %>
|
|
||||||
<%# locals: (texte:, href:, petit: false, **attributs) -%>
|
|
||||||
<%= link_to texte, href, class: ["bouton", ("bouton-petit" if petit)], **attributs %>
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<%# Calendrier d'humeurs décoratif : une case par jour, colonnes de 7 jours (une semaine par colonne).
|
|
||||||
jours : suite de lettres g (gris), o (orange), v (vert), r (rouge), en chaîne ou en tableau.
|
|
||||||
render "charte/calendrier", jours: "vvovvvg vvvvovv ovvrvvv" %>
|
|
||||||
<%# locals: (jours:) -%>
|
|
||||||
<% jours = jours.is_a?(String) ? jours.delete(" ").chars : Array(jours) %>
|
|
||||||
<div class="calendrier" aria-hidden="true">
|
|
||||||
<% jours.each do |jour| %>
|
|
||||||
<span class="jour jour-<%= jour %>"></span>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<%# Champ de formulaire de la charte (dans un form de classe "formulaire") :
|
|
||||||
libellé en gras au-dessus, précision facultative en encre douce. Le bloc donne la saisie.
|
|
||||||
render("charte/champ", form: f, attribut: :email_address, libelle: "Votre adresse mail", precision: "(obligatoire)") { f.email_field :email_address, required: true }
|
|
||||||
(en ERB, avec « do » / « end » et la saisie dans le bloc) %>
|
|
||||||
<%# locals: (form:, attribut:, libelle:, precision: nil) -%>
|
|
||||||
<div class="champ">
|
|
||||||
<%= form.label attribut do %>
|
|
||||||
<%= libelle %>
|
|
||||||
<% if precision %><span class="precision"><%= precision %></span><% end %>
|
|
||||||
<% end %>
|
|
||||||
<%= yield %>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
<%# Phrase mise en valeur, en serif, avec un trait orange.
|
|
||||||
render "charte/exergue", texte: "Une phrase à mettre en valeur." %>
|
|
||||||
<%# locals: (texte:) -%>
|
|
||||||
<%= tag.p texte, class: "exergue" %>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<%# Message d'erreur de formulaire (trait rouge sur fond rosé). Rien n'est affiché sans message.
|
|
||||||
render "charte/formulaire_erreur", messages: @day_log.errors.full_messages
|
|
||||||
render "charte/formulaire_erreur", messages: flash[:alert] %>
|
|
||||||
<%# locals: (messages:) -%>
|
|
||||||
<% messages = Array(messages).compact_blank %>
|
|
||||||
<% if messages.any? %>
|
|
||||||
<p class="formulaire-erreur" role="alert"><%= safe_join(messages, tag.br) %></p>
|
|
||||||
<% end %>
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
<%# Précision discrète, en fin de page.
|
|
||||||
render "charte/note", texte: "Une précision." %>
|
|
||||||
<%# locals: (texte:) -%>
|
|
||||||
<%= tag.p texte, class: "note" %>
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<%# Photo arrondie avec légende facultative. Toujours un alt descriptif, et width/height
|
|
||||||
pour éviter les sauts de mise en page (format du site : 1200 × 900).
|
|
||||||
render "charte/photo", src: "croisiere.jpg", alt: "…", width: 1200, height: 900, legende: "…" %>
|
|
||||||
<%# locals: (src:, alt:, width: nil, height: nil, legende: nil) -%>
|
|
||||||
<figure class="photo">
|
|
||||||
<%= image_tag src, alt: alt, width: width, height: height, loading: "lazy", decoding: "async" %>
|
|
||||||
<% if legende %>
|
|
||||||
<figcaption><%= legende %></figcaption>
|
|
||||||
<% end %>
|
|
||||||
</figure>
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<%# Emplacement de photo, en attendant l'image : cadre 3:2 en pointillés chêne.
|
|
||||||
render "charte/photo_attente", description: "La Kluk posée sur la table du petit-déjeuner." %>
|
|
||||||
<%# locals: (description:, titre: nil, legende: nil) -%>
|
|
||||||
<figure class="photo">
|
|
||||||
<div class="photo-attente">
|
|
||||||
<svg viewBox="0 0 40 32" aria-hidden="true" focusable="false"><path d="M3 3 L6 25 Q6.5 29 10.5 29 L24.5 29 Q28.5 29 29 25 L32 3 Z M31.5 8 C39 8 39.5 21 30 22" /></svg>
|
|
||||||
<p><strong><%= titre || "Photo à venir" %></strong><br><%= description %></p>
|
|
||||||
</div>
|
|
||||||
<% if legende %>
|
|
||||||
<figcaption><%= legende %></figcaption>
|
|
||||||
<% end %>
|
|
||||||
</figure>
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<%# Illustration « table et tasses », en SVG dans la page pour suivre les variables CSS.
|
|
||||||
Une seule par page (identifiants #tasse et #tasses-titre).
|
|
||||||
render "charte/tasses" %>
|
|
||||||
<%# locals: () -%>
|
|
||||||
<figure class="tasses">
|
|
||||||
<svg viewBox="0 0 580 214" role="img" aria-labelledby="tasses-titre">
|
|
||||||
<title id="tasses-titre">Illustration : trois tasses de couleur alignées sur la table, et une quatrième posée sur la Kluk, fumante.</title>
|
|
||||||
<defs>
|
|
||||||
<g id="tasse">
|
|
||||||
<path class="tasse-anse" d="M63 22 C86 20 88 60 60 62" />
|
|
||||||
<path class="tasse-corps" d="M3 6 L10 78 Q11 86 20 86 L48 86 Q57 86 58 78 L65 6 Z" />
|
|
||||||
<path class="tasse-reflet" d="M13 18 L17 62" />
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
<rect class="table" x="0" y="200" width="580" height="14" rx="3" />
|
|
||||||
<path class="table-fil" d="M0 200 H580" />
|
|
||||||
<use href="#tasse" x="34" y="114" class="humeur-gris" />
|
|
||||||
<use href="#tasse" x="132" y="114" class="humeur-orange" />
|
|
||||||
<use href="#tasse" x="230" y="114" class="humeur-rouge" />
|
|
||||||
<g class="kluk-illu" transform="translate(410 98)">
|
|
||||||
<g class="vapeur" aria-hidden="true">
|
|
||||||
<path d="M22 -6 c-9 -11 9 -20 0 -31 c-9 -11 9 -20 0 -31" />
|
|
||||||
<path d="M44 -10 c-9 -11 9 -20 0 -31 c-9 -11 9 -20 0 -31" />
|
|
||||||
</g>
|
|
||||||
<use href="#tasse" x="0" y="0" class="humeur-vert" />
|
|
||||||
<rect class="kluk-socle" x="-22" y="86" width="112" height="16" rx="8" />
|
|
||||||
<circle class="kluk-voyant" cx="34" cy="94" r="2.6" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</figure>
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
<% content_for :title, "Charte Kluk" %>
|
|
||||||
<article class="texte">
|
|
||||||
<h1>Charte graphique Kluk</h1>
|
|
||||||
<p><em>Les composants de la charte, rendus par les partials de app/views/charte.</em></p>
|
|
||||||
|
|
||||||
<p>Texte courant en Atkinson Hyperlegible Next, avec <a href="#">un lien souligné de vert</a>, du <strong>gras</strong>, de l’<em>italique</em> et du <code>code</code>.</p>
|
|
||||||
|
|
||||||
<p><%= render "charte/bouton", texte: "Bouton principal", href: "#" %></p>
|
|
||||||
<p><%= render "charte/bouton", texte: "Petit bouton", href: "#", petit: true %></p>
|
|
||||||
|
|
||||||
<%= render "charte/tasses" %>
|
|
||||||
|
|
||||||
<h2>Intertitre de section</h2>
|
|
||||||
<p>Les intertitres ménagent un grand espace au-dessus d’eux pour découper la page en respirations.</p>
|
|
||||||
|
|
||||||
<%= render "charte/exergue", texte: "Une phrase mise en exergue, en serif, soulignée d’un trait orange." %>
|
|
||||||
|
|
||||||
<%= render "charte/photo", src: "croisiere.jpg", alt: "Photo d’exemple", legende: "Légende facultative sous la photo." %>
|
|
||||||
<%= render "charte/photo_attente", description: "Description de la photo attendue." %>
|
|
||||||
|
|
||||||
<%= render "charte/calendrier", jours: "vvovvvg vvvvovv ovvrvvv vvgvvvo ovvvvvv rrovrrr rorrvvo gvggggg gggvggg gogggvg ggoggvg vvgvvvv ovvvovv vrvvvov vvvvovv ovvgvvv vvovvrv vvvovvv vgvvvvo vvvvovv" %>
|
|
||||||
|
|
||||||
<%= form_with url: "#", class: "formulaire", data: { turbo: false } do |f| %>
|
|
||||||
<%= render "charte/champ", form: f, attribut: :email, libelle: "Votre adresse mail", precision: "(obligatoire)" do %>
|
|
||||||
<%= f.email_field :email, autocomplete: "email" %>
|
|
||||||
<% end %>
|
|
||||||
<%= render "charte/champ", form: f, attribut: :mot_de_passe, libelle: "Mot de passe" do %>
|
|
||||||
<%= f.password_field :mot_de_passe %>
|
|
||||||
<% end %>
|
|
||||||
<%= render "charte/champ", form: f, attribut: :message, libelle: "Un mot, une idée, une question ?", precision: "(facultatif)" do %>
|
|
||||||
<%= f.text_area :message, rows: 5 %>
|
|
||||||
<% end %>
|
|
||||||
<%= render "charte/formulaire_erreur", messages: ["Message d’erreur du formulaire."] %>
|
|
||||||
<%= f.submit "Envoyer", class: "bouton" %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= render "charte/note", texte: "Une note discrète en fin de page." %>
|
|
||||||
</article>
|
|
||||||
20
app/views/day_logs/_form.html.haml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
= form_with model: day_log do |f|
|
||||||
|
- @day_log.errors.full_messages.each do |msg|
|
||||||
|
%p= msg
|
||||||
|
|
||||||
|
.field
|
||||||
|
= f.label :day
|
||||||
|
= f.date_field :day
|
||||||
|
|
||||||
|
.field
|
||||||
|
= f.label :info
|
||||||
|
= f.text_area :info
|
||||||
|
|
||||||
|
.field
|
||||||
|
= label_tag :mode_id, "Mode"
|
||||||
|
= select_tag "day_log[mode_id]",
|
||||||
|
options_from_collection_for_select(Current.user.modes, :id, :label, @mood&.mode_id),
|
||||||
|
include_blank: true
|
||||||
|
|
||||||
|
= f.submit
|
||||||
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
<% titre = @day_log.day ? date_longue(@day_log.day).capitalize : "Journal" %>
|
|
||||||
<% content_for :title, "#{titre} – Kluk" %>
|
|
||||||
<% mode_id = params.dig(:day_log, :mode_id).presence&.to_i || @mood&.mode_id %>
|
|
||||||
<% mode = Current.user.modes.find { |m| m.id == mode_id } %>
|
|
||||||
|
|
||||||
<div class="page-jour" data-controller="choix-mode">
|
|
||||||
<header class="page-jour-entete">
|
|
||||||
<p class="retour"><%= link_to "Retour à mes humeurs", dashboard_path(day: @day_log.day) %></p>
|
|
||||||
<h1 class="page-jour-titre"><%= titre %></h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<figure class="carte-humeur" data-choix-mode-target="carte" style="<%= "--humeur: #{mode.color}" if mode %>">
|
|
||||||
<%= image_tag (mode || Mode.new).image_url, alt: "", data: { choix_mode_target: "image" } %>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<%= form_with model: @day_log, class: "formulaire" do |f| %>
|
|
||||||
<%= render "charte/formulaire_erreur", messages: @day_log.errors.full_messages %>
|
|
||||||
<%= f.hidden_field :day %>
|
|
||||||
|
|
||||||
<fieldset class="champ">
|
|
||||||
<legend>Mode du jour</legend>
|
|
||||||
<ul class="choix-modes">
|
|
||||||
<% Current.user.modes.each do |m| %>
|
|
||||||
<li>
|
|
||||||
<%= radio_button_tag "day_log[mode_id]", m.id, m.id == mode_id,
|
|
||||||
data: { image: m.image_url, couleur: m.color, action: "choix-mode#changer" } %>
|
|
||||||
<%= label_tag "day_log_mode_id_#{m.id}" do %>
|
|
||||||
<span class="pastille" style="--humeur: <%= m.color %>"></span><%= m.label %>
|
|
||||||
<% end %>
|
|
||||||
</li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<%= render "charte/champ", form: f, attribut: :info, libelle: "Journal" do %>
|
|
||||||
<%= f.text_area :info, rows: 8, placeholder: "Ce qui a compté ce jour-là…" %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<%= f.submit "Enregistrer", class: "bouton" %>
|
|
||||||
<%= link_to "Annuler", dashboard_path(day: @day_log.day) %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
3
app/views/day_logs/edit.html.haml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
%h1 Modifier le journal
|
||||||
|
|
||||||
|
= render 'form', day_log: @day_log
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<%= render template: "day_logs/edit" %>
|
|
||||||
3
app/views/day_logs/new.html.haml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
%h1 Nouveau journal
|
||||||
|
|
||||||
|
= render 'form', day_log: @day_log
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
%article.texte
|
%h1 Comment ça KLUK ?
|
||||||
%h1 Comment ça KLUK ?
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,24 @@
|
||||||
<% content_for :title, "Bienvenue – Kluk" %>
|
<div>
|
||||||
<article class="texte texte-etroit">
|
<h1>Set up your password</h1>
|
||||||
<%= render "charte/tasses" %>
|
<p>Please set your password to activate your account.</p>
|
||||||
<h1>Bienvenue</h1>
|
|
||||||
<p>Choisissez un mot de passe pour activer votre compte et retrouver les humeurs de votre Kluk.</p>
|
|
||||||
|
|
||||||
<%= form_with model: @user, url: invitation_path(token: params[:token]), method: :patch, class: "formulaire" do |form| %>
|
<%= form_with model: @user, url: invitation_path(token: params[:token]), method: :patch do |form| %>
|
||||||
<%= render "charte/champ", form: form, attribut: :password, libelle: "Mot de passe" do %>
|
<div>
|
||||||
<%= form.password_field :password, required: true, autocomplete: "new-password", maxlength: 72 %>
|
<%= form.password_field :password,
|
||||||
<% end %>
|
required: true,
|
||||||
<%= render "charte/champ", form: form, attribut: :password_confirmation, libelle: "Le même, une seconde fois" do %>
|
autocomplete: "new-password",
|
||||||
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", maxlength: 72 %>
|
placeholder: "Enter new password" %>
|
||||||
<% end %>
|
</div>
|
||||||
<%= form.submit "Activer mon compte", class: "bouton" %>
|
|
||||||
|
<div>
|
||||||
|
<%= form.password_field :password_confirmation,
|
||||||
|
required: true,
|
||||||
|
autocomplete: "new-password",
|
||||||
|
placeholder: "Repeat new password" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= form.submit "Activate Account" %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</article>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,11 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title><%= content_for(:title) || "Kluk" %></title>
|
<title><%= content_for(:title) || "Moodie" %></title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<meta name="theme-color" content="#F5F6F0">
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<% titre_partage = content_for(:title) || "Comment ça Kluk ?" %>
|
|
||||||
<% description = content_for(:description) || "Pas besoin de demander comment ça va : il suffit de regarder la tasse." %>
|
|
||||||
<meta name="description" content="<%= description %>">
|
|
||||||
<meta property="og:type" content="website">
|
|
||||||
<meta property="og:site_name" content="Kluk">
|
|
||||||
<meta property="og:locale" content="fr_FR">
|
|
||||||
<meta property="og:title" content="<%= titre_partage %>">
|
|
||||||
<meta property="og:description" content="<%= description %>">
|
|
||||||
<meta property="og:url" content="<%= request.base_url + request.path %>">
|
|
||||||
<meta property="og:image" content="<%= image_url "charte/partage.png" %>">
|
|
||||||
<meta property="og:image:width" content="1200">
|
|
||||||
<meta property="og:image:height" content="630">
|
|
||||||
<meta property="og:image:alt" content="Trois tasses de couleur alignées sur une table, et une quatrième posée sur la Kluk, fumante.">
|
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
|
|
@ -29,11 +14,15 @@
|
||||||
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
||||||
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||||
|
|
||||||
<%= favicon_link_tag "charte/favicon.svg", type: "image/svg+xml" %>
|
<link rel="icon" href="/icon.png" type="image/png">
|
||||||
|
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||||
<link rel="apple-touch-icon" href="/icon.png">
|
<link rel="apple-touch-icon" href="/icon.png">
|
||||||
|
|
||||||
<%= preload_link_tag "young-serif-400.woff2" %>
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<%= preload_link_tag "atkinson-hyperlegible-next-400.woff2" %>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Delius&family=Sour+Gummy:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="https://kit.fontawesome.com/079b06296f.js" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||||
|
|
@ -41,12 +30,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<a class="evitement" href="#contenu">Aller au contenu</a>
|
<%= render 'menu' %>
|
||||||
<%= render "entete" %>
|
<%= yield %>
|
||||||
<main id="contenu" tabindex="-1">
|
|
||||||
<%= render "flash" %>
|
|
||||||
<%= yield %>
|
|
||||||
</main>
|
|
||||||
<%= render "pied" %>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<%# Calendrier des humeurs « à la GitHub » : une colonne par semaine, une ligne par jour,
|
|
||||||
regroupé par mois. Par défaut, un bloc par année, la plus récente d'abord ;
|
|
||||||
avec par_annee: false, tous les mois à la suite (l'année est ajoutée aux mois d'une autre année). %>
|
|
||||||
<%# locals: (history:, user:, interactive: false, journal: false, selected_date: nil, par_annee: true) -%>
|
|
||||||
<% blocs = par_annee ? history_by_year(history) : [ [ nil, history ] ] %>
|
|
||||||
<div class="calendrier-humeurs">
|
|
||||||
<% blocs.each do |year, months| %>
|
|
||||||
<%= tag.section class: "annee", "aria-labelledby": ("annee-#{year}" if year) do %>
|
|
||||||
<% if year %>
|
|
||||||
<h2 class="annee-titre" id="annee-<%= year %>"><%= year %></h2>
|
|
||||||
<% end %>
|
|
||||||
<div class="annee-mois">
|
|
||||||
<div class="semaine-jours" aria-hidden="true">
|
|
||||||
<span>lun.</span><span></span><span>mer.</span><span></span><span>ven.</span><span></span><span></span>
|
|
||||||
</div>
|
|
||||||
<% months.each do |month| %>
|
|
||||||
<div class="mois">
|
|
||||||
<h3 class="mois-nom"><%= l(month[:month], format: year || month[:month].year == Date.current.year ? "%B" : "%B %Y") %></h3>
|
|
||||||
<div class="mois-jours">
|
|
||||||
<% month[:weeks].flatten.each do |mood| %>
|
|
||||||
<%= case_jour(mood, user, interactive: interactive, journal: journal,
|
|
||||||
selected: mood[:recorded_at].to_date == selected_date) %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<%# locals: (user:, journal: false) -%>
|
|
||||||
<div class="legende">
|
|
||||||
<ul class="legende-modes" aria-label="Les modes">
|
|
||||||
<% user.modes.each do |mode| %>
|
|
||||||
<li><span class="pastille" style="--humeur: <%= mode.color %>"></span><%= mode.label %></li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
<ul class="legende-etats" aria-label="Les autres cases">
|
|
||||||
<% if user.guess? %>
|
|
||||||
<li><span class="pastille pastille-supposee"></span>Mode supposé, pas de tasse posée ce jour-là</li>
|
|
||||||
<% else %>
|
|
||||||
<li><span class="pastille pastille-inconnue"></span>Pas de tasse posée</li>
|
|
||||||
<% end %>
|
|
||||||
<% if journal %>
|
|
||||||
<li><span class="pastille pastille-journal"></span>Journal écrit</li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,31 +1,52 @@
|
||||||
<% content_for :title, "Mes humeurs – Kluk" %>
|
<main data-controller="mood" class="columns m-auto">
|
||||||
<% history = @user.history %>
|
<div class="current-day column is-hidden-mobile">
|
||||||
<% selected_date = (Date.parse(params[:day]) rescue Date.current) %>
|
<section class="m-4">
|
||||||
<% selected = find_day(history, selected_date) || find_day(history, selected_date = Date.current) %>
|
<figure class="image has-ratio">
|
||||||
<% status = day_status(selected, @user) if selected %>
|
<%= image_tag(@user.current_mode.image, "data-mood-target": "image") %>
|
||||||
<% mode = mode_for(selected, @user) if selected %>
|
</figure>
|
||||||
|
</section>
|
||||||
<div class="tableau" data-controller="calendrier">
|
|
||||||
<div class="tableau-calendrier">
|
|
||||||
<h1 class="tableau-titre">Mes humeurs</h1>
|
|
||||||
<%= render "moods/legende", user: @user, journal: true %>
|
|
||||||
<%= render "moods/calendrier", history: history, user: @user, interactive: true, journal: true, selected_date: selected_date %>
|
|
||||||
</div>
|
</div>
|
||||||
|
<section class="section column has-background-primary-light is-flex is-flex-direction-column is-justify-content-space-around">
|
||||||
|
<div class="tracker">
|
||||||
|
<div class="legend is-flex is-flex-wrap-wrap mb-5">
|
||||||
|
<div class="is-flex is-align-items-center mr-4">
|
||||||
|
<div class=""><strong>Légende</strong></div>
|
||||||
|
</div>
|
||||||
|
<% @user.modes.each do |mode| %>
|
||||||
|
<div class="is-flex is-align-items-center mr-4">
|
||||||
|
<div class="bar mr-1" style="<%= style_for_mode(mode) %>"></div>
|
||||||
|
<div class=""><%= mode.label %></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="container mb-4 p-3 has-background-white day-info">
|
||||||
|
<div class="title is-4">
|
||||||
|
<span class="icon"><i class="fa-regular fa-calendar"></i></span>
|
||||||
|
<span data-mood-target="modeDay">Aujourd'hui : <%= @user.current_mode.label %></span>
|
||||||
|
<%= link_to "Journal", edit_day_log_for_day_path(day: Date.today), "data-mood-target": "dayLogLink" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<aside class="detail" aria-live="polite" aria-label="Jour sélectionné">
|
<div class="logs">
|
||||||
<figure class="carte-humeur" data-calendrier-target="carte anime" style="<%= "--humeur: #{mode.color}" if mode %>">
|
<div class="is-flex is-flex-direction-row is-flex-wrap-wrap mb-3">
|
||||||
<%= image_tag (mode || Mode.new).image_url, alt: "", data: { calendrier_target: "image" } %>
|
<% @user.history.each do |month| %>
|
||||||
</figure>
|
<div class="mr-3">
|
||||||
<div class="detail-texte" data-calendrier-target="anime">
|
<div> <%= I18n.l(month[:month], format: "%b %y").capitalize %></div>
|
||||||
<h2 class="detail-date" data-calendrier-target="date"><%= date_longue(selected_date) %></h2>
|
<div class="is-flex is-flex-wrap-wrap">
|
||||||
<p class="detail-mode">
|
<% month[:weeks].each do |week| %>
|
||||||
<span data-calendrier-target="mode"><%= mode&.label || "Pas de tasse posée" %></span>
|
<div class="is-flex is-flex-direction-column is-flex-wrap-wrap mb-3">
|
||||||
<span class="detail-statut" data-calendrier-target="statut"><%= libelle_statut(status) if mode %></span>
|
<% week.each do |mood| %>
|
||||||
</p>
|
<% mode = mode_for(mood, @user) %>
|
||||||
<% info = selected&.dig(:day_log)&.info %>
|
<div data-image="<%= mode[:image_url] %>" data-mode="<%= mode[:label] %>" data-day="<%= l mood[:recorded_at].to_date %>" data-action="click->mood#updateDayInfo" title="<%= l(mood[:recorded_at].to_date) %> : <%= mode[:label] %>" class="day" style="<%= style_for_mode(mode) %>"></div>
|
||||||
<p class="detail-journal <%= "detail-journal-vide" unless info %>" data-calendrier-target="journal" data-vide="Rien d’écrit ce jour-là."><%= info || "Rien d’écrit ce jour-là." %></p>
|
<% end %>
|
||||||
<%= link_to info ? "Modifier le journal" : "Écrire dans le journal", edit_day_log_for_day_path(day: selected_date),
|
</div>
|
||||||
class: "bouton bouton-petit", data: { calendrier_target: "lien" } %>
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div id="end"></>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</section>
|
||||||
</div>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
<% content_for :title, "Comment ça Kluk chez #{@user.username} ?" %>
|
|
||||||
<% content_for :description, "Les humeurs #{de_nom(@user.username)}, jour après jour, au rythme des tasses posées sur la Kluk." %>
|
|
||||||
<% history = MoodCalendarService.generate_calendar(@user, start_date: 5.months.ago.beginning_of_month, end_date: Date.current) %>
|
|
||||||
<% selected_date = (Date.parse(params[:day]) rescue Date.current) %>
|
|
||||||
<% selected = find_day(history, selected_date) if selected_date < Date.current %>
|
|
||||||
<% selected_date = Date.current unless selected %>
|
|
||||||
<% aujourdhui = selected_date == Date.current %>
|
|
||||||
<% mode_du_jour = @user.moods.last&.mode %>
|
|
||||||
<% mode = aujourdhui ? mode_du_jour : mode_for(selected, @user) %>
|
|
||||||
<% status = day_status(selected, @user) unless aujourdhui %>
|
|
||||||
<% since = @user.current_mode_since %>
|
|
||||||
|
|
||||||
<article class="public" data-controller="calendrier"
|
|
||||||
data-calendrier-aujourdhui-value="<%= Date.current.iso8601 %>"
|
|
||||||
data-calendrier-aujourdhui-date-value="Aujourd’hui, <%= l(Date.current, format: "%A %-d %B") %>"
|
|
||||||
data-calendrier-aujourdhui-mode-value="<%= mode_du_jour&.label || "Pas de tasse posée" %>"
|
|
||||||
data-calendrier-aujourdhui-image-value="<%= (mode_du_jour || Mode.new).image_url %>"
|
|
||||||
data-calendrier-aujourdhui-couleur-value="<%= mode_du_jour&.color %>">
|
|
||||||
<h1 class="public-nom">Les humeurs <%= de_nom(@user.username) %></h1>
|
|
||||||
|
|
||||||
<figure class="carte-humeur carte-humeur-grande" data-calendrier-target="carte anime" style="<%= "--humeur: #{mode.color}" if mode %>">
|
|
||||||
<%= image_tag (mode || Mode.new).image_url, alt: "", data: { calendrier_target: "image" } %>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<div class="public-texte">
|
|
||||||
<div class="public-jour" data-calendrier-target="anime" aria-live="polite">
|
|
||||||
<p class="public-date" data-calendrier-target="date"><%= aujourdhui ? "Aujourd’hui, #{l(Date.current, format: "%A %-d %B")}" : date_longue(selected_date) %></p>
|
|
||||||
<p class="public-mode" data-calendrier-target="mode"><%= mode&.label || "Pas de tasse posée" %></p>
|
|
||||||
<p class="public-depuis">
|
|
||||||
<span data-calendrier-target="depuis" <%= "hidden" unless aujourdhui %>>
|
|
||||||
<% if since == Date.current %>
|
|
||||||
Depuis ce matin.
|
|
||||||
<% elsif since %>
|
|
||||||
Depuis le <%= l(since, format: since.year == Date.current.year ? "%A %-d %B" : "%-d %B %Y") %>.
|
|
||||||
<% end %>
|
|
||||||
</span>
|
|
||||||
<span class="public-autre-jour" data-calendrier-target="retour" <%= "hidden" if aujourdhui %>>
|
|
||||||
<span class="detail-statut" data-calendrier-target="statut"><%= libelle_statut(status) if mode %></span>
|
|
||||||
<button type="button" class="lien" data-action="calendrier#revenir">Revenir à aujourd’hui</button>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="public-historique" aria-labelledby="public-historique-titre">
|
|
||||||
<h2 id="public-historique-titre">Les derniers mois</h2>
|
|
||||||
<%= render "moods/legende", user: @user %>
|
|
||||||
<%= render "moods/calendrier", history: history, user: @user, par_annee: false,
|
|
||||||
interactive: true, selected_date: selected_date %>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
<% content_for :title, "Nouveau mot de passe – Kluk" %>
|
<h1>Update your password</h1>
|
||||||
<article class="texte texte-etroit">
|
|
||||||
<h1>Nouveau mot de passe</h1>
|
|
||||||
|
|
||||||
<%= form_with url: password_path(params[:token]), method: :put, class: "formulaire" do |form| %>
|
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
||||||
<%= render "charte/champ", form: form, attribut: :password, libelle: "Nouveau mot de passe" do %>
|
|
||||||
<%= form.password_field :password, required: true, autocomplete: "new-password", maxlength: 72 %>
|
<%= form_with url: password_path(params[:token]), method: :put do |form| %>
|
||||||
<% end %>
|
<%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %><br>
|
||||||
<%= render "charte/champ", form: form, attribut: :password_confirmation, libelle: "Le même, une seconde fois" do %>
|
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %><br>
|
||||||
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", maxlength: 72 %>
|
<%= form.submit "Save" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= form.submit "Enregistrer le mot de passe", class: "bouton" %>
|
|
||||||
<% end %>
|
|
||||||
</article>
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
<% content_for :title, "Mot de passe oublié – Kluk" %>
|
<h1>Forgot your password?</h1>
|
||||||
<article class="texte texte-etroit">
|
|
||||||
<h1>Mot de passe oublié</h1>
|
|
||||||
<p>Indiquez l’adresse mail de votre compte : vous recevrez un lien pour choisir un nouveau mot de passe.</p>
|
|
||||||
|
|
||||||
<%= form_with url: passwords_path, class: "formulaire" do |form| %>
|
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
||||||
<%= render "charte/champ", form: form, attribut: :email_address, libelle: "Adresse mail" do %>
|
|
||||||
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", value: params[:email_address] %>
|
<%= form_with url: passwords_path do |form| %>
|
||||||
<% end %>
|
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
|
||||||
<%= form.submit "Recevoir le lien", class: "bouton" %>
|
<%= form.submit "Email reset instructions" %>
|
||||||
<p class="formulaire-secours"><%= link_to "Retour à la connexion", new_session_path %></p>
|
<% end %>
|
||||||
<% end %>
|
|
||||||
</article>
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<% content_for :title, "Connexion – Kluk" %>
|
|
||||||
<article class="texte texte-etroit">
|
|
||||||
<%= render "charte/tasses" %>
|
|
||||||
<h1>Connexion</h1>
|
|
||||||
|
|
||||||
<%= form_with url: session_path, class: "formulaire" do |form| %>
|
|
||||||
<%= render "charte/champ", form: form, attribut: :email_address, libelle: "Adresse mail" do %>
|
|
||||||
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", value: params[:email_address] %>
|
|
||||||
<% end %>
|
|
||||||
<%= render "charte/champ", form: form, attribut: :password, libelle: "Mot de passe" do %>
|
|
||||||
<%= form.password_field :password, required: true, autocomplete: "current-password", maxlength: 72 %>
|
|
||||||
<% end %>
|
|
||||||
<%= form.submit "Se connecter", class: "bouton" %>
|
|
||||||
<p class="formulaire-secours"><%= link_to "Mot de passe oublié ?", new_password_path %></p>
|
|
||||||
<% end %>
|
|
||||||
</article>
|
|
||||||
22
app/views/sessions/new.html.haml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
%main.columns.m-auto{"data-controller" => "mood"}
|
||||||
|
.column.is-hidden-mobile
|
||||||
|
%section.m-4
|
||||||
|
%figure.image.has-ratio
|
||||||
|
= image_tag("croisiere.jpg")
|
||||||
|
%section.section.column.has-background-primary-light.is-flex.is-flex-direction-column.is-justify-content-center
|
||||||
|
%h1.title Connexion au tableau de bord de la KLUK
|
||||||
|
.flash.mb-2
|
||||||
|
.has-text-danger= flash[:alert] if flash[:alert]
|
||||||
|
.has-text-info= flash[:notice] if flash[:notice]
|
||||||
|
= form_with url: session_path do |form|
|
||||||
|
.field
|
||||||
|
= form.label :email_address, "Email"
|
||||||
|
.control
|
||||||
|
= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Entrez votre email", value: params[:email_address], class: "input"
|
||||||
|
.field
|
||||||
|
= form.label :password, "Mot de passe"
|
||||||
|
.control
|
||||||
|
= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Entrez votre mot de passe", maxlength: 72, class: "input"
|
||||||
|
.field
|
||||||
|
.control
|
||||||
|
%button.button.is-link Se connecter
|
||||||
|
|
@ -1,389 +0,0 @@
|
||||||
# Charte graphique Kluk
|
|
||||||
|
|
||||||
Charte extraite du site kluk.fr (`assets/css/style.scss`, `_layouts/`, `_includes/`).
|
|
||||||
Tout ce qu'il faut pour la réutiliser ailleurs se trouve dans le dossier [`charte/`](charte/) :
|
|
||||||
|
|
||||||
```
|
|
||||||
charte/
|
|
||||||
├── css/kluk.css Feuille de style complète, en CSS pur (sans Sass)
|
|
||||||
├── fonts/ Young Serif, Atkinson Hyperlegible Next (woff2) + licence OFL
|
|
||||||
├── img/
|
|
||||||
│ ├── favicon.svg Icône du site (tasse verte)
|
|
||||||
│ ├── illustration-tasses.svg Illustration « table et tasses », autonome (styles intégrés)
|
|
||||||
│ ├── tasse-{gris,orange,vert,rouge}.svg Une tasse seule, dans chaque couleur d'humeur
|
|
||||||
│ └── photo-attente.svg Pictogramme de l'emplacement de photo
|
|
||||||
└── exemple.html Page de démonstration de tous les composants
|
|
||||||
```
|
|
||||||
|
|
||||||
Pour démarrer : copier `charte/` dans le projet, puis dans le `<head>` :
|
|
||||||
|
|
||||||
```html
|
|
||||||
<meta name="theme-color" content="#F5F6F0">
|
|
||||||
<link rel="preload" href="charte/fonts/young-serif-400.woff2" as="font" type="font/woff2" crossorigin>
|
|
||||||
<link rel="preload" href="charte/fonts/atkinson-hyperlegible-next-400.woff2" as="font" type="font/woff2" crossorigin>
|
|
||||||
<link rel="stylesheet" href="charte/css/kluk.css">
|
|
||||||
<link rel="icon" href="charte/img/favicon.svg" type="image/svg+xml">
|
|
||||||
```
|
|
||||||
|
|
||||||
Les chemins des polices dans `kluk.css` sont relatifs (`../fonts/`) : garder la structure `css/` + `fonts/` côte à côte.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Esprit
|
|
||||||
|
|
||||||
Un univers de petit-déjeuner, calme et chaleureux : **la porcelaine du matin** (fond), **l'encre du thé** (texte), **le chêne de la table** (pied de page, illustration), et **les quatre couleurs des tasses** (gris, orange, vert, rouge) comme accents, qui sont aussi les quatre « humeurs ».
|
|
||||||
|
|
||||||
Principes :
|
|
||||||
|
|
||||||
- une seule colonne de lecture étroite (36 rem), beaucoup d'air ;
|
|
||||||
- des titres en serif ronde et amicale, un texte courant très lisible ;
|
|
||||||
- des formes arrondies (boutons en pilule, cartes arrondies), des traits d'encre épais façon dessin ;
|
|
||||||
- l'accessibilité d'abord : contrastes élevés, focus très visible, lien d'évitement, animations coupées si l'utilisateur le demande.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Couleurs
|
|
||||||
|
|
||||||
Toutes les couleurs sont des variables CSS déclarées sur `:root`.
|
|
||||||
|
|
||||||
### Fonds et texte
|
|
||||||
|
|
||||||
| Variable | Valeur | Rôle |
|
|
||||||
|---|---|---|
|
|
||||||
| `--porcelaine` | `#F5F6F0` | Fond de page, texte sur fond encre, `theme-color` |
|
|
||||||
| `--porcelaine-fonce` | `#E9ECE2` | Fond de `code`, emplacement de photo, liseré des cartes |
|
|
||||||
| `--encre` | `#2E2A26` | Texte, bordures, fond des boutons |
|
|
||||||
| `--encre-douce` | `#5A534C` | Texte secondaire : chapeau, notes, légendes, précisions |
|
|
||||||
| `--chene` | `#D9BC94` | Pied de page, plateau de la table (illustration) |
|
|
||||||
| `--chene-fonce` | `#B48D5F` | Socle de la Kluk, pointillés et picto de l'emplacement photo |
|
|
||||||
|
|
||||||
### Humeurs (accents)
|
|
||||||
|
|
||||||
| Variable | Valeur | Rôle |
|
|
||||||
|---|---|---|
|
|
||||||
| `--gris` | `#8D97A8` | Humeur grise |
|
|
||||||
| `--orange` | `#EDA33D` | Humeur orange, trait de l'exergue |
|
|
||||||
| `--vert` | `#5F9B59` | Humeur verte, soulignement des liens, focus des champs, favicon |
|
|
||||||
| `--rouge` | `#D8573F` | Humeur rouge, trait des messages d'erreur |
|
|
||||||
| `--vert-lien` | `#3F7A3B` | Vert foncé pour les cases à cocher (`accent-color`) |
|
|
||||||
|
|
||||||
### Teintes utilitaires
|
|
||||||
|
|
||||||
Écrites en dur dans le site d'origine ; `kluk.css` les expose en variables, avec les mêmes valeurs.
|
|
||||||
|
|
||||||
| Variable | Valeur | Rôle |
|
|
||||||
|---|---|---|
|
|
||||||
| `--encre-survol` | `#45403A` | Fond des boutons au survol |
|
|
||||||
| `--surface` | `#FBFCF8` | Fond des cartes (formulaire, calendrier) |
|
|
||||||
| `--bordure-champ` | `#8A8279` | Bordure des champs de saisie |
|
|
||||||
| `--erreur-fond` | `#FBE9E5` | Fond des messages d'erreur |
|
|
||||||
| `--blanc` | `#fff` | Fond des champs, reflet des tasses, fond à l'impression |
|
|
||||||
|
|
||||||
### Contrastes (WCAG)
|
|
||||||
|
|
||||||
| Couple | Ratio | Usage autorisé |
|
|
||||||
|---|---|---|
|
|
||||||
| encre / porcelaine | 13,1 : 1 | tout texte |
|
|
||||||
| encre-douce / porcelaine | 7,0 : 1 | tout texte |
|
|
||||||
| encre / chêne | 7,8 : 1 | tout texte (pied de page) |
|
|
||||||
| porcelaine / encre-survol | 9,4 : 1 | texte des boutons au survol |
|
|
||||||
| vert-lien / porcelaine | 4,8 : 1 | texte possible |
|
|
||||||
| vert / porcelaine | 3,1 : 1 | éléments graphiques seulement (soulignés, contours) |
|
|
||||||
| rouge / porcelaine | 3,6 : 1 | éléments graphiques seulement |
|
|
||||||
| gris / porcelaine | 2,7 : 1 | décoratif seulement |
|
|
||||||
| orange / porcelaine | 2,0 : 1 | décoratif seulement |
|
|
||||||
|
|
||||||
Règle : **le texte est toujours en encre ou encre douce**. Les couleurs d'humeur ne servent qu'aux traits, soulignés, pastilles et illustrations, jamais au texte.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Typographies
|
|
||||||
|
|
||||||
| Variable | Pile | Usage |
|
|
||||||
|---|---|---|
|
|
||||||
| `--serif` | `"Young Serif", Georgia, serif` | Titres `h1`–`h3`, exergue, phrase du pied de page |
|
|
||||||
| `--sans` | `"Atkinson Hyperlegible Next", "Atkinson Hyperlegible", system-ui, sans-serif` | Tout le reste, y compris le logo |
|
|
||||||
|
|
||||||
Fichiers fournis (`charte/fonts/`, licence SIL OFL 1.1 dans `OFL.txt`) :
|
|
||||||
|
|
||||||
- `young-serif-400.woff2` : Young Serif, une seule graisse (400) ;
|
|
||||||
- `atkinson-hyperlegible-next-400.woff2` : Atkinson Hyperlegible Next **variable**, graisses 200 à 800 ;
|
|
||||||
- `atkinson-hyperlegible-next-400-italic.woff2` : italique 400.
|
|
||||||
|
|
||||||
Graisses utilisées : 400 (texte), 700 (boutons, libellés, `strong`), 800 (logo). Les titres restent en 400 : c'est la serif qui porte le caractère.
|
|
||||||
|
|
||||||
### Échelle
|
|
||||||
|
|
||||||
| Élément | Taille | Interlignage | Remarques |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Corps (`body`) | `1.1875rem` (19 px) | 1.65 | `font-kerning: normal` |
|
|
||||||
| `h1` | `clamp(2.35rem, 1.6rem + 3.6vw, 3.75rem)` | 1.15 | serif, `text-wrap: balance` |
|
|
||||||
| `h2` | `clamp(1.6rem, 1.35rem + 1vw, 2rem)` | 1.15 | serif |
|
|
||||||
| Chapeau | `1.3rem` | 1.55 | italique, encre douce |
|
|
||||||
| Exergue | `1.45rem` | 1.4 | serif |
|
|
||||||
| Logo | `1.35rem` | 1.2 | sans 800, `letter-spacing: 0.01em` |
|
|
||||||
| Phrase du pied | `1.15rem` | — | serif |
|
|
||||||
| Note | `1.0625rem` | — | encre douce |
|
|
||||||
| Navigation, petit bouton, pied, formulaire (erreur, secours), emplacement photo | `1rem` | — | |
|
|
||||||
| Légende de photo | `0.95rem` | — | encre douce |
|
|
||||||
| `code` | `0.9em` | — | |
|
|
||||||
|
|
||||||
Titres : `letter-spacing: -0.005em`.
|
|
||||||
|
|
||||||
### Typographie française
|
|
||||||
|
|
||||||
Le site applique automatiquement (plugin Jekyll `_plugins/typographie.rb`) une espace fine insécable avant `; ! ?` et une espace insécable avant `:` et `»` et après `«`. Dans un autre projet, taper ces espaces directement (` ` fine, ` ` insécable) ou reprendre ce plugin. Apostrophe typographique `’` et guillemets `« »`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Espacements et mise en page
|
|
||||||
|
|
||||||
| Variable | Valeur | Rôle |
|
|
||||||
|---|---|---|
|
|
||||||
| `--colonne` | `36rem` | Largeur de la colonne de texte (~ 65 caractères) |
|
|
||||||
| `--gouttiere` | `1.25rem` | Marge latérale sur mobile, padding horizontal des conteneurs |
|
|
||||||
| `--largeur-entete` | `64rem` | Largeur maximale de l'en-tête |
|
|
||||||
| `--arrondi-petit` | `0.5rem` | Logo, champs de saisie |
|
|
||||||
| `--arrondi` | `0.75rem` | Photos, cartes (formulaire, calendrier, emplacement photo) |
|
|
||||||
| `--arrondi-pilule` | `999px` | Boutons |
|
|
||||||
|
|
||||||
Rythme vertical (marges) :
|
|
||||||
|
|
||||||
| Élément | Marge |
|
|
||||||
|---|---|
|
|
||||||
| Paragraphe | `0 0 1.15rem` |
|
|
||||||
| `h1` | `1.5rem 0` |
|
|
||||||
| `h2` | `4.5rem 0 1.25rem` (grande respiration avant chaque section) |
|
|
||||||
| Paragraphe contenant seulement un bouton | `2rem 0` |
|
|
||||||
| Photo | `2rem 0` |
|
|
||||||
| Exergue, calendrier | `2.25rem 0` (calendrier : `2.25rem 0 1rem`) |
|
|
||||||
| Formulaire | `2.5rem 0 1rem` |
|
|
||||||
| Note | `margin-top: 2.5rem` |
|
|
||||||
| Colonne `.texte` | padding `2rem` en haut, `5rem` en bas |
|
|
||||||
|
|
||||||
Points de rupture : `30rem` (calendrier resserré), `40rem` (formulaire plus aéré), `48rem` (l'illustration déborde de 4 rem de chaque côté de la colonne).
|
|
||||||
|
|
||||||
Structure de page : `body` en flex colonne avec `min-height: 100vh`, `main` en `flex: 1`, pour que le pied de page reste en bas.
|
|
||||||
|
|
||||||
Traits : encre, 2 à 3 px (boutons actifs 2 px, logo 2,5 px, pied de page 3 px, illustration 3 px, anses 6 px). Trait latéral de 4 px pour l'exergue (orange) et les erreurs (rouge).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Composants
|
|
||||||
|
|
||||||
La page [`charte/exemple.html`](charte/exemple.html) les montre tous.
|
|
||||||
|
|
||||||
### Squelette de page
|
|
||||||
|
|
||||||
```html
|
|
||||||
<body>
|
|
||||||
<a class="evitement" href="#contenu">Aller au contenu</a>
|
|
||||||
<header class="entete">…</header>
|
|
||||||
<main id="contenu" tabindex="-1">
|
|
||||||
<article class="texte">
|
|
||||||
…contenu…
|
|
||||||
</article>
|
|
||||||
</main>
|
|
||||||
<footer class="pied">…</footer>
|
|
||||||
</body>
|
|
||||||
```
|
|
||||||
|
|
||||||
`.evitement` est caché au-dessus de l'écran et n'apparaît qu'au focus clavier.
|
|
||||||
|
|
||||||
### En-tête, logo et navigation
|
|
||||||
|
|
||||||
```html
|
|
||||||
<header class="entete">
|
|
||||||
<a class="logo" href="/" aria-label="Kluk, retour à l'accueil">kluk</a>
|
|
||||||
<nav class="nav" aria-label="Navigation principale">
|
|
||||||
<ul>
|
|
||||||
<li><a href="/" aria-current="page">Accueil</a></li>
|
|
||||||
<li><a href="/histoire/">Comment est née l'idée</a></li>
|
|
||||||
<li><a href="/je-veux-ma-kluk/" class="bouton bouton-petit">Je veux ma Kluk</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
```
|
|
||||||
|
|
||||||
- Le logo est un mot-symbole en minuscules, en Atkinson Hyperlegible Next graisse 800, encadré d'un trait d'encre arrondi.
|
|
||||||
- Les liens de navigation ne sont pas soulignés ; le soulignement vert apparaît au survol et sur la page courante (`aria-current="page"`).
|
|
||||||
- Une seule entrée de navigation est mise en valeur en bouton. Quand c'est la page courante, le bouton s'inverse (fond porcelaine, contour encre).
|
|
||||||
|
|
||||||
### Liens
|
|
||||||
|
|
||||||
Texte en encre, soulignement vert épais (`0.12em`, décalé de `0.2em`) qui passe à l'encre au survol. Dans le pied de page, soulignement encre fin (1 px, 2 px au survol).
|
|
||||||
|
|
||||||
### Focus
|
|
||||||
|
|
||||||
Contour encre de 3 px, décalé de 3 px, coins arrondis à 4 px, sur tous les éléments (`:focus-visible`). Les champs de formulaire utilisent un contour vert.
|
|
||||||
|
|
||||||
### Bouton
|
|
||||||
|
|
||||||
```html
|
|
||||||
<p><a class="bouton" href="/je-veux-ma-kluk/">Je veux ma Kluk</a></p>
|
|
||||||
|
|
||||||
<a class="bouton bouton-petit" href="…">Petit bouton</a>
|
|
||||||
|
|
||||||
<button class="bouton" type="submit">Envoyer</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
Pilule encre, texte porcelaine en 700. Un bouton seul dans son paragraphe prend une marge de 2 rem. Dans un `.formulaire`, un `<button class="bouton">` perd sa bordure native et passe à `opacity: 0.6` quand il est `disabled`.
|
|
||||||
|
|
||||||
### Titres, chapeau, texte
|
|
||||||
|
|
||||||
```html
|
|
||||||
<h1>Titre de la page</h1>
|
|
||||||
<p><em>Le chapeau : une phrase d'accroche en italique.</em></p>
|
|
||||||
<p>Texte courant…</p>
|
|
||||||
<h2>Intertitre</h2>
|
|
||||||
```
|
|
||||||
|
|
||||||
Le chapeau n'a pas de classe : c'est **le paragraphe qui suit directement le `h1` et ne contient qu'un `<em>`** (sélecteur `h1 + p:has(> em:only-child)`).
|
|
||||||
|
|
||||||
### Exergue
|
|
||||||
|
|
||||||
```html
|
|
||||||
<p class="exergue">Une phrase à mettre en valeur.</p>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Note
|
|
||||||
|
|
||||||
```html
|
|
||||||
<p class="note">Une précision discrète, en fin de page.</p>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code en ligne
|
|
||||||
|
|
||||||
```html
|
|
||||||
<code>exemple</code>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Photo
|
|
||||||
|
|
||||||
```html
|
|
||||||
<figure class="photo">
|
|
||||||
<img src="photo.jpg" alt="Description de la photo" width="1200" height="900" loading="lazy" decoding="async">
|
|
||||||
<figcaption>Légende facultative.</figcaption>
|
|
||||||
</figure>
|
|
||||||
```
|
|
||||||
|
|
||||||
Toujours un `alt` descriptif, et `width`/`height` pour éviter les sauts de mise en page. Format des photos du site : 1200 × 900 (4:3) ; image de partage : 1200 × 630.
|
|
||||||
|
|
||||||
### Emplacement de photo (en attendant l'image)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<figure class="photo">
|
|
||||||
<div class="photo-attente">
|
|
||||||
<svg viewBox="0 0 40 32" aria-hidden="true" focusable="false"><path d="M3 3 L6 25 Q6.5 29 10.5 29 L24.5 29 Q28.5 29 29 25 L32 3 Z M31.5 8 C39 8 39.5 21 30 22" /></svg>
|
|
||||||
<p><strong>Photo à venir</strong><br>Description de la photo attendue.</p>
|
|
||||||
</div>
|
|
||||||
</figure>
|
|
||||||
```
|
|
||||||
|
|
||||||
Cadre 3:2 en pointillés chêne foncé. Le pictogramme est aussi fourni seul : `img/photo-attente.svg`.
|
|
||||||
|
|
||||||
### Illustration « table et tasses »
|
|
||||||
|
|
||||||
Deux façons de l'utiliser :
|
|
||||||
|
|
||||||
1. **En image**, le plus simple (couleurs et animation intégrées au fichier) :
|
|
||||||
|
|
||||||
```html
|
|
||||||
<figure class="tasses">
|
|
||||||
<img src="charte/img/illustration-tasses.svg" alt="Trois tasses de couleur alignées sur la table, et une quatrième posée sur la Kluk, fumante.">
|
|
||||||
</figure>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **En SVG dans la page**, pour qu'elle suive les variables CSS : copier le bloc `<figure class="tasses">` de `charte/exemple.html`. La couleur de chaque tasse vient de sa classe d'humeur (`humeur-gris`, `humeur-orange`, `humeur-rouge`, `humeur-vert`) via `currentColor`. On peut donc réutiliser le symbole `#tasse` :
|
|
||||||
|
|
||||||
```html
|
|
||||||
<use href="#tasse" x="34" y="114" class="humeur-orange" />
|
|
||||||
```
|
|
||||||
|
|
||||||
La vapeur ondule doucement (5 s, deux volutes décalées de 2,5 s), seulement si `prefers-reduced-motion` ne le refuse pas. À partir de 48 rem, la figure déborde de 4 rem de part et d'autre de la colonne.
|
|
||||||
|
|
||||||
Tasses seules : `img/tasse-gris.svg`, `tasse-orange.svg`, `tasse-vert.svg`, `tasse-rouge.svg`.
|
|
||||||
|
|
||||||
### Calendrier d'humeurs (décoratif)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="calendrier" aria-hidden="true">
|
|
||||||
<span class="jour jour-v"></span>
|
|
||||||
<span class="jour jour-o"></span>
|
|
||||||
<span class="jour jour-g"></span>
|
|
||||||
<span class="jour jour-r"></span>
|
|
||||||
<!-- … une case par jour ; la grille remplit des colonnes de 7 jours (une semaine par colonne) -->
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
`jour-g` gris, `jour-o` orange, `jour-v` vert, `jour-r` rouge. Carte de fond `--surface` avec liseré intérieur `--porcelaine-fonce`.
|
|
||||||
|
|
||||||
### Formulaire
|
|
||||||
|
|
||||||
```html
|
|
||||||
<form class="formulaire">
|
|
||||||
<div class="champ">
|
|
||||||
<label for="champ-email">Votre adresse mail <span class="precision">(obligatoire)</span></label>
|
|
||||||
<input id="champ-email" name="Email" type="email" required autocomplete="email">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fieldset class="champ">
|
|
||||||
<legend>Ce que vous aimeriez faire <span class="precision">(cochez tout ce qui vous ressemble)</span></legend>
|
|
||||||
<ul class="choix">
|
|
||||||
<li>
|
|
||||||
<input id="choix-1" type="checkbox" name="choix[]" value="1">
|
|
||||||
<label for="choix-1">Premier choix</label>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div class="champ">
|
|
||||||
<label for="champ-message">Un mot, une idée, une question ? <span class="precision">(facultatif)</span></label>
|
|
||||||
<textarea id="champ-message" name="Message" rows="5"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Champ piège anti-robots : invisible, laissé vide par les humains -->
|
|
||||||
<div class="piege" aria-hidden="true">
|
|
||||||
<label for="champ-site">Ne pas remplir</label>
|
|
||||||
<input id="champ-site" name="_site" type="text" tabindex="-1" autocomplete="off">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="formulaire-erreur" role="alert" hidden></p>
|
|
||||||
|
|
||||||
<button class="bouton" type="submit">Envoyer</button>
|
|
||||||
|
|
||||||
<noscript><p class="formulaire-secours">Ce formulaire a besoin de JavaScript.</p></noscript>
|
|
||||||
</form>
|
|
||||||
```
|
|
||||||
|
|
||||||
- Libellés en 700 au-dessus des champs, précisions en 400 encre douce entre parenthèses.
|
|
||||||
- Champs : fond blanc, bordure 2 px `--bordure-champ`, arrondi 0,5 rem ; au focus, contour vert 3 px et bordure encre.
|
|
||||||
- Les styles ne visent que `input[type="email"]` et `textarea` : ajouter les autres types au sélecteur si besoin.
|
|
||||||
- Cases à cocher agrandies (1,35 rem), teintées `--vert-lien`.
|
|
||||||
- Message d'erreur : trait rouge à gauche sur fond rosé.
|
|
||||||
|
|
||||||
### Pied de page
|
|
||||||
|
|
||||||
```html
|
|
||||||
<footer class="pied">
|
|
||||||
<div class="pied-interieur">
|
|
||||||
<p class="pied-phrase">Une petite phrase facultative.</p>
|
|
||||||
<ul class="pied-liens">
|
|
||||||
<li><a href="/je-veux-ma-kluk/">Je veux ma Kluk</a></li>
|
|
||||||
<li><a href="/mentions-legales/">Mentions légales</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
```
|
|
||||||
|
|
||||||
C'est « la table en bois » : fond chêne, trait d'encre de 3 px en haut, comme le plateau de l'illustration.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Impression
|
|
||||||
|
|
||||||
La navigation, le pied de page, l'illustration et le calendrier sont masqués ; le fond devient blanc.
|
|
||||||
|
|
||||||
## 7. Icône
|
|
||||||
|
|
||||||
`img/favicon.svg` : une tasse verte (`--vert`) cernée d'encre. `theme-color` : `#F5F6F0`.
|
|
||||||
|
|
@ -1,528 +0,0 @@
|
||||||
/*
|
|
||||||
* Kluk — charte graphique, feuille de style réutilisable.
|
|
||||||
* Extraite de assets/css/style.scss (site kluk.fr), en CSS pur, sans Sass.
|
|
||||||
*
|
|
||||||
* Palette : la porcelaine du matin, l'encre du thé, le chêne de la table,
|
|
||||||
* et les quatre couleurs des tasses en accent.
|
|
||||||
*
|
|
||||||
* Sommaire
|
|
||||||
* 1. Polices
|
|
||||||
* 2. Variables (couleurs, typographies, mise en page)
|
|
||||||
* 3. Base
|
|
||||||
* 4. Liens, focus, lien d'évitement
|
|
||||||
* 5. Boutons
|
|
||||||
* 6. En-tête et navigation
|
|
||||||
* 7. Colonne de texte et titres
|
|
||||||
* 8. Illustration « tasses »
|
|
||||||
* 9. Calendrier d'humeurs
|
|
||||||
* 10. Photos
|
|
||||||
* 11. Formulaire
|
|
||||||
* 12. Pied de page
|
|
||||||
* 13. Impression
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* ---------- 1. Polices ---------- */
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Young Serif";
|
|
||||||
src: url("../fonts/young-serif-400.woff2") format("woff2");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
/* Fichier variable : couvre toutes les graisses de 200 à 800 */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Atkinson Hyperlegible Next";
|
|
||||||
src: url("../fonts/atkinson-hyperlegible-next-400.woff2") format("woff2");
|
|
||||||
font-weight: 200 800;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Atkinson Hyperlegible Next";
|
|
||||||
src: url("../fonts/atkinson-hyperlegible-next-400-italic.woff2") format("woff2");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 2. Variables ---------- */
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Fonds et texte */
|
|
||||||
--porcelaine: #F5F6F0;
|
|
||||||
--porcelaine-fonce: #E9ECE2;
|
|
||||||
--encre: #2E2A26;
|
|
||||||
--encre-douce: #5A534C;
|
|
||||||
--chene: #D9BC94;
|
|
||||||
--chene-fonce: #B48D5F;
|
|
||||||
|
|
||||||
/* Humeurs (les quatre tasses) */
|
|
||||||
--gris: #8D97A8;
|
|
||||||
--orange: #EDA33D;
|
|
||||||
--vert: #5F9B59;
|
|
||||||
--rouge: #D8573F;
|
|
||||||
--vert-lien: #3F7A3B;
|
|
||||||
|
|
||||||
/* Teintes utilitaires (écrites en dur dans le site d'origine) */
|
|
||||||
--encre-survol: #45403A;
|
|
||||||
--surface: #FBFCF8;
|
|
||||||
--bordure-champ: #8A8279;
|
|
||||||
--erreur-fond: #FBE9E5;
|
|
||||||
--blanc: #fff;
|
|
||||||
|
|
||||||
/* Typographies */
|
|
||||||
--serif: "Young Serif", Georgia, serif;
|
|
||||||
--sans: "Atkinson Hyperlegible Next", "Atkinson Hyperlegible", system-ui, sans-serif;
|
|
||||||
|
|
||||||
/* Mise en page */
|
|
||||||
--colonne: 36rem;
|
|
||||||
--gouttiere: 1.25rem;
|
|
||||||
--largeur-entete: 64rem;
|
|
||||||
|
|
||||||
/* Arrondis */
|
|
||||||
--arrondi-petit: 0.5rem;
|
|
||||||
--arrondi: 0.75rem;
|
|
||||||
--arrondi-pilule: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 3. Base ---------- */
|
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after { box-sizing: border-box; }
|
|
||||||
|
|
||||||
html {
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: var(--porcelaine);
|
|
||||||
color: var(--encre);
|
|
||||||
font-family: var(--sans);
|
|
||||||
font-size: 1.1875rem;
|
|
||||||
line-height: 1.65;
|
|
||||||
font-kerning: normal;
|
|
||||||
hyphens: manual;
|
|
||||||
}
|
|
||||||
|
|
||||||
main { flex: 1; outline: none; }
|
|
||||||
|
|
||||||
img, svg { display: block; max-width: 100%; }
|
|
||||||
|
|
||||||
/* ---------- 4. Liens, focus, lien d'évitement ---------- */
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--encre);
|
|
||||||
text-decoration-line: underline;
|
|
||||||
text-decoration-color: var(--vert);
|
|
||||||
text-decoration-thickness: 0.12em;
|
|
||||||
text-underline-offset: 0.2em;
|
|
||||||
}
|
|
||||||
a:hover { text-decoration-color: var(--encre); }
|
|
||||||
|
|
||||||
:focus-visible {
|
|
||||||
outline: 3px solid var(--encre);
|
|
||||||
outline-offset: 3px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.evitement {
|
|
||||||
position: absolute;
|
|
||||||
left: var(--gouttiere);
|
|
||||||
top: -4rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: var(--encre);
|
|
||||||
color: var(--porcelaine);
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
.evitement:focus { top: 0.75rem; }
|
|
||||||
|
|
||||||
/* ---------- 5. Boutons ---------- */
|
|
||||||
|
|
||||||
.bouton {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.8rem 1.6rem;
|
|
||||||
border-radius: var(--arrondi-pilule);
|
|
||||||
background: var(--encre);
|
|
||||||
color: var(--porcelaine);
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.3;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background-color 0.15s ease;
|
|
||||||
}
|
|
||||||
.bouton:hover { background: var(--encre-survol); color: var(--porcelaine); }
|
|
||||||
.bouton:focus-visible { outline-offset: 4px; border-radius: var(--arrondi-pilule); }
|
|
||||||
|
|
||||||
.bouton-petit {
|
|
||||||
padding: 0.45rem 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Un bouton seul dans son paragraphe respire davantage */
|
|
||||||
p:has(> .bouton:only-child) { margin: 2rem 0; }
|
|
||||||
|
|
||||||
/* ---------- 6. En-tête et navigation ---------- */
|
|
||||||
|
|
||||||
.entete {
|
|
||||||
max-width: var(--largeur-entete);
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 1.25rem var(--gouttiere);
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem 2rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.05rem 0.55rem 0.15rem;
|
|
||||||
border: 2.5px solid var(--encre);
|
|
||||||
border-radius: var(--arrondi-petit);
|
|
||||||
font-family: var(--sans);
|
|
||||||
font-weight: 800;
|
|
||||||
font-size: 1.35rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav ul {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem 1.35rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.nav a:not(.bouton) {
|
|
||||||
text-decoration-color: transparent;
|
|
||||||
}
|
|
||||||
.nav a:not(.bouton):hover,
|
|
||||||
.nav a[aria-current="page"]:not(.bouton) {
|
|
||||||
text-decoration-color: var(--vert);
|
|
||||||
}
|
|
||||||
.nav a.bouton[aria-current="page"] {
|
|
||||||
background: var(--porcelaine);
|
|
||||||
color: var(--encre);
|
|
||||||
box-shadow: inset 0 0 0 2px var(--encre);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 7. Colonne de texte et titres ---------- */
|
|
||||||
|
|
||||||
.texte {
|
|
||||||
max-width: calc(var(--colonne) + 2 * var(--gouttiere));
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem var(--gouttiere) 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3 {
|
|
||||||
font-family: var(--serif);
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.15;
|
|
||||||
letter-spacing: -0.005em;
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: clamp(2.35rem, 1.6rem + 3.6vw, 3.75rem);
|
|
||||||
margin: 1.5rem 0 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: clamp(1.6rem, 1.35rem + 1vw, 2rem);
|
|
||||||
margin: 4.5rem 0 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
p { margin: 0 0 1.15rem; }
|
|
||||||
|
|
||||||
/* Le chapeau : paragraphe entièrement en italique juste sous le titre */
|
|
||||||
h1 + p:has(> em:only-child) {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
line-height: 1.55;
|
|
||||||
color: var(--encre-douce);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note {
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
color: var(--encre-douce);
|
|
||||||
font-size: 1.0625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exergue {
|
|
||||||
margin: 2.25rem 0;
|
|
||||||
padding-left: 1.25rem;
|
|
||||||
border-left: 4px solid var(--orange);
|
|
||||||
font-family: var(--serif);
|
|
||||||
font-size: 1.45rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-size: 0.9em;
|
|
||||||
background: var(--porcelaine-fonce);
|
|
||||||
padding: 0.05em 0.3em;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 8. Illustration « tasses » ---------- */
|
|
||||||
|
|
||||||
.tasses {
|
|
||||||
margin: 1.5rem 0 0;
|
|
||||||
}
|
|
||||||
/* L'illustration déborde légèrement de la colonne sur grand écran */
|
|
||||||
@media (min-width: 48rem) {
|
|
||||||
.tasses { margin-inline: -4rem; }
|
|
||||||
}
|
|
||||||
.tasses svg { width: 100%; height: auto; overflow: visible; }
|
|
||||||
|
|
||||||
.table { fill: var(--chene); }
|
|
||||||
.table-fil,
|
|
||||||
.tasse-corps,
|
|
||||||
.tasse-anse,
|
|
||||||
.kluk-socle,
|
|
||||||
.vapeur path {
|
|
||||||
stroke: var(--encre);
|
|
||||||
stroke-width: 3;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
.tasse-anse { fill: none; stroke-width: 6; }
|
|
||||||
.tasse-corps { fill: currentColor; }
|
|
||||||
.tasse-reflet { fill: none; stroke: var(--blanc); stroke-opacity: 0.45; stroke-width: 4; stroke-linecap: round; }
|
|
||||||
.kluk-socle { fill: var(--chene-fonce); }
|
|
||||||
.kluk-voyant { fill: var(--porcelaine); }
|
|
||||||
.vapeur path { fill: none; stroke-width: 2.5; stroke: var(--encre-douce); }
|
|
||||||
|
|
||||||
.humeur-gris { color: var(--gris); }
|
|
||||||
.humeur-orange { color: var(--orange); }
|
|
||||||
.humeur-rouge { color: var(--rouge); }
|
|
||||||
.humeur-vert { color: var(--vert); }
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.vapeur path {
|
|
||||||
animation: vapeur 5s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
.vapeur path + path { animation-delay: 2.5s; }
|
|
||||||
}
|
|
||||||
@keyframes vapeur {
|
|
||||||
0%, 100% { opacity: 0.25; transform: translateY(6px); }
|
|
||||||
50% { opacity: 0.9; transform: translateY(-6px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 9. Calendrier d'humeurs (décoratif) ---------- */
|
|
||||||
|
|
||||||
.calendrier {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: repeat(7, auto);
|
|
||||||
grid-auto-flow: column;
|
|
||||||
grid-auto-columns: 1fr;
|
|
||||||
gap: 4px;
|
|
||||||
margin: 2.25rem 0 1rem;
|
|
||||||
padding: 1.25rem;
|
|
||||||
background: var(--surface);
|
|
||||||
border-radius: var(--arrondi);
|
|
||||||
box-shadow: inset 0 0 0 1px var(--porcelaine-fonce);
|
|
||||||
}
|
|
||||||
.jour {
|
|
||||||
aspect-ratio: 1;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
.jour-g { background: var(--gris); }
|
|
||||||
.jour-o { background: var(--orange); }
|
|
||||||
.jour-v { background: var(--vert); }
|
|
||||||
.jour-r { background: var(--rouge); }
|
|
||||||
|
|
||||||
@media (max-width: 30rem) {
|
|
||||||
.calendrier { gap: 3px; padding: 0.9rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 10. Photos ---------- */
|
|
||||||
|
|
||||||
.photo {
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
.photo img {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: var(--arrondi);
|
|
||||||
}
|
|
||||||
.photo figcaption {
|
|
||||||
margin-top: 0.6rem;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: var(--encre-douce);
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo-attente {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1.25rem;
|
|
||||||
aspect-ratio: 3 / 2;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border: 2px dashed var(--chene-fonce);
|
|
||||||
border-radius: var(--arrondi);
|
|
||||||
background: var(--porcelaine-fonce);
|
|
||||||
color: var(--encre-douce);
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
.photo-attente svg {
|
|
||||||
flex: none;
|
|
||||||
width: 2.75rem;
|
|
||||||
fill: none;
|
|
||||||
stroke: var(--chene-fonce);
|
|
||||||
stroke-width: 2.5;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
stroke-linecap: round;
|
|
||||||
}
|
|
||||||
.photo-attente p { margin: 0; }
|
|
||||||
.photo-attente strong { color: var(--encre); }
|
|
||||||
|
|
||||||
/* ---------- 11. Formulaire ---------- */
|
|
||||||
|
|
||||||
.formulaire {
|
|
||||||
margin: 2.5rem 0 1rem;
|
|
||||||
padding: 1.75rem 1.25rem;
|
|
||||||
background: var(--surface);
|
|
||||||
border-radius: var(--arrondi);
|
|
||||||
box-shadow: inset 0 0 0 1px var(--porcelaine-fonce);
|
|
||||||
}
|
|
||||||
@media (min-width: 40rem) {
|
|
||||||
.formulaire { padding: 2.25rem 2.5rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.champ {
|
|
||||||
margin: 0 0 1.75rem;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.champ > label,
|
|
||||||
.champ > legend {
|
|
||||||
display: block;
|
|
||||||
padding: 0;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.precision {
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--encre-douce);
|
|
||||||
}
|
|
||||||
|
|
||||||
.formulaire input[type="email"],
|
|
||||||
.formulaire textarea {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.7rem 0.85rem;
|
|
||||||
border: 2px solid var(--bordure-champ);
|
|
||||||
border-radius: var(--arrondi-petit);
|
|
||||||
background: var(--blanc);
|
|
||||||
color: var(--encre);
|
|
||||||
font: inherit;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
.formulaire textarea { resize: vertical; min-height: 8rem; }
|
|
||||||
|
|
||||||
.formulaire input[type="email"]:focus-visible,
|
|
||||||
.formulaire textarea:focus-visible {
|
|
||||||
outline: 3px solid var(--vert);
|
|
||||||
outline-offset: 1px;
|
|
||||||
border-color: var(--encre);
|
|
||||||
}
|
|
||||||
|
|
||||||
.choix {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.choix li {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.35rem 0;
|
|
||||||
}
|
|
||||||
.choix input {
|
|
||||||
flex: none;
|
|
||||||
width: 1.35rem;
|
|
||||||
height: 1.35rem;
|
|
||||||
margin: 0.2rem 0 0;
|
|
||||||
accent-color: var(--vert-lien);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.choix label { cursor: pointer; }
|
|
||||||
|
|
||||||
/* Champ piège anti-robots, invisible */
|
|
||||||
.piege {
|
|
||||||
position: absolute;
|
|
||||||
left: -9999px;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formulaire-erreur {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-left: 4px solid var(--rouge);
|
|
||||||
background: var(--erreur-fond);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formulaire .bouton {
|
|
||||||
border: 0;
|
|
||||||
font: inherit;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.formulaire .bouton:disabled { opacity: 0.6; cursor: progress; }
|
|
||||||
|
|
||||||
.formulaire-secours {
|
|
||||||
margin: 1rem 0 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--encre-douce);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 12. Pied de page : la table en bois ---------- */
|
|
||||||
|
|
||||||
.pied {
|
|
||||||
background: var(--chene);
|
|
||||||
border-top: 3px solid var(--encre);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.pied-interieur {
|
|
||||||
max-width: calc(var(--colonne) + 2 * var(--gouttiere));
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem var(--gouttiere) 2.5rem;
|
|
||||||
}
|
|
||||||
.pied-phrase {
|
|
||||||
font-family: var(--serif);
|
|
||||||
font-size: 1.15rem;
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
}
|
|
||||||
.pied-liens {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem 2rem;
|
|
||||||
}
|
|
||||||
.pied a { text-decoration-color: var(--encre); text-decoration-thickness: 1px; }
|
|
||||||
.pied a:hover { text-decoration-thickness: 2px; }
|
|
||||||
|
|
||||||
/* ---------- 13. Impression ---------- */
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.entete .nav, .pied, .tasses, .calendrier { display: none; }
|
|
||||||
body { background: var(--blanc); }
|
|
||||||
}
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="theme-color" content="#F5F6F0">
|
|
||||||
<title>Charte Kluk — exemple</title>
|
|
||||||
<link rel="preload" href="fonts/young-serif-400.woff2" as="font" type="font/woff2" crossorigin>
|
|
||||||
<link rel="preload" href="fonts/atkinson-hyperlegible-next-400.woff2" as="font" type="font/woff2" crossorigin>
|
|
||||||
<link rel="stylesheet" href="css/kluk.css">
|
|
||||||
<link rel="icon" href="img/favicon.svg" type="image/svg+xml">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<a class="evitement" href="#contenu">Aller au contenu</a>
|
|
||||||
|
|
||||||
<header class="entete">
|
|
||||||
<a class="logo" href="#" aria-label="Kluk, retour à l'accueil">kluk</a>
|
|
||||||
<nav class="nav" aria-label="Navigation principale">
|
|
||||||
<ul>
|
|
||||||
<li><a href="#" aria-current="page">Accueil</a></li>
|
|
||||||
<li><a href="#">Une autre page</a></li>
|
|
||||||
<li><a href="#" class="bouton bouton-petit">Appel à l’action</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main id="contenu" tabindex="-1">
|
|
||||||
<article class="texte">
|
|
||||||
<h1>Titre de page en Young Serif</h1>
|
|
||||||
<p><em>Le chapeau : un paragraphe entièrement en italique, juste sous le titre.</em></p>
|
|
||||||
|
|
||||||
<p>Texte courant en Atkinson Hyperlegible Next, avec <a href="#">un lien souligné de vert</a>, du <strong>gras</strong>, de l’<em>italique</em> et du <code>code</code>.</p>
|
|
||||||
|
|
||||||
<p><a class="bouton" href="#">Bouton principal</a></p>
|
|
||||||
|
|
||||||
<figure class="tasses">
|
|
||||||
<svg viewBox="0 0 580 214" role="img" aria-labelledby="tasses-titre">
|
|
||||||
<title id="tasses-titre">Illustration : trois tasses de couleur alignées sur la table, et une quatrième posée sur la Kluk, fumante.</title>
|
|
||||||
<defs>
|
|
||||||
<g id="tasse">
|
|
||||||
<path class="tasse-anse" d="M63 22 C86 20 88 60 60 62" />
|
|
||||||
<path class="tasse-corps" d="M3 6 L10 78 Q11 86 20 86 L48 86 Q57 86 58 78 L65 6 Z" />
|
|
||||||
<path class="tasse-reflet" d="M13 18 L17 62" />
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
<rect class="table" x="0" y="200" width="580" height="14" rx="3" />
|
|
||||||
<path class="table-fil" d="M0 200 H580" />
|
|
||||||
<use href="#tasse" x="34" y="114" class="humeur-gris" />
|
|
||||||
<use href="#tasse" x="132" y="114" class="humeur-orange" />
|
|
||||||
<use href="#tasse" x="230" y="114" class="humeur-rouge" />
|
|
||||||
<g class="kluk-illu" transform="translate(410 98)">
|
|
||||||
<g class="vapeur" aria-hidden="true">
|
|
||||||
<path d="M22 -6 c-9 -11 9 -20 0 -31 c-9 -11 9 -20 0 -31" />
|
|
||||||
<path d="M44 -10 c-9 -11 9 -20 0 -31 c-9 -11 9 -20 0 -31" />
|
|
||||||
</g>
|
|
||||||
<use href="#tasse" x="0" y="0" class="humeur-vert" />
|
|
||||||
<rect class="kluk-socle" x="-22" y="86" width="112" height="16" rx="8" />
|
|
||||||
<circle class="kluk-voyant" cx="34" cy="94" r="2.6" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<h2>Intertitre de section</h2>
|
|
||||||
<p>Les intertitres ménagent un grand espace au-dessus d’eux (4,5 rem) pour découper la page en respirations.</p>
|
|
||||||
|
|
||||||
<p class="exergue">Une phrase mise en exergue, en serif, soulignée d’un trait orange.</p>
|
|
||||||
|
|
||||||
<figure class="photo">
|
|
||||||
<div class="photo-attente">
|
|
||||||
<svg viewBox="0 0 40 32" aria-hidden="true" focusable="false"><path d="M3 3 L6 25 Q6.5 29 10.5 29 L24.5 29 Q28.5 29 29 25 L32 3 Z M31.5 8 C39 8 39.5 21 30 22" /></svg>
|
|
||||||
<p><strong>Photo à venir</strong><br>Description de la photo attendue.</p>
|
|
||||||
</div>
|
|
||||||
<figcaption>Légende facultative sous la photo.</figcaption>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<div class="calendrier" aria-hidden="true" id="calendrier"></div>
|
|
||||||
|
|
||||||
<form class="formulaire" onsubmit="return false">
|
|
||||||
<div class="champ">
|
|
||||||
<label for="champ-email">Votre adresse mail <span class="precision">(obligatoire)</span></label>
|
|
||||||
<input id="champ-email" name="Email" type="email" required autocomplete="email">
|
|
||||||
</div>
|
|
||||||
<fieldset class="champ">
|
|
||||||
<legend>Ce que vous aimeriez faire <span class="precision">(cochez tout ce qui vous ressemble)</span></legend>
|
|
||||||
<ul class="choix">
|
|
||||||
<li><input id="choix-1" type="checkbox" name="choix[]" value="1" checked><label for="choix-1">Premier choix</label></li>
|
|
||||||
<li><input id="choix-2" type="checkbox" name="choix[]" value="2"><label for="choix-2">Deuxième choix</label></li>
|
|
||||||
</ul>
|
|
||||||
</fieldset>
|
|
||||||
<div class="champ">
|
|
||||||
<label for="champ-message">Un mot, une idée, une question ? <span class="precision">(facultatif)</span></label>
|
|
||||||
<textarea id="champ-message" name="Message" rows="5"></textarea>
|
|
||||||
</div>
|
|
||||||
<p class="formulaire-erreur" role="alert">Message d’erreur (masqué par défaut avec l’attribut <code>hidden</code>).</p>
|
|
||||||
<button class="bouton" type="submit">Envoyer</button>
|
|
||||||
<p class="formulaire-secours">Texte de secours, affiché dans un <code><noscript></code>.</p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p class="note">Une note discrète en fin de page.</p>
|
|
||||||
</article>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="pied">
|
|
||||||
<div class="pied-interieur">
|
|
||||||
<p class="pied-phrase">Une petite phrase en serif, facultative.</p>
|
|
||||||
<ul class="pied-liens">
|
|
||||||
<li><a href="#">Premier lien</a></li>
|
|
||||||
<li><a href="#">Mentions légales</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Remplit le calendrier de démonstration : chaque groupe de 7 lettres est une semaine.
|
|
||||||
// g = gris, o = orange, v = vert, r = rouge.
|
|
||||||
var semaines = "vvovvvg vvvvovv ovvrvvv vvgvvvo ovvvvvv rrovrrr rorrvvo gvggggg gggvggg gogggvg ggoggvg vvgvvvv ovvvovv vrvvvov vvvvovv ovvgvvv vvovvrv vvvovvv vgvvvvo vvvvovv";
|
|
||||||
document.getElementById("calendrier").innerHTML = semaines.replace(/ /g, "").split("").map(function (j) {
|
|
||||||
return '<span class="jour jour-' + j + '"></span>';
|
|
||||||
}).join("");
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
Young Serif
|
|
||||||
Copyright 2023 The Young Serif Project Authors (https://github.com/noirblancrouge/YoungSerif)
|
|
||||||
|
|
||||||
Atkinson Hyperlegible Next
|
|
||||||
Copyright 2020-2024 The Atkinson Hyperlegible Next Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next)
|
|
||||||
|
|
||||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
||||||
This license is copied below, and is also available with a FAQ at:
|
|
||||||
http://scripts.sil.org/OFL
|
|
||||||
|
|
||||||
|
|
||||||
-----------------------------------------------------------
|
|
||||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
||||||
-----------------------------------------------------------
|
|
||||||
|
|
||||||
PREAMBLE
|
|
||||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
||||||
development of collaborative font projects, to support the font creation
|
|
||||||
efforts of academic and linguistic communities, and to provide a free and
|
|
||||||
open framework in which fonts may be shared and improved in partnership
|
|
||||||
with others.
|
|
||||||
|
|
||||||
The OFL allows the licensed fonts to be used, studied, modified and
|
|
||||||
redistributed freely as long as they are not sold by themselves. The
|
|
||||||
fonts, including any derivative works, can be bundled, embedded,
|
|
||||||
redistributed and/or sold with any software provided that any reserved
|
|
||||||
names are not used by derivative works. The fonts and derivatives,
|
|
||||||
however, cannot be released under any other type of license. The
|
|
||||||
requirement for fonts to remain under this license does not apply
|
|
||||||
to any document created using the fonts or their derivatives.
|
|
||||||
|
|
||||||
DEFINITIONS
|
|
||||||
"Font Software" refers to the set of files released by the Copyright
|
|
||||||
Holder(s) under this license and clearly marked as such. This may
|
|
||||||
include source files, build scripts and documentation.
|
|
||||||
|
|
||||||
"Reserved Font Name" refers to any names specified as such after the
|
|
||||||
copyright statement(s).
|
|
||||||
|
|
||||||
"Original Version" refers to the collection of Font Software components as
|
|
||||||
distributed by the Copyright Holder(s).
|
|
||||||
|
|
||||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
||||||
or substituting -- in part or in whole -- any of the components of the
|
|
||||||
Original Version, by changing formats or by porting the Font Software to a
|
|
||||||
new environment.
|
|
||||||
|
|
||||||
"Author" refers to any designer, engineer, programmer, technical
|
|
||||||
writer or other person who contributed to the Font Software.
|
|
||||||
|
|
||||||
PERMISSION & CONDITIONS
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
||||||
redistribute, and sell modified and unmodified copies of the Font
|
|
||||||
Software, subject to the following conditions:
|
|
||||||
|
|
||||||
1) Neither the Font Software nor any of its individual components,
|
|
||||||
in Original or Modified Versions, may be sold by itself.
|
|
||||||
|
|
||||||
2) Original or Modified Versions of the Font Software may be bundled,
|
|
||||||
redistributed and/or sold with any software, provided that each copy
|
|
||||||
contains the above copyright notice and this license. These can be
|
|
||||||
included either as stand-alone text files, human-readable headers or
|
|
||||||
in the appropriate machine-readable metadata fields within text or
|
|
||||||
binary files as long as those fields can be easily viewed by the user.
|
|
||||||
|
|
||||||
3) No Modified Version of the Font Software may use the Reserved Font
|
|
||||||
Name(s) unless explicit written permission is granted by the corresponding
|
|
||||||
Copyright Holder. This restriction only applies to the primary font name as
|
|
||||||
presented to the users.
|
|
||||||
|
|
||||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
||||||
Software shall not be used to promote, endorse or advertise any
|
|
||||||
Modified Version, except to acknowledge the contribution(s) of the
|
|
||||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
||||||
permission.
|
|
||||||
|
|
||||||
5) The Font Software, modified or unmodified, in part or in whole,
|
|
||||||
must be distributed entirely under this license, and must not be
|
|
||||||
distributed under any other license. The requirement for fonts to
|
|
||||||
remain under this license does not apply to any document created
|
|
||||||
using the Font Software.
|
|
||||||
|
|
||||||
TERMINATION
|
|
||||||
This license becomes null and void if any of the above conditions are
|
|
||||||
not met.
|
|
||||||
|
|
||||||
DISCLAIMER
|
|
||||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
||||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
||||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
||||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
||||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M4 4 L7 24 Q7.5 28 11.5 28 L19.5 28 Q23.5 28 24 24 L27 4 Z" fill="#5F9B59" stroke="#2E2A26" stroke-width="2.5" stroke-linejoin="round"/><path d="M26.5 9 C32 9 32 20 25 21" fill="none" stroke="#2E2A26" stroke-width="3" stroke-linecap="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 318 B |
|
|
@ -1,48 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 214" role="img" aria-labelledby="t">
|
|
||||||
<title id="t">Trois tasses de couleur alignées sur la table, et une quatrième posée sur la Kluk, fumante.</title>
|
|
||||||
<style>
|
|
||||||
.table { fill: #D9BC94; }
|
|
||||||
.table-fil, .tasse-corps, .tasse-anse, .kluk-socle, .vapeur path {
|
|
||||||
stroke: #2E2A26; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
.tasse-anse { fill: none; stroke-width: 6; }
|
|
||||||
.tasse-corps { fill: currentColor; }
|
|
||||||
.tasse-reflet { fill: none; stroke: #fff; stroke-opacity: 0.45; stroke-width: 4; stroke-linecap: round; }
|
|
||||||
.kluk-socle { fill: #B48D5F; }
|
|
||||||
.kluk-voyant { fill: #F5F6F0; }
|
|
||||||
.vapeur path { fill: none; stroke-width: 2.5; stroke: #5A534C; }
|
|
||||||
.humeur-gris { color: #8D97A8; }
|
|
||||||
.humeur-orange { color: #EDA33D; }
|
|
||||||
.humeur-rouge { color: #D8573F; }
|
|
||||||
.humeur-vert { color: #5F9B59; }
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.vapeur path { animation: vapeur 5s ease-in-out infinite; }
|
|
||||||
.vapeur path + path { animation-delay: 2.5s; }
|
|
||||||
}
|
|
||||||
@keyframes vapeur {
|
|
||||||
0%, 100% { opacity: 0.25; transform: translateY(6px); }
|
|
||||||
50% { opacity: 0.9; transform: translateY(-6px); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<defs>
|
|
||||||
<g id="tasse">
|
|
||||||
<path class="tasse-anse" d="M63 22 C86 20 88 60 60 62" />
|
|
||||||
<path class="tasse-corps" d="M3 6 L10 78 Q11 86 20 86 L48 86 Q57 86 58 78 L65 6 Z" />
|
|
||||||
<path class="tasse-reflet" d="M13 18 L17 62" />
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
<rect class="table" x="0" y="200" width="580" height="14" rx="3" />
|
|
||||||
<path class="table-fil" d="M0 200 H580" />
|
|
||||||
<use href="#tasse" x="34" y="114" class="humeur-gris" />
|
|
||||||
<use href="#tasse" x="132" y="114" class="humeur-orange" />
|
|
||||||
<use href="#tasse" x="230" y="114" class="humeur-rouge" />
|
|
||||||
<g transform="translate(410 98)">
|
|
||||||
<g class="vapeur">
|
|
||||||
<path d="M22 -6 c-9 -11 9 -20 0 -31 c-9 -11 9 -20 0 -31" />
|
|
||||||
<path d="M44 -10 c-9 -11 9 -20 0 -31 c-9 -11 9 -20 0 -31" />
|
|
||||||
</g>
|
|
||||||
<use href="#tasse" x="0" y="0" class="humeur-vert" />
|
|
||||||
<rect class="kluk-socle" x="-22" y="86" width="112" height="16" rx="8" />
|
|
||||||
<circle class="kluk-voyant" cx="34" cy="94" r="2.6" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 44 36"><path d="M3 3 L6 25 Q6.5 29 10.5 29 L24.5 29 Q28.5 29 29 25 L32 3 Z M31.5 8 C39 8 39.5 21 30 22" fill="none" stroke="#B48D5F" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 262 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 95 95"><path d="M63 22 C86 20 88 60 60 62" fill="none" stroke="#2E2A26" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 6 L10 78 Q11 86 20 86 L48 86 Q57 86 58 78 L65 6 Z" fill="#8D97A8" stroke="#2E2A26" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 18 L17 62" fill="none" stroke="#fff" stroke-opacity="0.45" stroke-width="4" stroke-linecap="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 472 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 95 95"><path d="M63 22 C86 20 88 60 60 62" fill="none" stroke="#2E2A26" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 6 L10 78 Q11 86 20 86 L48 86 Q57 86 58 78 L65 6 Z" fill="#EDA33D" stroke="#2E2A26" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 18 L17 62" fill="none" stroke="#fff" stroke-opacity="0.45" stroke-width="4" stroke-linecap="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 472 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 95 95"><path d="M63 22 C86 20 88 60 60 62" fill="none" stroke="#2E2A26" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 6 L10 78 Q11 86 20 86 L48 86 Q57 86 58 78 L65 6 Z" fill="#D8573F" stroke="#2E2A26" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 18 L17 62" fill="none" stroke="#fff" stroke-opacity="0.45" stroke-width="4" stroke-linecap="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 472 B |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 95 95"><path d="M63 22 C86 20 88 60 60 62" fill="none" stroke="#2E2A26" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 6 L10 78 Q11 86 20 86 L48 86 Q57 86 58 78 L65 6 Z" fill="#5F9B59" stroke="#2E2A26" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 18 L17 62" fill="none" stroke="#fff" stroke-opacity="0.45" stroke-width="4" stroke-linecap="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 472 B |
|
|
@ -13,7 +13,6 @@ Rails.application.routes.draw do
|
||||||
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
|
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
|
||||||
|
|
||||||
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
|
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
|
||||||
get "/charte", to: "charte#show" if Rails.env.development?
|
|
||||||
|
|
||||||
# Defines the root path route ("/")
|
# Defines the root path route ("/")
|
||||||
root "home#index"
|
root "home#index"
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
class ReplaceMoodModeStringWithModeReference < ActiveRecord::Migration[8.0]
|
class ReplaceMoodModeStringWithModeReference < ActiveRecord::Migration[8.0]
|
||||||
def up
|
def up
|
||||||
rename_column :moods, :mode, :mode_string
|
|
||||||
|
|
||||||
# 1. Ajouter la colonne de référence
|
# 1. Ajouter la colonne de référence
|
||||||
add_reference :moods, :mode, foreign_key: true
|
add_reference :moods, :mode, foreign_key: true
|
||||||
|
|
||||||
# 2. Pour chaque mood, trouver ou créer le Mode correspondant
|
# 2. Pour chaque mood, trouver ou créer le Mode correspondant
|
||||||
Mood.find_each do |mood|
|
Mood.find_each do |mood|
|
||||||
next if mood.mode_string.blank?
|
next if mood.mode.blank?
|
||||||
|
|
||||||
mode_record = Mode.find_or_create_by!(label: mood.mode_string, user_id: mood.user_id) do |m|
|
mode_record = Mode.find_or_create_by!(label: mood.mode, user_id: mood.user_id) do |m|
|
||||||
m.color = "#000000" # couleur par défaut
|
m.color = "#000000" # couleur par défaut
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -17,18 +15,16 @@ class ReplaceMoodModeStringWithModeReference < ActiveRecord::Migration[8.0]
|
||||||
end
|
end
|
||||||
|
|
||||||
# 3. Supprimer l'ancienne colonne string
|
# 3. Supprimer l'ancienne colonne string
|
||||||
remove_column :moods, :mode_string, :string
|
remove_column :moods, :mode, :string
|
||||||
end
|
end
|
||||||
|
|
||||||
def down
|
def down
|
||||||
add_column :moods, :mode_string, :string
|
add_column :moods, :mode, :string
|
||||||
|
|
||||||
Mood.find_each do |mood|
|
Mood.find_each do |mood|
|
||||||
mood.update_column(:mode_string, mood.mode&.label)
|
mood.update_column(:mode, mood.mode&.label)
|
||||||
end
|
end
|
||||||
|
|
||||||
remove_reference :moods, :mode, foreign_key: true
|
remove_reference :moods, :mode, foreign_key: true
|
||||||
|
|
||||||
rename_column :moods, :mode_string, :mode
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
class UpdateRfidTagsModeFromStringToMode < ActiveRecord::Migration[8.0]
|
|
||||||
def change
|
|
||||||
remove_column :rfid_tags, :mode
|
|
||||||
add_reference :rfid_tags, :mode
|
|
||||||
end
|
|
||||||
end
|
|
||||||
5
db/schema.rb
generated
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2026_07_29_144217) do
|
ActiveRecord::Schema[8.0].define(version: 2026_03_19_181607) do
|
||||||
create_table "active_storage_attachments", force: :cascade do |t|
|
create_table "active_storage_attachments", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "record_type", null: false
|
t.string "record_type", null: false
|
||||||
|
|
@ -70,13 +70,12 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_29_144217) do
|
||||||
|
|
||||||
create_table "rfid_tags", force: :cascade do |t|
|
create_table "rfid_tags", force: :cascade do |t|
|
||||||
t.string "identifier"
|
t.string "identifier"
|
||||||
|
t.string "mode"
|
||||||
t.datetime "created_at", default: -> { "CURRENT_DATE" }, null: false
|
t.datetime "created_at", default: -> { "CURRENT_DATE" }, null: false
|
||||||
t.datetime "updated_at", default: -> { "CURRENT_DATE" }, null: false
|
t.datetime "updated_at", default: -> { "CURRENT_DATE" }, null: false
|
||||||
t.string "chip_id"
|
t.string "chip_id"
|
||||||
t.integer "user_id"
|
t.integer "user_id"
|
||||||
t.integer "mode_id"
|
|
||||||
t.index ["identifier"], name: "index_rfid_tags_on_identifier", unique: true
|
t.index ["identifier"], name: "index_rfid_tags_on_identifier", unique: true
|
||||||
t.index ["mode_id"], name: "index_rfid_tags_on_mode_id"
|
|
||||||
t.index ["user_id"], name: "index_rfid_tags_on_user_id"
|
t.index ["user_id"], name: "index_rfid_tags_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
7
package-lock.json
generated
|
|
@ -6,6 +6,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "app",
|
"name": "app",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bulma": "^1.0.4",
|
||||||
"sass": "^1.94.2",
|
"sass": "^1.94.2",
|
||||||
"yarn": "^1.22.22"
|
"yarn": "^1.22.22"
|
||||||
}
|
}
|
||||||
|
|
@ -319,6 +320,12 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bulma": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
"name": "app",
|
"name": "app",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bulma": "^1.0.4",
|
||||||
"sass": "^1.94.2",
|
"sass": "^1.94.2",
|
||||||
"yarn": "^1.22.22"
|
"yarn": "^1.22.22"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:css": "sass ./app/assets/stylesheets/application.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
|
"build:css": "sass ./app/assets/stylesheets/application.bulma.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,5 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe User, type: :model do
|
RSpec.describe User, type: :model do
|
||||||
describe "#current_mode_since" do
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
let(:user) { create(:user) }
|
|
||||||
let(:croisiere) { create(:mode, user: user) }
|
|
||||||
let(:creatif) { create(:mode, user: user) }
|
|
||||||
|
|
||||||
it "returns nil without moods" do
|
|
||||||
expect(user.current_mode_since).to be_nil
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns the first day of the latest run of the current mode" do
|
|
||||||
create(:mood, user: user, mode: croisiere, recorded_at: DateTime.parse("2026-01-10 08:00"))
|
|
||||||
create(:mood, user: user, mode: creatif, recorded_at: DateTime.parse("2026-01-12 08:00"))
|
|
||||||
create(:mood, user: user, mode: creatif, recorded_at: DateTime.parse("2026-01-14 08:00"))
|
|
||||||
|
|
||||||
expect(user.current_mode_since).to eq(Date.parse("2026-01-12"))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe "Home", type: :request do
|
|
||||||
describe "GET / on a public user's subdomain" do
|
|
||||||
let(:user) { create(:user, username: "alice", public: true) }
|
|
||||||
|
|
||||||
before { host! "#{user.username}.example.com" }
|
|
||||||
|
|
||||||
it "exposes OpenGraph tags for sharing" do
|
|
||||||
get root_path, params: { day: "2026-01-15" }
|
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
page = Nokogiri::HTML(response.body)
|
|
||||||
meta = ->(attr, name) { page.at_css("meta[#{attr}='#{name}']")&.[]("content") }
|
|
||||||
|
|
||||||
expect(meta.("property", "og:title")).to eq("Comment ça Kluk chez alice\u202F?")
|
|
||||||
expect(meta.("property", "og:description")).to start_with("Les humeurs d’alice")
|
|
||||||
expect(meta.("property", "og:url")).to eq("http://alice.example.com/")
|
|
||||||
expect(meta.("property", "og:image")).to match(%r{\Ahttp://alice\.example\.com/assets/charte/partage-\h+\.png\z})
|
|
||||||
expect(meta.("name", "twitter:card")).to eq("summary_large_image")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,231 +1,330 @@
|
||||||
|
# spec/services/mood_calendar_service_spec.rb
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe MoodCalendarService do
|
RSpec.describe MoodCalendarService do
|
||||||
let(:user) { create(:user) }
|
|
||||||
let(:mode) { create(:mode, user: user) }
|
|
||||||
|
|
||||||
describe '.generate_calendar' do
|
describe '.generate_calendar' do
|
||||||
context 'avec des jours manquants' do
|
context 'avec un recorded_at pour chaque jour' do
|
||||||
it 'remplit les jours manquants avec mode: nil et guess égal au dernier mode' do
|
it 'génère un calendrier complet sans jours nil' do
|
||||||
mode2 = create(:mode, user: user)
|
# Données du 1er au 7 février 2025 (une semaine complète)
|
||||||
mode3 = create(:mode, user: user)
|
(1..7).each do |day|
|
||||||
create(:mood, user: user, mode: mode2, recorded_at: Time.zone.parse("2025-02-12 14:20:00"))
|
create(:mood, mode: "mood_#{day}", recorded_at: Time.zone.parse("2025-02-0#{day} 10:00:00"))
|
||||||
create(:mood, user: user, mode: mode3, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
end
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-18 16:45:00"))
|
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
Mood.all,
|
||||||
end_date: Date.parse("2025-02-28")
|
end_date: Date.parse("2025-02-28")
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(result.length).to be >= 1
|
expect(result.length).to eq(1) # Un seul mois
|
||||||
expect(result.any? { |m| m[:month] == Date.parse("2025-02-01") }).to be true
|
expect(result[0][:month]).to eq(Date.parse("2025-02-01"))
|
||||||
|
|
||||||
|
# Première semaine commence le lundi 3 février
|
||||||
|
first_week = result[0][:weeks][0]
|
||||||
|
expect(first_week.length).to eq(7) # Du lundi 3 au dimanche 9
|
||||||
|
|
||||||
|
# Vérifier que tous les jours du 3 au 7 ont un mode
|
||||||
|
(3..7).each do |day|
|
||||||
|
mood = first_week.find { |m| m[:recorded_at].day == day }
|
||||||
|
expect(mood[:mode]).to eq("mood_#{day}")
|
||||||
|
expect(mood).not_to have_key(:guess)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Le 8 et 9 février devraient avoir mode: nil avec guess: "mood_7"
|
||||||
|
mood_8 = first_week.find { |m| m[:recorded_at].day == 8 }
|
||||||
|
expect(mood_8[:mode]).to be_nil
|
||||||
|
expect(mood_8[:guess]).to eq("mood_7")
|
||||||
|
|
||||||
|
mood_9 = first_week.find { |m| m[:recorded_at].day == 9 }
|
||||||
|
expect(mood_9[:mode]).to be_nil
|
||||||
|
expect(mood_9[:guess]).to eq("mood_7")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
all_days = result.find { |m| m[:month] == Date.parse("2025-02-01") }[:weeks].flatten
|
context 'avec des jours manquants' do
|
||||||
|
it 'remplit les jours manquants avec mode: nil et guess égal au dernier mode' do
|
||||||
|
create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-12 08:30:00"))
|
||||||
|
create(:mood, mode: "content", recorded_at: Time.zone.parse("2025-02-12 14:20:00"))
|
||||||
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
|
create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-02-18 16:45:00"))
|
||||||
|
|
||||||
|
moods = Mood.all
|
||||||
|
result = MoodCalendarService.generate_calendar(
|
||||||
|
moods,
|
||||||
|
end_date: Date.parse("2025-02-28")
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.length).to eq(1)
|
||||||
|
expect(result[0][:month]).to eq(Date.parse("2025-02-01"))
|
||||||
|
|
||||||
|
# Trouver tous les jours
|
||||||
|
all_days = result[0][:weeks].flatten
|
||||||
|
|
||||||
|
# Le 12 février devrait avoir le dernier mood (content à 14:20)
|
||||||
mood_12 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-12") }
|
mood_12 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-12") }
|
||||||
expect(mood_12[:mode]).to eq(mode2)
|
expect(mood_12[:mode]).to eq("content")
|
||||||
expect(mood_12[:recorded_at].to_i).to eq(Time.zone.parse("2025-02-12 14:20:00").to_i)
|
expect(mood_12[:recorded_at].to_i).to eq(Time.zone.parse("2025-02-12 14:20:00").to_i)
|
||||||
|
|
||||||
|
# Le 13 février devrait être nil avec guess: "content"
|
||||||
mood_13 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-13") }
|
mood_13 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-13") }
|
||||||
expect(mood_13[:mode]).to be_nil
|
expect(mood_13[:mode]).to be_nil
|
||||||
expect(mood_13[:guess]).to eq(mode2)
|
expect(mood_13[:guess]).to eq("content")
|
||||||
|
|
||||||
|
# Le 14 février devrait être nil avec guess: "content"
|
||||||
mood_14 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-14") }
|
mood_14 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-14") }
|
||||||
expect(mood_14[:mode]).to be_nil
|
expect(mood_14[:mode]).to be_nil
|
||||||
expect(mood_14[:guess]).to eq(mode2)
|
expect(mood_14[:guess]).to eq("content")
|
||||||
|
|
||||||
|
# Le 15 février devrait avoir le mood "heureux"
|
||||||
mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
|
mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
|
||||||
expect(mood_15[:mode]).to eq(mode3)
|
expect(mood_15[:mode]).to eq("heureux")
|
||||||
|
|
||||||
|
# Le 16 février devrait être nil avec guess: "heureux"
|
||||||
mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") }
|
mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") }
|
||||||
expect(mood_16[:mode]).to be_nil
|
expect(mood_16[:mode]).to be_nil
|
||||||
expect(mood_16[:guess]).to eq(mode3)
|
expect(mood_16[:guess]).to eq("heureux")
|
||||||
|
|
||||||
|
# Les jours avant le 12 (à partir du premier lundi 3 février) devraient avoir guess: nil
|
||||||
mood_3 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-03") }
|
mood_3 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-03") }
|
||||||
expect(mood_3[:mode]).to be_nil
|
expect(mood_3[:mode]).to be_nil
|
||||||
expect(mood_3[:guess]).to be_nil
|
expect(mood_3[:guess]).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'gère correctement plusieurs mois avec des jours manquants' do
|
it 'gère correctement plusieurs mois avec des jours manquants' do
|
||||||
mode2 = create(:mode, user: user)
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "joyeux", recorded_at: Time.zone.parse("2025-04-20 11:30:00"))
|
||||||
create(:mood, user: user, mode: mode2, recorded_at: Time.zone.parse("2025-04-20 11:30:00"))
|
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
Mood.all,
|
||||||
end_date: Date.parse("2025-04-30")
|
end_date: Date.parse("2025-04-30")
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(result.length).to be >= 3
|
# Devrait avoir exactement février, mars et avril
|
||||||
expect(result.any? { |m| m[:month] == Date.parse("2025-02-01") }).to be true
|
expect(result.length).to eq(3)
|
||||||
expect(result.any? { |m| m[:month] == Date.parse("2025-03-01") }).to be true
|
|
||||||
expect(result.any? { |m| m[:month] == Date.parse("2025-04-01") }).to be true
|
# Vérifier février
|
||||||
|
february = result.find { |m| m[:month] == Date.parse("2025-02-01") }
|
||||||
|
expect(february).not_to be_nil
|
||||||
|
|
||||||
|
# Vérifier mars (tous les jours devraient avoir mode: nil et guess: "heureux")
|
||||||
march = result.find { |m| m[:month] == Date.parse("2025-03-01") }
|
march = result.find { |m| m[:month] == Date.parse("2025-03-01") }
|
||||||
|
expect(march).not_to be_nil
|
||||||
all_march_days = march[:weeks].flatten
|
all_march_days = march[:weeks].flatten
|
||||||
|
|
||||||
|
# Tous les jours de mars (y compris ceux qui débordent début avril) devraient avoir mode: nil
|
||||||
expect(all_march_days.all? { |d| d[:mode].nil? }).to be true
|
expect(all_march_days.all? { |d| d[:mode].nil? }).to be true
|
||||||
|
|
||||||
|
# Tous les jours de mars devraient avoir guess: "heureux"
|
||||||
|
# (le mois de mars va du 3 mars au 6 avril, avant le mood du 20 avril)
|
||||||
all_march_days.each do |day|
|
all_march_days.each do |day|
|
||||||
expect(day[:guess]).to be_present,
|
expect(day[:guess]).to eq("heureux"),
|
||||||
"Le jour #{day[:recorded_at].to_date} devrait avoir un guess"
|
"Le jour #{day[:recorded_at].to_date} devrait avoir guess: 'heureux' mais a: #{day[:guess].inspect}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Vérifier avril
|
||||||
april = result.find { |m| m[:month] == Date.parse("2025-04-01") }
|
april = result.find { |m| m[:month] == Date.parse("2025-04-01") }
|
||||||
|
expect(april).not_to be_nil
|
||||||
all_april_days = april[:weeks].flatten
|
all_april_days = april[:weeks].flatten
|
||||||
mood_20_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-20") }
|
mood_20_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-20") }
|
||||||
expect(mood_20_april[:mode]).to eq(mode2)
|
expect(mood_20_april[:mode]).to eq("joyeux")
|
||||||
|
|
||||||
|
# Les jours du 7 au 19 avril devraient avoir guess: "heureux"
|
||||||
mood_10_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-10") }
|
mood_10_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-10") }
|
||||||
expect(mood_10_april[:guess]).to eq(mode)
|
expect(mood_10_april[:guess]).to eq("heureux")
|
||||||
|
|
||||||
|
# Les jours après le 20 avril devraient avoir guess: "joyeux"
|
||||||
mood_21_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-21") }
|
mood_21_april = all_april_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-04-21") }
|
||||||
expect(mood_21_april[:guess]).to eq(mode2)
|
expect(mood_21_april[:guess]).to eq("joyeux")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'sans aucun recorded_at' do
|
context 'sans aucun recorded_at' do
|
||||||
it 'retourne un calendrier à partir de la date courante' do
|
it 'retourne un tableau vide' do
|
||||||
result = MoodCalendarService.generate_calendar(user)
|
result = MoodCalendarService.generate_calendar(Mood.none)
|
||||||
|
|
||||||
expect(result).not_to be_empty
|
expect(result).to eq([])
|
||||||
expect(result.first[:month]).to eq(Date.current.beginning_of_month)
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'avec plusieurs moods le même jour' do
|
||||||
|
it 'garde uniquement le mood le plus récent' do
|
||||||
|
create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-12 08:30:00"))
|
||||||
|
create(:mood, mode: "neutre", recorded_at: Time.zone.parse("2025-02-12 12:00:00"))
|
||||||
|
create(:mood, mode: "content", recorded_at: Time.zone.parse("2025-02-12 14:20:00"))
|
||||||
|
create(:mood, mode: "fatigué", recorded_at: Time.zone.parse("2025-02-12 09:15:00"))
|
||||||
|
|
||||||
|
moods = Mood.all
|
||||||
|
result = MoodCalendarService.generate_calendar(
|
||||||
|
moods,
|
||||||
|
end_date: Date.parse("2025-02-28")
|
||||||
|
)
|
||||||
|
|
||||||
|
all_days = result[0][:weeks].flatten
|
||||||
|
mood_12 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-12") }
|
||||||
|
|
||||||
|
expect(mood_12[:mode]).to eq("content")
|
||||||
|
expect(mood_12[:recorded_at].to_i).to eq(Time.zone.parse("2025-02-12 14:20:00").to_i)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'structure du calendrier' do
|
context 'structure du calendrier' do
|
||||||
it 'commence chaque mois par le premier lundi' do
|
it 'commence chaque mois par le premier lundi' do
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
Mood.all,
|
||||||
end_date: Date.parse("2025-02-28")
|
end_date: Date.parse("2025-02-28")
|
||||||
)
|
)
|
||||||
|
|
||||||
february = result[0]
|
february = result[0]
|
||||||
first_day = february[:weeks][0][0]
|
first_day = february[:weeks][0][0]
|
||||||
|
|
||||||
|
# Le premier jour de février 2025 devrait être le lundi 3 février
|
||||||
expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03"))
|
expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03"))
|
||||||
expect(first_day[:recorded_at].to_date.monday?).to be true
|
expect(first_day[:recorded_at].to_date.monday?).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'va du premier lundi du mois au dimanche avant le premier lundi du mois suivant' do
|
it 'va du premier lundi du mois au dimanche avant le premier lundi du mois suivant' do
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
Mood.all,
|
||||||
end_date: Date.parse("2025-03-31")
|
end_date: Date.parse("2025-03-31")
|
||||||
)
|
)
|
||||||
|
|
||||||
february = result[0]
|
february = result[0]
|
||||||
all_days = february[:weeks].flatten
|
all_days = february[:weeks].flatten
|
||||||
|
|
||||||
|
# Premier jour : lundi 3 février 2025
|
||||||
first_day = all_days.first
|
first_day = all_days.first
|
||||||
expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03"))
|
expect(first_day[:recorded_at].to_date).to eq(Date.parse("2025-02-03"))
|
||||||
expect(first_day[:recorded_at].to_date.monday?).to be true
|
expect(first_day[:recorded_at].to_date.monday?).to be true
|
||||||
|
|
||||||
|
# Dernier jour : dimanche 2 mars 2025 (veille du premier lundi de mars qui est le 3)
|
||||||
last_day = all_days.last
|
last_day = all_days.last
|
||||||
expect(last_day[:recorded_at].to_date).to eq(Date.parse("2025-03-02"))
|
expect(last_day[:recorded_at].to_date).to eq(Date.parse("2025-03-02"))
|
||||||
expect(last_day[:recorded_at].to_date.sunday?).to be true
|
expect(last_day[:recorded_at].to_date.sunday?).to be true
|
||||||
|
|
||||||
|
# Le 3 mars (premier lundi de mars) ne devrait PAS être dans février
|
||||||
expect(all_days.any? { |d| d[:recorded_at].to_date == Date.parse("2025-03-03") }).to be false
|
expect(all_days.any? { |d| d[:recorded_at].to_date == Date.parse("2025-03-03") }).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'toutes les semaines ont exactement 7 jours' do
|
it 'toutes les semaines ont exactement 7 jours' do
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
Mood.all,
|
||||||
end_date: Date.parse("2025-02-28")
|
end_date: Date.parse("2025-02-28")
|
||||||
)
|
)
|
||||||
|
|
||||||
february = result[0]
|
february = result[0]
|
||||||
|
|
||||||
|
# Toutes les semaines devraient avoir exactement 7 jours
|
||||||
february[:weeks].each do |week|
|
february[:weeks].each do |week|
|
||||||
expect(week.length).to eq(7)
|
expect(week.length).to eq(7)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'retourne month comme Date (le premier du mois)' do
|
it 'retourne month comme Date (le premier du mois)' do
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
Mood.all,
|
||||||
end_date: Date.parse("2025-02-28")
|
end_date: Date.parse("2025-02-28")
|
||||||
)
|
)
|
||||||
|
|
||||||
february = result[0]
|
february = result[0]
|
||||||
|
|
||||||
|
# month devrait être une Date
|
||||||
expect(february[:month]).to be_a(Date)
|
expect(february[:month]).to be_a(Date)
|
||||||
expect(february[:month]).to eq(Date.parse("2025-02-01"))
|
expect(february[:month]).to eq(Date.parse("2025-02-01"))
|
||||||
expect(february[:month].day).to eq(1)
|
expect(february[:month].day).to eq(1) # Premier du mois
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'avec paramètres par défaut' do
|
context 'avec paramètres par défaut' do
|
||||||
it "utilise le premier mood comme start_date et aujourd'hui comme end_date" do
|
it 'utilise le premier mood comme start_date et aujourd\'hui comme end_date' do
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(user)
|
result = MoodCalendarService.generate_calendar(Mood.all)
|
||||||
|
|
||||||
expect(result).not_to be_empty
|
expect(result).not_to be_empty
|
||||||
|
|
||||||
|
# Devrait commencer en février
|
||||||
expect(result.first[:month]).to eq(Date.parse("2025-02-01"))
|
expect(result.first[:month]).to eq(Date.parse("2025-02-01"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'avec start_date spécifié' do
|
context 'avec start_date spécifié' do
|
||||||
it 'commence à la date spécifiée' do
|
it 'commence à la date spécifiée' do
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
Mood.all,
|
||||||
start_date: Date.parse("2025-03-01"),
|
start_date: Date.parse("2025-03-01"),
|
||||||
end_date: Date.parse("2025-03-31")
|
end_date: Date.parse("2025-03-31")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Devrait commencer en mars même si le mood est en février
|
||||||
expect(result.first[:month]).to eq(Date.parse("2025-03-01"))
|
expect(result.first[:month]).to eq(Date.parse("2025-03-01"))
|
||||||
|
|
||||||
|
# Ne devrait pas contenir février
|
||||||
expect(result.any? { |m| m[:month] == Date.parse("2025-02-01") }).to be false
|
expect(result.any? { |m| m[:month] == Date.parse("2025-02-01") }).to be false
|
||||||
|
|
||||||
|
# Tous les jours de mars devraient avoir guess: "heureux" (du 15 février)
|
||||||
|
all_march_days = result.first[:weeks].flatten
|
||||||
|
puts all_march_days.inspect
|
||||||
|
expect(all_march_days.all? { |d| d[:guess] == "heureux" }).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'avec end_date spécifié' do
|
context 'avec end_date spécifié' do
|
||||||
it 'se termine à end_date' do
|
it 'se termine à la date spécifiée' do
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
Mood.all,
|
||||||
end_date: Date.parse("2025-09-30")
|
end_date: Date.parse("2025-02-28")
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(result.last[:month]).to eq(Date.parse("2025-09-01"))
|
# Devrait s'arrêter en février
|
||||||
|
expect(result.last[:month]).to eq(Date.parse("2025-02-01"))
|
||||||
|
|
||||||
|
# Ne devrait pas contenir mars ou au-delà
|
||||||
|
expect(result.any? { |m| m[:month] == Date.parse("2025-03-01") }).to be false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'avec start_date et end_date spécifiés' do
|
context 'avec start_date et end_date spécifiés' do
|
||||||
it 'génère le calendrier pour la plage spécifiée' do
|
it 'génère le calendrier pour la plage spécifiée' do
|
||||||
mode2 = create(:mode, user: user)
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-04-20 10:00:00"))
|
||||||
create(:mood, user: user, mode: mode2, recorded_at: Time.zone.parse("2025-04-20 10:00:00"))
|
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
Mood.all,
|
||||||
start_date: Date.parse("2025-03-01"),
|
start_date: Date.parse("2025-03-01"),
|
||||||
end_date: Date.parse("2025-03-31")
|
end_date: Date.parse("2025-03-31")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Devrait contenir uniquement mars
|
||||||
expect(result.length).to eq(1)
|
expect(result.length).to eq(1)
|
||||||
expect(result.first[:month]).to eq(Date.parse("2025-03-01"))
|
expect(result.first[:month]).to eq(Date.parse("2025-03-01"))
|
||||||
|
|
||||||
|
# Tous les jours de mars devraient avoir guess: "heureux" (du 15 février)
|
||||||
all_days = result.first[:weeks].flatten
|
all_days = result.first[:weeks].flatten
|
||||||
expect(all_days.all? { |d| d[:mode].nil? }).to be true
|
expect(all_days.all? { |d| d[:mode].nil? }).to be true
|
||||||
|
expect(all_days.all? { |d| d[:guess] == "heureux" }).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'gère une plage de plusieurs mois' do
|
it 'gère une plage de plusieurs mois' do
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
Mood.all,
|
||||||
start_date: Date.parse("2025-02-01"),
|
start_date: Date.parse("2025-02-01"),
|
||||||
end_date: Date.parse("2025-04-30")
|
end_date: Date.parse("2025-04-30")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Devrait contenir février, mars et avril
|
||||||
months = result.map { |m| m[:month] }
|
months = result.map { |m| m[:month] }
|
||||||
expect(months).to include(
|
expect(months).to include(
|
||||||
Date.parse("2025-02-01"),
|
Date.parse("2025-02-01"),
|
||||||
|
|
@ -235,10 +334,11 @@ RSpec.describe MoodCalendarService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'fonctionne avec DateTime en paramètres' do
|
it 'fonctionne avec DateTime en paramètres' do
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
|
|
||||||
|
# Passer des DateTime au lieu de Date
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
Mood.all,
|
||||||
start_date: Time.zone.parse("2025-02-01 00:00:00"),
|
start_date: Time.zone.parse("2025-02-01 00:00:00"),
|
||||||
end_date: Time.zone.parse("2025-02-28 23:59:59")
|
end_date: Time.zone.parse("2025-02-28 23:59:59")
|
||||||
)
|
)
|
||||||
|
|
@ -250,109 +350,72 @@ RSpec.describe MoodCalendarService do
|
||||||
|
|
||||||
context 'avec une plage ne contenant aucun mood' do
|
context 'avec une plage ne contenant aucun mood' do
|
||||||
it 'génère un calendrier avec tous les jours à nil et guess du dernier mood avant la plage' do
|
it 'génère un calendrier avec tous les jours à nil et guess du dernier mood avant la plage' do
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
Mood.all,
|
||||||
start_date: Date.parse("2025-04-01"),
|
start_date: Date.parse("2025-04-01"),
|
||||||
end_date: Date.parse("2025-04-30")
|
end_date: Date.parse("2025-04-30")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Devrait contenir avril
|
||||||
expect(result.first[:month]).to eq(Date.parse("2025-04-01"))
|
expect(result.first[:month]).to eq(Date.parse("2025-04-01"))
|
||||||
|
|
||||||
|
# Tous les jours devraient avoir mode: nil et guess: "heureux"
|
||||||
all_days = result.first[:weeks].flatten
|
all_days = result.first[:weeks].flatten
|
||||||
expect(all_days.all? { |d| d[:mode].nil? }).to be true
|
expect(all_days.all? { |d| d[:mode].nil? }).to be true
|
||||||
|
expect(all_days.all? { |d| d[:guess] == "heureux" }).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'avec plusieurs mois' do
|
context 'avec plusieurs mois' do
|
||||||
it 'chaque mois commence et finit correctement' do
|
it 'chaque mois commence et finit correctement' do
|
||||||
mode2 = create(:mode, user: user)
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "calme", recorded_at: Time.zone.parse("2025-03-20 10:00:00"))
|
||||||
create(:mood, user: user, mode: mode2, recorded_at: Time.zone.parse("2025-03-20 10:00:00"))
|
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
Mood.all,
|
||||||
end_date: Date.parse("2025-03-31")
|
end_date: Date.parse("2025-03-31")
|
||||||
)
|
)
|
||||||
|
|
||||||
february = result.find { |m| m[:month] == Date.parse("2025-02-01") }
|
february = result.find { |m| m[:month] == Date.parse("2025-02-01") }
|
||||||
march = result.find { |m| m[:month] == Date.parse("2025-03-01") }
|
march = result.find { |m| m[:month] == Date.parse("2025-03-01") }
|
||||||
|
|
||||||
|
# Février se termine le dimanche 2 mars
|
||||||
last_day_february = february[:weeks].flatten.last
|
last_day_february = february[:weeks].flatten.last
|
||||||
expect(last_day_february[:recorded_at].to_date).to eq(Date.parse("2025-03-02"))
|
expect(last_day_february[:recorded_at].to_date).to eq(Date.parse("2025-03-02"))
|
||||||
|
|
||||||
|
# Mars commence le lundi 3 mars
|
||||||
first_day_march = march[:weeks].flatten.first
|
first_day_march = march[:weeks].flatten.first
|
||||||
expect(first_day_march[:recorded_at].to_date).to eq(Date.parse("2025-03-03"))
|
expect(first_day_march[:recorded_at].to_date).to eq(Date.parse("2025-03-03"))
|
||||||
|
|
||||||
|
# Pas de chevauchement
|
||||||
expect(last_day_february[:recorded_at].to_date + 1.day).to eq(first_day_march[:recorded_at].to_date)
|
expect(last_day_february[:recorded_at].to_date + 1.day).to eq(first_day_march[:recorded_at].to_date)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'avec un scope spécifique' do
|
context 'avec un scope spécifique' do
|
||||||
it 'fonctionne avec des moods filtrés par user' do
|
it 'fonctionne avec des moods filtrés par user' do
|
||||||
|
user1 = create(:user)
|
||||||
user2 = create(:user)
|
user2 = create(:user)
|
||||||
mode2 = create(:mode, user: user2)
|
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
create(:mood, mode: "heureux", recorded_at: Time.zone.parse("2025-02-15 10:00:00"), user: user1)
|
||||||
create(:mood, user: user2, mode: mode2, recorded_at: Time.zone.parse("2025-02-16 10:00:00"))
|
create(:mood, mode: "triste", recorded_at: Time.zone.parse("2025-02-16 10:00:00"), user: user2)
|
||||||
|
|
||||||
|
moods = Mood.where(user: user1)
|
||||||
result = MoodCalendarService.generate_calendar(
|
result = MoodCalendarService.generate_calendar(
|
||||||
user,
|
moods,
|
||||||
end_date: Date.parse("2025-02-28")
|
end_date: Date.parse("2025-02-28")
|
||||||
)
|
)
|
||||||
|
|
||||||
all_days = result[0][:weeks].flatten
|
all_days = result[0][:weeks].flatten
|
||||||
mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
|
mood_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
|
||||||
expect(mood_15[:mode]).to eq(mode)
|
expect(mood_15[:mode]).to eq("heureux")
|
||||||
|
|
||||||
|
# Le mood du user2 ne devrait pas apparaître
|
||||||
mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") }
|
mood_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") }
|
||||||
expect(mood_16[:mode]).to be_nil
|
expect(mood_16[:mode]).to be_nil
|
||||||
expect(mood_16[:guess]).to eq(mode)
|
expect(mood_16[:guess]).to eq("heureux")
|
||||||
end
|
|
||||||
end
|
|
||||||
context 'avec day_log' do
|
|
||||||
it 'inclut le day_log dans les données du jour correspondant' do
|
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
|
||||||
day_log = create(:day_log, user: user, day: Date.parse("2025-02-15"), info: "Une belle journée")
|
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(user, end_date: Date.parse("2025-02-28"))
|
|
||||||
|
|
||||||
all_days = result.find { |m| m[:month] == Date.parse("2025-02-01") }[:weeks].flatten
|
|
||||||
day_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
|
|
||||||
expect(day_15[:day_log]).to eq(day_log)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'retourne nil pour day_log quand il n\'en existe pas pour ce jour' do
|
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(user, end_date: Date.parse("2025-02-28"))
|
|
||||||
|
|
||||||
all_days = result.find { |m| m[:month] == Date.parse("2025-02-01") }[:weeks].flatten
|
|
||||||
day_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
|
|
||||||
expect(day_15[:day_log]).to be_nil
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'inclut le day_log pour un jour sans mood' do
|
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
|
||||||
day_log = create(:day_log, user: user, day: Date.parse("2025-02-16"), info: "Jour sans mood")
|
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(user, end_date: Date.parse("2025-02-28"))
|
|
||||||
|
|
||||||
all_days = result.find { |m| m[:month] == Date.parse("2025-02-01") }[:weeks].flatten
|
|
||||||
day_16 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-16") }
|
|
||||||
expect(day_16[:mode]).to be_nil
|
|
||||||
expect(day_16[:day_log]).to eq(day_log)
|
|
||||||
end
|
|
||||||
it 'inclut le day_log pour un jour avec mood' do
|
|
||||||
create(:mood, user: user, mode: mode, recorded_at: Time.zone.parse("2025-02-15 10:00:00"))
|
|
||||||
day_log = create(:day_log, user: user, day: Date.parse("2025-02-15"), info: "Jour avec mood")
|
|
||||||
|
|
||||||
result = MoodCalendarService.generate_calendar(user, end_date: Date.parse("2025-02-28"))
|
|
||||||
|
|
||||||
all_days = result.find { |m| m[:month] == Date.parse("2025-02-01") }[:weeks].flatten
|
|
||||||
day_15 = all_days.find { |m| m[:recorded_at].to_date == Date.parse("2025-02-15") }
|
|
||||||
expect(day_15[:mode]).to eq(mode)
|
|
||||||
expect(day_15[:day_log]).to eq(day_log)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ braces@^3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.1.1"
|
fill-range "^7.1.1"
|
||||||
|
|
||||||
|
bulma@^1.0.4:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz"
|
||||||
|
integrity sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==
|
||||||
|
|
||||||
chokidar@^4.0.0:
|
chokidar@^4.0.0:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz"
|
resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz"
|
||||||
|
|
|
||||||