mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-29 04:55:11 +02:00
wip integration to review
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
<svg width="102" height="72" viewBox="0 0 102 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_13916_23662)">
|
||||
<g clip-path="url(#clip0_13916_23662)">
|
||||
<rect x="31.8" y="10.3999" width="38.4" height="51.2" rx="5.68889" fill="white"/>
|
||||
<path d="M31.7989 10.3999L69.7989 10.3999L69.7989 25.5999H31.7981L31.7989 10.3999Z" fill="#3677CC"/>
|
||||
<path d="M31.7989 10.3999L69.7989 10.3999L69.7989 25.5999H31.7981L31.7989 10.3999Z" fill="#F6F8F9" fill-opacity="0.975"/>
|
||||
<path d="M31.7989 10.3999L69.7989 10.3999L69.7989 25.5999H31.7981L31.7989 10.3999Z" fill="url(#paint0_linear_13916_23662)" fill-opacity="0.4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7602 25.5821C36.7602 24.7967 37.3969 24.1599 38.1824 24.1599H50.9824C51.7679 24.1599 52.4046 24.7967 52.4046 25.5821C52.4046 26.3676 51.7679 27.0043 50.9824 27.0043H38.1824C37.3969 27.0043 36.7602 26.3676 36.7602 25.5821Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7602 25.5821C36.7602 24.7967 37.3969 24.1599 38.1824 24.1599H50.9824C51.7679 24.1599 52.4046 24.7967 52.4046 25.5821C52.4046 26.3676 51.7679 27.0043 50.9824 27.0043H38.1824C37.3969 27.0043 36.7602 26.3676 36.7602 25.5821Z" fill="#F6F8F9" fill-opacity="0.15"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 31.662C36.7781 30.8765 37.4149 30.2397 38.2003 30.2397H63.8003C64.5858 30.2397 65.2226 30.8765 65.2226 31.662C65.2226 32.4474 64.5858 33.0842 63.8003 33.0842H38.2003C37.4149 33.0842 36.7781 32.4474 36.7781 31.662Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 31.662C36.7781 30.8765 37.4149 30.2397 38.2003 30.2397H63.8003C64.5858 30.2397 65.2226 30.8765 65.2226 31.662C65.2226 32.4474 64.5858 33.0842 63.8003 33.0842H38.2003C37.4149 33.0842 36.7781 32.4474 36.7781 31.662Z" fill="#F6F8F9" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 31.662C36.7781 30.8765 37.4149 30.2397 38.2003 30.2397H63.8003C64.5858 30.2397 65.2226 30.8765 65.2226 31.662C65.2226 32.4474 64.5858 33.0842 63.8003 33.0842H38.2003C37.4149 33.0842 36.7781 32.4474 36.7781 31.662Z" fill="url(#paint1_linear_13916_23662)" fill-opacity="0.35"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 36.462C36.7781 35.6765 37.4149 35.0397 38.2003 35.0397H63.8003C64.5858 35.0397 65.2226 35.6765 65.2226 36.462C65.2226 37.2474 64.5858 37.8842 63.8003 37.8842H38.2003C37.4149 37.8842 36.7781 37.2474 36.7781 36.462Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 36.462C36.7781 35.6765 37.4149 35.0397 38.2003 35.0397H63.8003C64.5858 35.0397 65.2226 35.6765 65.2226 36.462C65.2226 37.2474 64.5858 37.8842 63.8003 37.8842H38.2003C37.4149 37.8842 36.7781 37.2474 36.7781 36.462Z" fill="#F6F8F9" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 36.462C36.7781 35.6765 37.4149 35.0397 38.2003 35.0397H63.8003C64.5858 35.0397 65.2226 35.6765 65.2226 36.462C65.2226 37.2474 64.5858 37.8842 63.8003 37.8842H38.2003C37.4149 37.8842 36.7781 37.2474 36.7781 36.462Z" fill="url(#paint2_linear_13916_23662)" fill-opacity="0.35"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 41.262C36.7781 40.4765 37.4149 39.8397 38.2003 39.8397H63.8003C64.5858 39.8397 65.2226 40.4765 65.2226 41.262C65.2226 42.0474 64.5858 42.6842 63.8003 42.6842H38.2003C37.4149 42.6842 36.7781 42.0474 36.7781 41.262Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 41.262C36.7781 40.4765 37.4149 39.8397 38.2003 39.8397H63.8003C64.5858 39.8397 65.2226 40.4765 65.2226 41.262C65.2226 42.0474 64.5858 42.6842 63.8003 42.6842H38.2003C37.4149 42.6842 36.7781 42.0474 36.7781 41.262Z" fill="#F6F8F9" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 41.262C36.7781 40.4765 37.4149 39.8397 38.2003 39.8397H63.8003C64.5858 39.8397 65.2226 40.4765 65.2226 41.262C65.2226 42.0474 64.5858 42.6842 63.8003 42.6842H38.2003C37.4149 42.6842 36.7781 42.0474 36.7781 41.262Z" fill="url(#paint3_linear_13916_23662)" fill-opacity="0.35"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 46.062C36.7781 45.2765 37.4149 44.6397 38.2003 44.6397H63.8003C64.5858 44.6397 65.2226 45.2765 65.2226 46.062C65.2226 46.8474 64.5858 47.4842 63.8003 47.4842H38.2003C37.4149 47.4842 36.7781 46.8474 36.7781 46.062Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 46.062C36.7781 45.2765 37.4149 44.6397 38.2003 44.6397H63.8003C64.5858 44.6397 65.2226 45.2765 65.2226 46.062C65.2226 46.8474 64.5858 47.4842 63.8003 47.4842H38.2003C37.4149 47.4842 36.7781 46.8474 36.7781 46.062Z" fill="#F6F8F9" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 46.062C36.7781 45.2765 37.4149 44.6397 38.2003 44.6397H63.8003C64.5858 44.6397 65.2226 45.2765 65.2226 46.062C65.2226 46.8474 64.5858 47.4842 63.8003 47.4842H38.2003C37.4149 47.4842 36.7781 46.8474 36.7781 46.062Z" fill="url(#paint4_linear_13916_23662)" fill-opacity="0.35"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 50.862C36.7781 50.0765 37.4149 49.4397 38.2003 49.4397H63.8003C64.5858 49.4397 65.2226 50.0765 65.2226 50.862C65.2226 51.6474 64.5858 52.2842 63.8003 52.2842H38.2003C37.4149 52.2842 36.7781 51.6474 36.7781 50.862Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 50.862C36.7781 50.0765 37.4149 49.4397 38.2003 49.4397H63.8003C64.5858 49.4397 65.2226 50.0765 65.2226 50.862C65.2226 51.6474 64.5858 52.2842 63.8003 52.2842H38.2003C37.4149 52.2842 36.7781 51.6474 36.7781 50.862Z" fill="#F6F8F9" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 50.862C36.7781 50.0765 37.4149 49.4397 38.2003 49.4397H63.8003C64.5858 49.4397 65.2226 50.0765 65.2226 50.862C65.2226 51.6474 64.5858 52.2842 63.8003 52.2842H38.2003C37.4149 52.2842 36.7781 51.6474 36.7781 50.862Z" fill="url(#paint5_linear_13916_23662)" fill-opacity="0.35"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 55.662C36.7781 54.8765 37.4149 54.2397 38.2003 54.2397H63.8003C64.5858 54.2397 65.2226 54.8765 65.2226 55.662C65.2226 56.4474 64.5858 57.0842 63.8003 57.0842H38.2003C37.4149 57.0842 36.7781 56.4474 36.7781 55.662Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 55.662C36.7781 54.8765 37.4149 54.2397 38.2003 54.2397H63.8003C64.5858 54.2397 65.2226 54.8765 65.2226 55.662C65.2226 56.4474 64.5858 57.0842 63.8003 57.0842H38.2003C37.4149 57.0842 36.7781 56.4474 36.7781 55.662Z" fill="#F6F8F9" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 55.662C36.7781 54.8765 37.4149 54.2397 38.2003 54.2397H63.8003C64.5858 54.2397 65.2226 54.8765 65.2226 55.662C65.2226 56.4474 64.5858 57.0842 63.8003 57.0842H38.2003C37.4149 57.0842 36.7781 56.4474 36.7781 55.662Z" fill="url(#paint6_linear_13916_23662)" fill-opacity="0.35"/>
|
||||
</g>
|
||||
<rect x="32.36" y="10.9599" width="37.28" height="50.08" rx="5.12889" stroke="#3677CC" stroke-width="1.12"/>
|
||||
<rect x="32.36" y="10.9599" width="37.28" height="50.08" rx="5.12889" stroke="#F6F8F9" stroke-opacity="0.8" stroke-width="1.12"/>
|
||||
<rect x="32.36" y="10.9599" width="37.28" height="50.08" rx="5.12889" stroke="url(#paint7_linear_13916_23662)" stroke-opacity="0.2" stroke-width="1.12"/>
|
||||
</g>
|
||||
<path d="M51.2002 31.0498C51.6971 31.0498 52.1757 31.2112 52.5703 31.5029L52.7334 31.6357L52.7422 31.6436C54.7184 33.44 57.4571 34.7549 59.5127 34.7549C60.0918 34.7549 60.6394 34.9946 61.0371 35.4082C61.4335 35.8205 61.6503 36.3718 61.6504 36.9395V45.583C61.6504 48.9094 60.5202 51.4576 58.6973 53.4023C56.8979 55.322 54.4689 56.6063 51.9277 57.5273L51.9199 57.5303C51.4545 57.6943 50.9487 57.6876 50.4873 57.5146V57.5166C47.9393 56.6017 45.5066 55.3199 43.7051 53.4014C41.8801 51.4578 40.75 48.9093 40.75 45.583V36.9395C40.7501 36.3718 40.9668 35.8205 41.3633 35.4082C41.761 34.9946 42.3086 34.7549 42.8877 34.7549C44.9427 34.7549 47.6938 33.4287 49.6592 31.6436L49.667 31.6357C50.0891 31.2608 50.6325 31.0498 51.2002 31.0498Z" fill="#2F6ABB"/>
|
||||
<path d="M51.2002 31.0498C51.6971 31.0498 52.1757 31.2112 52.5703 31.5029L52.7334 31.6357L52.7422 31.6436C54.7184 33.44 57.4571 34.7549 59.5127 34.7549C60.0918 34.7549 60.6394 34.9946 61.0371 35.4082C61.4335 35.8205 61.6503 36.3718 61.6504 36.9395V45.583C61.6504 48.9094 60.5202 51.4576 58.6973 53.4023C56.8979 55.322 54.4689 56.6063 51.9277 57.5273L51.9199 57.5303C51.4545 57.6943 50.9487 57.6876 50.4873 57.5146V57.5166C47.9393 56.6017 45.5066 55.3199 43.7051 53.4014C41.8801 51.4578 40.75 48.9093 40.75 45.583V36.9395C40.7501 36.3718 40.9668 35.8205 41.3633 35.4082C41.761 34.9946 42.3086 34.7549 42.8877 34.7549C44.9427 34.7549 47.6938 33.4287 49.6592 31.6436L49.667 31.6357C50.0891 31.2608 50.6325 31.0498 51.2002 31.0498Z" fill="#F6F8F9" fill-opacity="0.25"/>
|
||||
<path d="M51.2002 31.0498C51.6971 31.0498 52.1757 31.2112 52.5703 31.5029L52.7334 31.6357L52.7422 31.6436C54.7184 33.44 57.4571 34.7549 59.5127 34.7549C60.0918 34.7549 60.6394 34.9946 61.0371 35.4082C61.4335 35.8205 61.6503 36.3718 61.6504 36.9395V45.583C61.6504 48.9094 60.5202 51.4576 58.6973 53.4023C56.8979 55.322 54.4689 56.6063 51.9277 57.5273L51.9199 57.5303C51.4545 57.6943 50.9487 57.6876 50.4873 57.5146V57.5166C47.9393 56.6017 45.5066 55.3199 43.7051 53.4014C41.8801 51.4578 40.75 48.9093 40.75 45.583V36.9395C40.7501 36.3718 40.9668 35.8205 41.3633 35.4082C41.761 34.9946 42.3086 34.7549 42.8877 34.7549C44.9427 34.7549 47.6938 33.4287 49.6592 31.6436L49.667 31.6357C50.0891 31.2608 50.6325 31.0498 51.2002 31.0498Z" fill="url(#paint8_linear_13916_23662)" fill-opacity="0.1"/>
|
||||
<path d="M51.2002 31.0498C51.6971 31.0498 52.1757 31.2112 52.5703 31.5029L52.7334 31.6357L52.7422 31.6436C54.7184 33.44 57.4571 34.7549 59.5127 34.7549C60.0918 34.7549 60.6394 34.9946 61.0371 35.4082C61.4335 35.8205 61.6503 36.3718 61.6504 36.9395V45.583C61.6504 48.9094 60.5202 51.4576 58.6973 53.4023C56.8979 55.322 54.4689 56.6063 51.9277 57.5273L51.9199 57.5303C51.4545 57.6943 50.9487 57.6876 50.4873 57.5146V57.5166C47.9393 56.6017 45.5066 55.3199 43.7051 53.4014C41.8801 51.4578 40.75 48.9093 40.75 45.583V36.9395C40.7501 36.3718 40.9668 35.8205 41.3633 35.4082C41.761 34.9946 42.3086 34.7549 42.8877 34.7549C44.9427 34.7549 47.6938 33.4287 49.6592 31.6436L49.667 31.6357C50.0891 31.2608 50.6325 31.0498 51.2002 31.0498Z" stroke="white" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M56 41L49.8125 48L47 44.8182" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<filter id="filter0_d_13916_23662" x="14.2" y="0.8" width="73.6" height="73.6" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.6"/>
|
||||
<feGaussianBlur stdDeviation="2.4"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0941176 0 0 0 0 0.105882 0 0 0 0 0.141176 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_13916_23662"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_13916_23662" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_13916_23662" x1="50.7996" y1="-6.4001" x2="50.8148" y2="26.266" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_13916_23662" x1="51" y1="39.662" x2="51" y2="57.1198" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_13916_23662" x1="51" y1="39.662" x2="51" y2="57.1198" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_13916_23662" x1="51" y1="39.662" x2="51" y2="57.1198" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_13916_23662" x1="51" y1="39.662" x2="51" y2="57.1198" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_13916_23662" x1="51" y1="39.662" x2="51" y2="57.1198" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_13916_23662" x1="51" y1="39.662" x2="51" y2="57.1198" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_13916_23662" x1="51" y1="35.9999" x2="50.9995" y2="61.6441" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_13916_23662" x1="51.2002" y1="32" x2="51.2002" y2="56.7" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F6F8F9" stop-opacity="0.975"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_13916_23662">
|
||||
<rect x="31.8" y="10.3999" width="38.4" height="51.2" rx="5.68889" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,90 @@
|
||||
<svg width="102" height="72" viewBox="0 0 102 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_13916_23720)">
|
||||
<g clip-path="url(#clip0_13916_23720)">
|
||||
<rect x="31.8" y="10.3999" width="38.4" height="51.2" rx="5.68889" fill="white"/>
|
||||
<path d="M31.7989 10.3999L69.7989 10.3999L69.7989 25.5999H31.7981L31.7989 10.3999Z" fill="#3677CC"/>
|
||||
<path d="M31.7989 10.3999L69.7989 10.3999L69.7989 25.5999H31.7981L31.7989 10.3999Z" fill="#F6F8F9" fill-opacity="0.975"/>
|
||||
<path d="M31.7989 10.3999L69.7989 10.3999L69.7989 25.5999H31.7981L31.7989 10.3999Z" fill="url(#paint0_linear_13916_23720)" fill-opacity="0.4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7602 25.5821C36.7602 24.7967 37.3969 24.1599 38.1824 24.1599H50.9824C51.7679 24.1599 52.4046 24.7967 52.4046 25.5821C52.4046 26.3676 51.7679 27.0043 50.9824 27.0043H38.1824C37.3969 27.0043 36.7602 26.3676 36.7602 25.5821Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7602 25.5821C36.7602 24.7967 37.3969 24.1599 38.1824 24.1599H50.9824C51.7679 24.1599 52.4046 24.7967 52.4046 25.5821C52.4046 26.3676 51.7679 27.0043 50.9824 27.0043H38.1824C37.3969 27.0043 36.7602 26.3676 36.7602 25.5821Z" fill="#F6F8F9" fill-opacity="0.15"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 31.662C36.7781 30.8765 37.4149 30.2397 38.2003 30.2397H63.8003C64.5858 30.2397 65.2226 30.8765 65.2226 31.662C65.2226 32.4474 64.5858 33.0842 63.8003 33.0842H38.2003C37.4149 33.0842 36.7781 32.4474 36.7781 31.662Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 31.662C36.7781 30.8765 37.4149 30.2397 38.2003 30.2397H63.8003C64.5858 30.2397 65.2226 30.8765 65.2226 31.662C65.2226 32.4474 64.5858 33.0842 63.8003 33.0842H38.2003C37.4149 33.0842 36.7781 32.4474 36.7781 31.662Z" fill="#F6F8F9" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 31.662C36.7781 30.8765 37.4149 30.2397 38.2003 30.2397H63.8003C64.5858 30.2397 65.2226 30.8765 65.2226 31.662C65.2226 32.4474 64.5858 33.0842 63.8003 33.0842H38.2003C37.4149 33.0842 36.7781 32.4474 36.7781 31.662Z" fill="url(#paint1_linear_13916_23720)" fill-opacity="0.35"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 36.462C36.7781 35.6765 37.4149 35.0397 38.2003 35.0397H63.8003C64.5858 35.0397 65.2226 35.6765 65.2226 36.462C65.2226 37.2474 64.5858 37.8842 63.8003 37.8842H38.2003C37.4149 37.8842 36.7781 37.2474 36.7781 36.462Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 36.462C36.7781 35.6765 37.4149 35.0397 38.2003 35.0397H63.8003C64.5858 35.0397 65.2226 35.6765 65.2226 36.462C65.2226 37.2474 64.5858 37.8842 63.8003 37.8842H38.2003C37.4149 37.8842 36.7781 37.2474 36.7781 36.462Z" fill="#F6F8F9" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 36.462C36.7781 35.6765 37.4149 35.0397 38.2003 35.0397H63.8003C64.5858 35.0397 65.2226 35.6765 65.2226 36.462C65.2226 37.2474 64.5858 37.8842 63.8003 37.8842H38.2003C37.4149 37.8842 36.7781 37.2474 36.7781 36.462Z" fill="url(#paint2_linear_13916_23720)" fill-opacity="0.35"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 41.262C36.7781 40.4765 37.4149 39.8397 38.2003 39.8397H63.8003C64.5858 39.8397 65.2226 40.4765 65.2226 41.262C65.2226 42.0474 64.5858 42.6842 63.8003 42.6842H38.2003C37.4149 42.6842 36.7781 42.0474 36.7781 41.262Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 41.262C36.7781 40.4765 37.4149 39.8397 38.2003 39.8397H63.8003C64.5858 39.8397 65.2226 40.4765 65.2226 41.262C65.2226 42.0474 64.5858 42.6842 63.8003 42.6842H38.2003C37.4149 42.6842 36.7781 42.0474 36.7781 41.262Z" fill="#F6F8F9" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 41.262C36.7781 40.4765 37.4149 39.8397 38.2003 39.8397H63.8003C64.5858 39.8397 65.2226 40.4765 65.2226 41.262C65.2226 42.0474 64.5858 42.6842 63.8003 42.6842H38.2003C37.4149 42.6842 36.7781 42.0474 36.7781 41.262Z" fill="url(#paint3_linear_13916_23720)" fill-opacity="0.35"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 46.062C36.7781 45.2765 37.4149 44.6397 38.2003 44.6397H63.8003C64.5858 44.6397 65.2226 45.2765 65.2226 46.062C65.2226 46.8474 64.5858 47.4842 63.8003 47.4842H38.2003C37.4149 47.4842 36.7781 46.8474 36.7781 46.062Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 46.062C36.7781 45.2765 37.4149 44.6397 38.2003 44.6397H63.8003C64.5858 44.6397 65.2226 45.2765 65.2226 46.062C65.2226 46.8474 64.5858 47.4842 63.8003 47.4842H38.2003C37.4149 47.4842 36.7781 46.8474 36.7781 46.062Z" fill="#F6F8F9" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 46.062C36.7781 45.2765 37.4149 44.6397 38.2003 44.6397H63.8003C64.5858 44.6397 65.2226 45.2765 65.2226 46.062C65.2226 46.8474 64.5858 47.4842 63.8003 47.4842H38.2003C37.4149 47.4842 36.7781 46.8474 36.7781 46.062Z" fill="url(#paint4_linear_13916_23720)" fill-opacity="0.35"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 50.862C36.7781 50.0765 37.4149 49.4397 38.2003 49.4397H63.8003C64.5858 49.4397 65.2226 50.0765 65.2226 50.862C65.2226 51.6474 64.5858 52.2842 63.8003 52.2842H38.2003C37.4149 52.2842 36.7781 51.6474 36.7781 50.862Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 50.862C36.7781 50.0765 37.4149 49.4397 38.2003 49.4397H63.8003C64.5858 49.4397 65.2226 50.0765 65.2226 50.862C65.2226 51.6474 64.5858 52.2842 63.8003 52.2842H38.2003C37.4149 52.2842 36.7781 51.6474 36.7781 50.862Z" fill="#F6F8F9" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 50.862C36.7781 50.0765 37.4149 49.4397 38.2003 49.4397H63.8003C64.5858 49.4397 65.2226 50.0765 65.2226 50.862C65.2226 51.6474 64.5858 52.2842 63.8003 52.2842H38.2003C37.4149 52.2842 36.7781 51.6474 36.7781 50.862Z" fill="url(#paint5_linear_13916_23720)" fill-opacity="0.35"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 55.662C36.7781 54.8765 37.4149 54.2397 38.2003 54.2397H63.8003C64.5858 54.2397 65.2226 54.8765 65.2226 55.662C65.2226 56.4474 64.5858 57.0842 63.8003 57.0842H38.2003C37.4149 57.0842 36.7781 56.4474 36.7781 55.662Z" fill="#3677CC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 55.662C36.7781 54.8765 37.4149 54.2397 38.2003 54.2397H63.8003C64.5858 54.2397 65.2226 54.8765 65.2226 55.662C65.2226 56.4474 64.5858 57.0842 63.8003 57.0842H38.2003C37.4149 57.0842 36.7781 56.4474 36.7781 55.662Z" fill="#F6F8F9" fill-opacity="0.8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7781 55.662C36.7781 54.8765 37.4149 54.2397 38.2003 54.2397H63.8003C64.5858 54.2397 65.2226 54.8765 65.2226 55.662C65.2226 56.4474 64.5858 57.0842 63.8003 57.0842H38.2003C37.4149 57.0842 36.7781 56.4474 36.7781 55.662Z" fill="url(#paint6_linear_13916_23720)" fill-opacity="0.35"/>
|
||||
</g>
|
||||
<rect x="32.36" y="10.9599" width="37.28" height="50.08" rx="5.12889" stroke="#3677CC" stroke-width="1.12"/>
|
||||
<rect x="32.36" y="10.9599" width="37.28" height="50.08" rx="5.12889" stroke="#F6F8F9" stroke-opacity="0.8" stroke-width="1.12"/>
|
||||
<rect x="32.36" y="10.9599" width="37.28" height="50.08" rx="5.12889" stroke="url(#paint7_linear_13916_23720)" stroke-opacity="0.2" stroke-width="1.12"/>
|
||||
</g>
|
||||
<path d="M51.2002 31.0498C51.6971 31.0498 52.1757 31.2112 52.5703 31.5029L52.7334 31.6357L52.7422 31.6436C54.7184 33.44 57.4571 34.7549 59.5127 34.7549C60.0918 34.7549 60.6394 34.9946 61.0371 35.4082C61.4335 35.8205 61.6503 36.3718 61.6504 36.9395V45.583C61.6504 48.9094 60.5202 51.4576 58.6973 53.4023C56.8979 55.322 54.4689 56.6063 51.9277 57.5273L51.9199 57.5303C51.4545 57.6943 50.9487 57.6876 50.4873 57.5146V57.5166C47.9393 56.6017 45.5066 55.3199 43.7051 53.4014C41.8801 51.4578 40.75 48.9093 40.75 45.583V36.9395C40.7501 36.3718 40.9668 35.8205 41.3633 35.4082C41.761 34.9946 42.3086 34.7549 42.8877 34.7549C44.9427 34.7549 47.6938 33.4287 49.6592 31.6436L49.667 31.6357C50.0891 31.2608 50.6325 31.0498 51.2002 31.0498Z" fill="#2F6ABB"/>
|
||||
<path d="M51.2002 31.0498C51.6971 31.0498 52.1757 31.2112 52.5703 31.5029L52.7334 31.6357L52.7422 31.6436C54.7184 33.44 57.4571 34.7549 59.5127 34.7549C60.0918 34.7549 60.6394 34.9946 61.0371 35.4082C61.4335 35.8205 61.6503 36.3718 61.6504 36.9395V45.583C61.6504 48.9094 60.5202 51.4576 58.6973 53.4023C56.8979 55.322 54.4689 56.6063 51.9277 57.5273L51.9199 57.5303C51.4545 57.6943 50.9487 57.6876 50.4873 57.5146V57.5166C47.9393 56.6017 45.5066 55.3199 43.7051 53.4014C41.8801 51.4578 40.75 48.9093 40.75 45.583V36.9395C40.7501 36.3718 40.9668 35.8205 41.3633 35.4082C41.761 34.9946 42.3086 34.7549 42.8877 34.7549C44.9427 34.7549 47.6938 33.4287 49.6592 31.6436L49.667 31.6357C50.0891 31.2608 50.6325 31.0498 51.2002 31.0498Z" fill="#F6F8F9" fill-opacity="0.25"/>
|
||||
<path d="M51.2002 31.0498C51.6971 31.0498 52.1757 31.2112 52.5703 31.5029L52.7334 31.6357L52.7422 31.6436C54.7184 33.44 57.4571 34.7549 59.5127 34.7549C60.0918 34.7549 60.6394 34.9946 61.0371 35.4082C61.4335 35.8205 61.6503 36.3718 61.6504 36.9395V45.583C61.6504 48.9094 60.5202 51.4576 58.6973 53.4023C56.8979 55.322 54.4689 56.6063 51.9277 57.5273L51.9199 57.5303C51.4545 57.6943 50.9487 57.6876 50.4873 57.5146V57.5166C47.9393 56.6017 45.5066 55.3199 43.7051 53.4014C41.8801 51.4578 40.75 48.9093 40.75 45.583V36.9395C40.7501 36.3718 40.9668 35.8205 41.3633 35.4082C41.761 34.9946 42.3086 34.7549 42.8877 34.7549C44.9427 34.7549 47.6938 33.4287 49.6592 31.6436L49.667 31.6357C50.0891 31.2608 50.6325 31.0498 51.2002 31.0498Z" fill="url(#paint8_linear_13916_23720)" fill-opacity="0.1"/>
|
||||
<path d="M51.2002 31.0498C51.6971 31.0498 52.1757 31.2112 52.5703 31.5029L52.7334 31.6357L52.7422 31.6436C54.7184 33.44 57.4571 34.7549 59.5127 34.7549C60.0918 34.7549 60.6394 34.9946 61.0371 35.4082C61.4335 35.8205 61.6503 36.3718 61.6504 36.9395V45.583C61.6504 48.9094 60.5202 51.4576 58.6973 53.4023C56.8979 55.322 54.4689 56.6063 51.9277 57.5273L51.9199 57.5303C51.4545 57.6943 50.9487 57.6876 50.4873 57.5146V57.5166C47.9393 56.6017 45.5066 55.3199 43.7051 53.4014C41.8801 51.4578 40.75 48.9093 40.75 45.583V36.9395C40.7501 36.3718 40.9668 35.8205 41.3633 35.4082C41.761 34.9946 42.3086 34.7549 42.8877 34.7549C44.9427 34.7549 47.6938 33.4287 49.6592 31.6436L49.667 31.6357C50.0891 31.2608 50.6325 31.0498 51.2002 31.0498Z" stroke="white" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M51.2998 50C54.3374 50 56.7998 47.5376 56.7998 44.5C56.7998 41.4624 54.3374 39 51.2998 39C48.2622 39 45.7998 41.4624 45.7998 44.5C45.7998 47.5376 48.2622 50 51.2998 50Z" stroke="white" stroke-width="1.26923" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M51.2998 41.2V44.5L53.4998 45.6" stroke="white" stroke-width="1.26923" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<filter id="filter0_d_13916_23720" x="14.2" y="0.8" width="73.6" height="73.6" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.6"/>
|
||||
<feGaussianBlur stdDeviation="2.4"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0941176 0 0 0 0 0.105882 0 0 0 0 0.141176 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_13916_23720"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_13916_23720" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_13916_23720" x1="50.7996" y1="-6.4001" x2="50.8148" y2="26.266" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_13916_23720" x1="51" y1="39.662" x2="51" y2="57.1198" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_13916_23720" x1="51" y1="39.662" x2="51" y2="57.1198" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_13916_23720" x1="51" y1="39.662" x2="51" y2="57.1198" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_13916_23720" x1="51" y1="39.662" x2="51" y2="57.1198" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_13916_23720" x1="51" y1="39.662" x2="51" y2="57.1198" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_13916_23720" x1="51" y1="39.662" x2="51" y2="57.1198" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_13916_23720" x1="51" y1="35.9999" x2="50.9995" y2="61.6441" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_13916_23720" x1="51.2002" y1="32" x2="51.2002" y2="56.7" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F6F8F9" stop-opacity="0.975"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_13916_23720">
|
||||
<rect x="31.8" y="10.3999" width="38.4" height="51.2" rx="5.68889" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -30,7 +30,7 @@ export const AccountMenu = () => {
|
||||
if (hasEncryptionSetup) {
|
||||
return {
|
||||
label: t('Encryption settings'),
|
||||
icon: 'lock',
|
||||
icon: 'verified_user',
|
||||
callback: () => setIsSettingsOpen(true),
|
||||
showSeparator: true,
|
||||
};
|
||||
@@ -38,7 +38,7 @@ export const AccountMenu = () => {
|
||||
|
||||
return {
|
||||
label: t('Enable encryption'),
|
||||
icon: 'lock_open',
|
||||
icon: 'add_moderator',
|
||||
callback: () => setIsOnboardingOpen(true),
|
||||
showSeparator: true,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Button, ModalSize } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Loading } from '@/components';
|
||||
import { useVaultClient } from '@/features/docs/doc-collaboration/vault';
|
||||
import { EncryptionModalContent } from '@/features/docs/doc-management/components/EncryptionLayout';
|
||||
|
||||
/**
|
||||
* The size the interface asks for its host modal: small (350px) by default, the
|
||||
* design system's medium one when the shown screen needs the room. Resets to
|
||||
* small whenever the modal closes, so the next opening starts at the default.
|
||||
*/
|
||||
export const useInterfaceModalSize = (isOpen: boolean): ModalSize => {
|
||||
const { client } = useVaultClient();
|
||||
const [size, setSize] = useState<ModalSize>(ModalSize.SMALL);
|
||||
|
||||
useEffect(() => {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleSize = ({ size: wanted }: { size: 'small' | 'medium' }) => {
|
||||
setSize(wanted === 'medium' ? ModalSize.MEDIUM : ModalSize.SMALL);
|
||||
};
|
||||
|
||||
client.on('interface:size', handleSize);
|
||||
|
||||
return () => {
|
||||
client.off('interface:size', handleSize);
|
||||
};
|
||||
}, [client]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setSize(ModalSize.SMALL);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return size;
|
||||
};
|
||||
|
||||
interface EncryptionHostBodyProps {
|
||||
/** Receives the element the interface iframe is mounted into. */
|
||||
hostRef: (element: HTMLDivElement | null) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The body of a modal hosting the encryption interface. The interface can only
|
||||
* be opened once the SDK script has loaded from the vault domain: until then a
|
||||
* loader, and if that load failed an explanation with a retry, instead of the
|
||||
* empty host the interface would otherwise never fill.
|
||||
*/
|
||||
export const EncryptionHostBody = ({
|
||||
hostRef,
|
||||
onClose,
|
||||
}: EncryptionHostBodyProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { client, isLoading, error } = useVaultClient();
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<EncryptionModalContent
|
||||
illustration="shield-x"
|
||||
title={t('Encryption service unavailable')}
|
||||
description={t(
|
||||
'The encryption service could not be loaded. Check your connection and try again.',
|
||||
)}
|
||||
actions={
|
||||
<>
|
||||
<Button onClick={() => window.location.reload()}>
|
||||
{t('Retry')}
|
||||
</Button>
|
||||
<Button variant="bordered" color="neutral" onClick={onClose}>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!client || isLoading) {
|
||||
return <Loading $minHeight="120px" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={hostRef}
|
||||
className="--docs--encryption-host"
|
||||
style={{ minHeight: '120px' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+21
-13
@@ -6,13 +6,18 @@
|
||||
* The product (Docs) doesn't manage public keys — it only stores fingerprints
|
||||
* on document accesses for UI purposes.
|
||||
*/
|
||||
import { Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Modal } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { useUserEncryption } from '@/docs/doc-collaboration';
|
||||
import { useVaultClient } from '@/features/docs/doc-collaboration/vault';
|
||||
|
||||
import {
|
||||
EncryptionHostBody,
|
||||
useInterfaceModalSize,
|
||||
} from './EncryptionHostBody';
|
||||
|
||||
interface ModalEncryptionOnboardingProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -24,6 +29,7 @@ export const ModalEncryptionOnboarding = ({
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: ModalEncryptionOnboardingProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { client: vaultClient, refreshKeyState } = useVaultClient();
|
||||
const { refreshEncryption } = useUserEncryption();
|
||||
const onboardingOpenedRef = useRef(false);
|
||||
@@ -70,10 +76,15 @@ export const ModalEncryptionOnboarding = ({
|
||||
};
|
||||
}, [vaultClient, refreshKeyState, refreshEncryption, onSuccess, onClose]);
|
||||
|
||||
// The modal's close control only ASKS the interface to close: it may hold an
|
||||
// unsaved recovery phrase and answer with its own confirmation. The modal goes
|
||||
// away on 'interface:closed', which the interface emits once really done.
|
||||
const handleClose = useCallback(() => {
|
||||
vaultClient?.closeInterface();
|
||||
onboardingOpenedRef.current = false;
|
||||
onClose();
|
||||
if (vaultClient) {
|
||||
vaultClient.requestClose();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}, [vaultClient, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -82,20 +93,17 @@ export const ModalEncryptionOnboarding = ({
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const size = useInterfaceModalSize(isOpen);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
closeOnClickOutside={false}
|
||||
onClose={handleClose}
|
||||
size={ModalSize.LARGE}
|
||||
hideCloseButton
|
||||
size={size}
|
||||
aria-label={t('Encryption')}
|
||||
>
|
||||
<Box $minHeight="400px">
|
||||
<div
|
||||
ref={setContainerEl}
|
||||
style={{ width: '100%', minHeight: '400px' }}
|
||||
/>
|
||||
</Box>
|
||||
<EncryptionHostBody hostRef={setContainerEl} onClose={onClose} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+21
-13
@@ -4,13 +4,18 @@
|
||||
* Opens the encryption service's settings interface iframe which handles:
|
||||
* fingerprint display, key deletion, device transfer export, and server key management.
|
||||
*/
|
||||
import { Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Modal } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { useUserEncryption } from '@/docs/doc-collaboration';
|
||||
import { useVaultClient } from '@/features/docs/doc-collaboration/vault';
|
||||
|
||||
import {
|
||||
EncryptionHostBody,
|
||||
useInterfaceModalSize,
|
||||
} from './EncryptionHostBody';
|
||||
|
||||
interface ModalEncryptionSettingsProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -21,6 +26,7 @@ export const ModalEncryptionSettings = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}: ModalEncryptionSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { client: vaultClient, refreshKeyState } = useVaultClient();
|
||||
const { refreshEncryption } = useUserEncryption();
|
||||
const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
|
||||
@@ -61,10 +67,15 @@ export const ModalEncryptionSettings = ({
|
||||
};
|
||||
}, [vaultClient, refreshKeyState, refreshEncryption, onClose]);
|
||||
|
||||
// The modal's close control only ASKS the interface to close: it may hold an
|
||||
// unsaved recovery phrase and answer with its own confirmation. The modal goes
|
||||
// away on 'interface:closed', which the interface emits once really done.
|
||||
const handleClose = useCallback(() => {
|
||||
vaultClient?.closeInterface();
|
||||
setSettingsOpened(false);
|
||||
onClose();
|
||||
if (vaultClient) {
|
||||
vaultClient.requestClose();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}, [vaultClient, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -73,20 +84,17 @@ export const ModalEncryptionSettings = ({
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const size = useInterfaceModalSize(isOpen);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
closeOnClickOutside={false}
|
||||
onClose={handleClose}
|
||||
size={ModalSize.LARGE}
|
||||
hideCloseButton
|
||||
size={size}
|
||||
aria-label={t('Encryption')}
|
||||
>
|
||||
<Box $minHeight="400px">
|
||||
<div
|
||||
ref={setContainerEl}
|
||||
style={{ width: '100%', minHeight: '400px' }}
|
||||
/>
|
||||
</Box>
|
||||
<EncryptionHostBody hostRef={setContainerEl} onClose={onClose} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+23
-2
@@ -19,6 +19,14 @@ export declare interface EncryptionClientEventMap {
|
||||
};
|
||||
/** Fired when the user cancels or closes the interface */
|
||||
[MSG_INTERFACE_CLOSED]: void;
|
||||
/**
|
||||
* Fired when the shown screen wants a modal of another width: the product
|
||||
* switches its modal between the design system's small (350px) and medium
|
||||
* (600px) sizes. Emitted on every screen change, so a product can also ignore it.
|
||||
*/
|
||||
'interface:size': {
|
||||
size: 'small' | 'medium';
|
||||
};
|
||||
/** Fired on errors from the vault or the interface */
|
||||
error: Error;
|
||||
/** Fired when keys changed from another tab/product (via BroadcastChannel) */
|
||||
@@ -218,8 +226,7 @@ export declare class VaultClient {
|
||||
* @param keyVersion - the recipient's encryption-key VERSION this wrap was
|
||||
* produced against, as stored by the product on the access row. The vault
|
||||
* unwraps with exactly that retained key (a version this device no longer
|
||||
* holds throws WRONG_SECRET_KEY). For Drive chains it is the version of the
|
||||
* ENTRY-point key; the chain links themselves are symmetric.
|
||||
* holds throws WRONG_SECRET_KEY).
|
||||
* @param encryptedKeyChain - optional chain of wrapped keys for Drive's key hierarchy.
|
||||
* When provided, resolves the chain from entry point to target before decrypting.
|
||||
*/
|
||||
@@ -385,6 +392,16 @@ export declare class VaultClient {
|
||||
* `userId` is the recipient's OIDC sub, like every id a product passes.
|
||||
*/
|
||||
openRecipientProfile(container: HTMLElement, userId: string, label: RecipientLabel): void;
|
||||
/**
|
||||
* Ask the interface to close, from the product's own close control (the X of
|
||||
* the modal hosting the iframe). The interface owns the decision: mid-backup
|
||||
* it shows its "cancel setup?" confirmation instead of closing, so a product
|
||||
* must NOT unmount its modal here. It waits for the 'interface:closed' event,
|
||||
* which fires once the interface has really closed (for this request or any
|
||||
* other reason). With no interface open this is a no-op that still emits
|
||||
* 'interface:closed', so a product's close handler stays uniform.
|
||||
*/
|
||||
requestClose(): void;
|
||||
/**
|
||||
* Close the interface iframe if it is open.
|
||||
*/
|
||||
@@ -526,6 +543,10 @@ export declare const VaultErrorCode: {
|
||||
readonly IFRAME_REQUIRED: "IFRAME_REQUIRED";
|
||||
/** Ciphertext / encrypted-key payload too short to be valid (truncated). */
|
||||
readonly CIPHERTEXT_TOO_SHORT: "CIPHERTEXT_TOO_SHORT";
|
||||
/** Ciphertext structure is inconsistent (e.g. a wrong length field): corrupt, not merely truncated. */
|
||||
readonly MALFORMED_CIPHERTEXT: "MALFORMED_CIPHERTEXT";
|
||||
/** A field cannot be represented in the canonical signed-payload encoding (length or number out of range). */
|
||||
readonly INVALID_CANONICAL_PAYLOAD: "INVALID_CANONICAL_PAYLOAD";
|
||||
/** Blob's leading version byte doesn't match a format this build can decode. */
|
||||
readonly UNSUPPORTED_CRYPTO_VERSION: "UNSUPPORTED_CRYPTO_VERSION";
|
||||
/** A signature public key didn't have the expected Ed25519 length. */
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DropdownMenuOption,
|
||||
Icon,
|
||||
IconOptions,
|
||||
Text,
|
||||
} from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import {
|
||||
@@ -149,7 +150,7 @@ export const DocToolBox = ({
|
||||
},
|
||||
{
|
||||
label: t('Encrypt document'),
|
||||
icon: 'https',
|
||||
icon: 'add_moderator',
|
||||
disabled: !doc.abilities.accesses_manage,
|
||||
callback: () => {
|
||||
setIsModalEncryptOpen(true);
|
||||
@@ -158,8 +159,8 @@ export const DocToolBox = ({
|
||||
showSeparator: isTopRoot ? true : false,
|
||||
},
|
||||
{
|
||||
label: t('Remove document encryption'),
|
||||
icon: 'no_encryption',
|
||||
label: t('Remove encryption'),
|
||||
icon: 'remove_moderator',
|
||||
disabled: !doc.abilities.accesses_manage,
|
||||
callback: () => {
|
||||
setIsModalRemoveEncryptionOpen(true);
|
||||
@@ -242,21 +243,15 @@ export const DocToolBox = ({
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="5px"
|
||||
$gap="4px"
|
||||
$css={css`
|
||||
margin-right: 5px;
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
iconName="lock"
|
||||
$size="md"
|
||||
$color={colorsTokens['success-600']}
|
||||
/>
|
||||
<span
|
||||
style={{ fontSize: '0.8rem', color: colorsTokens['success-600'] }}
|
||||
>
|
||||
<Icon iconName="verified_user" $size="sm" $theme="brand" />
|
||||
<Text $size="xs" $weight="500" $theme="brand">
|
||||
{t('Encrypted')}
|
||||
</span>
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
import type { StaticImageData } from 'next/image';
|
||||
import { ReactNode } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import DocumentEncrypted from '@/assets/encryption/document-encrypted.svg';
|
||||
import DocumentEncrypting from '@/assets/encryption/document-encrypting.svg';
|
||||
import documentShieldCheck from '@/assets/encryption/document-shield-check.png';
|
||||
import documentShieldX from '@/assets/encryption/document-shield-x.png';
|
||||
import shieldCheck from '@/assets/encryption/shield-check.png';
|
||||
import shieldX from '@/assets/encryption/shield-x.png';
|
||||
import { Box, Text } from '@/components';
|
||||
|
||||
const MODAL_ILLUSTRATIONS = {
|
||||
'shield-check': shieldCheck,
|
||||
'shield-x': shieldX,
|
||||
'document-shield-check': documentShieldCheck,
|
||||
'document-shield-x': documentShieldX,
|
||||
} satisfies Record<string, StaticImageData>;
|
||||
|
||||
const STATE_ILLUSTRATIONS = {
|
||||
'document-encrypted': DocumentEncrypted,
|
||||
'document-encrypting': DocumentEncrypting,
|
||||
};
|
||||
|
||||
export type ModalIllustration = keyof typeof MODAL_ILLUSTRATIONS;
|
||||
export type StateIllustration = keyof typeof STATE_ILLUSTRATIONS;
|
||||
|
||||
interface EncryptionModalContentProps {
|
||||
illustration?: ModalIllustration;
|
||||
title: ReactNode;
|
||||
titleId?: string;
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
actionsLayout?: 'stack' | 'row';
|
||||
}
|
||||
|
||||
/**
|
||||
* The content of an encryption modal: illustration, title, description, body
|
||||
* and stacked full-width actions. Rendered inside a Cunningham small modal
|
||||
* (widened to 350px by `globals.css`) with the modal's own close control.
|
||||
*/
|
||||
export const EncryptionModalContent = ({
|
||||
illustration,
|
||||
title,
|
||||
titleId,
|
||||
description,
|
||||
children,
|
||||
actions,
|
||||
actionsLayout = 'stack',
|
||||
}: EncryptionModalContentProps) => (
|
||||
<Box className="--docs--encryption-modal" $gap="base">
|
||||
{illustration && (
|
||||
<Box $align="center">
|
||||
<img
|
||||
src={MODAL_ILLUSTRATIONS[illustration].src}
|
||||
alt=""
|
||||
width={142}
|
||||
height={100}
|
||||
style={{ display: 'block', objectFit: 'contain' }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box $gap="3xs" $padding={{ right: 'base' }}>
|
||||
<Text
|
||||
as="h2"
|
||||
id={titleId}
|
||||
$size="18px"
|
||||
$weight="700"
|
||||
$margin="0"
|
||||
$css="line-height: 24px; overflow-wrap: anywhere;"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text
|
||||
$size="sm"
|
||||
$variation="secondary"
|
||||
$css="line-height: 18px; overflow-wrap: anywhere;"
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{children && <Box $gap="sm">{children}</Box>}
|
||||
{actions && (
|
||||
<Box
|
||||
$direction={actionsLayout === 'row' ? 'row' : 'column'}
|
||||
$justify={actionsLayout === 'row' ? 'flex-end' : undefined}
|
||||
$gap={actionsLayout === 'row' ? 'sm' : 'xs'}
|
||||
$wrap="wrap"
|
||||
$css={
|
||||
actionsLayout === 'row'
|
||||
? undefined
|
||||
: css`
|
||||
> .c__button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
`
|
||||
}
|
||||
>
|
||||
{actions}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
interface EncryptionEmptyStateProps {
|
||||
illustration?: StateIllustration;
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A compact centered state on a page or in a panel: small illustration, bold
|
||||
* title, short description and text-link actions.
|
||||
*/
|
||||
export const EncryptionEmptyState = ({
|
||||
illustration = 'document-encrypted',
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
actions,
|
||||
}: EncryptionEmptyStateProps) => {
|
||||
const Illustration = STATE_ILLUSTRATIONS[illustration];
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="--docs--encryption-empty-state"
|
||||
$align="center"
|
||||
$justify="center"
|
||||
$margin="auto"
|
||||
$gap="base"
|
||||
$padding="md"
|
||||
$css="text-align: center;"
|
||||
>
|
||||
<Box $align="center" $gap="3xs" $maxWidth="320px">
|
||||
<Illustration aria-hidden="true" width={102} height={72} />
|
||||
<Text as="p" $size="sm" $weight="700" $margin="0" $textAlign="center">
|
||||
{title}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text
|
||||
as="p"
|
||||
$size="xs"
|
||||
$variation="secondary"
|
||||
$margin="0"
|
||||
$textAlign="center"
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{children}
|
||||
{actions && (
|
||||
<Box $direction="row" $gap="xs" $wrap="wrap" $justify="center">
|
||||
{actions}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
+51
-42
@@ -1,7 +1,8 @@
|
||||
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { Icon, StyledLink, Text } from '@/components';
|
||||
import { useAuth } from '@/features/auth';
|
||||
import {
|
||||
fetchRegisteredKeys,
|
||||
@@ -10,6 +11,8 @@ import {
|
||||
|
||||
import type { Doc } from '../types';
|
||||
|
||||
import { EncryptionEmptyState } from './EncryptionLayout';
|
||||
|
||||
/**
|
||||
* True when the SDK threw a `VaultError` carrying the
|
||||
* `WRONG_SECRET_KEY` code. In docs this means the document was
|
||||
@@ -79,55 +82,61 @@ export const KeyMismatchPanel = ({ doc }: Props) => {
|
||||
};
|
||||
}, [vaultClient, user?.suite_user_id]);
|
||||
|
||||
const versionChip = (value: number) => (
|
||||
<Text
|
||||
as="span"
|
||||
$css={`
|
||||
font-family: monospace;
|
||||
background: var(--c--contextuals--background--surface--tertiary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
`}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box $align="center" $margin="auto" $gap="md" $padding="2rem">
|
||||
<Icon iconName="key_off" $size="3rem" $theme="warning" />
|
||||
<Text as="h2" $textAlign="center" $margin="0">
|
||||
{t('This document was encrypted with a different key')}
|
||||
</Text>
|
||||
<Box $maxWidth="500px" $gap="sm">
|
||||
<Text $variation="secondary" $textAlign="center">
|
||||
{t(
|
||||
'The document was encrypted for you at a time when you were using a different encryption key — possibly before you reset your keys or switched device without restoring a backup. Your current key can no longer decrypt it. Ask an owner or administrator of this document to remove you from the access list and add you back so it gets re-encrypted for your current key.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
<EncryptionEmptyState
|
||||
title={t('This document was encrypted with a different key')}
|
||||
description={t(
|
||||
'The document was encrypted for you at a time when you were using a different encryption key — possibly before you reset your keys or switched device without restoring a backup. Your current key can no longer decrypt it. Ask an owner or administrator of this document to remove you from the access list and add you back so it gets re-encrypted for your current key.',
|
||||
)}
|
||||
actions={
|
||||
<StyledLink href="/">
|
||||
<Button
|
||||
size="small"
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
icon={<Icon iconName="home" $withThemeInherited />}
|
||||
>
|
||||
{t('Home')}
|
||||
</Button>
|
||||
</StyledLink>
|
||||
}
|
||||
>
|
||||
{(shareTimeVersion !== null || currentVersion !== null) && (
|
||||
<Box $gap="2xs" $maxWidth="500px" $align="center">
|
||||
<Text
|
||||
as="div"
|
||||
$size="xs"
|
||||
$variation="secondary"
|
||||
$textAlign="center"
|
||||
$css="display: flex; flex-direction: column; gap: 4px;"
|
||||
>
|
||||
{shareTimeVersion !== null && (
|
||||
<Text $variation="secondary" $size="sm" $textAlign="center">
|
||||
<span>
|
||||
{t('Encryption key version at the time it was shared with you:')}{' '}
|
||||
<Text
|
||||
as="span"
|
||||
$css={`
|
||||
font-family: monospace;
|
||||
background: var(--c--theme--colors--greyscale-100, #f4f4f5);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
`}
|
||||
>
|
||||
{shareTimeVersion}
|
||||
</Text>
|
||||
</Text>
|
||||
{versionChip(shareTimeVersion)}
|
||||
</span>
|
||||
)}
|
||||
{currentVersion !== null && (
|
||||
<Text $variation="secondary" $size="sm" $textAlign="center">
|
||||
<span>
|
||||
{t('Your current encryption key version:')}{' '}
|
||||
<Text
|
||||
as="span"
|
||||
$css={`
|
||||
font-family: monospace;
|
||||
background: var(--c--theme--colors--greyscale-100, #f4f4f5);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
`}
|
||||
>
|
||||
{currentVersion}
|
||||
</Text>
|
||||
</Text>
|
||||
{versionChip(currentVersion)}
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</EncryptionEmptyState>
|
||||
);
|
||||
};
|
||||
|
||||
+81
-170
@@ -11,7 +11,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Box, ButtonCloseModal, Icon, Text, TextErrors } from '@/components';
|
||||
import { TextErrors } from '@/components';
|
||||
import { useUserEncryption } from '@/docs/doc-collaboration';
|
||||
import { createDocAttachment } from '@/docs/doc-editor/api';
|
||||
import { useAuth } from '@/features/auth';
|
||||
@@ -35,6 +35,8 @@ import { useDocAccesses } from '@/features/docs/doc-share/api/useDocAccesses';
|
||||
import { useDocInvitations } from '@/features/docs/doc-share/api/useDocInvitations';
|
||||
import { useKeyboardAction } from '@/hooks';
|
||||
|
||||
import { EncryptionModalContent } from './EncryptionLayout';
|
||||
|
||||
/**
|
||||
* encrypt existing unencrypted attachments and return:
|
||||
* - a modified Yjs state with URLs pointing to new encrypted files
|
||||
@@ -372,185 +374,94 @@ export const ModalEncryptDoc = ({ doc, onClose }: ModalEncryptDocProps) => {
|
||||
const handleCloseKeyDown = keyboardAction(handleClose);
|
||||
const handleEncryptKeyDown = keyboardAction(handleEncrypt);
|
||||
|
||||
// Only the conditions that are NOT met are worth a word: the modal describes
|
||||
// what encrypting does, and warns about what still blocks it.
|
||||
const blockers: string[] = [];
|
||||
if (!hasEncryptionKeys) {
|
||||
blockers.push(t('You must enable encryption from your account menu first'));
|
||||
}
|
||||
if (!isRestricted) {
|
||||
blockers.push(
|
||||
t('Document must be set to private (currently {{reach}})', {
|
||||
reach:
|
||||
effectiveReach === LinkReach.PUBLIC ? t('public') : t('connected'),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (hasPendingInvitations) {
|
||||
blockers.push(t('Pending invitations must be resolved first'));
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
closeOnClickOutside={!isPending}
|
||||
hideCloseButton
|
||||
onClose={handleClose}
|
||||
aria-describedby="modal-encrypt-doc-title"
|
||||
rightActions={
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={handleClose}
|
||||
onKeyDown={handleCloseKeyDown}
|
||||
disabled={isPending}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
color="warning"
|
||||
fullWidth
|
||||
onClick={handleEncrypt}
|
||||
onKeyDown={handleEncryptKeyDown}
|
||||
disabled={isPending || !canEncrypt}
|
||||
icon={
|
||||
isPending ? (
|
||||
<div>
|
||||
<Spinner size="sm" />
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{t('Confirm')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
size={ModalSize.MEDIUM}
|
||||
title={
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$align="center"
|
||||
$width="100%"
|
||||
>
|
||||
<Text
|
||||
$size="h6"
|
||||
as="h1"
|
||||
id="modal-encrypt-doc-title"
|
||||
$margin="0"
|
||||
$align="flex-start"
|
||||
>
|
||||
{t('Encrypt document')}
|
||||
</Text>
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the encrypt modal')}
|
||||
onClick={handleClose}
|
||||
onKeyDown={handleCloseKeyDown}
|
||||
disabled={isPending}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
size={ModalSize.SMALL}
|
||||
aria-label={t('Encrypt document')}
|
||||
>
|
||||
<Box className="--docs--modal-encrypt-doc" $gap="sm">
|
||||
{!isError && (
|
||||
<Box $gap="sm">
|
||||
<Alert type={VariantType.WARNING}>
|
||||
<Box $gap="xs">
|
||||
<Text $size="sm">
|
||||
{t(
|
||||
'Encrypting a document ensures that only authorized members can read its content. Keep in mind before proceeding any access will then require its user to do the encryption onboarding, with the complication of ensuring keys backups.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Alert>
|
||||
<EncryptionModalContent
|
||||
illustration="document-shield-check"
|
||||
title={t('Encrypt document')}
|
||||
titleId="modal-encrypt-doc-title"
|
||||
description={t(
|
||||
'The document and its attachments will be encrypted end-to-end. Only people you share it with will be able to access its contents.',
|
||||
)}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={handleEncrypt}
|
||||
onKeyDown={handleEncryptKeyDown}
|
||||
disabled={isPending || !canEncrypt}
|
||||
icon={
|
||||
isPending ? (
|
||||
<div>
|
||||
<Spinner size="sm" />
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{t('Encrypt')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="bordered"
|
||||
color="neutral"
|
||||
fullWidth
|
||||
onClick={handleClose}
|
||||
onKeyDown={handleCloseKeyDown}
|
||||
disabled={isPending}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{!isError && blockers.length > 0 && (
|
||||
<Alert type={VariantType.ERROR}>
|
||||
{blockers.length === 1 ? (
|
||||
blockers[0]
|
||||
) : (
|
||||
<ul style={{ margin: 0, paddingLeft: '1.25rem' }}>
|
||||
{blockers.map((blocker) => (
|
||||
<li key={blocker}>{blocker}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Text $size="sm" $variation="secondary">
|
||||
{t('Here the conditions that must be met:')}
|
||||
</Text>
|
||||
|
||||
<Box $gap="xs">
|
||||
<Box $direction="row" $align="center" $gap="xs">
|
||||
<Icon
|
||||
iconName={hasEncryptionKeys ? 'check_circle' : 'cancel'}
|
||||
$size="sm"
|
||||
$theme={hasEncryptionKeys ? 'success' : 'error'}
|
||||
/>
|
||||
<Text
|
||||
$size="sm"
|
||||
$weight={hasEncryptionKeys ? '400' : '600'}
|
||||
$theme={hasEncryptionKeys ? undefined : 'error'}
|
||||
>
|
||||
{hasEncryptionKeys
|
||||
? t('Encryption is enabled on your account')
|
||||
: t(
|
||||
'You must enable encryption from your account menu first',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box $direction="row" $align="center" $gap="xs">
|
||||
<Icon
|
||||
iconName={isRestricted ? 'check_circle' : 'cancel'}
|
||||
$size="sm"
|
||||
$theme={isRestricted ? 'success' : 'error'}
|
||||
/>
|
||||
<Text
|
||||
$size="sm"
|
||||
$weight={isRestricted ? '400' : '600'}
|
||||
$theme={isRestricted ? undefined : 'error'}
|
||||
>
|
||||
{isRestricted
|
||||
? t('Document access is private')
|
||||
: t(
|
||||
'Document must be set to private (currently {{reach}})',
|
||||
{
|
||||
reach:
|
||||
effectiveReach === LinkReach.PUBLIC
|
||||
? t('public')
|
||||
: t('connected'),
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box $direction="row" $align="center" $gap="xs">
|
||||
<Icon
|
||||
iconName={!hasPendingInvitations ? 'check_circle' : 'cancel'}
|
||||
$size="sm"
|
||||
$theme={!hasPendingInvitations ? 'success' : 'error'}
|
||||
/>
|
||||
<Text
|
||||
$size="sm"
|
||||
$weight={!hasPendingInvitations ? '400' : '600'}
|
||||
$theme={!hasPendingInvitations ? undefined : 'error'}
|
||||
>
|
||||
{!hasPendingInvitations
|
||||
? t('No pending invitations')
|
||||
: t('Pending invitations must be resolved first')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box $gap="3xs">
|
||||
<Box $direction="row" $align="center" $gap="xs">
|
||||
<Icon
|
||||
iconName={
|
||||
othersWithoutKey.length === 0
|
||||
? 'check_circle'
|
||||
: 'hourglass_empty'
|
||||
}
|
||||
$size="sm"
|
||||
$theme={
|
||||
othersWithoutKey.length === 0 ? 'success' : 'warning'
|
||||
}
|
||||
/>
|
||||
<Text $size="sm">
|
||||
{othersWithoutKey.length === 0
|
||||
? t('All members have encryption enabled')
|
||||
: t(
|
||||
'{{count}} member(s) haven’t completed encryption onboarding yet. They will be added as pending and won’t be able to decrypt the document until another validated collaborator accepts them from the share dialog.',
|
||||
{ count: othersWithoutKey.length },
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
{othersWithoutKey.length > 0 && (
|
||||
<Box $margin={{ left: 'sm' }} $gap="3xs">
|
||||
{othersWithoutKey.map((access) => (
|
||||
<Text key={access.id} $size="xs" $variation="secondary">
|
||||
{access.user.full_name || access.user.email}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
{!isError && blockers.length === 0 && othersWithoutKey.length > 0 && (
|
||||
<Alert type={VariantType.WARNING}>
|
||||
{t(
|
||||
'{{count}} collaborator(s) have not enabled encryption yet. They will be added as pending and cannot open the document until someone accepts them from the share dialog.',
|
||||
{ count: othersWithoutKey.length },
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isError && <TextErrors causes={error.cause} />}
|
||||
</Box>
|
||||
</EncryptionModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+34
-40
@@ -10,7 +10,7 @@ import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Box, ButtonCloseModal, Text, TextErrors } from '@/components';
|
||||
import { TextErrors } from '@/components';
|
||||
import { createDocAttachment } from '@/docs/doc-editor/api';
|
||||
import { useAuth } from '@/features/auth';
|
||||
import { useVaultClient } from '@/features/docs/doc-collaboration/vault';
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
} from '@/features/docs/doc-management';
|
||||
import { useKeyboardAction } from '@/hooks';
|
||||
|
||||
import { EncryptionModalContent } from './EncryptionLayout';
|
||||
|
||||
/**
|
||||
* Decrypt existing encrypted attachments using the vault and upload decrypted copies.
|
||||
*/
|
||||
@@ -188,47 +190,39 @@ export const ModalRemoveDocEncryption = ({
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
onClose={handleClose}
|
||||
size={ModalSize.MEDIUM}
|
||||
rightActions={
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleClose}
|
||||
disabled={isPending}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
onClick={() => void handleRemoveEncryption()}
|
||||
disabled={isPending}
|
||||
{...keyboardAction(() => void handleRemoveEncryption())}
|
||||
>
|
||||
{isPending ? <Spinner /> : t('Confirm')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
title={
|
||||
<Box $direction="row" $justify="space-between" $align="center">
|
||||
<Text as="h1" $size="h6" $align="flex-start" $margin="0">
|
||||
{t('Remove document encryption')}
|
||||
</Text>
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the modal')}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
hideCloseButton
|
||||
size={ModalSize.SMALL}
|
||||
aria-label={t('Remove encryption')}
|
||||
>
|
||||
<Box $margin={{ top: 'sm' }} $gap="sm">
|
||||
<Text $variation="secondary">
|
||||
{t(
|
||||
'This will permanently remove encryption from this document. All content will be stored in plain text.',
|
||||
)}
|
||||
</Text>
|
||||
<EncryptionModalContent
|
||||
illustration="document-shield-x"
|
||||
title={t('Remove encryption')}
|
||||
description={t(
|
||||
'The document will be decrypted and stored in plain text on the server.',
|
||||
)}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={() => void handleRemoveEncryption()}
|
||||
disabled={isPending}
|
||||
{...keyboardAction(() => void handleRemoveEncryption())}
|
||||
>
|
||||
{isPending ? <Spinner /> : t('Remove encryption')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="bordered"
|
||||
color="neutral"
|
||||
fullWidth
|
||||
onClick={handleClose}
|
||||
disabled={isPending}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{isError && error && <TextErrors causes={error.cause} />}
|
||||
</Box>
|
||||
</EncryptionModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+3
@@ -93,10 +93,12 @@ export const DocShareInvitationItem = ({
|
||||
type DocShareModalInviteUserRowProps = {
|
||||
user: User;
|
||||
suffix?: string;
|
||||
suffixIcon?: string;
|
||||
};
|
||||
export const DocShareModalInviteUserRow = ({
|
||||
user,
|
||||
suffix,
|
||||
suffixIcon,
|
||||
}: DocShareModalInviteUserRowProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
@@ -108,6 +110,7 @@ export const DocShareModalInviteUserRow = ({
|
||||
<SearchUserRow
|
||||
user={user}
|
||||
suffix={suffix}
|
||||
suffixIcon={suffixIcon}
|
||||
right={
|
||||
<BoxButton
|
||||
className="right-hover"
|
||||
|
||||
@@ -24,6 +24,7 @@ type Props = {
|
||||
access: Access;
|
||||
isInherited?: boolean;
|
||||
suffix?: string;
|
||||
suffixIcon?: string;
|
||||
onSuffixClick?: () => void;
|
||||
};
|
||||
export const DocShareMemberItem = ({
|
||||
@@ -31,6 +32,7 @@ export const DocShareMemberItem = ({
|
||||
access,
|
||||
isInherited = false,
|
||||
suffix,
|
||||
suffixIcon,
|
||||
onSuffixClick,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -76,6 +78,7 @@ export const DocShareMemberItem = ({
|
||||
alwaysShowRight={true}
|
||||
user={access.user}
|
||||
suffix={suffix}
|
||||
suffixIcon={suffixIcon}
|
||||
onSuffixClick={onSuffixClick}
|
||||
right={
|
||||
<Box $direction="row" $align="center" $gap={spacingsTokens['2xs']}>
|
||||
@@ -147,12 +150,13 @@ export const QuickSearchGroupMember = ({
|
||||
(!uid || !doc.accesses_versions_per_user?.[uid]);
|
||||
|
||||
let suffix: string | undefined;
|
||||
let suffixIcon: string | undefined;
|
||||
if (hasMismatch) {
|
||||
suffix = t('DIFFERENT PUBLIC KEY, PLEASE VERIFY');
|
||||
suffix = t('Verify key');
|
||||
suffixIcon = 'gpp_maybe';
|
||||
} else if (hasNoEncryptionKey) {
|
||||
suffix = t(
|
||||
'ENCRYPTION DISABLED - consider removing this member since unable to read the document',
|
||||
);
|
||||
suffix = t('No encryption');
|
||||
suffixIcon = 'gpp_bad';
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -160,6 +164,7 @@ export const QuickSearchGroupMember = ({
|
||||
doc={doc}
|
||||
access={access}
|
||||
suffix={suffix}
|
||||
suffixIcon={suffixIcon}
|
||||
onSuffixClick={
|
||||
hasMismatch && uid ? () => setMismatchUserId(uid) : undefined
|
||||
}
|
||||
|
||||
+41
-66
@@ -9,7 +9,6 @@ import {
|
||||
Box,
|
||||
ButtonCloseModal,
|
||||
HorizontalSeparator,
|
||||
Icon,
|
||||
Loading,
|
||||
Text,
|
||||
} from '@/components';
|
||||
@@ -27,6 +26,10 @@ import type { DocumentEncryptionSettings } from '@/docs/doc-collaboration/hook/u
|
||||
import type { PublicKeyMismatch } from '@/docs/doc-collaboration/hook/usePublicKeyRegistry';
|
||||
import { Doc } from '@/docs/doc-management';
|
||||
import { User, useAuth } from '@/features/auth';
|
||||
import {
|
||||
EncryptionEmptyState,
|
||||
EncryptionModalContent,
|
||||
} from '@/features/docs/doc-management/components/EncryptionLayout';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
import { isValidEmail } from '@/utils';
|
||||
|
||||
@@ -272,39 +275,23 @@ export const DocShareModal = ({
|
||||
</div>
|
||||
{isEncryptionDeriving && <Loading />}
|
||||
{!isEncryptionDeriving && derivedEncryptionError && (
|
||||
<Box $align="center" $gap="sm" $padding="lg">
|
||||
<Icon iconName="lock" $size="2rem" $theme="warning" />
|
||||
<Text as="h3" $textAlign="center" $margin="0">
|
||||
{t('Encryption keys unavailable')}
|
||||
</Text>
|
||||
<Text $variation="secondary" $textAlign="center" $size="sm">
|
||||
{t(
|
||||
'This is an encrypted document, but your current device does not have the required encryption keys to decrypt it.',
|
||||
)}
|
||||
</Text>
|
||||
{(encryptionError === 'missing_private_key' ||
|
||||
encryptionError === 'missing_public_key') && (
|
||||
<Text $variation="secondary" $textAlign="center" $size="sm">
|
||||
{t(
|
||||
'This usually happens when you switch to a new device or browser without restoring your encryption backup, please go to your "Encryption Settings" to fix it.',
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
{documentEncryptionError === 'missing_symmetric_key' && (
|
||||
<Text $variation="secondary" $textAlign="center" $size="sm">
|
||||
{t(
|
||||
'You do not have access to this encrypted document. Ask the document owner to share it with you again.',
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
{documentEncryptionError === 'decryption_failed' && (
|
||||
<Text $variation="secondary" $textAlign="center" $size="sm">
|
||||
{t(
|
||||
'Your encryption keys could not decrypt this document. This may happen if your keys were recreated. Ask the document owner to share it with you again.',
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<EncryptionEmptyState
|
||||
title={t('Encrypted document')}
|
||||
description={
|
||||
encryptionError === 'missing_private_key' ||
|
||||
encryptionError === 'missing_public_key'
|
||||
? t(
|
||||
'This document is encrypted. You must enable encryption on your account to access it.',
|
||||
)
|
||||
: documentEncryptionError === 'missing_symmetric_key'
|
||||
? t(
|
||||
'You do not have access to this encrypted document. Ask the document owner to share it with you again.',
|
||||
)
|
||||
: t(
|
||||
'You do not have the correct encryption key to decrypt this document. Ask the document owner to share it with you again.',
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!isEncryptionDeriving && !derivedEncryptionError && (
|
||||
<Box
|
||||
@@ -534,14 +521,14 @@ const QuickSearchInviteInputSection = ({
|
||||
const getUserSuffix = useCallback(
|
||||
(user: User): string | undefined => {
|
||||
if (user.suite_user_id && keyMismatchUserIds?.has(user.suite_user_id)) {
|
||||
return t('DIFFERENT PUBLIC KEY, PLEASE VERIFY');
|
||||
return t('Verify key');
|
||||
}
|
||||
if (
|
||||
isEncrypted &&
|
||||
(!user.suite_user_id ||
|
||||
!doc.accesses_versions_per_user?.[user.suite_user_id])
|
||||
) {
|
||||
return t(`(encryption not enabled)`);
|
||||
return t('No encryption');
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
@@ -560,6 +547,11 @@ const QuickSearchInviteInputSection = ({
|
||||
<DocShareModalInviteUserRow
|
||||
user={user}
|
||||
suffix={getUserSuffix(user)}
|
||||
suffixIcon={
|
||||
user.suite_user_id && keyMismatchUserIds?.has(user.suite_user_id)
|
||||
? 'gpp_maybe'
|
||||
: 'gpp_bad'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -580,37 +572,20 @@ const QuickSearchInviteInputSection = ({
|
||||
closeOnClickOutside
|
||||
onClose={() => setShowNoKeyModal(false)}
|
||||
size={ModalSize.SMALL}
|
||||
rightActions={
|
||||
<Button onClick={() => setShowNoKeyModal(false)}>
|
||||
{t('Understood')}
|
||||
</Button>
|
||||
}
|
||||
title={
|
||||
<Text
|
||||
as="h1"
|
||||
$gap="0.7rem"
|
||||
$size="h6"
|
||||
$align="flex-start"
|
||||
$direction="row"
|
||||
$margin="0"
|
||||
>
|
||||
<Icon iconName="lock" />
|
||||
{t('Encryption required')}
|
||||
</Text>
|
||||
}
|
||||
aria-label={t('Encryption required')}
|
||||
>
|
||||
<Box $direction="column" $gap="0.35rem" $margin={{ top: 'sm' }}>
|
||||
<Text $variation="secondary">
|
||||
{t(
|
||||
'This user has not enabled encryption on their account yet. It is not possible to share encrypted content with them.',
|
||||
)}
|
||||
</Text>
|
||||
<Text $variation="secondary">
|
||||
{t(
|
||||
'Please ask them to enable encryption in their account settings first.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
<EncryptionModalContent
|
||||
illustration="document-shield-x"
|
||||
title={t('Encryption required')}
|
||||
description={t(
|
||||
'This person has not enabled encryption yet, so the document cannot be shared with them. Ask them to enable encryption first.',
|
||||
)}
|
||||
actions={
|
||||
<Button fullWidth onClick={() => setShowNoKeyModal(false)}>
|
||||
{t('Understood')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{mismatchUser &&
|
||||
|
||||
+65
-77
@@ -1,8 +1,15 @@
|
||||
import { Button, Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Modal,
|
||||
ModalSize,
|
||||
VariantType,
|
||||
} from '@gouvfr-lasuite/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { Box, Text } from '@/components';
|
||||
import { useKeyFingerprint } from '@/docs/doc-collaboration';
|
||||
import { EncryptionModalContent } from '@/features/docs/doc-management/components/EncryptionLayout';
|
||||
|
||||
interface ModalKeyMismatchProps {
|
||||
onClose: () => void;
|
||||
@@ -11,6 +18,21 @@ interface ModalKeyMismatchProps {
|
||||
currentKey?: string;
|
||||
}
|
||||
|
||||
const Fingerprint = ({ label, value }: { label: string; value: string }) => (
|
||||
<Box $gap="3xs">
|
||||
<Text $size="xs" $weight="600" $variation="secondary">
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
$size="sm"
|
||||
$weight="700"
|
||||
$css="font-family: monospace; letter-spacing: 0.08em; overflow-wrap: anywhere;"
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const ModalKeyMismatch = ({
|
||||
onClose,
|
||||
onAcceptKey,
|
||||
@@ -26,94 +48,60 @@ export const ModalKeyMismatch = ({
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
onClose={onClose}
|
||||
size={ModalSize.MEDIUM}
|
||||
rightActions={
|
||||
<>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
{onAcceptKey && (
|
||||
<Button
|
||||
color="warning"
|
||||
onClick={() => {
|
||||
onAcceptKey();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t('I trust this key')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
title={
|
||||
<Text
|
||||
as="h1"
|
||||
$gap="0.7rem"
|
||||
$size="h6"
|
||||
$align="flex-start"
|
||||
$direction="row"
|
||||
$margin="0"
|
||||
>
|
||||
<Icon iconName="warning" $theme="warning" />
|
||||
{t('Public key change detected')}
|
||||
</Text>
|
||||
}
|
||||
size={ModalSize.SMALL}
|
||||
aria-label={t('Verify encryption key')}
|
||||
>
|
||||
<Box $direction="column" $gap="0.5rem" $margin={{ top: 'sm' }}>
|
||||
<Text $variation="secondary">
|
||||
<EncryptionModalContent
|
||||
title={t('Verify encryption key')}
|
||||
description={t(
|
||||
'We recommend verifying with this person directly (for example on a call) that they really changed their encryption key before proceeding.',
|
||||
)}
|
||||
actionsLayout="row"
|
||||
actions={
|
||||
<>
|
||||
<Button variant="bordered" color="error" onClick={onClose}>
|
||||
{t("Don't trust")}
|
||||
</Button>
|
||||
{onAcceptKey && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onAcceptKey();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t('Trust')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Alert type={VariantType.WARNING}>
|
||||
{t(
|
||||
"This user's encryption public key has changed since you last interacted with them.",
|
||||
"This person's encryption key has changed. Verify it before continuing.",
|
||||
)}
|
||||
</Text>
|
||||
<Text $variation="secondary">
|
||||
{t(
|
||||
'This could mean the user has regenerated their encryption keys, but it could also indicate that their account has been compromised.',
|
||||
)}
|
||||
</Text>
|
||||
<Text $variation="secondary" $weight="600">
|
||||
{t(
|
||||
'We recommend verifying with this person directly (e.g. via video call) that they have indeed changed their encryption key before proceeding.',
|
||||
)}
|
||||
</Text>
|
||||
</Alert>
|
||||
{(knownFingerprint || currentFingerprint) && (
|
||||
<Box
|
||||
$direction="column"
|
||||
$gap="0.25rem"
|
||||
$margin={{ top: 'xs' }}
|
||||
$padding="sm"
|
||||
$background="#f5f5f5"
|
||||
$border="1px solid #ddd"
|
||||
$gap="xs"
|
||||
$padding="xs"
|
||||
$radius="4px"
|
||||
$css="border: 1px solid var(--c--contextuals--border--surface--primary);"
|
||||
>
|
||||
{knownFingerprint && (
|
||||
<Box $direction="row" $gap="0.5rem" $align="center">
|
||||
<Text $size="xs" $weight="600" $variation="secondary">
|
||||
{t('Previously known:')}
|
||||
</Text>
|
||||
<Text
|
||||
$size="xs"
|
||||
style={{ fontFamily: 'monospace', letterSpacing: '0.05em' }}
|
||||
>
|
||||
{knownFingerprint}
|
||||
</Text>
|
||||
</Box>
|
||||
<Fingerprint
|
||||
label={t('Previously known:')}
|
||||
value={knownFingerprint}
|
||||
/>
|
||||
)}
|
||||
{currentFingerprint && (
|
||||
<Box $direction="row" $gap="0.5rem" $align="center">
|
||||
<Text $size="xs" $weight="600" $variation="secondary">
|
||||
{t('Current key:')}
|
||||
</Text>
|
||||
<Text
|
||||
$size="xs"
|
||||
style={{ fontFamily: 'monospace', letterSpacing: '0.05em' }}
|
||||
>
|
||||
{currentFingerprint}
|
||||
</Text>
|
||||
</Box>
|
||||
<Fingerprint
|
||||
label={t('Current key:')}
|
||||
value={currentFingerprint}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</EncryptionModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+60
-38
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { UserAvatar } from '@/features/auth';
|
||||
import type { DocumentEncryptionSettings } from '@/features/docs/doc-collaboration/hook/useDocumentEncryption';
|
||||
import {
|
||||
fetchRegisteredKeys,
|
||||
@@ -167,24 +168,14 @@ export const PendingEncryptionSection = ({
|
||||
|
||||
return (
|
||||
<Box
|
||||
$padding="sm"
|
||||
className="--docs--pending-encryption"
|
||||
$margin={{ horizontal: 'base', bottom: 'sm' }}
|
||||
$gap="xs"
|
||||
$css={`
|
||||
background: var(--c--theme--colors--warning-050, #fffbf0);
|
||||
border: 1px solid var(--c--theme--colors--warning-300, #ffd591);
|
||||
border-radius: 4px;
|
||||
`}
|
||||
>
|
||||
<Box $direction="row" $align="center" $gap="xs">
|
||||
<Icon iconName="hourglass_empty" $size="sm" $theme="warning" />
|
||||
<Text $weight="600" $size="sm">
|
||||
{t('Users pending encryption access ({{count}})', {
|
||||
count: pending.length,
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box $gap="2xs">
|
||||
<Text $size="xs" $weight="700" $variation="secondary">
|
||||
{t('Action needed')}
|
||||
</Text>
|
||||
<Box $gap="xs">
|
||||
{pending.map((access) => {
|
||||
const sub = access.user?.suite_user_id;
|
||||
const isBusy = inFlight.has(access.id);
|
||||
@@ -195,45 +186,76 @@ export const PendingEncryptionSection = ({
|
||||
!!documentEncryptionSettings &&
|
||||
hasPublicKey === true &&
|
||||
!probing;
|
||||
const name = access.user?.full_name || access.user?.email || '';
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={access.id}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="xs"
|
||||
$wrap="wrap"
|
||||
$padding={{ vertical: '3xs' }}
|
||||
$minHeight="42px"
|
||||
>
|
||||
<Box $flex={1} $minWidth="0">
|
||||
<Text $weight="500" $size="sm">
|
||||
{access.user?.full_name || access.user?.email}
|
||||
</Text>
|
||||
{access.user?.email && access.user?.full_name && (
|
||||
<Text $size="xs" $variation="secondary">
|
||||
{access.user.email}
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="xs"
|
||||
$flex={1}
|
||||
$minWidth="0"
|
||||
>
|
||||
<UserAvatar fullName={name} />
|
||||
<Box $minWidth="0">
|
||||
<Text $size="sm" $weight="500" $ellipsis>
|
||||
{name}
|
||||
</Text>
|
||||
)}
|
||||
{!canAccept && !probing && (
|
||||
<Text $size="xs" $variation="secondary">
|
||||
{t(
|
||||
"Waiting for this user to complete their encryption onboarding. You'll be able to accept them once they have.",
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
{error && (
|
||||
<Text $size="xs" $theme="error">
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
{access.user?.email && access.user?.full_name && (
|
||||
<Text $size="xs" $variation="secondary" $ellipsis>
|
||||
{access.user.email}
|
||||
</Text>
|
||||
)}
|
||||
{!canAccept && !probing && (
|
||||
<Text $size="xs" $variation="secondary">
|
||||
{t(
|
||||
'Waiting for them to enable encryption. You will be able to accept them once they have.',
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
{error && (
|
||||
<Text $size="xs" $theme="error">
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{canAccept && (
|
||||
{canAccept ? (
|
||||
<Button
|
||||
size="small"
|
||||
variant="bordered"
|
||||
onClick={() => void handleAccept(access)}
|
||||
disabled={isBusy}
|
||||
>
|
||||
{isBusy ? t('Accepting…') : t('Accept')}
|
||||
</Button>
|
||||
) : (
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="3xs"
|
||||
$padding={{ horizontal: '2xs' }}
|
||||
$radius="4px"
|
||||
$height="24px"
|
||||
$background="var(--c--contextuals--background--surface--tertiary)"
|
||||
>
|
||||
<Icon iconName="schedule" $size="sm" $variation="tertiary" />
|
||||
<Text
|
||||
$size="xs"
|
||||
$weight="500"
|
||||
$variation="tertiary"
|
||||
$css="white-space: nowrap;"
|
||||
>
|
||||
{t('Pending encryption')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
+14
-12
@@ -1,4 +1,4 @@
|
||||
import { Box, Text } from '@/components';
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import {
|
||||
QuickSearchItemContent,
|
||||
QuickSearchItemContentProps,
|
||||
@@ -11,7 +11,10 @@ type Props = {
|
||||
alwaysShowRight?: boolean;
|
||||
right?: QuickSearchItemContentProps['right'];
|
||||
isInvitation?: boolean;
|
||||
/** A short status ("Verify key", "No encryption") shown as an icon with the text as tooltip. */
|
||||
suffix?: string;
|
||||
/** Material icon for the suffix; the shield-with-question mark by default. */
|
||||
suffixIcon?: string;
|
||||
onSuffixClick?: () => void;
|
||||
};
|
||||
|
||||
@@ -21,6 +24,7 @@ export const SearchUserRow = ({
|
||||
alwaysShowRight = false,
|
||||
isInvitation = false,
|
||||
suffix,
|
||||
suffixIcon = 'gpp_maybe',
|
||||
onSuffixClick,
|
||||
}: Props) => {
|
||||
const hasFullName = !!user.full_name;
|
||||
@@ -47,10 +51,13 @@ export const SearchUserRow = ({
|
||||
{hasFullName ? user.full_name : user.email}
|
||||
</Text>
|
||||
{suffix && (
|
||||
<Text
|
||||
$size="xs"
|
||||
$weight="600"
|
||||
$color={colorsTokens['warning-600']}
|
||||
<Icon
|
||||
iconName={suffixIcon}
|
||||
$size="sm"
|
||||
$theme={onSuffixClick ? 'warning' : 'neutral'}
|
||||
$variation={onSuffixClick ? undefined : 'tertiary'}
|
||||
aria-label={suffix}
|
||||
title={suffix}
|
||||
{...(onSuffixClick && {
|
||||
onClick: (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
@@ -58,14 +65,9 @@ export const SearchUserRow = ({
|
||||
},
|
||||
role: 'button',
|
||||
tabIndex: 0,
|
||||
style: {
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
style: { cursor: 'pointer' },
|
||||
})}
|
||||
>
|
||||
{suffix}
|
||||
</Text>
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{hasFullName && (
|
||||
|
||||
@@ -1000,7 +1000,50 @@
|
||||
"pdf": "pdf",
|
||||
"{{action}}, current role: {{role}}": "{{action}}, rôle actuel : {{role}}",
|
||||
"{{name}} added to invite list. Add more members or press Tab to select role and invite.": "{{name}} a été ajouté à la liste d'invitation. Ajoutez plus de membres ou appuyez sur Tab pour sélectionner le rôle et inviter.",
|
||||
"{{name}} removed from invite list": "{{name}} a été retiré de la liste d'invitation"
|
||||
"{{name}} removed from invite list": "{{name}} a été retiré de la liste d'invitation",
|
||||
"Encryption": "Chiffrement",
|
||||
"Remove encryption": "Retirer le chiffrement",
|
||||
"Encrypted": "Chiffré",
|
||||
"Encrypt": "Chiffrer",
|
||||
"The document and its attachments will be encrypted end-to-end. Only people you share it with will be able to access its contents.": "Le document et ses pièces jointes seront chiffrés de bout en bout. Seules les personnes avec qui vous le partagez pourront y accéder.",
|
||||
"The document will be decrypted and stored in plain text on the server.": "Le document sera déchiffré et stocké en clair sur le serveur.",
|
||||
"{{count}} collaborator(s) have not enabled encryption yet. They will be added as pending and cannot open the document until someone accepts them from the share dialog.": "{{count}} collaborateur(s) n'ont pas encore activé le chiffrement. Ils seront ajoutés en attente et ne pourront pas ouvrir le document tant qu'une personne ne les aura pas acceptés depuis la fenêtre de partage.",
|
||||
"You must enable encryption from your account menu first": "Vous devez d'abord activer le chiffrement depuis le menu de votre compte",
|
||||
"Document must be set to private (currently {{reach}})": "Le document doit être privé (actuellement {{reach}})",
|
||||
"Pending invitations must be resolved first": "Les invitations en attente doivent d'abord être traitées",
|
||||
"Encrypted document": "Document chiffré",
|
||||
"Encryption service unavailable": "Service de chiffrement indisponible",
|
||||
"The encryption service could not be loaded. Check your connection and try again.": "Le service de chiffrement n'a pas pu être chargé. Vérifiez votre connexion et réessayez.",
|
||||
"This document is encrypted and the encryption service could not be loaded. Check your connection and try again.": "Ce document est chiffré et le service de chiffrement n'a pas pu être chargé. Vérifiez votre connexion et réessayez.",
|
||||
"Retry": "Réessayer",
|
||||
"Close": "Fermer",
|
||||
"This document is encrypted. You must enable encryption on your account to access it.": "Ce document est chiffré. Vous devez activer le chiffrement sur votre compte pour y accéder.",
|
||||
"You do not have access to this encrypted document. Ask the document owner to share it with you again.": "Vous n'avez pas accès à ce document chiffré. Demandez à son propriétaire de le partager à nouveau avec vous.",
|
||||
"You do not have the correct encryption key to decrypt this document. Ask the document owner to share it with you again.": "Vous n'avez pas la bonne clé de chiffrement pour déchiffrer ce document. Demandez à son propriétaire de le partager à nouveau avec vous.",
|
||||
"Enable encryption": "Activer le chiffrement",
|
||||
"Encryption in progress": "Chiffrement en cours",
|
||||
"Removing encryption": "Retrait du chiffrement",
|
||||
"The document owner is encrypting this document. Please wait.": "Le propriétaire du document est en train de le chiffrer. Veuillez patienter.",
|
||||
"The document owner is removing encryption from this document. Please wait.": "Le propriétaire du document est en train d'en retirer le chiffrement. Veuillez patienter.",
|
||||
"This document was encrypted with a different key": "Ce document a été chiffré avec une autre clé",
|
||||
"Verify key": "Vérifier la clé",
|
||||
"No encryption": "Pas de chiffrement",
|
||||
"Encryption required": "Chiffrement requis",
|
||||
"This person has not enabled encryption yet, so the document cannot be shared with them. Ask them to enable encryption first.": "Cette personne n'a pas encore activé le chiffrement, le document ne peut donc pas être partagé avec elle. Demandez-lui d'activer le chiffrement d'abord.",
|
||||
"Understood": "Compris",
|
||||
"Verify encryption key": "Vérifier la clé de chiffrement",
|
||||
"This person's encryption key has changed. Verify it before continuing.": "La clé de chiffrement de cette personne a changé. Vérifiez-la avant de continuer.",
|
||||
"We recommend verifying with this person directly (for example on a call) that they really changed their encryption key before proceeding.": "Nous vous recommandons de vérifier directement avec cette personne (par exemple lors d'un appel) qu'elle a bien changé sa clé de chiffrement avant de continuer.",
|
||||
"Don't trust": "Ne pas faire confiance",
|
||||
"Trust": "Faire confiance",
|
||||
"Previously known:": "Clé connue :",
|
||||
"Current key:": "Clé actuelle :",
|
||||
"Action needed": "Action requise",
|
||||
"Waiting for them to enable encryption. You will be able to accept them once they have.": "En attente de l'activation du chiffrement par cette personne. Vous pourrez l'accepter une fois que ce sera fait.",
|
||||
"Accept": "Accepter",
|
||||
"Accepting…": "Acceptation…",
|
||||
"Pending encryption": "Chiffrement en attente",
|
||||
"Encryption settings": "Paramètres de chiffrement"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
@@ -1449,7 +1492,9 @@
|
||||
"{{name}} removed from invite list": "{{name}} verwijderd uit de uitnodigingslijst"
|
||||
}
|
||||
},
|
||||
"pt": { "translation": {} },
|
||||
"pt": {
|
||||
"translation": {}
|
||||
},
|
||||
"ru": {
|
||||
"translation": {
|
||||
"\"{{email}}\" is already invited to the document.": "\"{{email}}\" уже имеет приглашение для этого документа.",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
||||
import { Spinner, TreeProvider } from '@gouvfr-lasuite/ui-kit';
|
||||
import { TreeProvider } from '@gouvfr-lasuite/ui-kit';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Icon, Loading, StyledLink, Text, TextErrors } from '@/components';
|
||||
import { Box, Icon, Loading, StyledLink, TextErrors } from '@/components';
|
||||
import { DEFAULT_QUERY_RETRY } from '@/core';
|
||||
import { DocEditor } from '@/docs/doc-editor';
|
||||
import {
|
||||
@@ -19,11 +19,18 @@ import {
|
||||
useProviderStore,
|
||||
useTrans,
|
||||
} from '@/docs/doc-management/';
|
||||
import { KEY_AUTH, setAuthUrl, useAuth } from '@/features/auth';
|
||||
import {
|
||||
KEY_AUTH,
|
||||
ModalEncryptionOnboarding,
|
||||
setAuthUrl,
|
||||
useAuth,
|
||||
} from '@/features/auth';
|
||||
import {
|
||||
useDocumentEncryption,
|
||||
useUserEncryption,
|
||||
} from '@/features/docs/doc-collaboration';
|
||||
import { useVaultClient } from '@/features/docs/doc-collaboration/vault';
|
||||
import { EncryptionEmptyState } from '@/features/docs/doc-management/components/EncryptionLayout';
|
||||
import { KeyMismatchPanel } from '@/features/docs/doc-management/components/KeyMismatchPanel';
|
||||
import { getDocChildren, subPageToTree } from '@/features/docs/doc-tree/';
|
||||
import { useSkeletonStore } from '@/features/skeletons';
|
||||
@@ -102,6 +109,9 @@ const DocPage = ({ id }: DocProps) => {
|
||||
|
||||
const { authenticated, user } = useAuth();
|
||||
const [doc, setDoc] = useState<Doc>();
|
||||
const [isOnboardingOpen, setIsOnboardingOpen] = useState(false);
|
||||
const { isEnabled: isEncryptionEnabled, error: vaultClientError } =
|
||||
useVaultClient();
|
||||
const { encryptionLoading, encryptionError } = useUserEncryption();
|
||||
const {
|
||||
documentEncryptionLoading,
|
||||
@@ -295,112 +305,114 @@ const DocPage = ({ id }: DocProps) => {
|
||||
return <KeyMismatchPanel doc={doc} />;
|
||||
}
|
||||
|
||||
if (doc.is_encrypted && (encryptionError || documentEncryptionError)) {
|
||||
if (doc.is_encrypted && vaultClientError) {
|
||||
return (
|
||||
<Box $align="center" $margin="auto" $gap="md" $padding="2rem">
|
||||
<Icon iconName="lock" $size="3rem" $theme="warning" />
|
||||
<Text as="h2" $textAlign="center" $margin="0">
|
||||
{t('Encryption keys unavailable')}
|
||||
</Text>
|
||||
<Box $maxWidth="500px" $gap="sm">
|
||||
<Text $variation="secondary" $textAlign="center">
|
||||
{t(
|
||||
'This is an encrypted document, but your current device does not have the required encryption keys to decrypt it.',
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{(encryptionError === 'missing_private_key' ||
|
||||
encryptionError === 'missing_public_key') && (
|
||||
<Text $variation="secondary" $textAlign="center">
|
||||
{t(
|
||||
'This usually happens when you switch to a new device or browser without restoring your encryption backup, please go to your "Encryption Settings" to fix it.',
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{documentEncryptionError === 'missing_symmetric_key' && (
|
||||
<Text $variation="secondary" $textAlign="center">
|
||||
{t(
|
||||
'You do not have access to this encrypted document. Ask the document owner to share it with you again.',
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{documentEncryptionError === 'decryption_failed' && (
|
||||
<Text $variation="secondary" $textAlign="center">
|
||||
{t(
|
||||
'Your encryption keys could not decrypt this document. This may happen if your keys were recreated. Ask the document owner to share it with you again.',
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{(encryptionError === 'missing_private_key' ||
|
||||
encryptionError === 'missing_public_key') && (
|
||||
<Box $gap="xs" $maxWidth="500px">
|
||||
<Box
|
||||
$padding="sm"
|
||||
$background="#f0f7ff"
|
||||
$border="1px solid #c5ddf5"
|
||||
$radius="4px"
|
||||
$gap="xs"
|
||||
>
|
||||
<Text $weight="600" $size="sm">
|
||||
{t('Restore from backup (recommended)')}
|
||||
</Text>
|
||||
<Text $size="sm" $variation="secondary">
|
||||
{t(
|
||||
'If you have previously exported your encryption backup, you can restore it in your account settings to regain access to all your encrypted documents.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
$padding="sm"
|
||||
$background="#fff8f0"
|
||||
$border="1px solid #f5dfc5"
|
||||
$radius="4px"
|
||||
$gap="xs"
|
||||
>
|
||||
<Text $weight="600" $size="sm">
|
||||
{t('Recreate encryption keys (not recommended)')}
|
||||
</Text>
|
||||
<Text $size="sm" $variation="secondary">
|
||||
{t(
|
||||
'Creating new encryption keys means you will lose access to all previously encrypted documents. Document owners will need to share them with you again.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<EncryptionEmptyState
|
||||
title={t('Encryption service unavailable')}
|
||||
description={t(
|
||||
'This document is encrypted and the encryption service could not be loaded. Check your connection and try again.',
|
||||
)}
|
||||
actions={
|
||||
<>
|
||||
<StyledLink href="/">
|
||||
<Button
|
||||
size="small"
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
icon={<Icon iconName="home" $withThemeInherited />}
|
||||
>
|
||||
{t('Home')}
|
||||
</Button>
|
||||
</StyledLink>
|
||||
<Button
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={() => window.location.reload()}
|
||||
icon={<Icon iconName="refresh" $withThemeInherited />}
|
||||
>
|
||||
{t('Retry')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
<StyledLink href="/">
|
||||
<Button
|
||||
color="neutral"
|
||||
icon={<Icon iconName="home" $withThemeInherited />}
|
||||
>
|
||||
{t('Back to home')}
|
||||
</Button>
|
||||
</StyledLink>
|
||||
</Box>
|
||||
if (doc.is_encrypted && (encryptionError || documentEncryptionError)) {
|
||||
const needsSetup =
|
||||
encryptionError === 'missing_private_key' ||
|
||||
encryptionError === 'missing_public_key';
|
||||
|
||||
return (
|
||||
<>
|
||||
<EncryptionEmptyState
|
||||
title={t('Encrypted document')}
|
||||
description={
|
||||
needsSetup
|
||||
? t(
|
||||
'This document is encrypted. You must enable encryption on your account to access it.',
|
||||
)
|
||||
: documentEncryptionError === 'missing_symmetric_key'
|
||||
? t(
|
||||
'You do not have access to this encrypted document. Ask the document owner to share it with you again.',
|
||||
)
|
||||
: t(
|
||||
'You do not have the correct encryption key to decrypt this document. Ask the document owner to share it with you again.',
|
||||
)
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<StyledLink href="/">
|
||||
<Button
|
||||
size="small"
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
icon={<Icon iconName="home" $withThemeInherited />}
|
||||
>
|
||||
{t('Home')}
|
||||
</Button>
|
||||
</StyledLink>
|
||||
{needsSetup && isEncryptionEnabled && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={() => setIsOnboardingOpen(true)}
|
||||
icon={<Icon iconName="verified_user" $withThemeInherited />}
|
||||
>
|
||||
{t('Enable encryption')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{isOnboardingOpen && (
|
||||
<ModalEncryptionOnboarding
|
||||
isOpen
|
||||
onClose={() => setIsOnboardingOpen(false)}
|
||||
onSuccess={() => setIsOnboardingOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (encryptionTransition) {
|
||||
return (
|
||||
<Box
|
||||
$align="center"
|
||||
$justify="center"
|
||||
$height="100%"
|
||||
$width="100%"
|
||||
$gap="md"
|
||||
>
|
||||
<Spinner />
|
||||
<Text $textAlign="center" $variation="secondary">
|
||||
{encryptionTransition === 'encrypting'
|
||||
? t('Document encryption in progress, please wait...')
|
||||
: t('Removing document encryption, please wait...')}
|
||||
</Text>
|
||||
</Box>
|
||||
<EncryptionEmptyState
|
||||
illustration="document-encrypting"
|
||||
title={
|
||||
encryptionTransition === 'encrypting'
|
||||
? t('Encryption in progress')
|
||||
: t('Removing encryption')
|
||||
}
|
||||
description={
|
||||
encryptionTransition === 'encrypting'
|
||||
? t('The document owner is encrypting this document. Please wait.')
|
||||
: t(
|
||||
'The document owner is removing encryption from this document. Please wait.',
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,40 @@ body > #__next > .c__app > div:has(> .c__loader) {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* The modals hosting the encryption service's screens use the design system's
|
||||
350px "small" modal; Cunningham's small is 300px. */
|
||||
.c__modal--small:has(.--docs--encryption-host),
|
||||
.c__modal--small:has(.--docs--encryption-modal) {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
/* The interface pads its own screens: the modal gives its iframe the bare box. */
|
||||
.c__modal:has(.--docs--encryption-host) .c__modal__scroller {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* The design system's close control is a zero-height sticky spot inside the
|
||||
scroller: while the modal does not scroll it sits in the top padding, and once
|
||||
it scrolls it pins elsewhere, so the cross jumps and the content shows through
|
||||
around it. Anchoring it to the modal box itself keeps it in the same corner
|
||||
in every case, and a halo in the modal's colour masks what scrolls under it. */
|
||||
.c__modal:has(.--docs--encryption-host) .c__modal__close,
|
||||
.c__modal:has(.--docs--encryption-modal) .c__modal__close {
|
||||
position: absolute;
|
||||
top: 0.4rem;
|
||||
right: 0.4rem;
|
||||
height: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.c__modal:has(.--docs--encryption-host) .c__modal__close .c__button,
|
||||
.c__modal:has(.--docs--encryption-modal) .c__modal__close .c__button {
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: var(--c--components--modal--background-color);
|
||||
box-shadow: 0 0 0 4px var(--c--components--modal--background-color);
|
||||
}
|
||||
|
||||
main ::-webkit-scrollbar,
|
||||
.ReactModalPortal ::-webkit-scrollbar {
|
||||
width: 20px;
|
||||
|
||||
Reference in New Issue
Block a user