@junoput01/junoui 0.3.0 → 0.5.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 (49) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/README.md +23 -19
  3. package/dist/css/juno-custom-media.css +32 -0
  4. package/dist/css/juno.css +1216 -60
  5. package/dist/icons/inline.js +22 -0
  6. package/dist/icons/juno-icons.svg +1 -0
  7. package/docs/accessibility.md +55 -34
  8. package/docs/boot-shell.md +295 -0
  9. package/docs/components/README.md +46 -43
  10. package/docs/components/dock.md +167 -8
  11. package/docs/components/drawer.md +47 -7
  12. package/docs/components/fold-slot.md +31 -0
  13. package/docs/components/icon-loader.md +85 -18
  14. package/docs/components/icon.md +21 -0
  15. package/docs/components/load-state.md +131 -0
  16. package/docs/components/loader.md +13 -5
  17. package/docs/components/pillbar.md +157 -10
  18. package/docs/components/reload.md +41 -0
  19. package/docs/components/skeleton.md +22 -15
  20. package/docs/components/thumb.md +34 -14
  21. package/docs/design-guidelines.md +26 -0
  22. package/docs/getting-started.md +23 -0
  23. package/docs/icon-subsetting.md +59 -0
  24. package/docs/ios-conformance.md +224 -0
  25. package/docs/layout.md +71 -1
  26. package/docs/web.md +6 -0
  27. package/package.json +7 -2
  28. package/src/css/base.css +169 -6
  29. package/src/css/components/dock.css +322 -0
  30. package/src/css/components/drawer.css +40 -3
  31. package/src/css/components/fold-slot.css +44 -0
  32. package/src/css/components/icon-loader.css +32 -18
  33. package/src/css/components/icon.css +6 -4
  34. package/src/css/components/load-state.css +136 -0
  35. package/src/css/components/loader.css +6 -0
  36. package/src/css/components/menu.css +4 -0
  37. package/src/css/components/modal.css +24 -3
  38. package/src/css/components/navbar.css +5 -1
  39. package/src/css/components/pillbar.css +207 -7
  40. package/src/css/components/reload.css +48 -0
  41. package/src/css/components/skeleton.css +41 -13
  42. package/src/css/components/tabs.css +5 -0
  43. package/src/css/components/thumb.css +63 -1
  44. package/src/css/components/toast.css +5 -1
  45. package/src/css/density.css +22 -0
  46. package/src/css/layout.css +30 -2
  47. package/src/css/utilities.css +4 -1
  48. package/src/icons/cloud-slash.svg +1 -0
  49. package/tools/subset-sprite.mjs +56 -0
@@ -9,10 +9,22 @@
9
9
  * <figure class="juno-thumb" style="aspect-ratio: 1">
10
10
  * <img src="…" alt="Sunset clip" onerror="this.remove()" />
11
11
  * </figure>
12
- * <!-- known-missing: ship no <img>; label the figure instead -->
12
+ * \3c !-- known-missing: ship no <img>; label the figure instead -->
13
13
  * <figure class="juno-thumb juno-thumb--video" role="img" aria-label="Preview unavailable"></figure>
14
14
  * The onerror one-liner is the whole JS contract (optional, stateless):
15
15
  * a broken image removes itself and the placeholder underneath shows.
16
+ * Aspect is locked by default (--juno-thumb-ratio, square) so a media wall's
17
+ * scroll height is stable before anything loads; override per instance with
18
+ * style="--juno-thumb-ratio: 16/9" (an inline aspect-ratio still wins too).
19
+ * Selection is an outline (never a border — a border would shift the frame)
20
+ * toggled by a class/data-attr; junoui never decides what's selected.
21
+ * Corner slots anchor badges/duration chips over the media with logical
22
+ * inset so they swap sides correctly under dir="rtl".
23
+ * Usage:
24
+ * <figure class="juno-thumb juno-thumb--selected">
25
+ * <img src="…" alt="Sunset clip" onerror="this.remove()" />
26
+ * <span class="juno-thumb__corner juno-thumb__corner--top-end badge">0:42</span>
27
+ * </figure>
16
28
  * ════════════════════════════════════════════════════════════════════ */
17
29
 
18
30
  .juno-thumb {
@@ -23,12 +35,30 @@
23
35
  justify-content: center;
24
36
  gap: var(--juno-space-4);
25
37
  margin: 0;
38
+ aspect-ratio: var(--juno-thumb-ratio, 1);
26
39
  overflow: hidden;
27
40
  background: var(--juno-s2);
28
41
  border: var(--juno-border-width-1) solid var(--juno-border);
29
42
  border-radius: var(--juno-radius-3);
30
43
  }
31
44
 
45
+ /* Flush — drop the border/radius for tight walls where the frame's own edge
46
+ would fight the grid (full-bleed tiles); the media stays edge-to-edge via
47
+ object-fit: cover regardless. */
48
+ .juno-thumb--flush {
49
+ border: none;
50
+ border-radius: 0;
51
+ }
52
+
53
+ /* Selected — an inset outline, not a border: it overlays the existing frame
54
+ edge instead of adding to the box, so nothing reflows when selection
55
+ toggles. Role-neutral (--juno-active), matching the checkbox/switch focus
56
+ ring elsewhere in the system. */
57
+ .juno-thumb--selected {
58
+ outline: var(--juno-border-width-2) solid var(--juno-active);
59
+ outline-offset: calc(-1 * var(--juno-border-width-2));
60
+ }
61
+
32
62
  /* placeholder glyph (Phosphor "images", masked so it takes any color) */
33
63
  .juno-thumb::before {
34
64
  --juno-thumb-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath 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'/%3E%3C/svg%3E");
@@ -67,3 +97,35 @@
67
97
  block-size: 100%;
68
98
  object-fit: cover;
69
99
  }
100
+
101
+ /* Corner overlay — anchor a badge, check, or duration chip over the media
102
+ without joining flow or touching the frame's aspect-ratio. Pick one
103
+ position modifier; the base only sets stacking + inset from the frame
104
+ edge. Logical inset (not top/left) so start/end swap under dir="rtl" —
105
+ and the modifiers are named start/end for the same reason: a `--top-right`
106
+ that renders top-LEFT in RTL would be a trap. */
107
+ .juno-thumb__corner {
108
+ position: absolute;
109
+ z-index: 1;
110
+ margin: var(--juno-space-4);
111
+ }
112
+
113
+ .juno-thumb__corner--top-start {
114
+ inset-block-start: 0;
115
+ inset-inline-start: 0;
116
+ }
117
+
118
+ .juno-thumb__corner--top-end {
119
+ inset-block-start: 0;
120
+ inset-inline-end: 0;
121
+ }
122
+
123
+ .juno-thumb__corner--bottom-start {
124
+ inset-block-end: 0;
125
+ inset-inline-start: 0;
126
+ }
127
+
128
+ .juno-thumb__corner--bottom-end {
129
+ inset-block-end: 0;
130
+ inset-inline-end: 0;
131
+ }
@@ -91,7 +91,11 @@
91
91
  @media (width <= 639.98px) {
92
92
  .juno-toast-stack {
93
93
  inset-inline: var(--juno-space-12);
94
- inset-block-end: calc(var(--juno-space-12) + env(safe-area-inset-bottom, 0));
94
+
95
+ /* unit-bearing env() fallback: inside calc() a unitless `0` is a <number>,
96
+ which invalidates the sum and drops the whole declaration (the stack
97
+ would then sit flush at 0 on any device without a safe area). */
98
+ inset-block-end: calc(var(--juno-space-12) + env(safe-area-inset-bottom, 0px));
95
99
  inline-size: auto;
96
100
  }
97
101
 
@@ -38,3 +38,25 @@
38
38
  --juno-tile-min: 108px;
39
39
  --juno-gap-content: var(--juno-space-4);
40
40
  }
41
+
42
+ /* auto — OPT-IN. `comfortable`/`compact` never change meaning; a consumer
43
+ * must explicitly write `data-juno-density="auto"` to get this behavior.
44
+ * Baseline (any pointer, any viewport ≥ 640px) is comfortable — no rule
45
+ * needed here, it inherits the :root defaults above by not matching
46
+ * `[data-juno-density='compact']`. Only a small-viewport COARSE pointer
47
+ * (phone, not a touch laptop) re-densifies, mirroring the precedent already
48
+ * set by base.css's own `@media (pointer: coarse)` tap-target rule — this is
49
+ * a viewport-level document concern (set on <html>/<body>), not a component
50
+ * in an unknown layout, so a viewport query is the right tool here (container
51
+ * queries can't see pointer type anyway).
52
+ *
53
+ * NEVER shrinks a control's min-height — --juno-size-tap-min stays governed
54
+ * solely by base.css's coarse-pointer rule (20260802-020 / WCAG 2.5.5).
55
+ * This block only shrinks padding/gap/tile sizing, same contract as compact. */
56
+ @media (pointer: coarse) and (width <= 639.98px) {
57
+ [data-juno-density='auto'] {
58
+ --juno-tile-min: 104px;
59
+ --juno-gap-content: var(--juno-space-2);
60
+ --juno-pad-surface-inline: var(--juno-space-12);
61
+ }
62
+ }
@@ -82,13 +82,41 @@
82
82
  flex-basis: calc((var(--juno-switcher-threshold, var(--juno-bp-sm)) - 100%) * 999);
83
83
  }
84
84
 
85
- /* Reelhorizontal scroll row (overflowing chips, timelines). */
85
+ /* Scrollerbare scroll-container primitive underneath every scrolling
86
+ region in the library (.juno-reel, .juno-app-shell__main, tab strips…).
87
+ Three knobs, each a custom prop with the sane-default fallback: overflow
88
+ axis, overscroll containment, and snap type — so a consumer opts into
89
+ containment/snap without re-deriving them, and opts OUT (snap: none is
90
+ the default here) without fighting a hardcoded value. `--x`/`--y` pick a
91
+ single scroll axis; `--bare` hides the scrollbar cross-browser; a
92
+ `.juno-snap` child opts into the snap stop once the container snaps.
93
+ <div class="juno-scroller juno-scroller--y">…</div>
94
+ <div class="juno-scroller juno-scroller--x juno-scroller--bare"
95
+ style="--juno-scroller-snap:x proximity">
96
+ <div class="juno-snap">…</div>
97
+ </div> */
98
+ .juno-scroller {
99
+ overflow: var(--juno-scroller-overflow, auto);
100
+ overscroll-behavior: var(--juno-scroller-overscroll, contain);
101
+ scroll-snap-type: var(--juno-scroller-snap, none);
102
+ }
103
+ .juno-scroller--x { overflow: auto hidden; }
104
+ .juno-scroller--y { overflow: hidden auto; }
105
+ .juno-scroller--bare { scrollbar-width: none; }
106
+ .juno-scroller--bare::-webkit-scrollbar { display: none; }
107
+ .juno-scroller > .juno-snap { scroll-snap-align: var(--juno-snap-align, start); }
108
+
109
+ /* Reel — horizontal scroll row (overflowing chips, timelines). A
110
+ .juno-scroller preset: single inline axis, mandatory snap by default —
111
+ override the mode per instance with --juno-scroller-snap (e.g. `inline
112
+ proximity` for a softer magnet, or `none` to opt out and keep only the
113
+ containment + layout). Default output is unchanged from before. */
86
114
  .juno-reel {
87
115
  display: flex;
88
116
  gap: var(--juno-reel-space, var(--juno-space-12));
89
117
  overflow-x: auto;
90
118
  overscroll-behavior-inline: contain;
91
- scroll-snap-type: inline mandatory;
119
+ scroll-snap-type: var(--juno-scroller-snap, inline mandatory);
92
120
  }
93
121
  .juno-reel > * { scroll-snap-align: start; flex: 0 0 auto; }
94
122
 
@@ -40,8 +40,11 @@
40
40
  font-variant-numeric: tabular-nums;
41
41
  }
42
42
 
43
+ /* --juno-label-size: optional size override, set by an ancestor (e.g. a
44
+ * density wrapper or a "compact" section) — never declared here, so it
45
+ * isn't shadowed for descendants that also want to read it. */
43
46
  .juno-label {
44
- font-size: var(--juno-font-size-13);
47
+ font-size: var(--juno-label-size, var(--juno-font-size-13));
45
48
  color: var(--juno-label);
46
49
  }
47
50
 
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M56.88,31.93A12,12,0,1,0,39.12,48.07L71.79,84A68,68,0,0,0,72,220h88a91.26,91.26,0,0,0,30.66-5.24l8.46,9.31a12,12,0,0,0,17.76-16.14ZM160,196H72a44,44,0,0,1-1.8-87.95A91.91,91.91,0,0,0,68,128a12,12,0,0,0,24,0,68.22,68.22,0,0,1,2.66-18.84l77.88,85.67A68.67,68.67,0,0,1,160,196Zm92-68a91.32,91.32,0,0,1-17.53,54,12,12,0,1,1-19.41-14.11,68,68,0,0,0-89.57-98.53,12,12,0,0,1-12.2-20.66A92,92,0,0,1,252,128Z"/></svg>
@@ -0,0 +1,56 @@
1
+ // ════════════════════════════════════════════════════════════════════════
2
+ // junoui — icon sprite subsetter (build tooling for consumers)
3
+ // ════════════════════════════════════════════════════════════════════════
4
+ // The sprite ships every icon junoui has (65 and growing). An app that
5
+ // inlines it — which is what Safari's flaky external-<use> rendering forces
6
+ // (see scripts/build-icons.mjs) — therefore ships every icon in its main
7
+ // bundle, not the ~20 it draws. This module is the missing half: a pure,
8
+ // dependency-free subsetter a consumer's build step (Vite/webpack plugin,
9
+ // npm script) runs over the shipped sprite.
10
+ //
11
+ // Node-only tooling, deliberately NOT part of the browser CSS/JS surface:
12
+ // subsetting at runtime would defeat the point.
13
+ //
14
+ // import { subsetSprite, spriteSymbolNames } from '@junoput01/junoui/subset';
15
+ // const svg = subsetSprite(readFileSync(spritePath, 'utf8'), ['gear', 'x']);
16
+ //
17
+ // Contract: names are bare icon names ('gear'), never prefixed ids. The
18
+ // result keeps the sprite's banner and wrapper attributes verbatim, so the
19
+ // license notice travels with every subset (the icons are MIT Phosphor and
20
+ // the notice is a condition of redistribution).
21
+ // ════════════════════════════════════════════════════════════════════════
22
+
23
+ const SYMBOL_RE = /<symbol\s+id="juno-i-([^"]+)"[\s\S]*?<\/symbol>/g;
24
+
25
+ /** Every icon name the sprite defines, in document order. */
26
+ export function spriteSymbolNames(sprite) {
27
+ return [...sprite.matchAll(SYMBOL_RE)].map((m) => m[1]);
28
+ }
29
+
30
+ /**
31
+ * The same sprite carrying only `names`.
32
+ *
33
+ * Throws on a name the sprite does not define: a silently dropped icon
34
+ * renders as an empty <svg> in the consuming app — visible to nobody until a
35
+ * user reports a blank space — so an unknown name is a build failure, not a
36
+ * warning. Unused names in the sprite are simply omitted; order follows the
37
+ * sprite, not the request, so the output is stable under a reordered list.
38
+ */
39
+ export function subsetSprite(sprite, names) {
40
+ const wanted = new Set(names);
41
+ const have = new Set(spriteSymbolNames(sprite));
42
+ const missing = [...wanted].filter((n) => !have.has(n));
43
+ if (missing.length) {
44
+ throw new Error(
45
+ `subsetSprite: the sprite defines no icon named ${missing.map((n) => `'${n}'`).join(', ')} — ` +
46
+ `available: ${[...have].join(', ')}`,
47
+ );
48
+ }
49
+ const kept = [];
50
+ for (const m of sprite.matchAll(SYMBOL_RE)) if (wanted.has(m[1])) kept.push(m[0]);
51
+ // Keep everything outside the symbols verbatim (banner + <svg> open/close),
52
+ // so the license notice and the wrapper's fill/display attributes survive.
53
+ const head = sprite.slice(0, sprite.indexOf('<symbol'));
54
+ const tail = sprite.slice(sprite.lastIndexOf('</symbol>') + '</symbol>'.length);
55
+ return `${head}${kept.join('\n')}${tail}`;
56
+ }