@junoput01/junoui 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +251 -0
  2. package/README.md +23 -19
  3. package/dist/css/juno-custom-media.css +32 -0
  4. package/dist/css/juno.css +1355 -40
  5. package/dist/icons/inline.js +22 -0
  6. package/docs/accessibility.md +188 -0
  7. package/docs/boot-shell.md +295 -0
  8. package/docs/components/README.md +71 -0
  9. package/docs/components/accordion.md +49 -0
  10. package/docs/components/alert.md +46 -0
  11. package/docs/components/avatar.md +45 -0
  12. package/docs/components/badge.md +35 -0
  13. package/docs/components/breadcrumb.md +31 -0
  14. package/docs/components/button.md +32 -0
  15. package/docs/components/card.md +41 -0
  16. package/docs/components/checkbox.md +41 -0
  17. package/docs/components/chip.md +44 -0
  18. package/docs/components/divider.md +32 -0
  19. package/docs/components/dock.md +215 -0
  20. package/docs/components/drawer.md +100 -0
  21. package/docs/components/field.md +44 -0
  22. package/docs/components/fold-slot.md +31 -0
  23. package/docs/components/gauge.md +48 -0
  24. package/docs/components/icon-loader.md +122 -0
  25. package/docs/components/icon.md +70 -0
  26. package/docs/components/input.md +45 -0
  27. package/docs/components/list.md +76 -0
  28. package/docs/components/load-state.md +131 -0
  29. package/docs/components/loader.md +88 -0
  30. package/docs/components/menu.md +54 -0
  31. package/docs/components/modal.md +52 -0
  32. package/docs/components/navbar.md +53 -0
  33. package/docs/components/pagination.md +42 -0
  34. package/docs/components/pillbar.md +211 -0
  35. package/docs/components/popover.md +43 -0
  36. package/docs/components/rail.md +54 -0
  37. package/docs/components/readout.md +39 -0
  38. package/docs/components/reload.md +41 -0
  39. package/docs/components/segmented.md +46 -0
  40. package/docs/components/select.md +33 -0
  41. package/docs/components/skeleton.md +45 -0
  42. package/docs/components/slider.md +48 -0
  43. package/docs/components/spark.md +51 -0
  44. package/docs/components/status.md +30 -0
  45. package/docs/components/stepper.md +46 -0
  46. package/docs/components/switch.md +38 -0
  47. package/docs/components/table.md +126 -0
  48. package/docs/components/tabs.md +61 -0
  49. package/docs/components/thumb.md +81 -0
  50. package/docs/components/toast.md +48 -0
  51. package/docs/components/toggle-button.md +40 -0
  52. package/docs/components/tooltip.md +63 -0
  53. package/docs/design-guidelines.md +173 -0
  54. package/docs/flutter.md +42 -0
  55. package/docs/getting-started.md +93 -0
  56. package/docs/integration.md +95 -0
  57. package/docs/ios-conformance.md +224 -0
  58. package/docs/layout.md +271 -0
  59. package/docs/native.md +52 -0
  60. package/docs/roadmap.md +106 -0
  61. package/docs/tokens-reference.md +223 -0
  62. package/docs/web.md +95 -0
  63. package/package.json +7 -3
  64. package/src/css/base.css +171 -1
  65. package/src/css/components/dock.css +335 -0
  66. package/src/css/components/drawer.css +40 -3
  67. package/src/css/components/fold-slot.css +44 -0
  68. package/src/css/components/icon-loader.css +67 -0
  69. package/src/css/components/icon.css +6 -4
  70. package/src/css/components/load-state.css +136 -0
  71. package/src/css/components/loader.css +6 -0
  72. package/src/css/components/menu.css +4 -0
  73. package/src/css/components/modal.css +24 -3
  74. package/src/css/components/navbar.css +5 -1
  75. package/src/css/components/pillbar.css +222 -7
  76. package/src/css/components/rail.css +8 -0
  77. package/src/css/components/reload.css +48 -0
  78. package/src/css/components/skeleton.css +41 -13
  79. package/src/css/components/tabs.css +5 -0
  80. package/src/css/components/thumb.css +63 -1
  81. package/src/css/components/toast.css +5 -1
  82. package/src/css/density.css +22 -0
  83. package/src/css/layout.css +95 -5
  84. package/src/css/utilities.css +4 -1
@@ -0,0 +1,22 @@
1
+ // junoui — inline icon sprite injector. Generated; do not edit.
2
+ // Fixes Safari dropping external <use href="file.svg#id"> refs: this injects
3
+ // the sprite into the document so you reference icons same-document instead:
4
+ // import '@junoput01/junoui/icons/inline';
5
+ // <svg class="juno-icon"><use href="#juno-i-gear" /></svg>
6
+ const SPRITE = "<svg xmlns=\"http://www.w3.org/2000/svg\" style=\"display:none\" fill=\"currentColor\">\n <symbol id=\"juno-i-arrow-clockwise\" viewBox=\"0 0 256 256\"><path d=\"M244,56v48a12,12,0,0,1-12,12H184a12,12,0,1,1,0-24H201.1l-19-17.38c-.13-.12-.26-.24-.38-.37A76,76,0,1,0,127,204h1a75.53,75.53,0,0,0,52.15-20.72,12,12,0,0,1,16.49,17.45A99.45,99.45,0,0,1,128,228h-1.37A100,100,0,1,1,198.51,57.06L220,76.72V56a12,12,0,0,1,24,0Z\"/></symbol>\n <symbol id=\"juno-i-arrow-down\" viewBox=\"0 0 256 256\"><path d=\"M208.49,152.49l-72,72a12,12,0,0,1-17,0l-72-72a12,12,0,0,1,17-17L116,187V40a12,12,0,0,1,24,0V187l51.51-51.52a12,12,0,0,1,17,17Z\"/></symbol>\n <symbol id=\"juno-i-arrow-left\" viewBox=\"0 0 256 256\"><path d=\"M228,128a12,12,0,0,1-12,12H69l51.52,51.51a12,12,0,0,1-17,17l-72-72a12,12,0,0,1,0-17l72-72a12,12,0,0,1,17,17L69,116H216A12,12,0,0,1,228,128Z\"/></symbol>\n <symbol id=\"juno-i-arrow-right\" viewBox=\"0 0 256 256\"><path d=\"M224.49,136.49l-72,72a12,12,0,0,1-17-17L187,140H40a12,12,0,0,1,0-24H187L135.51,64.48a12,12,0,0,1,17-17l72,72A12,12,0,0,1,224.49,136.49Z\"/></symbol>\n <symbol id=\"juno-i-arrow-up\" viewBox=\"0 0 256 256\"><path d=\"M208.49,120.49a12,12,0,0,1-17,0L140,69V216a12,12,0,0,1-24,0V69L64.49,120.49a12,12,0,0,1-17-17l72-72a12,12,0,0,1,17,0l72,72A12,12,0,0,1,208.49,120.49Z\"/></symbol>\n <symbol id=\"juno-i-arrows-clockwise\" viewBox=\"0 0 256 256\"><path d=\"M228,48V96a12,12,0,0,1-12,12H168a12,12,0,0,1,0-24h19l-7.8-7.8a75.55,75.55,0,0,0-53.32-22.26h-.43A75.49,75.49,0,0,0,72.39,75.57,12,12,0,1,1,55.61,58.41a99.38,99.38,0,0,1,69.87-28.47H126A99.42,99.42,0,0,1,196.2,59.23L204,67V48a12,12,0,0,1,24,0ZM183.61,180.43a75.49,75.49,0,0,1-53.09,21.63h-.43A75.55,75.55,0,0,1,76.77,179.8L69,172H88a12,12,0,0,0,0-24H40a12,12,0,0,0-12,12v48a12,12,0,0,0,24,0V189l7.8,7.8A99.42,99.42,0,0,0,130,226.06h.56a99.38,99.38,0,0,0,69.87-28.47,12,12,0,0,0-16.78-17.16Z\"/></symbol>\n <symbol id=\"juno-i-arrows-out\" viewBox=\"0 0 256 256\"><path d=\"M220,48V96a12,12,0,0,1-24,0V77l-35.51,35.52a12,12,0,0,1-17-17L179,60H160a12,12,0,0,1,0-24h48A12,12,0,0,1,220,48ZM95.51,143.51,60,179V160a12,12,0,0,0-24,0v48a12,12,0,0,0,12,12H96a12,12,0,0,0,0-24H77l35.52-35.51a12,12,0,0,0-17-17ZM208,148a12,12,0,0,0-12,12v19l-35.51-35.52a12,12,0,0,0-17,17L179,196H160a12,12,0,0,0,0,24h48a12,12,0,0,0,12-12V160A12,12,0,0,0,208,148ZM77,60H96a12,12,0,0,0,0-24H48A12,12,0,0,0,36,48V96a12,12,0,0,0,24,0V77l35.51,35.52a12,12,0,0,0,17-17Z\"/></symbol>\n <symbol id=\"juno-i-bell\" viewBox=\"0 0 256 256\"><path d=\"M225.29,165.93C216.61,151,212,129.57,212,104a84,84,0,0,0-168,0c0,25.58-4.59,47-13.27,61.93A20.08,20.08,0,0,0,30.66,186,19.77,19.77,0,0,0,48,196H84.18a44,44,0,0,0,87.64,0H208a19.77,19.77,0,0,0,17.31-10A20.08,20.08,0,0,0,225.29,165.93ZM128,212a20,20,0,0,1-19.6-16h39.2A20,20,0,0,1,128,212ZM54.66,172C63.51,154,68,131.14,68,104a60,60,0,0,1,120,0c0,27.13,4.48,50,13.33,68Z\"/></symbol>\n <symbol id=\"juno-i-calendar-blank\" viewBox=\"0 0 256 256\"><path d=\"M208,28H188V24a12,12,0,0,0-24,0v4H92V24a12,12,0,0,0-24,0v4H48A20,20,0,0,0,28,48V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V48A20,20,0,0,0,208,28ZM68,52a12,12,0,0,0,24,0h72a12,12,0,0,0,24,0h16V76H52V52ZM52,204V100H204V204Z\"/></symbol>\n <symbol id=\"juno-i-caret-down\" viewBox=\"0 0 256 256\"><path d=\"M216.49,104.49l-80,80a12,12,0,0,1-17,0l-80-80a12,12,0,0,1,17-17L128,159l71.51-71.52a12,12,0,0,1,17,17Z\"/></symbol>\n <symbol id=\"juno-i-caret-left\" viewBox=\"0 0 256 256\"><path d=\"M168.49,199.51a12,12,0,0,1-17,17l-80-80a12,12,0,0,1,0-17l80-80a12,12,0,0,1,17,17L97,128Z\"/></symbol>\n <symbol id=\"juno-i-caret-right\" viewBox=\"0 0 256 256\"><path d=\"M184.49,136.49l-80,80a12,12,0,0,1-17-17L159,128,87.51,56.49a12,12,0,1,1,17-17l80,80A12,12,0,0,1,184.49,136.49Z\"/></symbol>\n <symbol id=\"juno-i-caret-up\" viewBox=\"0 0 256 256\"><path d=\"M216.49,168.49a12,12,0,0,1-17,0L128,97,56.49,168.49a12,12,0,0,1-17-17l80-80a12,12,0,0,1,17,0l80,80A12,12,0,0,1,216.49,168.49Z\"/></symbol>\n <symbol id=\"juno-i-chat-circle\" viewBox=\"0 0 256 256\"><path d=\"M128,20A108,108,0,0,0,31.85,177.23L21,209.66A20,20,0,0,0,46.34,235l32.43-10.81A108,108,0,1,0,128,20Zm0,192a84,84,0,0,1-42.06-11.27,12,12,0,0,0-6-1.62,12.1,12.1,0,0,0-3.8.62l-29.79,9.93,9.93-29.79a12,12,0,0,0-1-9.81A84,84,0,1,1,128,212Z\"/></symbol>\n <symbol id=\"juno-i-check-circle\" viewBox=\"0 0 256 256\"><path d=\"M176.49,95.51a12,12,0,0,1,0,17l-56,56a12,12,0,0,1-17,0l-24-24a12,12,0,1,1,17-17L112,143l47.51-47.52A12,12,0,0,1,176.49,95.51ZM236,128A108,108,0,1,1,128,20,108.12,108.12,0,0,1,236,128Zm-24,0a84,84,0,1,0-84,84A84.09,84.09,0,0,0,212,128Z\"/></symbol>\n <symbol id=\"juno-i-check\" viewBox=\"0 0 256 256\"><path d=\"M232.49,80.49l-128,128a12,12,0,0,1-17,0l-56-56a12,12,0,1,1,17-17L96,183,215.51,63.51a12,12,0,0,1,17,17Z\"/></symbol>\n <symbol id=\"juno-i-clock\" viewBox=\"0 0 256 256\"><path d=\"M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212Zm68-84a12,12,0,0,1-12,12H128a12,12,0,0,1-12-12V72a12,12,0,0,1,24,0v44h44A12,12,0,0,1,196,128Z\"/></symbol>\n <symbol id=\"juno-i-cloud-arrow-down\" viewBox=\"0 0 256 256\"><path d=\"M192.49,167.51a12,12,0,0,1,0,17l-32,32a12,12,0,0,1-17,0l-32-32a12,12,0,1,1,17-17L140,179V128a12,12,0,0,1,24,0v51l11.51-11.52A12,12,0,0,1,192.49,167.51ZM160,36A92.08,92.08,0,0,0,79,84.37,68,68,0,1,0,72,220H84a12,12,0,0,0,0-24H72a44,44,0,0,1-1.81-87.95A91.7,91.7,0,0,0,68,128a12,12,0,0,0,24,0,68,68,0,0,1,136,0,67.27,67.27,0,0,1-7.25,30.59,12,12,0,1,0,21.42,10.82A91.08,91.08,0,0,0,252,128,92.1,92.1,0,0,0,160,36Z\"/></symbol>\n <symbol id=\"juno-i-cloud\" viewBox=\"0 0 256 256\"><path d=\"M160,36A92.09,92.09,0,0,0,79,84.36,68,68,0,1,0,72,220h88a92,92,0,0,0,0-184Zm0,160H72a44,44,0,0,1-1.82-88A91.86,91.86,0,0,0,68,128a12,12,0,0,0,24,0,68,68,0,1,1,68,68Z\"/></symbol>\n <symbol id=\"juno-i-copy\" viewBox=\"0 0 256 256\"><path d=\"M216,28H88A12,12,0,0,0,76,40V76H40A12,12,0,0,0,28,88V216a12,12,0,0,0,12,12H168a12,12,0,0,0,12-12V180h36a12,12,0,0,0,12-12V40A12,12,0,0,0,216,28ZM156,204H52V100H156Zm48-48H180V88a12,12,0,0,0-12-12H100V52H204Z\"/></symbol>\n <symbol id=\"juno-i-cpu\" viewBox=\"0 0 256 256\"><path d=\"M156,88H100a12,12,0,0,0-12,12v56a12,12,0,0,0,12,12h56a12,12,0,0,0,12-12V100A12,12,0,0,0,156,88Zm-12,56H112V112h32Zm88-4H220V116h12a12,12,0,0,0,0-24H220V56a20,20,0,0,0-20-20H164V24a12,12,0,0,0-24,0V36H116V24a12,12,0,0,0-24,0V36H56A20,20,0,0,0,36,56V92H24a12,12,0,0,0,0,24H36v24H24a12,12,0,0,0,0,24H36v36a20,20,0,0,0,20,20H92v12a12,12,0,0,0,24,0V220h24v12a12,12,0,0,0,24,0V220h36a20,20,0,0,0,20-20V164h12a12,12,0,0,0,0-24Zm-36,56H60V60H196Z\"/></symbol>\n <symbol id=\"juno-i-dots-three-vertical\" viewBox=\"0 0 256 256\"><path d=\"M112,60a16,16,0,1,1,16,16A16,16,0,0,1,112,60Zm16,52a16,16,0,1,0,16,16A16,16,0,0,0,128,112Zm0,68a16,16,0,1,0,16,16A16,16,0,0,0,128,180Z\"/></symbol>\n <symbol id=\"juno-i-dots-three\" viewBox=\"0 0 256 256\"><path d=\"M144,128a16,16,0,1,1-16-16A16,16,0,0,1,144,128ZM60,112a16,16,0,1,0,16,16A16,16,0,0,0,60,112Zm136,0a16,16,0,1,0,16,16A16,16,0,0,0,196,112Z\"/></symbol>\n <symbol id=\"juno-i-download\" viewBox=\"0 0 256 256\"><path d=\"M71.51,88.49a12,12,0,0,1,17-17L116,99V24a12,12,0,0,1,24,0V99l27.51-27.52a12,12,0,0,1,17,17l-48,48a12,12,0,0,1-17,0ZM224,116H188a12,12,0,0,0,0,24h32v56H36V140H68a12,12,0,0,0,0-24H32a20,20,0,0,0-20,20v64a20,20,0,0,0,20,20H224a20,20,0,0,0,20-20V136A20,20,0,0,0,224,116Zm-20,52a16,16,0,1,0-16,16A16,16,0,0,0,204,168Z\"/></symbol>\n <symbol id=\"juno-i-envelope\" viewBox=\"0 0 256 256\"><path d=\"M224,44H32A12,12,0,0,0,20,56V192a20,20,0,0,0,20,20H216a20,20,0,0,0,20-20V56A12,12,0,0,0,224,44Zm-96,83.72L62.85,68h130.3ZM92.79,128,44,172.72V83.28Zm17.76,16.28,9.34,8.57a12,12,0,0,0,16.22,0l9.34-8.57L193.15,188H62.85ZM163.21,128,212,83.28v89.44Z\"/></symbol>\n <symbol id=\"juno-i-eye-slash\" viewBox=\"0 0 256 256\"><path d=\"M56.88,31.93A12,12,0,1,0,39.12,48.07l16,17.65C20.67,88.66,5.72,121.58,5,123.13a12.08,12.08,0,0,0,0,9.75c.37.82,9.13,20.26,28.49,39.61C59.37,198.34,92,212,128,212a131.34,131.34,0,0,0,51-10l20.09,22.1a12,12,0,0,0,17.76-16.14ZM128,188c-29.59,0-55.47-10.73-76.91-31.88A130.69,130.69,0,0,1,29.52,128c5.27-9.31,18.79-29.9,42-44.29l90.09,99.11A109.33,109.33,0,0,1,128,188Zm123-55.12c-.36.81-9,20-28,39.16a12,12,0,1,1-17-16.9A130.48,130.48,0,0,0,226.48,128a130.36,130.36,0,0,0-21.57-28.12C183.46,78.73,157.59,68,128,68c-3.35,0-6.7.14-10,.42a12,12,0,1,1-2-23.91c3.93-.34,8-.51,12-.51,36,0,68.63,13.67,94.49,39.52,19.35,19.35,28.11,38.8,28.48,39.61A12.08,12.08,0,0,1,251,132.88Z\"/></symbol>\n <symbol id=\"juno-i-eye\" viewBox=\"0 0 256 256\"><path d=\"M251,123.13c-.37-.81-9.13-20.26-28.48-39.61C196.63,57.67,164,44,128,44S59.37,57.67,33.51,83.52C14.16,102.87,5.4,122.32,5,123.13a12.08,12.08,0,0,0,0,9.75c.37.82,9.13,20.26,28.49,39.61C59.37,198.34,92,212,128,212s68.63-13.66,94.48-39.51c19.36-19.35,28.12-38.79,28.49-39.61A12.08,12.08,0,0,0,251,123.13Zm-46.06,33C183.47,177.27,157.59,188,128,188s-55.47-10.73-76.91-31.88A130.36,130.36,0,0,1,29.52,128,130.45,130.45,0,0,1,51.09,99.89C72.54,78.73,98.41,68,128,68s55.46,10.73,76.91,31.89A130.36,130.36,0,0,1,226.48,128,130.45,130.45,0,0,1,204.91,156.12ZM128,84a44,44,0,1,0,44,44A44.05,44.05,0,0,0,128,84Zm0,64a20,20,0,1,1,20-20A20,20,0,0,1,128,148Z\"/></symbol>\n <symbol id=\"juno-i-file\" viewBox=\"0 0 256 256\"><path d=\"M216.49,79.52l-56-56A12,12,0,0,0,152,20H56A20,20,0,0,0,36,40V216a20,20,0,0,0,20,20H200a20,20,0,0,0,20-20V88A12,12,0,0,0,216.49,79.52ZM160,57l23,23H160ZM60,212V44h76V92a12,12,0,0,0,12,12h48V212Z\"/></symbol>\n <symbol id=\"juno-i-film-strip\" viewBox=\"0 0 256 256\"><path d=\"M216,36H40A20,20,0,0,0,20,56V200a20,20,0,0,0,20,20H216a20,20,0,0,0,20-20V56A20,20,0,0,0,216,36ZM44,100h72v56H44Zm96-24V60h24V76Zm-24,0H92V60h24Zm0,104v16H92V180Zm24,0h24v16H140Zm0-24V100h72v56Zm72-80H188V60h24ZM68,60V76H44V60ZM44,180H68v16H44Zm144,16V180h24v16Z\"/></symbol>\n <symbol id=\"juno-i-floppy-disk\" viewBox=\"0 0 256 256\"><path d=\"M222.14,69.17,186.83,33.86A19.86,19.86,0,0,0,172.69,28H48A20,20,0,0,0,28,48V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V83.31A19.86,19.86,0,0,0,222.14,69.17ZM164,204H92V160h72Zm40,0H188V156a20,20,0,0,0-20-20H88a20,20,0,0,0-20,20v48H52V52H171l33,33ZM164,84a12,12,0,0,1-12,12H96a12,12,0,0,1,0-24h56A12,12,0,0,1,164,84Z\"/></symbol>\n <symbol id=\"juno-i-folder\" viewBox=\"0 0 256 256\"><path d=\"M216,68H133.39l-26-29.29a20,20,0,0,0-15-6.71H40A20,20,0,0,0,20,52V200.62A19.41,19.41,0,0,0,39.38,220H216.89A19.13,19.13,0,0,0,236,200.89V88A20,20,0,0,0,216,68ZM44,56H90.61l10.67,12H44ZM212,196H44V92H212Z\"/></symbol>\n <symbol id=\"juno-i-funnel\" viewBox=\"0 0 256 256\"><path d=\"M234.29,47.91A20,20,0,0,0,216,36H40A20,20,0,0,0,25.2,69.45l.12.14L92,140.75V216a20,20,0,0,0,31.1,16.64l32-21.33A20,20,0,0,0,164,194.66V140.75l66.67-71.16.12-.14A20,20,0,0,0,234.29,47.91Zm-91,79.89A12,12,0,0,0,140,136v56.52l-24,16V136a12,12,0,0,0-3.25-8.2L49.23,60H206.77Z\"/></symbol>\n <symbol id=\"juno-i-gear\" viewBox=\"0 0 256 256\"><path d=\"M128,76a52,52,0,1,0,52,52A52.06,52.06,0,0,0,128,76Zm0,80a28,28,0,1,1,28-28A28,28,0,0,1,128,156Zm92-27.21v-1.58l14-17.51a12,12,0,0,0,2.23-10.59A111.75,111.75,0,0,0,225,71.89,12,12,0,0,0,215.89,66L193.61,63.5l-1.11-1.11L190,40.1A12,12,0,0,0,184.11,31a111.67,111.67,0,0,0-27.23-11.27A12,12,0,0,0,146.3,22L128.79,36h-1.58L109.7,22a12,12,0,0,0-10.59-2.23A111.75,111.75,0,0,0,71.89,31.05,12,12,0,0,0,66,40.11L63.5,62.39,62.39,63.5,40.1,66A12,12,0,0,0,31,71.89,111.67,111.67,0,0,0,19.77,99.12,12,12,0,0,0,22,109.7l14,17.51v1.58L22,146.3a12,12,0,0,0-2.23,10.59,111.75,111.75,0,0,0,11.29,27.22A12,12,0,0,0,40.11,190l22.28,2.48,1.11,1.11L66,215.9A12,12,0,0,0,71.89,225a111.67,111.67,0,0,0,27.23,11.27A12,12,0,0,0,109.7,234l17.51-14h1.58l17.51,14a12,12,0,0,0,10.59,2.23A111.75,111.75,0,0,0,184.11,225a12,12,0,0,0,5.91-9.06l2.48-22.28,1.11-1.11L215.9,190a12,12,0,0,0,9.06-5.91,111.67,111.67,0,0,0,11.27-27.23A12,12,0,0,0,234,146.3Zm-24.12-4.89a70.1,70.1,0,0,1,0,8.2,12,12,0,0,0,2.61,8.22l12.84,16.05A86.47,86.47,0,0,1,207,166.86l-20.43,2.27a12,12,0,0,0-7.65,4,69,69,0,0,1-5.8,5.8,12,12,0,0,0-4,7.65L166.86,207a86.47,86.47,0,0,1-10.49,4.35l-16.05-12.85a12,12,0,0,0-7.5-2.62c-.24,0-.48,0-.72,0a70.1,70.1,0,0,1-8.2,0,12.06,12.06,0,0,0-8.22,2.6L99.63,211.33A86.47,86.47,0,0,1,89.14,207l-2.27-20.43a12,12,0,0,0-4-7.65,69,69,0,0,1-5.8-5.8,12,12,0,0,0-7.65-4L49,166.86a86.47,86.47,0,0,1-4.35-10.49l12.84-16.05a12,12,0,0,0,2.61-8.22,70.1,70.1,0,0,1,0-8.2,12,12,0,0,0-2.61-8.22L44.67,99.63A86.47,86.47,0,0,1,49,89.14l20.43-2.27a12,12,0,0,0,7.65-4,69,69,0,0,1,5.8-5.8,12,12,0,0,0,4-7.65L89.14,49a86.47,86.47,0,0,1,10.49-4.35l16.05,12.85a12.06,12.06,0,0,0,8.22,2.6,70.1,70.1,0,0,1,8.2,0,12,12,0,0,0,8.22-2.6l16.05-12.85A86.47,86.47,0,0,1,166.86,49l2.27,20.43a12,12,0,0,0,4,7.65,69,69,0,0,1,5.8,5.8,12,12,0,0,0,7.65,4L207,89.14a86.47,86.47,0,0,1,4.35,10.49l-12.84,16.05A12,12,0,0,0,195.88,123.9Z\"/></symbol>\n <symbol id=\"juno-i-hard-drives\" viewBox=\"0 0 256 256\"><path d=\"M208,36H48A20,20,0,0,0,28,56V200a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V56A20,20,0,0,0,208,36Zm-4,24v56H52V60ZM52,196V140H204v56ZM160,88a16,16,0,1,1,16,16A16,16,0,0,1,160,88Zm32,80a16,16,0,1,1-16-16A16,16,0,0,1,192,168Z\"/></symbol>\n <symbol id=\"juno-i-heart\" viewBox=\"0 0 256 256\"><path d=\"M178,36c-20.09,0-37.92,7.93-50,21.56C115.92,43.93,98.09,36,78,36a66.08,66.08,0,0,0-66,66c0,72.34,105.81,130.14,110.31,132.57a12,12,0,0,0,11.38,0C138.19,232.14,244,174.34,244,102A66.08,66.08,0,0,0,178,36Zm-5.49,142.36A328.69,328.69,0,0,1,128,210.16a328.69,328.69,0,0,1-44.51-31.8C61.82,159.77,36,131.42,36,102A42,42,0,0,1,78,60c17.8,0,32.7,9.4,38.89,24.54a12,12,0,0,0,22.22,0C145.3,69.4,160.2,60,178,60a42,42,0,0,1,42,42C220,131.42,194.18,159.77,172.51,178.36Z\"/></symbol>\n <symbol id=\"juno-i-hexagon\" viewBox=\"0 0 256 256\"><path d=\"M225.6,62.64l-88-48.17a19.91,19.91,0,0,0-19.2,0l-88,48.17A20,20,0,0,0,20,80.19v95.62a20,20,0,0,0,10.4,17.55l88,48.17a19.89,19.89,0,0,0,19.2,0l88-48.17A20,20,0,0,0,236,175.81V80.19A20,20,0,0,0,225.6,62.64ZM212,173.44l-84,46-84-46V82.56l84-46,84,46Z\"/></symbol>\n <symbol id=\"juno-i-house\" viewBox=\"0 0 256 256\"><path d=\"M222.14,105.85l-80-80a20,20,0,0,0-28.28,0l-80,80A19.86,19.86,0,0,0,28,120v96a12,12,0,0,0,12,12h64a12,12,0,0,0,12-12V164h24v52a12,12,0,0,0,12,12h64a12,12,0,0,0,12-12V120A19.86,19.86,0,0,0,222.14,105.85ZM204,204H164V152a12,12,0,0,0-12-12H104a12,12,0,0,0-12,12v52H52V121.65l76-76,76,76Z\"/></symbol>\n <symbol id=\"juno-i-images\" viewBox=\"0 0 256 256\"><path d=\"M160,88a16,16,0,1,1,16,16A16,16,0,0,1,160,88Zm76-32V160a20,20,0,0,1-20,20H204v20a20,20,0,0,1-20,20H40a20,20,0,0,1-20-20V88A20,20,0,0,1,40,68H60V56A20,20,0,0,1,80,36H216A20,20,0,0,1,236,56ZM180,180H80a20,20,0,0,1-20-20V92H44V196H180Zm-21.66-24L124,121.66,89.66,156ZM212,60H84v67.72l25.86-25.86a20,20,0,0,1,28.28,0L192.28,156H212Z\"/></symbol>\n <symbol id=\"juno-i-info\" viewBox=\"0 0 256 256\"><path d=\"M108,84a16,16,0,1,1,16,16A16,16,0,0,1,108,84Zm128,44A108,108,0,1,1,128,20,108.12,108.12,0,0,1,236,128Zm-24,0a84,84,0,1,0-84,84A84.09,84.09,0,0,0,212,128Zm-72,36.68V132a20,20,0,0,0-20-20,12,12,0,0,0-4,23.32V168a20,20,0,0,0,20,20,12,12,0,0,0,4-23.32Z\"/></symbol>\n <symbol id=\"juno-i-link\" viewBox=\"0 0 256 256\"><path d=\"M117.18,188.74a12,12,0,0,1,0,17l-5.12,5.12A58.26,58.26,0,0,1,70.6,228h0A58.62,58.62,0,0,1,29.14,127.92L63.89,93.17a58.64,58.64,0,0,1,98.56,28.11,12,12,0,1,1-23.37,5.44,34.65,34.65,0,0,0-58.22-16.58L46.11,144.89A34.62,34.62,0,0,0,70.57,204h0a34.41,34.41,0,0,0,24.49-10.14l5.11-5.12A12,12,0,0,1,117.18,188.74ZM226.83,45.17a58.65,58.65,0,0,0-82.93,0l-5.11,5.11a12,12,0,0,0,17,17l5.12-5.12a34.63,34.63,0,1,1,49,49L175.1,145.86A34.39,34.39,0,0,1,150.61,156h0a34.63,34.63,0,0,1-33.69-26.72,12,12,0,0,0-23.38,5.44A58.64,58.64,0,0,0,150.56,180h.05a58.28,58.28,0,0,0,41.47-17.17l34.75-34.75a58.62,58.62,0,0,0,0-82.91Z\"/></symbol>\n <symbol id=\"juno-i-list\" viewBox=\"0 0 256 256\"><path d=\"M228,128a12,12,0,0,1-12,12H40a12,12,0,0,1,0-24H216A12,12,0,0,1,228,128ZM40,76H216a12,12,0,0,0,0-24H40a12,12,0,0,0,0,24ZM216,180H40a12,12,0,0,0,0,24H216a12,12,0,0,0,0-24Z\"/></symbol>\n <symbol id=\"juno-i-lock-open\" viewBox=\"0 0 256 256\"><path d=\"M208,76H100V56a28,28,0,0,1,28-28c13.51,0,25.65,9.62,28.24,22.39a12,12,0,1,0,23.52-4.78C174.87,21.5,153.1,4,128,4A52.06,52.06,0,0,0,76,56V76H48A20,20,0,0,0,28,96V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V96A20,20,0,0,0,208,76Zm-4,128H52V100H204Zm-92-52a16,16,0,1,1,16,16A16,16,0,0,1,112,152Z\"/></symbol>\n <symbol id=\"juno-i-lock\" viewBox=\"0 0 256 256\"><path d=\"M208,76H180V56A52,52,0,0,0,76,56V76H48A20,20,0,0,0,28,96V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V96A20,20,0,0,0,208,76ZM100,56a28,28,0,0,1,56,0V76H100ZM204,204H52V100H204Zm-60-52a16,16,0,1,1-16-16A16,16,0,0,1,144,152Z\"/></symbol>\n <symbol id=\"juno-i-magnifying-glass\" viewBox=\"0 0 256 256\"><path d=\"M232.49,215.51,185,168a92.12,92.12,0,1,0-17,17l47.53,47.54a12,12,0,0,0,17-17ZM44,112a68,68,0,1,1,68,68A68.07,68.07,0,0,1,44,112Z\"/></symbol>\n <symbol id=\"juno-i-minus\" viewBox=\"0 0 256 256\"><path d=\"M228,128a12,12,0,0,1-12,12H40a12,12,0,0,1,0-24H216A12,12,0,0,1,228,128Z\"/></symbol>\n <symbol id=\"juno-i-moon\" viewBox=\"0 0 256 256\"><path d=\"M236.37,139.4a12,12,0,0,0-12-3A84.07,84.07,0,0,1,119.6,31.59a12,12,0,0,0-15-15A108.86,108.86,0,0,0,49.69,55.07,108,108,0,0,0,136,228a107.09,107.09,0,0,0,64.93-21.69,108.86,108.86,0,0,0,38.44-54.94A12,12,0,0,0,236.37,139.4Zm-49.88,47.74A84,84,0,0,1,68.86,69.51,84.93,84.93,0,0,1,92.27,48.29Q92,52.13,92,56A108.12,108.12,0,0,0,200,164q3.87,0,7.71-.27A84.79,84.79,0,0,1,186.49,187.14Z\"/></symbol>\n <symbol id=\"juno-i-pencil-simple\" viewBox=\"0 0 256 256\"><path d=\"M230.14,70.54,185.46,25.85a20,20,0,0,0-28.29,0L33.86,149.17A19.85,19.85,0,0,0,28,163.31V208a20,20,0,0,0,20,20H92.69a19.86,19.86,0,0,0,14.14-5.86L230.14,98.82a20,20,0,0,0,0-28.28ZM91,204H52V165l84-84,39,39ZM192,103,153,64l18.34-18.34,39,39Z\"/></symbol>\n <symbol id=\"juno-i-play\" viewBox=\"0 0 256 256\"><path d=\"M234.49,111.07,90.41,22.94A20,20,0,0,0,60,39.87V216.13a20,20,0,0,0,30.41,16.93l144.08-88.13a19.82,19.82,0,0,0,0-33.86ZM84,208.85V47.15L216.16,128Z\"/></symbol>\n <symbol id=\"juno-i-plus\" viewBox=\"0 0 256 256\"><path d=\"M228,128a12,12,0,0,1-12,12H140v76a12,12,0,0,1-24,0V140H40a12,12,0,0,1,0-24h76V40a12,12,0,0,1,24,0v76h76A12,12,0,0,1,228,128Z\"/></symbol>\n <symbol id=\"juno-i-puzzle-piece\" viewBox=\"0 0 256 256\"><path d=\"M222.41,155.16a12,12,0,0,0-11.56-.69A16,16,0,0,1,188,139,16.2,16.2,0,0,1,202.8,124a15.83,15.83,0,0,1,8,1.5A12,12,0,0,0,228,114.7V72a20,20,0,0,0-20-20H176a40.15,40.15,0,0,0-12.62-29.16,39.67,39.67,0,0,0-29.94-10.76,40.08,40.08,0,0,0-37.34,37C96,50.07,96,51,96,52H64A20,20,0,0,0,44,72v28a40.15,40.15,0,0,0-29.16,12.62A40,40,0,0,0,41.1,179.9a28.3,28.3,0,0,0,2.9.1v28a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V165.31A12,12,0,0,0,222.41,155.16ZM204,204H68V165.31a12,12,0,0,0-17.15-10.84A15.9,15.9,0,0,1,42.8,156,16.2,16.2,0,0,1,28,141.06a16,16,0,0,1,22.82-15.52A12,12,0,0,0,68,114.7V76h42.7a12,12,0,0,0,10.83-17.15A15.9,15.9,0,0,1,120,50.8,16.19,16.19,0,0,1,134.94,36a16,16,0,0,1,15.53,22.81A12,12,0,0,0,161.31,76H204v24c-1,0-1.93,0-2.9.11A40,40,0,0,0,204,180h0Z\"/></symbol>\n <symbol id=\"juno-i-question\" viewBox=\"0 0 256 256\"><path d=\"M144,180a16,16,0,1,1-16-16A16,16,0,0,1,144,180Zm92-52A108,108,0,1,1,128,20,108.12,108.12,0,0,1,236,128Zm-24,0a84,84,0,1,0-84,84A84.09,84.09,0,0,0,212,128ZM128,64c-24.26,0-44,17.94-44,40v4a12,12,0,0,0,24,0v-4c0-8.82,9-16,20-16s20,7.18,20,16-9,16-20,16a12,12,0,0,0-12,12v8a12,12,0,0,0,23.73,2.56C158.31,137.88,172,122.37,172,104,172,81.94,152.26,64,128,64Z\"/></symbol>\n <symbol id=\"juno-i-sliders-horizontal\" viewBox=\"0 0 256 256\"><path d=\"M40,92H70.06a36,36,0,0,0,67.88,0H216a12,12,0,0,0,0-24H137.94a36,36,0,0,0-67.88,0H40a12,12,0,0,0,0,24Zm64-24A12,12,0,1,1,92,80,12,12,0,0,1,104,68Zm112,96H201.94a36,36,0,0,0-67.88,0H40a12,12,0,0,0,0,24h94.06a36,36,0,0,0,67.88,0H216a12,12,0,0,0,0-24Zm-48,24a12,12,0,1,1,12-12A12,12,0,0,1,168,188Z\"/></symbol>\n <symbol id=\"juno-i-sliders\" viewBox=\"0 0 256 256\"><path d=\"M68,102.06V40a12,12,0,0,0-24,0v62.06a36,36,0,0,0,0,67.88V216a12,12,0,0,0,24,0V169.94a36,36,0,0,0,0-67.88ZM56,148a12,12,0,1,1,12-12A12,12,0,0,1,56,148ZM164,88a36.07,36.07,0,0,0-24-33.94V40a12,12,0,0,0-24,0V54.06a36,36,0,0,0,0,67.88V216a12,12,0,0,0,24,0V121.94A36.07,36.07,0,0,0,164,88Zm-36,12a12,12,0,1,1,12-12A12,12,0,0,1,128,100Zm108,68a36.07,36.07,0,0,0-24-33.94V40a12,12,0,0,0-24,0v94.06a36,36,0,0,0,0,67.88V216a12,12,0,0,0,24,0V201.94A36.07,36.07,0,0,0,236,168Zm-36,12a12,12,0,1,1,12-12A12,12,0,0,1,200,180Z\"/></symbol>\n <symbol id=\"juno-i-squares-four\" viewBox=\"0 0 256 256\"><path d=\"M100,36H56A20,20,0,0,0,36,56v44a20,20,0,0,0,20,20h44a20,20,0,0,0,20-20V56A20,20,0,0,0,100,36ZM96,96H60V60H96ZM200,36H156a20,20,0,0,0-20,20v44a20,20,0,0,0,20,20h44a20,20,0,0,0,20-20V56A20,20,0,0,0,200,36Zm-4,60H160V60h36Zm-96,40H56a20,20,0,0,0-20,20v44a20,20,0,0,0,20,20h44a20,20,0,0,0,20-20V156A20,20,0,0,0,100,136Zm-4,60H60V160H96Zm104-60H156a20,20,0,0,0-20,20v44a20,20,0,0,0,20,20h44a20,20,0,0,0,20-20V156A20,20,0,0,0,200,136Zm-4,60H160V160h36Z\"/></symbol>\n <symbol id=\"juno-i-star\" viewBox=\"0 0 256 256\"><path d=\"M243,96a20.33,20.33,0,0,0-17.74-14l-56.59-4.57L146.83,24.62a20.36,20.36,0,0,0-37.66,0L87.35,77.44,30.76,82A20.45,20.45,0,0,0,19.1,117.88l43.18,37.24-13.2,55.7A20.37,20.37,0,0,0,79.57,233L128,203.19,176.43,233a20.39,20.39,0,0,0,30.49-22.15l-13.2-55.7,43.18-37.24A20.43,20.43,0,0,0,243,96ZM172.53,141.7a12,12,0,0,0-3.84,11.86L181.58,208l-47.29-29.08a12,12,0,0,0-12.58,0L74.42,208l12.89-54.4a12,12,0,0,0-3.84-11.86L41.2,105.24l55.4-4.47a12,12,0,0,0,10.13-7.38L128,41.89l21.27,51.5a12,12,0,0,0,10.13,7.38l55.4,4.47Z\"/></symbol>\n <symbol id=\"juno-i-sun\" viewBox=\"0 0 256 256\"><path d=\"M116,36V20a12,12,0,0,1,24,0V36a12,12,0,0,1-24,0Zm80,92a68,68,0,1,1-68-68A68.07,68.07,0,0,1,196,128Zm-24,0a44,44,0,1,0-44,44A44.05,44.05,0,0,0,172,128ZM51.51,68.49a12,12,0,1,0,17-17l-12-12a12,12,0,0,0-17,17Zm0,119-12,12a12,12,0,0,0,17,17l12-12a12,12,0,1,0-17-17ZM196,72a12,12,0,0,0,8.49-3.51l12-12a12,12,0,0,0-17-17l-12,12A12,12,0,0,0,196,72Zm8.49,115.51a12,12,0,0,0-17,17l12,12a12,12,0,0,0,17-17ZM48,128a12,12,0,0,0-12-12H20a12,12,0,0,0,0,24H36A12,12,0,0,0,48,128Zm80,80a12,12,0,0,0-12,12v16a12,12,0,0,0,24,0V220A12,12,0,0,0,128,208Zm108-92H220a12,12,0,0,0,0,24h16a12,12,0,0,0,0-24Z\"/></symbol>\n <symbol id=\"juno-i-trash\" viewBox=\"0 0 256 256\"><path d=\"M216,48H180V36A28,28,0,0,0,152,8H104A28,28,0,0,0,76,36V48H40a12,12,0,0,0,0,24h4V208a20,20,0,0,0,20,20H192a20,20,0,0,0,20-20V72h4a12,12,0,0,0,0-24ZM100,36a4,4,0,0,1,4-4h48a4,4,0,0,1,4,4V48H100Zm88,168H68V72H188ZM116,104v64a12,12,0,0,1-24,0V104a12,12,0,0,1,24,0Zm48,0v64a12,12,0,0,1-24,0V104a12,12,0,0,1,24,0Z\"/></symbol>\n <symbol id=\"juno-i-upload-simple\" viewBox=\"0 0 256 256\"><path d=\"M228,144v64a12,12,0,0,1-12,12H40a12,12,0,0,1-12-12V144a12,12,0,0,1,24,0v52H204V144a12,12,0,0,1,24,0ZM96.49,80.49,116,61v83a12,12,0,0,0,24,0V61l19.51,19.52a12,12,0,1,0,17-17l-40-40a12,12,0,0,0-17,0l-40,40a12,12,0,1,0,17,17Z\"/></symbol>\n <symbol id=\"juno-i-upload\" viewBox=\"0 0 256 256\"><path d=\"M188,184a16,16,0,1,1,16-16A16,16,0,0,1,188,184Zm36-68H180a12,12,0,0,0,0,24h40v56H36V140H76a12,12,0,0,0,0-24H32a20,20,0,0,0-20,20v64a20,20,0,0,0,20,20H224a20,20,0,0,0,20-20V136A20,20,0,0,0,224,116ZM88.49,80.49,116,53v75a12,12,0,0,0,24,0V53l27.51,27.52a12,12,0,1,0,17-17l-48-48a12,12,0,0,0-17,0l-48,48a12,12,0,1,0,17,17Z\"/></symbol>\n <symbol id=\"juno-i-user\" viewBox=\"0 0 256 256\"><path d=\"M234.38,210a123.36,123.36,0,0,0-60.78-53.23,76,76,0,1,0-91.2,0A123.36,123.36,0,0,0,21.62,210a12,12,0,1,0,20.77,12c18.12-31.32,50.12-50,85.61-50s67.49,18.69,85.61,50a12,12,0,0,0,20.77-12ZM76,96a52,52,0,1,1,52,52A52.06,52.06,0,0,1,76,96Z\"/></symbol>\n <symbol id=\"juno-i-users\" viewBox=\"0 0 256 256\"><path d=\"M125.18,156.94a64,64,0,1,0-82.36,0,100.23,100.23,0,0,0-39.49,32,12,12,0,0,0,19.35,14.2,76,76,0,0,1,122.64,0,12,12,0,0,0,19.36-14.2A100.33,100.33,0,0,0,125.18,156.94ZM44,108a40,40,0,1,1,40,40A40,40,0,0,1,44,108Zm206.1,97.67a12,12,0,0,1-16.78-2.57A76.31,76.31,0,0,0,172,172a12,12,0,0,1,0-24,40,40,0,1,0-10.3-78.67,12,12,0,1,1-6.16-23.19,64,64,0,0,1,57.64,110.8,100.23,100.23,0,0,1,39.49,32A12,12,0,0,1,250.1,205.67Z\"/></symbol>\n <symbol id=\"juno-i-warning-circle\" viewBox=\"0 0 256 256\"><path d=\"M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212Zm-12-80V80a12,12,0,0,1,24,0v52a12,12,0,0,1-24,0Zm28,40a16,16,0,1,1-16-16A16,16,0,0,1,144,172Z\"/></symbol>\n <symbol id=\"juno-i-warning\" viewBox=\"0 0 256 256\"><path d=\"M240.26,186.1,152.81,34.23h0a28.74,28.74,0,0,0-49.62,0L15.74,186.1a27.45,27.45,0,0,0,0,27.71A28.31,28.31,0,0,0,40.55,228h174.9a28.31,28.31,0,0,0,24.79-14.19A27.45,27.45,0,0,0,240.26,186.1Zm-20.8,15.7a4.46,4.46,0,0,1-4,2.2H40.55a4.46,4.46,0,0,1-4-2.2,3.56,3.56,0,0,1,0-3.73L124,46.2a4.77,4.77,0,0,1,8,0l87.44,151.87A3.56,3.56,0,0,1,219.46,201.8ZM116,136V104a12,12,0,0,1,24,0v32a12,12,0,0,1-24,0Zm28,40a16,16,0,1,1-16-16A16,16,0,0,1,144,176Z\"/></symbol>\n <symbol id=\"juno-i-x-circle\" viewBox=\"0 0 256 256\"><path d=\"M168.49,104.49,145,128l23.52,23.51a12,12,0,0,1-17,17L128,145l-23.51,23.52a12,12,0,0,1-17-17L111,128,87.51,104.49a12,12,0,0,1,17-17L128,111l23.51-23.52a12,12,0,0,1,17,17ZM236,128A108,108,0,1,1,128,20,108.12,108.12,0,0,1,236,128Zm-24,0a84,84,0,1,0-84,84A84.09,84.09,0,0,0,212,128Z\"/></symbol>\n <symbol id=\"juno-i-x\" viewBox=\"0 0 256 256\"><path d=\"M208.49,191.51a12,12,0,0,1-17,17L128,145,64.49,208.49a12,12,0,0,1-17-17L111,128,47.51,64.49a12,12,0,0,1,17-17L128,111l63.51-63.52a12,12,0,0,1,17,17L145,128Z\"/></symbol>\n</svg>\n";
7
+
8
+ /** Inject the sprite into `doc` once (id-guarded, safe to call repeatedly). */
9
+ export function installJunoIcons(doc = typeof document !== 'undefined' ? document : undefined) {
10
+ if (!doc || doc.getElementById('juno-icon-sprite')) return;
11
+ const holder = doc.createElement('div');
12
+ holder.id = 'juno-icon-sprite';
13
+ holder.style.display = 'none';
14
+ holder.setAttribute('aria-hidden', 'true');
15
+ holder.innerHTML = SPRITE;
16
+ (doc.body || doc.documentElement).prepend(holder);
17
+ }
18
+
19
+ // Auto-install on import in a browser (no-op server-side / pre-hydration).
20
+ if (typeof document !== 'undefined') installJunoIcons();
21
+
22
+ export default installJunoIcons;
@@ -0,0 +1,188 @@
1
+ # Accessibility
2
+
3
+ Accessibility is a primary goal of junoui, not an afterthought. The design system
4
+ ships the **look and the accessibility contract**; the _interaction behavior_ (focus
5
+ trapping, ARIA state updates, keyboard handlers) lives in your app or a sibling
6
+ `junoui-<framework>` package — but this page states exactly what junoui guarantees, to
7
+ which published standard, and what you must wire up.
8
+
9
+ ## Standards we build to
10
+
11
+ junoui targets the international web accessibility standards and references the
12
+ specific criteria it meets — so a claim can be checked, not just trusted:
13
+
14
+ - **[WCAG 2.2](https://www.w3.org/TR/WCAG22/)** (W3C Recommendation) — **AA across the
15
+ board, AAA where it matters** (see the specific success criteria cited below).
16
+ - **[WAI-ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/)** roles/states/properties, and
17
+ the **[ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/patterns/)**
18
+ for the keyboard/interaction patterns in [the contract table](#the-aria-contract-per-component).
19
+
20
+ Criteria referenced on this page, by number:
21
+
22
+ | SC | Name | Level | Where |
23
+ | ---------- | --------------------------- | ----- | --------------------------------- |
24
+ | **1.4.1** | Use of Color | A | [Color](#color) |
25
+ | **1.4.3** | Contrast (Minimum) | AA | [Color](#color) |
26
+ | **1.4.6** | Contrast (Enhanced) | AAA | [Color](#color) |
27
+ | **1.4.11** | Non-text Contrast | AA | [Focus](#focus) |
28
+ | **2.4.7** | Focus Visible | AA | [Focus](#focus) |
29
+ | **2.4.11** | Focus Appearance | AA | [Focus](#focus) |
30
+ | **2.2.2** | Pause, Stop, Hide | A | [Motion](#motion) |
31
+ | **2.3.3** | Animation from Interactions | AAA | [Motion](#motion) |
32
+ | **2.5.8** | Target Size (Minimum) | AA | [Targets](#targets) |
33
+ | **2.5.5** | Target Size (Enhanced) | AAA | [Targets](#targets) |
34
+ | **1.4.12** | Text Spacing | AA | [Density](#density--text-spacing) |
35
+
36
+ This coverage is expected to grow: junoui adds standards references as components and
37
+ tokens land, and never removes a guarantee without a semver-major note.
38
+
39
+ ## Color
40
+
41
+ - **Never make color the only signal** (WCAG **1.4.1** Use of Color, A). Every status
42
+ pairs color with a text label, icon, or shape. A badge says `WARNING`; a dot sits
43
+ beside its word.
44
+ - The **colorblind** palette (IBM Carbon universal set) is distinguishable across
45
+ deuteranopia, protanopia, and tritanopia. On the dark base–card surfaces (`s0`–`s2`)
46
+ every status role passes WCAG **1.4.6** Contrast (Enhanced), AAA (≥ 7:1); on the
47
+ selected/overlay surface (`s3`) it holds AA (≈ 7:1, ≥ 4.5:1). Use it for
48
+ accessibility-critical or universal audiences.
49
+ - Contrast (measured — regenerate with `node scripts/audit-contrast.mjs`):
50
+ - `data` (values) meets **1.4.6** (AAA, ≥ 7:1) on **every** surface `s0`–`s3`, both
51
+ themes and all three palettes (dark ≈ 13–17:1).
52
+ - `label` (secondary text) meets **1.4.3** Contrast (Minimum), AA (≥ 4.5:1) on **every**
53
+ surface `s0`–`s3` (dark ≈ 4.6–5.9:1). Never use it for values.
54
+ - Status roles (`nominal`/`active`/`caution`/`warning`/`target`) meet **1.4.3** AA on
55
+ `s0`–`s3`; most reach AAA on `s0`–`s2`.
56
+ - `muted` and `data-dim` are **decorative / WCAG-exempt roles** — disabled controls,
57
+ placeholders, faint tick labels. They intentionally sit below 4.5:1; never carry
58
+ meaningful text in them (a real timestamp or value uses `label`/`data`, not `data-dim`).
59
+ Exact values: [tokens-reference.md](./tokens-reference.md).
60
+ - **Surfaces & borders (dark).** Adjacent surface fills differ by only ~1.05:1, so a
61
+ block reads against its background through its **border**, not its fill. The dark
62
+ `border` / `border-strong` tokens are lifted (≈ 1.5:1 / 2.2:1 vs the base surface) so
63
+ cards, panels and dividers stay perceptible. Container boundaries are decorative and
64
+ exempt from **1.4.11** Non-text Contrast. Component-_identifying_ edges use the
65
+ `--juno-control-edge` family instead: input / checkbox / slider / avatar edges
66
+ (`-strong`, = `label`) clear ≥ 3:1 (dark ≈ 5.3:1) as **1.4.11** requires; table
67
+ gridlines (`--juno-control-edge`, = `muted`, ≈ 2.9:1) are supplementary rules — table
68
+ structure is carried by `<th>`/alignment, not the line — so they fall under the
69
+ decorative exemption.
70
+
71
+ ## Focus
72
+
73
+ - A visible focus ring is defined in the base layer for links, buttons, inputs, and
74
+ anything with `[tabindex]`: 2px solid `--juno-active`, 2px offset. It tracks the
75
+ theme and is never removed (WCAG **2.4.7** Focus Visible, AA).
76
+ - The ring's size, offset and `active`-role contrast satisfy **2.4.11** Focus
77
+ Appearance (AA, WCAG 2.2) and **1.4.11** Non-text Contrast (AA) against adjacent
78
+ surfaces.
79
+ - Don't suppress `:focus-visible`. If you build a custom control, give it a tabstop
80
+ and let the ring apply.
81
+ - Text inputs / select / textarea swap the outline for an equivalent visible signal:
82
+ an `active` border + 1px ring. Never remove focus indication outright.
83
+
84
+ ## Motion
85
+
86
+ - All animations respect `prefers-reduced-motion: reduce` (base layer slows them to
87
+ near-static). Loader pulses and the `--live` dot honor it automatically — covering
88
+ WCAG **2.2.2** Pause, Stop, Hide (A) and **2.3.3** Animation from Interactions (AAA).
89
+ - Use the `determinate` loaders when progress is known — less motion, more trust.
90
+ - **Script can read the preference too.** A CSS media query never reaches imperative
91
+ JS (a `scrollIntoView({behavior})` choice, a rAF-driven transform). The base layer
92
+ exposes the same preference as a custom property, so one read decides:
93
+
94
+ ```js
95
+ const motion = getComputedStyle(document.documentElement)
96
+ .getPropertyValue('--juno-motion')
97
+ .trim(); // 'auto' | 'none'
98
+ el.scrollIntoView({ behavior: motion === 'none' ? 'auto' : 'smooth' });
99
+ ```
100
+
101
+ `--juno-motion-scale` (`1` | `0`) is the multiplier form, for CSS that authors a
102
+ duration as `calc(var(--juno-motion-duration-base) * var(--juno-motion-scale))`.
103
+
104
+ ## Targets
105
+
106
+ - `--juno-size-tap-min` = 24px — WCAG **2.5.8** Target Size (Minimum), AA (WCAG 2.2).
107
+ - `--juno-size-tap-comfortable` = 44px — WCAG **2.5.5** Target Size (Enhanced), AAA;
108
+ recommended for primary actions on touch.
109
+ - `.juno-btn` already sets `min-height: var(--juno-size-tap-min)`. The other
110
+ touch surfaces — `.juno-menu__item` (dock overflow lands here on phones) and
111
+ `.juno-navbar__actions > *` — do too, so nothing routed onto a phone falls
112
+ under the minimum. Don't use `.juno-btn--sm` on a touch surface: it is
113
+ deliberately below the tap minimum (see `button.css`).
114
+ - **On coarse pointers this is automatic:** under `@media (pointer: coarse)` the
115
+ base layer raises `--juno-size-tap-min` to the comfortable 44px, so every
116
+ control sized off the tap minimum grows on touch devices. A cascade override —
117
+ the token values themselves don't change.
118
+ - Hover-revealed affordances get a touch fallback: table row actions stay
119
+ visible under `@media (hover: none)`.
120
+
121
+ ## Forced colors / high contrast
122
+
123
+ - The base layer opts components into the system palette under
124
+ `@media (forced-colors: active)`: borders fall back to `CanvasText`, focus to
125
+ `Highlight`. Badges keep their fill (`forced-color-adjust: none`) so status stays
126
+ legible.
127
+
128
+ ## Writing direction (RTL)
129
+
130
+ - Components use **CSS logical properties** (`margin-inline`, `padding-inline`,
131
+ `inset`, `min-inline-size`) so they mirror correctly under `dir="rtl"` with no
132
+ extra CSS. Avoid reintroducing physical `left`/`right` in consuming code.
133
+
134
+ ## Density & text spacing
135
+
136
+ - Type tokens set line-height and spacing with enough headroom that user or
137
+ browser-imposed **text spacing** (line-height 1.5×, paragraph/letter/word spacing)
138
+ doesn't clip or overlap content — WCAG **1.4.12** Text Spacing (AA).
139
+ - Compact density shrinks padding, never a control's `min-height` (the tap target
140
+ under [Targets](#targets)) and never the type scale — readability holds at every
141
+ density.
142
+
143
+ ## The ARIA contract (per component)
144
+
145
+ junoui gives structure + style; you add roles/state:
146
+
147
+ | Component | You must add |
148
+ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
149
+ | Badge / status dot | Text label in the DOM (not color alone). For live values, `aria-live="polite"`. |
150
+ | Button | Use a real `<button>`; `disabled` for disabled (not just the class). |
151
+ | Card | Heading semantics (`<h2>`…) in `__head` if it titles a region. |
152
+ | Loader (indeterminate) | `role="status"` + `aria-label="Loading"`. |
153
+ | Loader (determinate) | `role="progressbar"` + `aria-valuenow/min/max`. |
154
+ | Input / select / textarea | A `<label>` tied by `for`/`id`. Errors: `aria-invalid="true"` + `aria-describedby` → the error text. Required: `aria-required`. |
155
+ | Checkbox / radio | Real `<input>` inside its `<label>`; radios share a `name`. State is native `checked`. |
156
+ | Switch | `role="switch"` on the input; app keeps `checked` + `aria-checked` in sync. |
157
+ | Toggle button | Real `<button>` with `aria-pressed`; app flips it. Legend text is in the DOM, not color alone. |
158
+ | Slider | Value-driven `div`: add `role="slider"`, `tabindex`, `aria-valuenow/min/max` (+ `aria-valuetext` for units); app wires drag + arrow/Page keys and keeps `--juno-slider-pct` in sync. |
159
+ | Modal / drawer / sheet | Native `<dialog>` + `showModal()` (focus-trap, ESC, inert background, scrim-click come free). Name it via `aria-labelledby` → the title. **`<dialog>` is the only supported root** — a hand-rolled `<div>` overlay (own scrim, own positioning) loses focus-trap and `inert` silently; there is no CSS-only substitute. Bottom sheet (`.juno-drawer--bottom`): the grab handle (`.juno-sheet__handle`) is decorative, `aria-hidden="true"` — it carries no label and no drag behavior; dragging to resize/dismiss is the app's responsibility, not junoui's. |
160
+ | Tooltip | Trigger needs a tabstop; bubble `role="tooltip"` + `aria-describedby` so it reveals on focus, not only hover. Promote to `popover="hint"` to escape ancestor clipping (top layer). |
161
+ | Popover | Native `popover` + `popovertarget` button (top layer; ESC + outside-click dismiss + `aria-expanded` come free). Keep `aria-haspopup`; move focus in on open, restore on close. |
162
+ | Menu / dropdown | `role="menu"` + `role="menuitem"`; native `popover` + `popovertarget` (free dismiss/ESC/`aria-expanded`), items `popovertargetaction="hide"`. App wires arrow-key roving focus + Enter. |
163
+ | Table / data grid | Real `<table>`/`<th>`/`<td>` (use `scope`). Sortable headers: `aria-sort` per column, kept in sync as you reorder. Selected rows: `aria-selected="true"`. Truncated cells: a `title` (or tooltip) with the full value. |
164
+ | Alert | `role="alert"` for urgent (assertive), `role="status"` for passive. Icon is `aria-hidden`; the message text carries the meaning. Dismiss is a real `<button>` with `aria-label`. |
165
+ | Toast | Stack is a live region: `aria-live="polite"` (`assertive` for errors). Each toast `role="status"`; keep it short. Dismiss `<button>` + `aria-label`; don't rely on the auto-timeout alone for critical info. |
166
+ | Tabs | `role="tablist"` + `role="tab"` (each `aria-controls` its `role="tabpanel"`, which is `aria-labelledby` the tab). App keeps one `aria-selected="true"`, hides the rest, moves roving `tabindex`, and wires ← / → / Home / End. |
167
+ | Accordion | Native `<details>`/`<summary>` — open state, focus, and Enter/Space are built in. For one-open-at-a-time, give the `<details>` a shared `name`. No ARIA to add. |
168
+ | Icon | Decorative: `aria-hidden="true"`. Meaningful (icon-only control): `role="img"` + `aria-label`, or label the surrounding control. Never the only signal for status. |
169
+ | Skeleton | Wrap the loading region in `aria-busy="true"` (+ `aria-live="polite"`); swap in real content when loaded. The shimmer itself is decorative. |
170
+ | Reload | Refetch-over-content: `role="status"` + `aria-label="Reloading"` announces politely (not `alert`). Keep `pointer-events: none` so it never blocks the content it floats over. First-paint uses the skeleton instead. |
171
+ | Load state — shimmer | Work in progress with no ETA: `aria-busy="true"` on the region (+ `aria-live="polite"` if it isn't otherwise announced). The band is decorative. |
172
+ | Load state — fault | `role="status"` on `.juno-fault` itself — per-region and polite. NOT `role="alert"`; that is `.juno-alert`'s job for page-level failures. Always pair the icon with text. |
173
+ | Load state — empty | No extra role — static content. "Nothing here" is not an error and must not announce as one. |
174
+ | Avatar | `<img>` needs a meaningful `alt`. The status ring is decorative — pair it with a `title` / visible label so status isn't color-only. |
175
+ | Divider | Semantic break: use `<hr>`. Purely decorative: a `<span>`/`<div>` (no role needed). |
176
+ | Chip / tag | Toggle chip: real `<button>` + `aria-pressed`. Removable: the `__remove` `<button>` needs an `aria-label` ("Remove X"). Meaning is in the text, not color. |
177
+ | Breadcrumb | `<nav aria-label="Breadcrumb">` around an `<ol>`; mark the last crumb `aria-current="page"` and drop its `href`. |
178
+ | Pagination | `<nav aria-label="Pagination">`; current page `aria-current="page"`; icon-only prev/next need `aria-label`; disable the ends with `disabled`. |
179
+ | Stepper | Real `<ol>`; set `data-state` per step and `aria-current="step"` on the active one. State drives the color — don't rely on color alone. |
180
+ | Rail / dock | `<nav aria-label="Primary">` of real links; active item `aria-current="page"`. Collapsed rail items (icons-only) need `aria-label`/`title`. When both navs render (rail + dock swap), the hidden one leaves the tree via `display:none`. Pill variant (`--pill`) hides labels — **every item needs an `aria-label`**. Section-loading arc gets `role="status"` + `aria-label`. The dock's `--collapsible` variant: the `__knob` needs an `aria-label`; `data-juno-collapsed` hides tray items from the tab order, so move focus to the knob before setting it if focus was inside. |
181
+ | Pillbar | `<nav aria-label>` when it navigates; links get `aria-current="page"`, toggle buttons `aria-pressed`. Icon-only items need `aria-label`. The `__input` search field needs its own `aria-label` / visually-hidden `<label>` — a placeholder is not a label. The `--collapsible` variant's `__toggle` carries `aria-expanded` + `aria-label`; collapsing hides tray items from the tab order, so move focus to the toggle before collapsing if it was inside. Never render both a dock and a pillbar on one screen. |
182
+ | Navbar | Back is a real `<a>` (parent route) or `<button>` (history); `aria-label="Back"` when the caret has no text. Title is usually the screen's `<h1>`. The bar itself needs no role — it's a `<header>`. |
183
+ | List | Rows that navigate are `<a>` (or `<button>`) wrapping the whole row — never a bare chevron target. Keep `<ul>`/`<li>` when rows form a set. A trailing switch/checkbox is its own labeled control; don't nest it inside a link row. |
184
+
185
+ These are interaction concerns — junoui can't enforce them in CSS, so they're your
186
+ responsibility (or the widget package's). Each row follows the matching
187
+ [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/) pattern;
188
+ when in doubt, implement the APG keyboard model for that role.
@@ -0,0 +1,295 @@
1
+ # Boot shell — fast first paint, everything else in the background
2
+
3
+ How a junoui app gets from a cold tap to a usable screen in under a frame's
4
+ worth of network, and loads everything else behind that first paint. This is
5
+ the pattern the Nexora web client ships (reference implementation:
6
+ `nexora/web/index.html`, `src/prefetch.ts`, `src/sw.ts`); the pieces are
7
+ junoui's, the ordering rules are what this page adds.
8
+
9
+ The measured budget it buys (Nexora, production build, cold cache, mid-tier
10
+ phone emulation): **first paint ~100 ms** from the HTML alone, ten requests and
11
+ ~150 kB compressed to an interactive library, and every later screen arriving
12
+ from a background-warmed cache instead of a tap-time download.
13
+
14
+ ## The boot ladder
15
+
16
+ A bundle-rendered app paints nothing until its JS is fetched, parsed and run —
17
+ on a phone that is a black screen for however long the network takes. The fix
18
+ is not one trick but a ladder; each rung paints strictly earlier than the one
19
+ below it, and each hands over to the next without moving a pixel:
20
+
21
+ | Rung | Paints | Powered by |
22
+ | ---------------------- | ----------------------------------------- | --------------------------------------- |
23
+ | 1. Pre-bundle shell | App chrome + skeleton, first HTML parse | Inline `<style>` + static markup, no JS |
24
+ | 2. Cache-aware chrome | Cached content at first framework commit | Storage probe before the bundle |
25
+ | 3. Default screen only | The one screen users land on | Route-level code splitting |
26
+ | 4. Background warming | Nothing — pre-loads what a tap needs next | Idle + intent prefetch, link-class gate |
27
+ | 5. Offline shell | Rung 1–3 with the network gone | App-shell service worker |
28
+
29
+ The invariant across all five: **content gets the network first.** Nothing
30
+ speculative — a chunk, a font half, a screen nobody opened — may run before the
31
+ first screenful of real content has been requested.
32
+
33
+ ## Rung 1 — the pre-bundle shell
34
+
35
+ The first HTML parse paints the product frame: background, rail or dock, top
36
+ bar, and a shimmering tile wall — before any stylesheet or script arrives.
37
+ That means an inline `<style>` and static skeleton markup in `index.html`,
38
+ mirroring the geometry of the real [`.juno-app-shell`](./layout.md) that will
39
+ replace it. (In-app loading states use [`.juno-skeleton`](./components/skeleton.md);
40
+ this rung hand-rolls the same shimmer only because `juno.css` has not arrived
41
+ yet.)
42
+
43
+ `juno.css` is not loaded yet at this point in the document — that is the whole
44
+ point — so the shell uses **token literals**: the handful of `--juno-*` values
45
+ it needs, written out by hand. For the standard palette that is:
46
+
47
+ | Token | Dark | Light |
48
+ | --------------- | --------------------- | --------------------- |
49
+ | `--juno-s0` | `oklch(11% 0.010 85)` | `oklch(97% 0.008 85)` |
50
+ | `--juno-s1` | `oklch(15% 0.012 85)` | `oklch(93% 0.010 85)` |
51
+ | `--juno-s2` | `oklch(19% 0.012 85)` | `oklch(89% 0.010 85)` |
52
+ | `--juno-border` | `oklch(30% 0.012 85)` | `oklch(82% 0.010 85)` |
53
+
54
+ Minimal skeleton (adapt the boxes to your shell — the rule is that every box
55
+ mirrors a real chrome element at its real size, so the React/Vue/whatever
56
+ commit replaces it without anything jumping):
57
+
58
+ ```html
59
+ <style>
60
+ /* Token literals — juno.css is not here yet. Each mirrors a --juno-* token;
61
+ keep them asserted against the consumed build (see "Guarding the copies"). */
62
+ html {
63
+ background: oklch(11% 0.01 85); /* --juno-s0, dark */
64
+ }
65
+ html[data-juno-mode='light'] {
66
+ background: oklch(97% 0.008 85);
67
+ }
68
+ .boot {
69
+ position: fixed;
70
+ inset: 0;
71
+ display: flex;
72
+ --b-s1: oklch(15% 0.012 85);
73
+ --b-s2: oklch(19% 0.012 85);
74
+ --b-border: oklch(30% 0.012 85);
75
+ }
76
+ .boot-rail {
77
+ flex: 0 0 180px;
78
+ background: var(--b-s1);
79
+ border-right: 1px solid var(--b-border);
80
+ }
81
+ .boot-wall {
82
+ flex: 1;
83
+ display: grid;
84
+ gap: 10px;
85
+ padding: 14px 16px;
86
+ align-content: start;
87
+ /* match the real wall's column math or tiles resnap at handover */
88
+ grid-template-columns: repeat(auto-fill, minmax(min(108px, 100%), 1fr));
89
+ }
90
+ .boot-wall i {
91
+ aspect-ratio: 1;
92
+ border-radius: 6px;
93
+ background: var(--b-s2);
94
+ animation: boot-pulse 1.4s ease-in-out infinite;
95
+ }
96
+ @keyframes boot-pulse {
97
+ 0%,
98
+ 100% {
99
+ opacity: 1;
100
+ }
101
+ 50% {
102
+ opacity: 0.45;
103
+ }
104
+ }
105
+ </style>
106
+ ...
107
+ <body>
108
+ <div id="root">
109
+ <div class="boot" aria-hidden="true">
110
+ <div class="boot-rail"></div>
111
+ <div class="boot-wall"><i></i><i></i><i></i>…</div>
112
+ </div>
113
+ </div>
114
+ </body>
115
+ ```
116
+
117
+ Rules that make this work rather than merely exist:
118
+
119
+ - **The frame is real chrome, not a wireframe.** Grey bands where the rail,
120
+ top bar and dock will be read as a loading page. The same boxes styled
121
+ exactly like their live counterparts — the dock as its floating pill with
122
+ the active item's bubble already filled, the rail as brand + one row per
123
+ destination with the accent edge on the active one, the top bar as its
124
+ chips and search field — read as the app, already open. The shimmer is
125
+ confined to the slots whose content genuinely is not there yet: icons,
126
+ labels, thumbnails. Placeholders are for content, never for chrome.
127
+ - **No real text in the shell.** The fonts arrive with the bundle, and a
128
+ system-font word swapping to the real face at handover is exactly the kind
129
+ of jump the shell exists to prevent. Label and wordmark slots are bars.
130
+ - **Every literal mirrors one real element.** Safe-area padding, tile column
131
+ minimums (`--juno-tile-min`), tap-target sizes (`--juno-size-tap-comfortable`
132
+ = 44px), dock geometry — if the skeleton reserves a different amount than the
133
+ real chrome, the layout jumps by exactly the difference at handover, which
134
+ reads worse than a plain spinner.
135
+ - **The framework replaces `#root`'s children at its first commit** — no
136
+ removal code, no fade choreography. The skeleton needs zero JS.
137
+ - **`aria-hidden="true"`** on the whole skeleton: it is paint, not content.
138
+ - Honor `data-juno-mode` in the literals too (both modes in the snippet above),
139
+ or a light-mode user gets a dark flash.
140
+
141
+ ## Rung 2 — cache-aware chrome
142
+
143
+ The pulsing skeleton is right exactly once: on a true first run, when the
144
+ device holds nothing to draw. Every later launch should hydrate its first
145
+ framework commit from local cache (a persisted window of content in
146
+ `localStorage`/IndexedDB) — and shimmering in front of data the device already
147
+ has is a loading state in front of nothing.
148
+
149
+ So a tiny inline script — before the shell's markup — probes storage and
150
+ stamps the answer on the root element:
151
+
152
+ ```html
153
+ <script>
154
+ (function () {
155
+ var warm = false;
156
+ try {
157
+ var win = JSON.parse(localStorage.getItem('app.contentCache') || 'null');
158
+ warm = !!(win && win.tiles && win.tiles.length);
159
+ } catch (e) {
160
+ /* unreadable storage = first run */
161
+ }
162
+ document.documentElement.setAttribute('data-boot', warm ? 'warm' : 'cold');
163
+ })();
164
+ </script>
165
+ ```
166
+
167
+ ```css
168
+ /* warm: chrome outlines only — the real content is about to commit */
169
+ html[data-boot='warm'] .boot-wall > i {
170
+ display: none;
171
+ }
172
+ ```
173
+
174
+ Two contracts, both load-bearing:
175
+
176
+ - **Fail to `cold`.** A broken read produces today's behaviour (a skeleton),
177
+ never a wrongly quiet screen. The key strings are duplicated from the app's
178
+ cache module by necessity — no module has loaded yet — so drift must land on
179
+ the harmless side.
180
+ - **This is chrome, never content, and never authorization.** The probe decides
181
+ which skeleton to paint. The app's own gated read path is still the only
182
+ thing that may put cached data on screen — a cache read is an authorization
183
+ boundary, and this script must never become one.
184
+
185
+ ## Rung 3 — ship only the default screen
186
+
187
+ The first bundle contains the screen users land on and nothing else. Every
188
+ other screen is a route-level lazy chunk, fetched on first navigation:
189
+
190
+ ```jsx
191
+ const Settings = lazy(() => import('./screens/Settings'));
192
+ ```
193
+
194
+ The shell stays painted while a chunk loads — `Suspense` (or your framework's
195
+ equivalent) drops a [`.juno-arc`](./components/loader.md) into the main
196
+ region, and a nav destination that is loading gets `.juno-icon-loader` (the
197
+ icon ringed by the spinning arc) instead of a bare spinner. Heavy libraries a
198
+ rare media kind needs (3D, panorama) split again below the screen level, so
199
+ "Settings" never pays for three.js.
200
+
201
+ ## Rung 4 — background warming
202
+
203
+ Lazy chunks trade boot weight for a spinner at tap time — the worst moment for
204
+ one, because the user just pointed at a specific thing. The trade only
205
+ completes when the chunks arrive **in the background, before the tap**:
206
+
207
+ - **After content, never before.** Warming starts once the first page of real
208
+ content has landed (or provably isn't coming: no backend configured,
209
+ connection failed). Content gets the network first — that is the ordering
210
+ rule, and it should be a pure, testable predicate in code.
211
+ - **Idle trigger:** one chunk per `requestIdleCallback` slot, ordered by how
212
+ likely a phone user is to reach each screen — never the whole list in one
213
+ burst.
214
+ - **Intent trigger:** first `pointerdown` on anything that leads to a lazy
215
+ screen warms that screen immediately — insurance for when idle never comes.
216
+ - **Gate on the link class.** `Save-Data`, metered connections, or an explicit
217
+ user setting mean speculative bytes are not yours to spend. One module owns
218
+ that judgement; warming asks it, never decides itself.
219
+ - **Don't warm everything.** Screens that are large and rarely reached from a
220
+ phone stay cold on purpose. Warming is a bet; size the stake to the odds.
221
+
222
+ The same triggers and the same gate apply to warming **data** (next pages of
223
+ content, thumbnails around the viewport) — same budget, same ordering rule.
224
+
225
+ ## Rung 5 — the offline shell
226
+
227
+ An app-shell service worker precaches rungs 1–3 (`js`, `css`, `html`, `woff2`,
228
+ icon `svg`) so a cold start with no network still paints the full ladder.
229
+ Boundaries that keep it honest:
230
+
231
+ - **Precache the shell, not the long tail.** The rarely-used heavy chunks from
232
+ rung 3 are excluded — precaching them would make every install download
233
+ hundreds of kB to speed up a path almost nobody takes.
234
+ - **Content requests bypass the worker entirely** when the app has its own
235
+ authenticated content cache — two caches for one byte is a purge bug waiting
236
+ to happen.
237
+ - **Hand over on cold start only** (no mid-session `skipWaiting`), and ship a
238
+ kill switch (`?sw=off`) — a cached shell can otherwise hide fresh code from
239
+ every reload.
240
+
241
+ ### Fonts
242
+
243
+ Fonts block the first _text_ paint, so they ride the critical path: subset by
244
+ `unicode-range` (basic Latin separate from accented extensions) and let the
245
+ service worker precache all halves for later sessions. Nexora's split took the
246
+ critical-path font bytes from 72 kB to 28 kB with identical glyph coverage.
247
+
248
+ ## Guarding the copies
249
+
250
+ The pattern requires literal duplicates — token values, cache key strings,
251
+ safe-area expressions — in a file no bundler touches. Prose ("keep in sync")
252
+ does not keep them in sync; tests do:
253
+
254
+ - **Assert literals against the consumed build.** A unit test reads
255
+ `node_modules/junoui/dist/css/juno.css` (or `dist/json/tokens.json`) and the
256
+ app's `index.html`, and fails when a literal no longer matches its token —
257
+ including after a junoui version bump, which is exactly when it drifts.
258
+ - **Fail toward the harmless side.** Every probe in rung 2 is written so an
259
+ error lands on `cold`/absent — the pre-existing behaviour — never on a wrong
260
+ positive.
261
+ - **Verify through the server, not the repo.** First-paint claims are measured
262
+ from a served build with a cold cache (headless browser, CPU throttled), not
263
+ inferred from bundle sizes. A dev server is the wrong instrument: it serves
264
+ the unbundled module graph (Nexora: 80 requests where the build makes 10) and
265
+ its numbers say nothing about what users get.
266
+
267
+ ## Serving the ladder
268
+
269
+ None of the rungs exist on a dev server: it ships the unbundled module graph,
270
+ React's dev build, no minification, and it cannot register a service worker.
271
+ The dev server is the workshop; every demo anyone judges the app's speed by
272
+ serves the **production build**. A standing demo that still picks up every
273
+ change is three processes under one supervisor:
274
+
275
+ ```sh
276
+ vite build --watch # rebuilds dist/ a few seconds after any source change
277
+ vite preview --host # serves dist/ (with the app's API proxy)
278
+ ```
279
+
280
+ plus whatever restart loop keeps them alive. Properties that fall out:
281
+
282
+ - **Rebuild is content-addressed.** Hashed asset names mean an unchanged
283
+ module keeps its URL — reloads after a rebuild refetch only what changed.
284
+ - **The watch build skips type-checking.** Fine for a demo (CI still gates);
285
+ know that a type error will not stop the demo from rebuilding.
286
+ - **Each rebuild empties the output directory.** Anything placed in `dist/`
287
+ by hand (a connect page, a fixture) needs a guard that puts it back.
288
+ - **Secure context or no service worker.** On real devices the SW half of the
289
+ ladder needs HTTPS; plain-http demos still get the pre-bundle shell, warm
290
+ boot and the content caches — relaunches paint instantly and refetch only
291
+ code.
292
+
293
+ Measured on the reference implementation the day this section landed, same
294
+ port that previously ran the dev server: cold = 13 requests; relaunch = 12 of
295
+ 13 served by the service worker, first-paint 32 ms.