Compare commits

..

No commits in common. "main" and "with-bulma" have entirely different histories.

124 changed files with 472 additions and 4716 deletions

View file

@ -18,7 +18,7 @@ gem "stimulus-rails"
gem "jbuilder"
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
gem "bcrypt", "~> 3.1.7"
# gem "bcrypt", "~> 3.1.7"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]
@ -51,7 +51,6 @@ group :development, :test do
gem "rubocop-rails-omakase", require: false
gem "rspec-rails", "~> 8.0.0"
gem "factory_bot_rails"
end
group :development do
@ -59,7 +58,6 @@ group :development do
gem "web-console"
gem "solargraph", require: false
gem "solargraph-rails", require: false
gem "letter_opener_web"
end
group :test do
@ -70,5 +68,3 @@ end
gem "cssbundling-rails", "~> 1.4"
gem "rails-i18n"
gem "haml"

View file

@ -1,29 +1,29 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.4)
actionpack (= 8.0.4)
activesupport (= 8.0.4)
actioncable (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.4)
actionpack (= 8.0.4)
activejob (= 8.0.4)
activerecord (= 8.0.4)
activestorage (= 8.0.4)
activesupport (= 8.0.4)
actionmailbox (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
mail (>= 2.8.0)
actionmailer (8.0.4)
actionpack (= 8.0.4)
actionview (= 8.0.4)
activejob (= 8.0.4)
activesupport (= 8.0.4)
actionmailer (8.0.2)
actionpack (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activesupport (= 8.0.2)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.4)
actionview (= 8.0.4)
activesupport (= 8.0.4)
actionpack (8.0.2)
actionview (= 8.0.2)
activesupport (= 8.0.2)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@ -31,35 +31,35 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.4)
actionpack (= 8.0.4)
activerecord (= 8.0.4)
activestorage (= 8.0.4)
activesupport (= 8.0.4)
actiontext (8.0.2)
actionpack (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.4)
activesupport (= 8.0.4)
actionview (8.0.2)
activesupport (= 8.0.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.4)
activesupport (= 8.0.4)
activejob (8.0.2)
activesupport (= 8.0.2)
globalid (>= 0.3.6)
activemodel (8.0.4)
activesupport (= 8.0.4)
activerecord (8.0.4)
activemodel (= 8.0.4)
activesupport (= 8.0.4)
activemodel (8.0.2)
activesupport (= 8.0.2)
activerecord (8.0.2)
activemodel (= 8.0.2)
activesupport (= 8.0.2)
timeout (>= 0.4.0)
activestorage (8.0.4)
actionpack (= 8.0.4)
activejob (= 8.0.4)
activerecord (= 8.0.4)
activesupport (= 8.0.4)
activestorage (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activesupport (= 8.0.2)
marcel (~> 1.0)
activesupport (8.0.4)
activesupport (8.0.2)
base64
benchmark (>= 0.3)
bigdecimal
@ -72,19 +72,18 @@ GEM
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.3)
backport (1.2.0)
base64 (0.3.0)
bcrypt (3.1.21)
bcrypt_pbkdf (1.1.2)
benchmark (0.5.0)
bigdecimal (4.0.1)
bcrypt_pbkdf (1.1.1)
benchmark (0.4.1)
bigdecimal (3.2.2)
bindex (0.8.1)
bootsnap (1.23.0)
bootsnap (1.18.6)
msgpack (~> 1.2)
brakeman (8.0.2)
brakeman (7.0.2)
racc
builder (3.3.0)
capybara (3.40.0)
@ -96,57 +95,45 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
childprocess (5.1.0)
logger (~> 1.5)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
crass (1.0.6)
cssbundling-rails (1.4.3)
railties (>= 6.0.0)
date (3.5.1)
debug (1.11.1)
date (3.4.1)
debug (1.11.0)
irb (~> 1.10)
reline (>= 0.3.8)
diff-lcs (1.6.2)
dotenv (3.2.0)
dotenv (3.1.8)
drb (2.2.3)
ed25519 (1.4.0)
erb (6.0.1)
erb (5.0.1)
erubi (1.13.1)
et-orbi (1.4.0)
et-orbi (1.2.11)
tzinfo
factory_bot (6.5.6)
activesupport (>= 6.1.0)
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
fugit (1.12.1)
et-orbi (~> 1.4)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
globalid (1.3.0)
globalid (1.2.1)
activesupport (>= 6.1)
haml (7.2.0)
temple (>= 0.8.2)
thor
tilt
i18n (1.14.8)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
importmap-rails (2.2.3)
importmap-rails (2.1.0)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.8.2)
irb (1.17.0)
io-console (0.8.0)
irb (1.15.2)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jaro_winkler (1.7.0)
jbuilder (2.14.1)
actionview (>= 7.0.0)
activesupport (>= 7.0.0)
json (2.18.1)
kamal (2.10.1)
jaro_winkler (1.6.1)
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.12.2)
kamal (2.7.0)
activesupport (>= 7.0)
base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0)
@ -157,40 +144,27 @@ GEM
sshkit (>= 1.23.0, < 2.0)
thor (~> 1.3)
zeitwerk (>= 2.6.18, < 3.0)
kramdown (2.5.2)
rexml (>= 3.4.4)
kramdown (2.5.1)
rexml (>= 3.3.9)
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
language_server-protocol (3.17.0.5)
launchy (3.1.1)
addressable (~> 2.8)
childprocess (~> 5.0)
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (3.0.0)
actionmailer (>= 6.1)
letter_opener (~> 1.9)
railties (>= 6.1)
rexml
lint_roller (1.1.0)
logger (1.7.0)
loofah (2.25.0)
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.9.0)
logger
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.1.0)
marcel (1.0.4)
matrix (0.4.3)
mini_mime (1.1.5)
minitest (6.0.1)
prism (~> 1.5)
minitest (5.25.5)
msgpack (1.8.0)
net-imap (0.6.3)
net-imap (0.5.9)
date
net-protocol
net-pop (0.1.2)
@ -204,64 +178,64 @@ GEM
net-smtp (0.5.1)
net-protocol
net-ssh (7.3.0)
nio4r (2.7.5)
nokogiri (1.19.1-aarch64-linux-gnu)
nio4r (2.7.4)
nokogiri (1.18.8-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.1-aarch64-linux-musl)
nokogiri (1.18.8-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.19.1-arm-linux-gnu)
nokogiri (1.18.8-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.1-arm-linux-musl)
nokogiri (1.18.8-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-gnu)
nokogiri (1.18.8-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.1-x86_64-linux-musl)
nokogiri (1.18.8-x86_64-linux-musl)
racc (~> 1.4)
observer (0.1.2)
open3 (0.2.1)
ostruct (0.6.3)
ostruct (0.6.2)
parallel (1.27.0)
parser (3.3.10.2)
parser (3.3.8.0)
ast (~> 2.4.1)
racc
pp (0.6.3)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
propshaft (1.3.1)
prism (1.4.0)
propshaft (1.1.0)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
psych (5.3.1)
railties (>= 7.0.0)
psych (5.2.6)
date
stringio
public_suffix (7.0.2)
puma (7.2.0)
public_suffix (6.0.2)
puma (6.6.0)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.2.5)
rack (3.1.16)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.3.1)
rackup (2.2.1)
rack (>= 3)
rails (8.0.4)
actioncable (= 8.0.4)
actionmailbox (= 8.0.4)
actionmailer (= 8.0.4)
actionpack (= 8.0.4)
actiontext (= 8.0.4)
actionview (= 8.0.4)
activejob (= 8.0.4)
activemodel (= 8.0.4)
activerecord (= 8.0.4)
activestorage (= 8.0.4)
activesupport (= 8.0.4)
rails (8.0.2)
actioncable (= 8.0.2)
actionmailbox (= 8.0.2)
actionmailer (= 8.0.2)
actionpack (= 8.0.2)
actiontext (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activemodel (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
bundler (>= 1.15.0)
railties (= 8.0.4)
railties (= 8.0.2)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
@ -272,39 +246,36 @@ GEM
rails-i18n (8.1.0)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
railties (8.0.4)
actionpack (= 8.0.4)
activesupport (= 8.0.4)
railties (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.1)
rbs (3.10.3)
rake (13.3.0)
rbs (3.9.5)
logger
tsort
rdoc (7.2.0)
rdoc (6.14.2)
erb
psych (>= 4.0.0)
tsort
regexp_parser (2.11.3)
reline (0.6.3)
regexp_parser (2.10.0)
reline (0.6.1)
io-console (~> 0.5)
reverse_markdown (3.0.2)
reverse_markdown (3.0.0)
nokogiri
rexml (3.4.4)
rspec-core (3.13.6)
rexml (3.4.1)
rspec-core (3.13.5)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.7)
rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (8.0.3)
rspec-rails (8.0.2)
actionpack (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
@ -312,8 +283,8 @@ GEM
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.7)
rubocop (1.84.2)
rspec-support (3.13.6)
rubocop (1.77.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@ -321,17 +292,17 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.49.0, < 2.0)
rubocop-ast (>= 1.45.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.0)
rubocop-ast (1.45.1)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-performance (1.26.1)
prism (~> 1.4)
rubocop-performance (1.25.0)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
rubocop-rails (2.34.3)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.32.0)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
@ -342,26 +313,24 @@ GEM
rubocop-performance (>= 1.24)
rubocop-rails (>= 2.30)
ruby-progressbar (1.13.0)
rubyzip (3.2.2)
rubyzip (2.4.1)
securerandom (0.4.1)
selenium-webdriver (4.41.0)
selenium-webdriver (4.34.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
solargraph (0.58.2)
ast (~> 2.4.3)
solargraph (0.57.0)
backport (~> 1.2)
benchmark (~> 0.4)
bundler (>= 2.0)
bundler (~> 2.0)
diff-lcs (~> 1.4)
jaro_winkler (~> 1.6, >= 1.6.1)
kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.1)
logger (~> 1.6)
observer (~> 0.1)
open3 (~> 0.2.1)
ostruct (~> 0.6)
parser (~> 3.0)
prism (~> 1.4)
@ -373,32 +342,32 @@ GEM
yard (~> 0.9, >= 0.9.24)
yard-activesupport-concern (~> 0.0)
yard-solargraph (~> 0.1)
solargraph-rails (1.3)
solargraph-rails (1.2.4)
activesupport
solargraph (>= 0.48.0)
solid_cable (3.0.12)
solargraph (>= 0.48.0, <= 0.57)
solid_cable (3.0.11)
actioncable (>= 7.2)
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_cache (1.0.10)
solid_cache (1.0.7)
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_queue (1.3.2)
solid_queue (1.1.5)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
fugit (~> 1.11)
fugit (~> 1.11.0)
railties (>= 7.1)
thor (>= 1.3.1)
sqlite3 (2.9.0-aarch64-linux-gnu)
sqlite3 (2.9.0-aarch64-linux-musl)
sqlite3 (2.9.0-arm-linux-gnu)
sqlite3 (2.9.0-arm-linux-musl)
sqlite3 (2.9.0-x86_64-linux-gnu)
sqlite3 (2.9.0-x86_64-linux-musl)
sshkit (1.25.0)
thor (~> 1.3.1)
sqlite3 (2.7.2-aarch64-linux-gnu)
sqlite3 (2.7.2-aarch64-linux-musl)
sqlite3 (2.7.2-arm-linux-gnu)
sqlite3 (2.7.2-arm-linux-musl)
sqlite3 (2.7.2-x86_64-linux-gnu)
sqlite3 (2.7.2-x86_64-linux-musl)
sshkit (1.24.0)
base64
logger
net-scp (>= 1.1.2)
@ -407,24 +376,22 @@ GEM
ostruct
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.2.0)
temple (0.10.4)
thor (1.5.0)
thruster (0.1.18)
thruster (0.1.18-aarch64-linux)
thruster (0.1.18-x86_64-linux)
tilt (2.7.0)
timeout (0.6.0)
tsort (0.2.0)
turbo-rails (2.0.23)
stringio (3.1.7)
thor (1.3.2)
thruster (0.1.14)
thruster (0.1.14-aarch64-linux)
thruster (0.1.14-x86_64-linux)
tilt (2.6.1)
timeout (0.4.3)
turbo-rails (2.0.16)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
uri (1.1.1)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
useragent (0.16.11)
web-console (4.2.1)
actionview (>= 6.0.0)
@ -438,12 +405,12 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.38)
yard (0.9.37)
yard-activesupport-concern (0.0.1)
yard (>= 0.8)
yard-solargraph (0.1.0)
yard (~> 0.9)
zeitwerk (2.7.5)
zeitwerk (2.7.3)
PLATFORMS
aarch64-linux
@ -456,18 +423,14 @@ PLATFORMS
x86_64-linux-musl
DEPENDENCIES
bcrypt (~> 3.1.7)
bootsnap
brakeman
capybara
cssbundling-rails (~> 1.4)
debug
factory_bot_rails
haml
importmap-rails
jbuilder
kamal
letter_opener_web
propshaft
puma (>= 5.0)
rails (~> 8.0.2)

View file

@ -1,2 +1,2 @@
web: env RUBY_DEBUG_OPEN=true bin/rails server
css: node_modules/yarn/bin/yarn build:css --watch
css: yarn build:css --watch

View file

@ -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.

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View file

@ -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; }
}

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

View file

@ -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';

View file

@ -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); }
}

View file

@ -0,0 +1,99 @@
/*
* 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;
}
.tracker {
align-content: center;
}
.logs {
height: 60vh;
overflow: scroll;
scrollbar-width: none;
}
.current-day {
background-color: white;
position: sticky;
top: 0;
align-content: center;
}
.bar-afond {
background: black;
width: 10px;
height: 10px;
}
.bar-creatif {
background: red;
width: 10px;
height: 10px;
}
.bar-frigo {
background: gray;
width: 10px;
height: 10px;
}
.bar-croisiere {
background: green;
width: 10px;
height: 10px;
}
.bar-en-charge {
background: orange;
width: 10px;
height: 10px;
}
.day {
border: 1px;
margin: 4px;
width: 15px;
height: 15px;
}
.creatif {
background-color: red;
}
.en-charge {
background-color: orange;
}
.frigo-vide {
background-color: grey;
}
.croisiere {
background-color: green;
}
.afond {
background-color: black;
}
.info {
margin: 30px;
}

View file

@ -1,5 +1,4 @@
class ApplicationController < ActionController::Base
include Authentication
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
# allow_browser versions: :modern
end

View file

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

View file

@ -1,57 +0,0 @@
module Authentication
extend ActiveSupport::Concern
included do
before_action :require_authentication
helper_method :authenticated?
helper_method :current_user
end
class_methods do
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
end
end
private
def authenticated?
resume_session
end
def current_user
authenticated?&.user
end
def require_authentication
resume_session || request_authentication
end
def resume_session
Current.session ||= find_session_by_cookie
end
def find_session_by_cookie
Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
end
def request_authentication
session[:return_to_after_authenticating] = request.url
redirect_to new_session_path
end
def after_authentication_url
session.delete(:return_to_after_authenticating) || root_url
end
def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
Current.session = session
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
end
end
def terminate_session
Current.session.destroy
cookies.delete(:session_id)
end
end

View file

@ -1,50 +0,0 @@
class DayLogsController < ApplicationController
before_action :set_day_log, only: [ :update ]
def edit
@day_log = Current.user.day_logs.find_or_initialize_by(day: params[:day])
@mood = Current.user.moods.find_by(recorded_at: params[:day].to_date.beginning_of_day..params[:day].to_date.end_of_day)
end
def create
@day_log = Current.user.day_logs.build(day_log_params)
if @day_log.save
handle_mood(@day_log.day, params[:day_log][:mode_id])
redirect_to dashboard_path(day: @day_log.day)
else
render :new, status: :unprocessable_content
end
end
def update
if @day_log.update(day_log_params)
handle_mood(@day_log.day, params[:day_log][:mode_id])
redirect_to dashboard_path(day: @day_log.day)
else
render :edit, status: :unprocessable_content
end
end
private
def set_day_log
@day_log = Current.user.day_logs.find(params[:id])
end
def handle_mood(day, mode_id)
return if mode_id.blank?
mood = Current.user.moods.find_by(recorded_at: day.beginning_of_day..day.end_of_day)
if mood
mood.update(mode_id: mode_id)
else
Current.user.moods.create(mode_id: mode_id, recorded_at: day.to_datetime)
end
end
def day_log_params
params.expect(day_log: [ :day, :info ])
end
end

View file

@ -1,12 +0,0 @@
class HomeController < ApplicationController
allow_unauthenticated_access
def index
@user = User.find_by(username: request.subdomain)
if @user&.public?
render "moods/public"
else
redirect_to dashboard_path
end
end
end

View file

@ -1,25 +0,0 @@
class InvitationsController < ApplicationController
before_action :set_user_by_token, only: %i[ edit update ]
allow_unauthenticated_access
def edit
end
def update
password_params = params.expect(user: [ :password, :password_confirmation ])
if @user.update(password_params.merge(invitation_accepted_at: Time.current))
redirect_to root_path, notice: "Compte activé. Bienvenue !"
else
redirect_to invitation_path(params[:token]), alert: "Les deux mots de passe ne sont pas identiques."
end
end
private
def set_user_by_token
@user = User.find_by_invitation_token(params[:token])
rescue ActiveSupport::MessageVerifier::InvalidSignature
redirect_to root_path, alert: "Ce lien d’invitation n’est plus valable."
end
end

View file

@ -1,6 +1,6 @@
class MoodsController < ApplicationController
def index
@user = Current.user
redirect_to dashboard_path(day: Date.today) unless params[:day]
@mode = Mood.last&.mode || "croisiere"
@mood_log = Mood.history_for_a_year || []
end
end

View file

@ -1,33 +0,0 @@
class PasswordsController < ApplicationController
allow_unauthenticated_access
before_action :set_user_by_token, only: %i[ edit update ]
def new
end
def create
if user = User.find_by(email_address: params[:email_address])
PasswordsMailer.reset(user).deliver_later
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é."
end
def edit
end
def update
if @user.update(params.permit(:password, :password_confirmation))
redirect_to new_session_path, notice: "Mot de passe changé. Vous pouvez vous connecter."
else
redirect_to edit_password_path(params[:token]), alert: "Les deux mots de passe ne sont pas identiques."
end
end
private
def set_user_by_token
@user = User.find_by_password_reset_token!(params[:token])
rescue ActiveSupport::MessageVerifier::InvalidSignature
redirect_to new_password_path, alert: "Ce lien n’est plus valable. Demandez-en un nouveau."
end
end

View file

@ -1,10 +1,9 @@
class RfidTagsController < ApplicationController
skip_forgery_protection
allow_unauthenticated_access
def create
rfid_tag_params = params.expect(rfid_tag: [ :chip_id, :identifier ])
rfid_tag = RfidTag.find_or_initialize_by(chip_id: rfid_tag_params[:chip_id], identifier: rfid_tag_params[:identifier])
identifier = params.expect(:identifier)
rfid_tag = RfidTag.find_or_initialize_by(identifier:)
if rfid_tag.new_record?
register rfid_tag
return
@ -16,14 +15,13 @@ class RfidTagsController < ApplicationController
private
def register(rfid_tag)
rfid_tag.user = RfidTag.where(chip_id: rfid_tag.chip_id).first&.user
rfid_tag.save!
head :created, code: :registered
end
def create_mood(rfid_tag)
if rfid_tag.mode
Mood.create!(recorded_at: DateTime.now, mode: rfid_tag.mode, user: rfid_tag.user)
Mood.create!(recorded_at: DateTime.now, mode: rfid_tag.mode)
head :created, code: :recorded
else
head :unprocessable_entity

View file

@ -1,21 +0,0 @@
class SessionsController < ApplicationController
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." }
def new
end
def create
if user = User.authenticate_by(params.permit(:email_address, :password))
start_new_session_for user
redirect_to after_authentication_url
else
redirect_to new_session_path, alert: "Adresse mail ou mot de passe incorrect."
end
end
def destroy
terminate_session
redirect_to new_session_path
end
end

View file

@ -1,8 +1,2 @@
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

View file

@ -1,86 +1,2 @@
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)
case day_status(mood, user)
when :filled then mood[:mode]
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
tag.span(**options, "aria-hidden": true)
end
end
end

View file

@ -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")
})
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,15 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = [ "image", "modeDay", "modeDayMobile" ]
updateDayInfo(event) {
const image = this.imageTarget;
const modeDay = this.modeDayTarget;
const modeDayMobile = this.modeDayMobileTarget;
const modeDayContent = event.target.dataset.day + ' : ' + event.target.dataset.mode;
image.src = event.target.dataset.image;
modeDayMobile.textContent = modeDayContent;
modeDay.textContent = modeDayContent;
}
}

View file

@ -1,6 +0,0 @@
class PasswordsMailer < ApplicationMailer
def reset(user)
@user = user
mail subject: "Reset your password", to: user.email_address
end
end

View file

@ -1,8 +0,0 @@
class UserMailer < ApplicationMailer
def invitation_email(user, token)
@user = user
@invite_url = edit_invitation_url(token: token)
mail(to: @user.email_address, subject: "Vous êtes invité à rejoindre la kluk")
end
end

View file

@ -1,4 +0,0 @@
class Current < ActiveSupport::CurrentAttributes
attribute :session
delegate :user, to: :session, allow_nil: true
end

View file

@ -1,6 +0,0 @@
class DayLog < ApplicationRecord
belongs_to :user
validates :day, presence: true
validates :info, presence: true
end

View file

@ -1,8 +0,0 @@
class Mode < ApplicationRecord
belongs_to :user
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

View file

@ -1,19 +1,45 @@
class Mood < ApplicationRecord
belongs_to :user
belongs_to :mode
validates :recorded_at, presence: true
validate :unique_per_day_and_user
class << self
def history_for_a_year
history(Date.today - 1.year, Date.today)
end
private
def history(from, to)
return [] if Mood.count < 1
def unique_per_day_and_user
return unless recorded_at && user
first_monday = monday_of_the_week(first_mood_date(from))
existing = user.moods.where.not(id: id).any? do |mood|
mood.recorded_at.to_date == recorded_at.to_date
current_date = first_monday
current_mode = last_mode_before(current_date)
log_mood = []
mood_range = Mood.order(:recorded_at).where(recorded_at: (first_monday..to))
mood_range.each do |mood|
while current_date < mood.recorded_at.to_date do
log_mood << [ current_date.to_s, current_mode ]
current_date += 1
end
current_mode = mood.mode
end
errors.add(:recorded_at, :taken) if existing
while current_date <= to.to_date do
log_mood << [ current_date.to_s, current_mode ]
current_date += 1
end
log_mood.each_slice(7).to_a
end
def first_mood_date(from)
Mood.order(recorded_at: :asc).where("recorded_at >= ?", from).first.recorded_at.to_date
end
def monday_of_the_week(date)
date - date.wday + 1
end
def last_mode_before(date)
mood = Mood.where("recorded_at < ?", date).last
mood&.mode || "croisiere"
end
end
end

View file

@ -1,4 +1,2 @@
class RfidTag < ApplicationRecord
belongs_to :user, required: false
belongs_to :mode, required: false
end

View file

@ -1,3 +0,0 @@
class Session < ApplicationRecord
belongs_to :user
end

View file

@ -1,57 +0,0 @@
class User < ApplicationRecord
has_secure_password
has_many :sessions, dependent: :destroy
has_many :moods, -> { order "recorded_at" }
has_many :modes
has_many :day_logs
normalizes :email_address, with: ->(e) { e.strip.downcase }
generates_token_for :invitation, expires_in: 5.days do
invitation_accepted_at
end
def invite_user!(email)
invited_user = User.create!(
email_address: email,
password: SecureRandom.hex(16) # temporary password
)
# Send invitation email using the generated token
token = invited_user.generate_token_for(:invitation)
UserMailer.invitation_email(invited_user, token).deliver_later
invited_user
end
def self.find_by_invitation_token(token)
find_by_token_for(:invitation, token)
end
def invitation_token
generate_token_for(:invitation)
end
def history
MoodCalendarService.generate_calendar(self)
end
def current_mode
self.moods.last&.mode || "croisiere"
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

View file

@ -1,69 +0,0 @@
class MoodCalendarService
def self.generate_calendar(user, start_date: nil, end_date: nil)
data = user.moods.includes(:mode)
.order(:recorded_at)
.map { |mood| { mode: mood.mode, recorded_at: mood.recorded_at } }
day_logs_by_date = user.day_logs.index_by(&:day)
if data.empty?
start_date = Date.current
else
start_date ||= data.first[:recorded_at].to_date
end
start_date = start_date.to_date
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
else
end_date = end_date.to_date
end
=end
data_by_date = data.group_by { |d| d[:recorded_at].to_date }
.transform_values { |entries| entries.max_by { |e| e[:recorded_at] } }
last_mode = data.select { |d| d[:recorded_at].to_date < start_date }
.max_by { |d| d[:recorded_at] }
&.[](:mode)
complete_data = (start_date..end_date).map do |date|
day_log = day_logs_by_date[date]
if data_by_date[date]
last_mode = data_by_date[date][:mode]
data_by_date[date].merge(day_log: day_log)
else
{ mode: nil, recorded_at: date.to_datetime, guess: last_mode, day_log: day_log }
end
end
complete_data.group_by { |d| d[:recorded_at].to_date.beginning_of_month }
.map do |month_start, _|
first_monday = month_start
first_monday = first_monday.next_occurring(:monday) unless first_monday.monday?
next_month_start = month_start.next_month.beginning_of_month
next_first_monday = next_month_start
next_first_monday = next_first_monday.next_occurring(:monday) unless next_first_monday.monday?
month_end = next_first_monday - 1.day
data_hash = complete_data.index_by { |d| d[:recorded_at].to_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 }
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

View file

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

View file

@ -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 %>

View file

@ -1,9 +0,0 @@
<footer class="pied">
<div class="pied-interieur">
<p class="pied-phrase">Comment ça Kluk aujourd’hui&#8239;?</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>

View file

@ -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 %>

View file

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

View file

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

View file

@ -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" %>

View file

@ -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 %>

View file

@ -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" %>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
<%= render template: "day_logs/edit" %>

View file

@ -1,2 +0,0 @@
%article.texte
%h1 Comment ça KLUK ?

View file

@ -1,16 +0,0 @@
<% content_for :title, "Bienvenue – Kluk" %>
<article class="texte texte-etroit">
<%= render "charte/tasses" %>
<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| %>
<%= render "charte/champ", form: form, attribut: :password, libelle: "Mot de passe" do %>
<%= form.password_field :password, required: true, autocomplete: "new-password", maxlength: 72 %>
<% end %>
<%= 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", maxlength: 72 %>
<% end %>
<%= form.submit "Activer mon compte", class: "bouton" %>
<% end %>
</article>

View file

@ -1,26 +1,11 @@
<!DOCTYPE html>
<html lang="fr">
<html>
<head>
<title><%= content_for(:title) || "Kluk" %></title>
<title><%= content_for(:title) || "Moodie" %></title>
<meta charset="utf-8">
<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="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 %>
<%= csp_meta_tag %>
@ -29,11 +14,13 @@
<%# 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) %>
<%= 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">
<%= preload_link_tag "young-serif-400.woff2" %>
<%= preload_link_tag "atkinson-hyperlegible-next-400.woff2" %>
<link rel="preconnect" href="https://fonts.googleapis.com">
<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">
<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
@ -41,12 +28,6 @@
</head>
<body>
<a class="evitement" href="#contenu">Aller au contenu</a>
<%= render "entete" %>
<main id="contenu" tabindex="-1">
<%= render "flash" %>
<%= yield %>
</main>
<%= render "pied" %>
</body>
</html>

View file

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

View file

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

View file

@ -1,31 +1,64 @@
<% content_for :title, "Mes humeurs – Kluk" %>
<% history = @user.history %>
<% selected_date = (Date.parse(params[:day]) rescue Date.current) %>
<% selected = find_day(history, selected_date) || find_day(history, selected_date = Date.current) %>
<% status = day_status(selected, @user) if selected %>
<% mode = mode_for(selected, @user) if selected %>
<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 %>
<main data-controller="mood" class="columns m-auto">
<div class="current-day column">
<section class="m-4">
<h1 class="title is-3 is-spaced">Comment ça KLUK ?</h1>
<div class="is-hidden-tablet mb-4">
<div class="" data-mood-target="modeDayMobile">Aujourd'hui</div>
</div>
<aside class="detail" aria-live="polite" aria-label="Jour sélectionné">
<figure class="carte-humeur" data-calendrier-target="carte anime" style="<%= "--humeur: #{mode.color}" if mode %>">
<%= image_tag (mode || Mode.new).image_url, alt: "", data: { calendrier_target: "image" } %>
<figure class="image is-hidden-mobile has-ratio ">
<%= image_tag(@mode + ".jpg", "data-mood-target": "image") %>
</figure>
<div class="detail-texte" data-calendrier-target="anime">
<h2 class="detail-date" data-calendrier-target="date"><%= date_longue(selected_date) %></h2>
<p class="detail-mode">
<span data-calendrier-target="mode"><%= mode&.label || "Pas de tasse posée" %></span>
<span class="detail-statut" data-calendrier-target="statut"><%= libelle_statut(status) if mode %></span>
</p>
<% info = selected&.dig(:day_log)&.info %>
<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>
<%= link_to info ? "Modifier le journal" : "Écrire dans le journal", edit_day_log_for_day_path(day: selected_date),
class: "bouton bouton-petit", data: { calendrier_target: "lien" } %>
</section>
</div>
</aside>
</div>
<section class="section column has-background-primary-light is-flex is-flex-direction-column is-justify-content-space-around">
<div class="is-hidden-mobile mb-4">
<div class="title is-3" data-mood-target="modeDay">Aujourd'hui</div>
</div>
<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="bar-frigo mr-1"></div>
<div class="">Triste</div>
</div>
<div class="is-flex is-align-items-center mr-4">
<div class="bar-en-charge mr-1"></div>
<div class="">En charge</div>
</div>
<div class="is-flex is-align-items-center mr-4">
<div class="bar-croisiere mr-1"></div>
<div class="">Croisiere</div>
</div>
<div class="is-flex is-align-items-center mr-4">
<div class="bar-creatif mr-1"></div>
<div>Creatif</div>
</div>
<div class="is-flex is-align-items-center mr-4">
<div class="bar-afond mr-1"></div>
<div class="">A fond</div>
</div>
</div>
<div class="logs">
<div class="is-flex is-flex-wrap-wrap">
<% @mood_log.each do |week| %>
<div class="is-flex is-flex-direction-column is-flex-wrap-wrap mb-3">
<div class="is-size-7 day">
<% day = Date.parse(week.first[0]) %>
<% if day.day < 8 && day.monday? %>
<%= l(day, format: "%b") %>
<% end %>
</div>
<% week.each do |d| %>
<% day = Date.parse(d[0]) %>
<% if d[1] %>
<div data-image="<%= asset_path(d[1] + ".jpg") %>" data-mode="<%= d[1] %>" data-day="<%= d[0] %>" data-action="click->mood#updateDayInfo mouseover->mood#updateDayInfo mouseleave->mood#updateDayInfo" title="<%= d[0] %> : <%= d[1] %>" class="day <%= d[1] %>"></div>
<% else %>
<div class="day"></div>
<% end %>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
</section>
</main>

View file

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

View file

@ -1,14 +0,0 @@
<% content_for :title, "Nouveau mot de passe – Kluk" %>
<article class="texte texte-etroit">
<h1>Nouveau mot de passe</h1>
<%= form_with url: password_path(params[:token]), method: :put, class: "formulaire" do |form| %>
<%= render "charte/champ", form: form, attribut: :password, libelle: "Nouveau mot de passe" do %>
<%= form.password_field :password, required: true, autocomplete: "new-password", maxlength: 72 %>
<% end %>
<%= 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", maxlength: 72 %>
<% end %>
<%= form.submit "Enregistrer le mot de passe", class: "bouton" %>
<% end %>
</article>

View file

@ -1,13 +0,0 @@
<% content_for :title, "Mot de passe oublié – Kluk" %>
<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| %>
<%= 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 %>
<%= form.submit "Recevoir le lien", class: "bouton" %>
<p class="formulaire-secours"><%= link_to "Retour à la connexion", new_session_path %></p>
<% end %>
</article>

View file

@ -1,4 +0,0 @@
<p>
You can reset your password within the next 15 minutes on
<%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.
</p>

View file

@ -1,2 +0,0 @@
You can reset your password within the next 15 minutes on this password reset page:
<%= edit_password_url(@user.password_reset_token) %>

View file

@ -0,0 +1,2 @@
<h1>RfidTags#post</h1>
<p>Find me in app/views/rfid_tags/post.html.erb</p>

View file

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

View file

@ -1,3 +0,0 @@
<h1>You're invited!</h1>
<p>Click here to set up your account:</p>
<%= link_to "Set up account", @invite_url %>

View file

@ -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 (`&#8239;` fine, `&nbsp;` 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`.

View file

@ -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); }
}

View file

@ -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&nbsp;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&nbsp;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>&lt;noscript&gt;</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>

View file

@ -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.

Binary file not shown.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,9 +34,6 @@ Rails.application.configure do
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.perform_deliveries = true
# Make template changes take effect immediately.
config.action_mailer.perform_caching = false
@ -72,8 +69,4 @@ Rails.application.configure do
# Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
# config.generators.apply_rubocop_autocorrect_after_generate!
#
config.hosts << "localhost.localdomain:3000"
config.hosts << "robi.localhost.localdomain:3000"
config.hosts << "marie.localhost.localdomain:3000"
end

View file

@ -80,10 +80,10 @@ Rails.application.configure do
config.active_record.attributes_for_inspect = [ :id ]
# Enable DNS rebinding protection and other `Host` header attacks.
config.hosts = [
"kluk.fr",
/.*\.kluk\.fr/
]
# config.hosts = [
# "example.com", # Allow requests from example.com
# /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
# ]
#
# Skip DNS rebinding protection for the default health check endpoint.
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } }

View file

@ -1,7 +1,5 @@
Rails.application.routes.draw do
resource :session
resources :passwords, param: :token
post "/rfid_tags" => "rfid_tags#create"
post '/rfid_tags' => 'rfid_tags#create'
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
@ -12,17 +10,6 @@ Rails.application.routes.draw do
# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
get "/charte", to: "charte#show" if Rails.env.development?
# Defines the root path route ("/")
root "home#index"
get "/invite/:token", to: "invitations#edit", as: :edit_invitation
patch "/invite/:token", to: "invitations#update", as: :invitation
get "/moods", to: "moods#index", as: :dashboard
get "/day_logs/edit", to: "day_logs#edit", as: :edit_day_log_for_day
resources :day_logs, only: [ :create, :update ]
root "moods#index"
end

View file

@ -1,11 +0,0 @@
class CreateUsers < ActiveRecord::Migration[8.0]
def change
create_table :users do |t|
t.string :email_address, null: false
t.string :password_digest, null: false
t.timestamps
end
add_index :users, :email_address, unique: true
end
end

View file

@ -1,11 +0,0 @@
class CreateSessions < ActiveRecord::Migration[8.0]
def change
create_table :sessions do |t|
t.references :user, null: false, foreign_key: true
t.string :ip_address
t.string :user_agent
t.timestamps
end
end
end

View file

@ -1,5 +0,0 @@
class AddInvitationAcceptedAtToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :invitation_accepted_at, :datetime
end
end

View file

@ -1,6 +0,0 @@
class AddChipIdAndUsersToRfidTags < ActiveRecord::Migration[8.0]
def change
add_column :rfid_tags, :chip_id, :string
add_reference :rfid_tags, :user, foreign_key: true
end
end

View file

@ -1,5 +0,0 @@
class AddUserToMoods < ActiveRecord::Migration[8.0]
def change
add_reference :moods, :user, foreign_key: true
end
end

View file

@ -1,7 +0,0 @@
class AddUsernameToUsers < ActiveRecord::Migration[8.0]
def up
add_column :users, :username, :string
User.update_all(username: "temporary")
change_column_null :users, :username, false
end
end

View file

@ -1,5 +0,0 @@
class AddPublicToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :public, :boolean, default: false
end
end

View file

@ -1,5 +0,0 @@
class AddGuessToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :guess, :boolean, default: false
end
end

Some files were not shown because too many files have changed in this diff Show more