@junoput01/junoui 0.2.0 β†’ 0.3.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 (61) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/css/juno.css +162 -3
  3. package/docs/accessibility.md +167 -0
  4. package/docs/components/README.md +68 -0
  5. package/docs/components/accordion.md +49 -0
  6. package/docs/components/alert.md +46 -0
  7. package/docs/components/avatar.md +45 -0
  8. package/docs/components/badge.md +35 -0
  9. package/docs/components/breadcrumb.md +31 -0
  10. package/docs/components/button.md +32 -0
  11. package/docs/components/card.md +41 -0
  12. package/docs/components/checkbox.md +41 -0
  13. package/docs/components/chip.md +44 -0
  14. package/docs/components/divider.md +32 -0
  15. package/docs/components/dock.md +56 -0
  16. package/docs/components/drawer.md +60 -0
  17. package/docs/components/field.md +44 -0
  18. package/docs/components/gauge.md +48 -0
  19. package/docs/components/icon-loader.md +55 -0
  20. package/docs/components/icon.md +49 -0
  21. package/docs/components/input.md +45 -0
  22. package/docs/components/list.md +76 -0
  23. package/docs/components/loader.md +80 -0
  24. package/docs/components/menu.md +54 -0
  25. package/docs/components/modal.md +52 -0
  26. package/docs/components/navbar.md +53 -0
  27. package/docs/components/pagination.md +42 -0
  28. package/docs/components/pillbar.md +64 -0
  29. package/docs/components/popover.md +43 -0
  30. package/docs/components/rail.md +54 -0
  31. package/docs/components/readout.md +39 -0
  32. package/docs/components/segmented.md +46 -0
  33. package/docs/components/select.md +33 -0
  34. package/docs/components/skeleton.md +38 -0
  35. package/docs/components/slider.md +48 -0
  36. package/docs/components/spark.md +51 -0
  37. package/docs/components/status.md +30 -0
  38. package/docs/components/stepper.md +46 -0
  39. package/docs/components/switch.md +38 -0
  40. package/docs/components/table.md +126 -0
  41. package/docs/components/tabs.md +61 -0
  42. package/docs/components/thumb.md +61 -0
  43. package/docs/components/toast.md +48 -0
  44. package/docs/components/toggle-button.md +40 -0
  45. package/docs/components/tooltip.md +63 -0
  46. package/docs/design-guidelines.md +147 -0
  47. package/docs/flutter.md +42 -0
  48. package/docs/getting-started.md +70 -0
  49. package/docs/integration.md +95 -0
  50. package/docs/layout.md +201 -0
  51. package/docs/native.md +52 -0
  52. package/docs/roadmap.md +106 -0
  53. package/docs/tokens-reference.md +223 -0
  54. package/docs/web.md +89 -0
  55. package/package.json +3 -2
  56. package/src/css/base.css +7 -0
  57. package/src/css/components/dock.css +13 -0
  58. package/src/css/components/icon-loader.css +53 -0
  59. package/src/css/components/pillbar.css +15 -0
  60. package/src/css/components/rail.css +8 -0
  61. package/src/css/layout.css +65 -3
@@ -0,0 +1,106 @@
1
+ # Roadmap β€” missing capabilities
2
+
3
+ Gap analysis vs mature design systems (Material, Carbon, Radix, Polaris, Tailwind).
4
+ What junoui does **not** yet have, prioritised. The framework line holds throughout:
5
+ junoui ships the **look + the accessibility/structure contract**; stateful behavior
6
+ (focus traps, open/close, positioning, data) lives in apps or a sibling
7
+ `junoui-<framework>` package.
8
+
9
+ Priority key: πŸ”΄ must Β· 🟑 high Β· 🟒 nice.
10
+
11
+ ## Have today
12
+
13
+ Tokens (color Β· spacing Β· type Β· radius Β· border Β· size Β· breakpoints Β· motion Β·
14
+ z-index Β· elevation Β· opacity), density modes, themes (3 palettes Γ— dark/light), CSS
15
+ components (badge, button, card, readout, status dot, loaders, **form controls** β€”
16
+ field Β· input Β· select Β· checkbox Β· radio Β· switch Β· slider; **overlays** β€” modal Β·
17
+ drawer Β· tooltip Β· popover Β· menu), layout primitives + container queries,
18
+ multi-platform outputs, docs, lint + tests + CI.
19
+
20
+ ## Foundations (token gaps) β€” βœ… shipped 2026-06
21
+
22
+ All foundation token gaps are now built. Values: [tokens-reference.md](./tokens-reference.md);
23
+ usage: [design-guidelines.md](./design-guidelines.md).
24
+
25
+ | Done | What shipped |
26
+ | --------------------------------- | -------------------------------------------------------------------------------------------------------------- |
27
+ | **Motion** (duration + easing) | `--juno-motion-duration-{instant,quick,base,deliberate}` + `--juno-motion-ease-{decel,accel,standard,spring}`. |
28
+ | **Z-index scale** | `--juno-z-*` layer stack: surface Β· raised Β· anchored Β· overlay Β· alert. |
29
+ | **Elevation / shadow** | `--juno-shadow-{1,2,3}` (e1–e3) β€” border-first depth; shadow for lifted surfaces. |
30
+ | **Opacity scale** | `--juno-opacity-{disabled,muted,scrim}`. |
31
+ | **Density modes** (compact/comfy) | `data-juno-density` swaps semantic padding aliases (`--juno-pad-*`, `--juno-gap-control`). |
32
+
33
+ ## Components
34
+
35
+ | Missing | Priority |
36
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
37
+ | ~~**Form controls** β€” input, textarea, select, checkbox, radio, switch, slider~~ | βœ… shipped |
38
+ | ~~**Field** wrapper β€” label + control + help + error + required~~ | βœ… shipped |
39
+ | ~~**Overlays** β€” modal/dialog, drawer, tooltip, popover, menu/dropdown~~ | βœ… shipped |
40
+ | ~~**Table / data grid** styling~~ | βœ… shipped |
41
+ | ~~**Alert / toast / inline notification**~~ | βœ… shipped |
42
+ | ~~**Tabs**, accordion / disclosure~~ | βœ… shipped |
43
+ | ~~**Skeleton** loading placeholder (pairs with loaders)~~ | βœ… shipped |
44
+ | ~~Avatar, chip/tag, divider, breadcrumb, pagination, stepper~~ | βœ… shipped |
45
+ | **Chat layer** (`junoui-chat`) β€” message bubble, list, day separator, system pill, attachment cards, conversation header, chat-row, rail. Requested by the buzz integration; highest-leverage gap for chat apps. | 🟑 |
46
+ | Calendar **visuals** (event chip, day-cell) | 🟒 |
47
+
48
+ ## Field-driven β€” βœ… shipped 2026-07
49
+
50
+ Gaps surfaced by the first production consumer (a media-server web client, 2026-07):
51
+ everything a real app had to hand-roll on top of junoui, rebuilt here as generic,
52
+ reusable pieces. The rule stands β€” features land app-agnostic; only the origin is
53
+ logged.
54
+
55
+ | Done | What shipped |
56
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
57
+ | **Dense sizes** | `.juno-btn--sm` + `.juno-switch--sm` for 40–46px toolbar chrome / per-row toggles. |
58
+ | **Segmented control** | `.juno-seg` β€” radio-driven exclusive pill row, zero JS; `aria-pressed` button flavor. |
59
+ | **Gauge** | `.juno-gauge` β€” determinate metric ring (conic-gradient), value centered, threshold recipe doc. |
60
+ | **Spark** | `.juno-spark` β€” sparkline size/stroke/color contract (app supplies points; no charting shipped). |
61
+ | **Micro badge** | `.juno-badge--micro` β€” mono data-UI atom for provenance/kind tags + threshold values. |
62
+ | **Rail + app shell** | `.juno-rail` (collapsible, `aria-current`-styled) + `layout.md#app-shell` recipe. |
63
+ | **Content density** | `--juno-tile-min`/`--juno-gap-content` archetype + `.juno-grid-auto--tiles`. |
64
+ | **Slide-over recipe** | Drawer doc: scrim + header/body/pinned-footer composition from existing parts. |
65
+ | **Media/system icons** | 14 Phosphor glyphs: squares-four Β· images Β· hexagon Β· puzzle-piece Β· play Β· film-strip Β· cloud Β· cloud-arrow-down Β· arrows-clockwise Β· cpu Β· hard-drives Β· sliders Β· arrows-out Β· upload-simple. |
66
+
67
+ ## Mobile β€” βœ… shipped 2026-07
68
+
69
+ Mobile-first structures for narrow viewports and touch, automatic where safe,
70
+ opt-in where markup is needed:
71
+
72
+ | Done | What shipped |
73
+ | -------------------- | --------------------------------------------------------------------------------------------------------------------- |
74
+ | **Dock** | `.juno-dock` β€” bottom nav, phone counterpart of the rail; sticky, safe-area padded; shell swap recipe in `layout.md`. |
75
+ | **Bottom sheet** | Modal auto-converts below `bp.sm`: full-width, top-rounded, slides up, stretched footer actions. |
76
+ | **Drawer phone fit** | Side drawers cap at `85vw` (scrim stays tappable); bottom drawer pads for the home indicator. |
77
+ | **Toast snackbar** | Full-width bottom stack on phones; slides up instead of sideways. |
78
+ | **Scrollable tabs** | Tab strip scrolls sideways instead of overflowing. |
79
+ | **Stacked table** | `.juno-table--stack` + `data-label` β€” card rows below a 480px container; real `<table>` semantics kept. |
80
+ | **Touch ergonomics** | `pointer: coarse` raises `--juno-size-tap-min` β†’ 44px; `hover: none` keeps table row actions visible. |
81
+ | **Pillbar** | `.juno-pillbar` β€” floating pill bar (iOS-style): 2–5 icon destinations/actions, translucent + blurred, safe-area. |
82
+ | **Navbar** | `.juno-navbar` β€” stack top bar: back control always on the start edge, centered truncating title, trailing actions. |
83
+ | **List** | `.juno-list` β€” grouped rows (settings pattern): icon + label/support + trailing value/control/chevron. |
84
+ | **Tab + stack** | Shell recipe in `layout.md`: dock/pillbar switches sections, navbar backs out of pushed views. |
85
+
86
+ ## Quality / infra
87
+
88
+ | Missing | Why | Priority |
89
+ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------ | -------- |
90
+ | ~~**Visual regression** snapshots (Playwright)~~ | βœ… shipped β€” `npm run test:visual` diffs every showcase page (dark + light). | βœ… |
91
+ | ~~**Changesets**~~ | βœ… shipped β€” `npm run changeset`; manual Version PR (`npm run version`) β†’ CI publishes on merge. | βœ… |
92
+ | ~~**Icon convention / set**~~ | βœ… shipped β€” `.juno-icon` + SVG sprite (Phosphor bold, MIT). | βœ… |
93
+ | **eslint** (flat config) | JS surface is small; prettier + tests cover most. | 🟒 |
94
+
95
+ Done already: stylelint + prettier, `node:test` integrity suite, a11y doc + ARIA
96
+ contract, forced-colors, RTL via logical properties, CI gate.
97
+
98
+ ## Recommended order
99
+
100
+ 1. ~~**Foundation tokens** β€” motion, z-index, elevation, opacity, density.~~ βœ… done.
101
+ 2. ~~**Form controls + field** β€” the single biggest capability gap.~~ βœ… done.
102
+ 3. ~~**Overlays** β€” modal, tooltip, menu.~~ βœ… done.
103
+ 4. ~~**Table / data grid** β€” sortable header, cell types, row states, overflow, skeleton/empty.~~ βœ… done.
104
+ 5. ~~**Quality** β€” visual-regression snapshots + changesets.~~ βœ… done.
105
+
106
+ Biggest leverage: **#1 + #2.**
@@ -0,0 +1,223 @@
1
+ <!-- GENERATED by scripts/gen-docs.mjs β€” do not edit by hand. Run `npm run gen-docs`. -->
2
+
3
+ # Token reference
4
+
5
+ Every junoui token with its value and the identifier to use on each platform.
6
+ Colors show the authored value and the resolved sRGB hex (use the hex in any
7
+ tool that does not understand `oklch()`).
8
+
9
+ ## How names map across platforms
10
+
11
+ | Platform | Pattern | Example (`color.standard.dark.nominal`) |
12
+ |---|---|---|
13
+ | CSS β€” semantic | `var(--juno-<role>)` (switches with theme attrs) | `var(--juno-nominal)` |
14
+ | SCSS | `$juno-color-<palette>-<mode>-<role>` | `$juno-color-standard-dark-nominal` |
15
+ | JS / TS | `TOKENS.<palette>.<mode>.<role>` | `TOKENS.standard.dark.nominal` |
16
+ | JSON (DTCG) | `color.<palette>.<mode>.<role>` | `color.standard.dark.nominal` |
17
+ | Android | `@color/<palette>_<mode>_<role>` | `@color/standard_dark_nominal` |
18
+ | iOS (Swift) | `JunoTokens.<camelCase>` | `JunoTokens.standardDarkNominal` |
19
+ | Flutter (Dart) | `JunoTokens.<camelCase>` | `JunoTokens.standardDarkNominal` |
20
+
21
+ Core tokens (spacing, radius, typography, …) use `--juno-<path-joined-by->`,
22
+ e.g. `space.16` β†’ `var(--juno-space-16)`, `@dimen/space_16`, `JunoTokens.space16`.
23
+
24
+ ## Color roles
25
+
26
+ Color encodes status, never decoration. Each role has exactly one meaning.
27
+
28
+ ### Palette: colorblind
29
+
30
+ | Role | Dark value | Dark hex | Light value | Light hex |
31
+ |---|---|---|---|---|
32
+ | `nominal` | `#78A9FF` | `#78A9FF` | `#0043CE` | `#0043CE` |
33
+ | `active` | `#BE95FF` | `#BE95FF` | `#6929C4` | `#6929C4` |
34
+ | `target` | `#FF7EB6` | `#FF7EB6` | `#9F1853` | `#9F1853` |
35
+ | `caution` | `#F1C21B` | `#F1C21B` | `#7D4F00` | `#7D4F00` |
36
+ | `warning` | `#FF832B` | `#FF832B` | `#BA4300` | `#BA4300` |
37
+ | `data` | `oklch(93% 0.008 85)` | `#EAE7E2` | `oklch(14% 0.008 85)` | `#0B0906` |
38
+ | `data-dim` | `oklch(38% 0.010 85)` | `#45423C` | `oklch(70% 0.008 85)` | `#A19E99` |
39
+ | `label` | `oklch(63% 0.012 85)` | `#8C8981` | `oklch(42% 0.010 85)` | `#504D47` |
40
+ | `muted` | `oklch(48.5% 0.010 85)` | `#625F59` | `oklch(63% 0.008 85)` | `#8B8984` |
41
+ | `border` | `oklch(30% 0.012 85)` | `#312D27` | `oklch(82% 0.010 85)` | `#C7C4BD` |
42
+ | `border-strong` | `oklch(40% 0.012 85)` | `#4B4740` | `oklch(74% 0.010 85)` | `#AEAAA4` |
43
+ | `s0` | `oklch(11% 0.010 85)` | `#060402` | `oklch(97% 0.008 85)` | `#F8F5EF` |
44
+ | `s1` | `oklch(15% 0.012 85)` | `#0D0B06` | `oklch(93% 0.010 85)` | `#EBE7E0` |
45
+ | `s2` | `oklch(19% 0.012 85)` | `#16130E` | `oklch(89% 0.010 85)` | `#DEDAD3` |
46
+ | `s3` | `oklch(24% 0.012 85)` | `#221F19` | `oklch(85% 0.010 85)` | `#D1CDC7` |
47
+
48
+ ### Palette: soft
49
+
50
+ | Role | Dark value | Dark hex | Light value | Light hex |
51
+ |---|---|---|---|---|
52
+ | `nominal` | `oklch(63% 0.13 155)` | `#38A065` | `oklch(40% 0.11 155)` | `#00582C` |
53
+ | `active` | `oklch(67% 0.12 210)` | `#00A8BE` | `oklch(44% 0.11 210)` | `#006174` |
54
+ | `target` | `oklch(58% 0.14 318)` | `#9C5CAF` | `oklch(42% 0.12 318)` | `#673477` |
55
+ | `caution` | `oklch(72% 0.12 75)` | `#D09945` | `oklch(50% 0.12 75)` | `#8A5700` |
56
+ | `warning` | `oklch(62% 0.15 30)` | `#D15D4D` | `oklch(42% 0.13 30)` | `#86281D` |
57
+ | `data` | `oklch(90% 0.008 88)` | `#E0DED8` | `oklch(14% 0.008 88)` | `#0A0906` |
58
+ | `data-dim` | `oklch(39% 0.010 88)` | `#47453F` | `oklch(69% 0.008 88)` | `#9D9B96` |
59
+ | `label` | `oklch(63% 0.010 88)` | `#8C8982` | `oklch(44% 0.010 88)` | `#55524C` |
60
+ | `muted` | `oklch(48.5% 0.010 88)` | `#615F59` | `oklch(62.5% 0.008 88)` | `#8A8782` |
61
+ | `border` | `oklch(31% 0.010 88)` | `#32302B` | `oklch(84% 0.010 88)` | `#CDCAC3` |
62
+ | `border-strong` | `oklch(41% 0.010 88)` | `#4D4A44` | `oklch(76% 0.010 88)` | `#B4B1AA` |
63
+ | `s0` | `oklch(12% 0.012 88)` | `#070602` | `oklch(96% 0.010 88)` | `#F4F2EA` |
64
+ | `s1` | `oklch(16% 0.012 88)` | `#0F0D08` | `oklch(92% 0.010 88)` | `#E7E4DD` |
65
+ | `s2` | `oklch(20% 0.012 88)` | `#181610` | `oklch(88% 0.010 88)` | `#DAD7D0` |
66
+ | `s3` | `oklch(25% 0.012 88)` | `#24211B` | `oklch(84% 0.010 88)` | `#CDCAC3` |
67
+
68
+ ### Palette: standard
69
+
70
+ | Role | Dark value | Dark hex | Light value | Light hex |
71
+ |---|---|---|---|---|
72
+ | `nominal` | `oklch(73% 0.22 148)` | `#00CB4C` | `oklch(42% 0.18 148)` | `#006400` |
73
+ | `active` | `oklch(76% 0.16 204)` | `#00CDDF` | `oklch(47% 0.16 220)` | `#006B98` |
74
+ | `target` | `oklch(65% 0.26 324)` | `#D63DE6` | `oklch(45% 0.22 324)` | `#8B0097` |
75
+ | `caution` | `oklch(77% 0.17 73)` | `#F4A000` | `oklch(50% 0.17 73)` | `#9B4C00` |
76
+ | `warning` | `oklch(63% 0.22 28)` | `#F13A32` | `oklch(42% 0.21 26)` | `#A10000` |
77
+ | `data` | `oklch(93% 0.008 85)` | `#EAE7E2` | `oklch(14% 0.008 85)` | `#0B0906` |
78
+ | `data-dim` | `oklch(38% 0.010 85)` | `#45423C` | `oklch(70% 0.008 85)` | `#A19E99` |
79
+ | `label` | `oklch(63% 0.012 85)` | `#8C8981` | `oklch(42% 0.010 85)` | `#504D47` |
80
+ | `muted` | `oklch(48.5% 0.010 85)` | `#625F59` | `oklch(63% 0.008 85)` | `#8B8984` |
81
+ | `border` | `oklch(30% 0.012 85)` | `#312D27` | `oklch(82% 0.010 85)` | `#C7C4BD` |
82
+ | `border-strong` | `oklch(40% 0.012 85)` | `#4B4740` | `oklch(74% 0.010 85)` | `#AEAAA4` |
83
+ | `s0` | `oklch(11% 0.010 85)` | `#060402` | `oklch(97% 0.008 85)` | `#F8F5EF` |
84
+ | `s1` | `oklch(15% 0.012 85)` | `#0D0B06` | `oklch(93% 0.010 85)` | `#EBE7E0` |
85
+ | `s2` | `oklch(19% 0.012 85)` | `#16130E` | `oklch(89% 0.010 85)` | `#DEDAD3` |
86
+ | `s3` | `oklch(24% 0.012 85)` | `#221F19` | `oklch(85% 0.010 85)` | `#D1CDC7` |
87
+
88
+ ## Core tokens
89
+
90
+ ### Borders
91
+
92
+ | Token | Value | CSS variable | Flutter / iOS |
93
+ |---|---|---|---|
94
+ | `border.width.1` | `1px` | `var(--juno-border-width-1)` | `JunoTokens.borderWidth1` |
95
+ | `border.width.2` | `2px` | `var(--juno-border-width-2)` | `JunoTokens.borderWidth2` |
96
+ | `border.width.3` | `3px` | `var(--juno-border-width-3)` | `JunoTokens.borderWidth3` |
97
+
98
+ ### Breakpoints
99
+
100
+ | Token | Value | CSS variable | Flutter / iOS |
101
+ |---|---|---|---|
102
+ | `bp.sm` | `640px` | `var(--juno-bp-sm)` | `JunoTokens.bpSm` |
103
+ | `bp.md` | `768px` | `var(--juno-bp-md)` | `JunoTokens.bpMd` |
104
+ | `bp.lg` | `1024px` | `var(--juno-bp-lg)` | `JunoTokens.bpLg` |
105
+ | `bp.xl` | `1280px` | `var(--juno-bp-xl)` | `JunoTokens.bpXl` |
106
+ | `bp.2xl` | `1536px` | `var(--juno-bp-2xl)` | `JunoTokens.bp2xl` |
107
+
108
+ ### Elevation
109
+
110
+ | Token | Value | CSS variable | Flutter / iOS |
111
+ |---|---|---|---|
112
+ | `shadow.1` | `0 1px 2px rgb(0 0 0 / 0.30)` | `var(--juno-shadow-1)` | `JunoTokens.shadow1` |
113
+ | `shadow.2` | `0 4px 14px rgb(0 0 0 / 0.35)` | `var(--juno-shadow-2)` | `JunoTokens.shadow2` |
114
+ | `shadow.3` | `0 12px 32px rgb(0 0 0 / 0.50)` | `var(--juno-shadow-3)` | `JunoTokens.shadow3` |
115
+
116
+ ### Motion
117
+
118
+ | Token | Value | CSS variable | Flutter / iOS |
119
+ |---|---|---|---|
120
+ | `motion.duration.instant` | `80ms` | `var(--juno-motion-duration-instant)` | `JunoTokens.motionDurationInstant` |
121
+ | `motion.duration.quick` | `140ms` | `var(--juno-motion-duration-quick)` | `JunoTokens.motionDurationQuick` |
122
+ | `motion.duration.base` | `200ms` | `var(--juno-motion-duration-base)` | `JunoTokens.motionDurationBase` |
123
+ | `motion.duration.deliberate` | `300ms` | `var(--juno-motion-duration-deliberate)` | `JunoTokens.motionDurationDeliberate` |
124
+ | `motion.ease.decel` | `cubic-bezier(0.2, 0.8, 0.2, 1)` | `var(--juno-motion-ease-decel)` | `JunoTokens.motionEaseDecel` |
125
+ | `motion.ease.accel` | `cubic-bezier(0.4, 0, 1, 1)` | `var(--juno-motion-ease-accel)` | `JunoTokens.motionEaseAccel` |
126
+ | `motion.ease.standard` | `cubic-bezier(0.2, 0.7, 0.3, 1)` | `var(--juno-motion-ease-standard)` | `JunoTokens.motionEaseStandard` |
127
+ | `motion.ease.spring` | `cubic-bezier(0.2, 0.9, 0.3, 1.2)` | `var(--juno-motion-ease-spring)` | `JunoTokens.motionEaseSpring` |
128
+
129
+ ### Opacity
130
+
131
+ | Token | Value | CSS variable | Flutter / iOS |
132
+ |---|---|---|---|
133
+ | `opacity.disabled` | `0.45` | `var(--juno-opacity-disabled)` | `JunoTokens.opacityDisabled` |
134
+ | `opacity.muted` | `0.65` | `var(--juno-opacity-muted)` | `JunoTokens.opacityMuted` |
135
+ | `opacity.scrim` | `0.62` | `var(--juno-opacity-scrim)` | `JunoTokens.opacityScrim` |
136
+
137
+ ### Radius
138
+
139
+ | Token | Value | CSS variable | Flutter / iOS |
140
+ |---|---|---|---|
141
+ | `radius.2` | `2px` | `var(--juno-radius-2)` | `JunoTokens.radius2` |
142
+ | `radius.3` | `3px` | `var(--juno-radius-3)` | `JunoTokens.radius3` |
143
+ | `radius.4` | `4px` | `var(--juno-radius-4)` | `JunoTokens.radius4` |
144
+ | `radius.5` | `5px` | `var(--juno-radius-5)` | `JunoTokens.radius5` |
145
+ | `radius.8` | `8px` | `var(--juno-radius-8)` | `JunoTokens.radius8` |
146
+
147
+ ### Sizing
148
+
149
+ | Token | Value | CSS variable | Flutter / iOS |
150
+ |---|---|---|---|
151
+ | `size.tap.min` | `24px` | `var(--juno-size-tap-min)` | `JunoTokens.sizeTapMin` |
152
+ | `size.tap.comfortable` | `44px` | `var(--juno-size-tap-comfortable)` | `JunoTokens.sizeTapComfortable` |
153
+ | `size.dot.sm` | `8px` | `var(--juno-size-dot-sm)` | `JunoTokens.sizeDotSm` |
154
+ | `size.dot.md` | `10px` | `var(--juno-size-dot-md)` | `JunoTokens.sizeDotMd` |
155
+
156
+ ### Spacing
157
+
158
+ | Token | Value | CSS variable | Flutter / iOS |
159
+ |---|---|---|---|
160
+ | `space.2` | `2px` | `var(--juno-space-2)` | `JunoTokens.space2` |
161
+ | `space.4` | `4px` | `var(--juno-space-4)` | `JunoTokens.space4` |
162
+ | `space.8` | `8px` | `var(--juno-space-8)` | `JunoTokens.space8` |
163
+ | `space.10` | `10px` | `var(--juno-space-10)` | `JunoTokens.space10` |
164
+ | `space.12` | `12px` | `var(--juno-space-12)` | `JunoTokens.space12` |
165
+ | `space.16` | `16px` | `var(--juno-space-16)` | `JunoTokens.space16` |
166
+ | `space.20` | `20px` | `var(--juno-space-20)` | `JunoTokens.space20` |
167
+ | `space.24` | `24px` | `var(--juno-space-24)` | `JunoTokens.space24` |
168
+ | `space.28` | `28px` | `var(--juno-space-28)` | `JunoTokens.space28` |
169
+ | `space.32` | `32px` | `var(--juno-space-32)` | `JunoTokens.space32` |
170
+ | `space.40` | `40px` | `var(--juno-space-40)` | `JunoTokens.space40` |
171
+ | `space.56` | `56px` | `var(--juno-space-56)` | `JunoTokens.space56` |
172
+ | `space.72` | `72px` | `var(--juno-space-72)` | `JunoTokens.space72` |
173
+ | `space.96` | `96px` | `var(--juno-space-96)` | `JunoTokens.space96` |
174
+
175
+ ### Typography
176
+
177
+ | Token | Value | CSS variable | Flutter / iOS |
178
+ |---|---|---|---|
179
+ | `font.family.sans` | `'B612', sans-serif` | `var(--juno-font-family-sans)` | `JunoTokens.fontFamilySans` |
180
+ | `font.family.mono` | `'B612 Mono', monospace` | `var(--juno-font-family-mono)` | `JunoTokens.fontFamilyMono` |
181
+ | `font.weight.light` | `300` | `var(--juno-font-weight-light)` | `JunoTokens.fontWeightLight` |
182
+ | `font.weight.regular` | `400` | `var(--juno-font-weight-regular)` | `JunoTokens.fontWeightRegular` |
183
+ | `font.weight.medium` | `500` | `var(--juno-font-weight-medium)` | `JunoTokens.fontWeightMedium` |
184
+ | `font.weight.semibold` | `600` | `var(--juno-font-weight-semibold)` | `JunoTokens.fontWeightSemibold` |
185
+ | `font.weight.bold` | `700` | `var(--juno-font-weight-bold)` | `JunoTokens.fontWeightBold` |
186
+ | `font.size.10` | `10px` | `var(--juno-font-size-10)` | `JunoTokens.fontSize10` |
187
+ | `font.size.11` | `11px` | `var(--juno-font-size-11)` | `JunoTokens.fontSize11` |
188
+ | `font.size.12` | `12px` | `var(--juno-font-size-12)` | `JunoTokens.fontSize12` |
189
+ | `font.size.13` | `13px` | `var(--juno-font-size-13)` | `JunoTokens.fontSize13` |
190
+ | `font.size.14` | `14px` | `var(--juno-font-size-14)` | `JunoTokens.fontSize14` |
191
+ | `font.size.16` | `16px` | `var(--juno-font-size-16)` | `JunoTokens.fontSize16` |
192
+ | `font.size.18` | `18px` | `var(--juno-font-size-18)` | `JunoTokens.fontSize18` |
193
+ | `font.size.20` | `20px` | `var(--juno-font-size-20)` | `JunoTokens.fontSize20` |
194
+ | `font.size.28` | `28px` | `var(--juno-font-size-28)` | `JunoTokens.fontSize28` |
195
+ | `font.size.32` | `32px` | `var(--juno-font-size-32)` | `JunoTokens.fontSize32` |
196
+ | `font.size.36` | `36px` | `var(--juno-font-size-36)` | `JunoTokens.fontSize36` |
197
+ | `font.size.38` | `38px` | `var(--juno-font-size-38)` | `JunoTokens.fontSize38` |
198
+ | `font.size.48` | `48px` | `var(--juno-font-size-48)` | `JunoTokens.fontSize48` |
199
+ | `font.size.52` | `52px` | `var(--juno-font-size-52)` | `JunoTokens.fontSize52` |
200
+ | `font.lineHeight.none` | `1` | `var(--juno-font-lineHeight-none)` | `JunoTokens.fontLineHeightNone` |
201
+ | `font.lineHeight.tight` | `1.1` | `var(--juno-font-lineHeight-tight)` | `JunoTokens.fontLineHeightTight` |
202
+ | `font.lineHeight.snug` | `1.2` | `var(--juno-font-lineHeight-snug)` | `JunoTokens.fontLineHeightSnug` |
203
+ | `font.lineHeight.normal` | `1.4` | `var(--juno-font-lineHeight-normal)` | `JunoTokens.fontLineHeightNormal` |
204
+ | `font.lineHeight.relaxed` | `1.5` | `var(--juno-font-lineHeight-relaxed)` | `JunoTokens.fontLineHeightRelaxed` |
205
+ | `font.lineHeight.loose` | `1.7` | `var(--juno-font-lineHeight-loose)` | `JunoTokens.fontLineHeightLoose` |
206
+ | `font.tracking.tight` | `0.02em` | `var(--juno-font-tracking-tight)` | `JunoTokens.fontTrackingTight` |
207
+ | `font.tracking.normal` | `0.06em` | `var(--juno-font-tracking-normal)` | `JunoTokens.fontTrackingNormal` |
208
+ | `font.tracking.label` | `0.08em` | `var(--juno-font-tracking-label)` | `JunoTokens.fontTrackingLabel` |
209
+ | `font.tracking.wide` | `0.12em` | `var(--juno-font-tracking-wide)` | `JunoTokens.fontTrackingWide` |
210
+ | `font.tracking.caps` | `0.15em` | `var(--juno-font-tracking-caps)` | `JunoTokens.fontTrackingCaps` |
211
+ | `font.tracking.wider` | `0.2em` | `var(--juno-font-tracking-wider)` | `JunoTokens.fontTrackingWider` |
212
+ | `font.tracking.widest` | `0.3em` | `var(--juno-font-tracking-widest)` | `JunoTokens.fontTrackingWidest` |
213
+
214
+ ### Z-index
215
+
216
+ | Token | Value | CSS variable | Flutter / iOS |
217
+ |---|---|---|---|
218
+ | `z.surface` | `0` | `var(--juno-z-surface)` | `JunoTokens.zSurface` |
219
+ | `z.raised` | `100` | `var(--juno-z-raised)` | `JunoTokens.zRaised` |
220
+ | `z.anchored` | `2000` | `var(--juno-z-anchored)` | `JunoTokens.zAnchored` |
221
+ | `z.overlay` | `4000` | `var(--juno-z-overlay)` | `JunoTokens.zOverlay` |
222
+ | `z.alert` | `5000` | `var(--juno-z-alert)` | `JunoTokens.zAlert` |
223
+
package/docs/web.md ADDED
@@ -0,0 +1,89 @@
1
+ # Web (CSS / SCSS / JS)
2
+
3
+ ## CSS β€” the fast path
4
+
5
+ Import the full stylesheet (tokens + base + utilities + components):
6
+
7
+ ```js
8
+ import '@junoput01/junoui/css'; // bundler
9
+ ```
10
+
11
+ ```css
12
+ @import '@junoput01/junoui/css'; /* plain CSS */
13
+ ```
14
+
15
+ Set the theme with two attributes on `<html>`:
16
+
17
+ ```html
18
+ <html data-juno-palette="standard" data-juno-mode="dark"></html>
19
+ ```
20
+
21
+ Both are optional. **Omit `data-juno-mode` and the theme follows the OS**
22
+ (`prefers-color-scheme`, live β€” no JS): dark systems get dark, light systems
23
+ light, in whichever palette is set (default `standard`). The base layer also
24
+ sets `color-scheme` so scrollbars and native form chrome match. An explicit
25
+ `data-juno-mode` pins the mode and ignores the OS.
26
+
27
+ Other system preferences honored out of the box: `prefers-reduced-motion`
28
+ (animations collapse), `prefers-contrast: more` (hairlines step up to the
29
+ strong border), `prefers-reduced-transparency` (translucent surfaces go
30
+ solid), `forced-colors` (system palette). Language is the app's job β€” junoui
31
+ ships no strings and is RTL-safe via logical properties; set `lang`/`dir` and
32
+ the layout follows.
33
+
34
+ Switch at runtime:
35
+
36
+ ```js
37
+ document.documentElement.dataset.junoPalette = 'colorblind';
38
+ document.documentElement.dataset.junoMode = 'light';
39
+ delete document.documentElement.dataset.junoMode; // back to following the OS
40
+ ```
41
+
42
+ Use semantic variables and component classes:
43
+
44
+ ```html
45
+ <span class="juno-badge juno--warning">WARNING</span>
46
+
47
+ <div style="color: var(--juno-nominal); padding: var(--juno-space-16);">…</div>
48
+ ```
49
+
50
+ Want variables only (bring your own components)? Import `@junoput01/junoui/css/tokens`.
51
+
52
+ ## SCSS
53
+
54
+ ```scss
55
+ @use '@junoput01/junoui/scss' as juno; // $juno-color-standard-dark-nominal, $juno-space-16, …
56
+ ```
57
+
58
+ All variables are `!default`, so you can override before `@use` if you fork values.
59
+
60
+ ## JS / TS
61
+
62
+ ```ts
63
+ import { TOKENS, CORE, getTokens } from '@junoput01/junoui';
64
+
65
+ getTokens('colorblind', 'light').warning; // "#BA4300"
66
+ TOKENS.standard.dark.nominal; // "oklch(73% 0.22 148)"
67
+ CORE.space['16']; // "16px"
68
+ ```
69
+
70
+ Types ship with the package (`JunoPalette`, `JunoMode`, `JunoRole`, `JunoTheme`).
71
+
72
+ ## Semantic vs explicit variables
73
+
74
+ - **Semantic** `--juno-nominal` … resolve to the _current_ theme. Use these in components.
75
+ - **Explicit** values for a specific theme live in SCSS/JS as
76
+ `…-standard-dark-nominal`. Use only when you need a fixed theme regardless of context.
77
+
78
+ ## Fonts
79
+
80
+ junoui **never fetches fonts** (a cross-origin `@import` would break a strict CSP and
81
+ phone home). `base.css` names B612 / B612 Mono via `--juno-font-family-*` but doesn't
82
+ load them. Two options:
83
+
84
+ ```js
85
+ import '@junoput01/junoui/fonts.css'; // opt-in: self-hosted B612 woff2, no network (CSP-safe)
86
+ ```
87
+
88
+ …or bring your own B612 (e.g. `@fontsource/b612`). Without either, the UI falls back to
89
+ system sans/mono. See [integration.md](./integration.md#2-fonts-opt-in-csp-safe).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junoput01/junoui",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "junoui β€” a token-driven design system. Color carries semantic meaning, never decoration: every hue has one assigned role (NOMINAL / ACTIVE / TARGET / CAUTION / WARNING). Ships multi-platform tokens (CSS, SCSS, JS/TS, JSON, Android, iOS, Flutter) plus a framework-agnostic CSS component layer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "git@github.com-junoput:junoput/junoui.git"
14
+ "url": "git+https://github.com/junoput/junoui.git"
15
15
  },
16
16
  "keywords": [
17
17
  "design-system",
@@ -54,6 +54,7 @@
54
54
  "src/css",
55
55
  "src/icons",
56
56
  "src/fonts",
57
+ "docs",
57
58
  "README.md",
58
59
  "CHANGELOG.md"
59
60
  ],
package/src/css/base.css CHANGED
@@ -89,6 +89,13 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
89
89
  the contract; a phone just reads the bigger one. */
90
90
  @media (pointer: coarse) {
91
91
  :root { --juno-size-tap-min: var(--juno-size-tap-comfortable); }
92
+
93
+ /* iOS Safari zooms the page onto any focused text field whose font-size is
94
+ below 16px. Hold text-entry controls (.juno-input covers input, textarea
95
+ and the select's inner control) at a 16px floor on touch so focusing never
96
+ triggers that jump. max() keeps the floor even under a scaled-down
97
+ --juno-font-scale, and still grows when scaled up. */
98
+ .juno-input { font-size: max(16px, var(--juno-font-size-16)); }
92
99
  }
93
100
 
94
101
  @keyframes juno-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
@@ -72,3 +72,16 @@
72
72
  outline: var(--juno-border-width-2) solid var(--juno-active);
73
73
  outline-offset: calc(-1 * var(--juno-space-2));
74
74
  }
75
+
76
+ /* Fixed β€” same story as the pillbar: sticky only pins while the column
77
+ overflows, so on a short page the dock lands mid-content. Pin to the
78
+ viewport foot in a page-scroll shell where content height varies. Since
79
+ fixed leaves the flow, reserve its height at the page foot (e.g.
80
+ padding-block-end) so it doesn't cover the last row. In-flow
81
+ .juno-app-shell (main is the scroller, dock at the body foot) needs none
82
+ of this β€” prefer it. */
83
+ .juno-dock--fixed {
84
+ position: fixed;
85
+ inset-inline: 0;
86
+ inset-block-end: 0;
87
+ }
@@ -0,0 +1,53 @@
1
+ /* ════════════════════════════════════════════════════════════════════
2
+ * Component β€” Icon loader (nav loading ring)
3
+ * A destination icon ringed by the spinning arc while that section loads β€”
4
+ * the "this is loading" affordance for a rail / dock / pillbar item. The
5
+ * icon stays static on top; the arc sizes in em around it and eats no
6
+ * pointer events, so the nav item still clicks through.
7
+ *
8
+ * The app owns the state: add .juno-arc--indeterminate to spin, drop it
9
+ * when loaded (gate the blink to first-load, not every refetch). For a
10
+ * determinate ring, set --juno-progress on the .juno-arc instead.
11
+ * Usage:
12
+ * <a class="juno-rail__item" href="…" aria-current="page">
13
+ * <span class="juno-icon-loader">
14
+ * <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-squares-four" /></svg>
15
+ * <span class="juno-arc juno-arc--indeterminate" role="status" aria-label="Loading"></span>
16
+ * </span>
17
+ * <span class="juno-rail__label">Library</span>
18
+ * </a>
19
+ *
20
+ * Gotcha encoded here: the arc's rotate animation overrides `transform`, so
21
+ * the ring is centred with inset:0 + margin:auto β€” never translate(-50%,-50%),
22
+ * which the spin would clobber.
23
+ * ════════════════════════════════════════════════════════════════════ */
24
+
25
+ .juno-icon-loader {
26
+ --juno-role: var(--juno-active);
27
+
28
+ /* Single-cell grid: the icon and the ring occupy the SAME cell and are
29
+ both centred in it, so they're guaranteed concentric β€” no transform (the
30
+ arc's rotation would clobber a translate) and no fragile margin math. The
31
+ ring is the larger child, so the cell sizes to it (--juno-icon-loader-ring). */
32
+ display: inline-grid;
33
+ place-items: center;
34
+ line-height: 0; /* drop the inline strut so the box hugs the ring */
35
+ }
36
+
37
+ .juno-icon-loader > .juno-icon,
38
+ .juno-icon-loader > .juno-arc {
39
+ grid-area: 1 / 1;
40
+ }
41
+
42
+ /* icon sits above the ring */
43
+ .juno-icon-loader > .juno-icon {
44
+ z-index: 1;
45
+ }
46
+
47
+ /* the ring: sized in em (relative to the icon), transparent to clicks */
48
+ .juno-icon-loader > .juno-arc {
49
+ --juno-arc-size: var(--juno-icon-loader-ring, 1.9em);
50
+ --juno-arc-width: 0.14em;
51
+
52
+ pointer-events: none;
53
+ }
@@ -102,6 +102,21 @@
102
102
  flex-shrink: 0;
103
103
  }
104
104
 
105
+ /* Fixed β€” pin to the viewport instead of flowing at the column's end.
106
+ Sticky only pins while the column overflows; on a short (non-scrolling)
107
+ page the sticky pillbar lands mid-content. Use --fixed in a page-scroll
108
+ shell where content height varies. Centered via inset-inline:0 + the base
109
+ margin-inline:auto (NOT translateX, which the app-shell model avoids
110
+ anyway). Leaves the viewport foot free β€” reserve space so it doesn't cover
111
+ the last row (e.g. padding-block-end on the scroll region). Prefer the
112
+ in-flow .juno-app-shell (main scrolls, pillbar sits at the body foot) β€”
113
+ that needs no fixed at all. */
114
+ .juno-pillbar--fixed {
115
+ position: fixed;
116
+ inset-inline: 0;
117
+ z-index: var(--juno-z-anchored);
118
+ }
119
+
105
120
  /* reduced-transparency preference β†’ solid surface, no blur */
106
121
  @media (prefers-reduced-transparency: reduce) {
107
122
  .juno-pillbar {
@@ -94,3 +94,11 @@
94
94
  justify-content: center;
95
95
  padding-inline: 0;
96
96
  }
97
+
98
+ /* Responsive β€” the rail self-hides below md, so you pair it with a
99
+ .juno-dock (or .juno-pillbar) carrying .juno-hide-from-md and get the
100
+ rail↔dock swap without hanging a juno-hide-below-md on the rail yourself.
101
+ Reciprocal of the dock's md pairing; see dock.css / docs/layout.md#app-shell. */
102
+ @media (width <= 767.98px) {
103
+ .juno-rail--responsive { display: none !important; }
104
+ }
@@ -92,8 +92,70 @@
92
92
  }
93
93
  .juno-reel > * { scroll-snap-align: start; flex: 0 0 auto; }
94
94
 
95
+ /* App shell β€” the standard product frame, as classes instead of the
96
+ copy-pasted \3c style> block every consumer used to hand-roll. A rail beside
97
+ a body column; the body holds an optional topbar, a scrolling main region,
98
+ and a dock/pillbar pinned at its foot. The MAIN region is the scroller
99
+ (not the page), so the dock never needs sticky/fixed to stay put and the
100
+ mobile address bar can't clip it. Height is 100dvh (dynamic viewport) so
101
+ it fills the visible area under shrinking browser chrome. Pairs with the
102
+ viewport helpers below to swap rail↔dock across md; see docs/layout.md#app-shell.
103
+ <div class="juno-app-shell">
104
+ <nav class="juno-rail juno-rail--responsive" aria-label="Primary">…</nav>
105
+ <div class="juno-app-shell__body">
106
+ <header class="juno-app-shell__topbar">…</header>
107
+ <main class="juno-app-shell__main">…</main>
108
+ <nav class="juno-dock juno-hide-from-md" aria-label="Primary">…</nav>
109
+ </div>
110
+ </div> */
111
+ .juno-app-shell {
112
+ display: flex;
113
+ block-size: 100dvh;
114
+ overflow: hidden; /* the main region scrolls, not the shell */
115
+ padding-inline: env(safe-area-inset-left, 0) env(safe-area-inset-right, 0);
116
+ }
117
+
118
+ .juno-app-shell__body {
119
+ flex: 1;
120
+ min-inline-size: 0;
121
+ display: flex;
122
+ flex-direction: column;
123
+ min-block-size: 0; /* let __main shrink and scroll instead of the shell */
124
+ }
125
+
126
+ .juno-app-shell__topbar {
127
+ flex-shrink: 0;
128
+ display: flex;
129
+ align-items: center;
130
+ gap: var(--juno-space-12);
131
+ min-block-size: var(--juno-app-shell-topbar-size, 46px);
132
+ padding-inline: var(--juno-pad-surface-inline);
133
+ padding-block-start: env(safe-area-inset-top, 0);
134
+ background: var(--juno-s1);
135
+ border-block-end: var(--juno-border-width-1) solid var(--juno-border);
136
+ }
137
+
138
+ .juno-app-shell__main {
139
+ flex: 1;
140
+ min-block-size: 0;
141
+ overflow: auto;
142
+ overscroll-behavior: contain;
143
+ padding: var(--juno-pad-surface-block) var(--juno-pad-surface-inline);
144
+ }
145
+
95
146
  /* Hide/show by viewport breakpoint β€” the rare case an intrinsic primitive
96
- can't express (e.g. swap a nav for a menu button). */
97
- @media (width <= 767.98px) { .juno-hide-below-md { display: none !important; } }
147
+ can't express (e.g. swap a nav for a menu button). `hide-below-X` hides
148
+ under breakpoint X; `hide-from-X` hides at X and up. `show-*` are the
149
+ readable inverse aliases (show-from-X ≑ hide-below-X, show-below-X ≑
150
+ hide-from-X) β€” same media rule, name whichever reads clearer at the site. */
151
+ @media (width <= 639.98px) { .juno-hide-below-sm, .juno-show-from-sm { display: none !important; } }
152
+
153
+ @media (width >= 640px) { .juno-hide-from-sm, .juno-show-below-sm { display: none !important; } }
154
+
155
+ @media (width <= 767.98px) { .juno-hide-below-md, .juno-show-from-md { display: none !important; } }
156
+
157
+ @media (width >= 768px) { .juno-hide-from-md, .juno-show-below-md { display: none !important; } }
158
+
159
+ @media (width <= 1023.98px) { .juno-hide-below-lg, .juno-show-from-lg { display: none !important; } }
98
160
 
99
- @media (width >= 768px) { .juno-hide-from-md { display: none !important; } }
161
+ @media (width >= 1024px) { .juno-hide-from-lg, .juno-show-below-lg { display: none !important; } }