altitude-design 0.1.2 → 0.2.1

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.
package/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  A React component library where **height is the interface**. Its one idea: the ground is the record. Settled content sits on the ground; anything live rises above it; anything waiting to receive sinks below it.
4
4
 
5
- - **Storybook:** every component, with its guidelines and a live preview in all four schemes: https://richlaconte.github.io/altitude/
5
+ - **Site:** https://richlaconte.github.io/altitude/
6
+ - **Storybook:** every component, with its guidelines and a live preview in all four schemes: https://richlaconte.github.io/altitude/storybook/
6
7
  - **The design guide:** [`project/README.md`](https://github.com/richlaconte/altitude/blob/main/project/README.md), then [Elevation](https://github.com/richlaconte/altitude/blob/main/project/Elevation.md), [Motion](https://github.com/richlaconte/altitude/blob/main/project/Motion.md), [Theming](https://github.com/richlaconte/altitude/blob/main/project/Theming.md) and [Research](https://github.com/richlaconte/altitude/blob/main/project/Research.md)
7
8
 
8
9
  ## Install
@@ -33,6 +34,8 @@ Render `AltitudeProvider` once around your app. Schemes are `daylight`, `nightfa
33
34
  |---|---|
34
35
  | `altitude-design` | the components (ESM and CommonJS, with types) |
35
36
  | `altitude-design/altitude.css` | tokens and component styles in one file (it also loads Instrument Sans and JetBrains Mono from Google Fonts) |
37
+ | `altitude-design/altitude.no-fonts.css` | the same styles without the Google Fonts import, so the page makes no third-party request; self-host the fonts or set `--font-sans` and `--font-mono` to your own (both fall back to system fonts) |
38
+ | `altitude-design/fonts.css` | just the Google Fonts import, to pair with `tokens.css` or `altitude.no-fonts.css` |
36
39
  | `altitude-design/tokens.css`, `altitude-design/tokens.json` | the tokens on their own |
37
40
  | `altitude-design/altitude.iife.js` | a `<script>` build that sets `window.Altitude`, for pages without a bundler; it expects React 18's UMD builds as `window.React` and `window.ReactDOM` |
38
41
 
@@ -42,7 +45,8 @@ Render `AltitudeProvider` once around your app. Schemes are `daylight`, `nightfa
42
45
  project/ the design system itself: guides, tokens.json, components/src/*.tsx, bundle.css,
43
46
  and the built bundle.js and previews (the same files as the design system on claude.ai)
44
47
  build/ the kit that builds and checks project/ (see build/SETUP.md)
45
- scripts/ the npm package build and the Storybook story generator
48
+ scripts/ the npm package build, the Storybook story generator and the landing page build
49
+ site/ the landing page (React, built from the components' own sources into site-dist/)
46
50
  .storybook/
47
51
  ```
48
52
 
@@ -53,6 +57,7 @@ There is one source for each thing. Components live in `project/components/src`,
53
57
  ```sh
54
58
  npm install && npm install --prefix build
55
59
  npm run storybook # http://localhost:6006
60
+ npm run build-site # the landing page, into site-dist/index.html
56
61
  ```
57
62
 
58
63
  After changing a component, rebuild the design system's bundle and previews, and commit what they produce:
@@ -69,7 +74,7 @@ Every push and pull request runs the checks: the bundle and previews are rebuilt
69
74
 
70
75
  A push to `main` then also:
71
76
 
72
- - deploys Storybook to GitHub Pages;
77
+ - deploys the landing page to GitHub Pages, with Storybook under `/storybook/`;
73
78
  - publishes to npm **if the version in `package.json` isn't published yet**, and tags the commit `v<version>`. To release, bump the version (`npm version patch`, `minor` or `major`) and push.
74
79
 
75
80
  ## Found a bug, or need something it doesn't do?
package/dist/altitude.css CHANGED
@@ -300,16 +300,19 @@ body { margin: 0; background: var(--surface-ground); color: var(--ink); font-fam
300
300
  transition: box-shadow var(--dur-travel-1) var(--ease-rise), background-color var(--dur-travel-1) var(--ease-rise),
301
301
  color var(--dur-travel-1) var(--ease-rise), transform var(--dur-travel-2) var(--ease-settle);
302
302
  }
303
- /* Focus ring: wraps the whole slab, face and wall, so it follows the button's real shape at every height. */
303
+ /* Focus ring: wraps the whole slab, face and wall, so it follows the button's real shape at every height. It's an
304
+ outline 2px off a box that covers the face and the wall, because an outline never adds to the page's scrollable
305
+ area: a button at the edge of a narrow screen must not give the page a sideways scroll. At rest the box is the face
306
+ alone, so only a keyboard-focused button's wall (the box's bottom edge, at most 5px) reaches past its own box. */
304
307
  .alt-btn:focus-visible, .alt-crumbs button:focus-visible { outline: none; }
305
308
  .alt-btn::after, .alt-crumbs button::after {
306
309
  content: ""; position: absolute; pointer-events: none; opacity: 0;
307
- inset: -4px -4px calc(-4px - var(--wall, 0px) - 1px) -4px; border: 2px solid var(--accent-ink); border-radius: calc(var(--radius-md) + 4px);
308
- transition: opacity var(--dur-travel-1) var(--ease-rise), inset var(--dur-press) ease-out;
310
+ inset: 0; outline: 2px solid var(--accent-ink); outline-offset: 2px; border-radius: var(--radius-md);
311
+ transition: opacity var(--dur-travel-1) var(--ease-rise), bottom var(--dur-press) ease-out;
309
312
  }
310
- .alt-btn[data-size="sm"]::after, .alt-crumbs button::after { border-radius: calc(var(--radius-sm) + 4px); }
311
- .alt-btn:focus-visible::after, .alt-crumbs button:focus-visible::after { opacity: 1; }
312
- .alt-btn:active::after, .alt-btn[data-loading]::after { inset: -4px; }
313
+ .alt-btn[data-size="sm"]::after, .alt-crumbs button::after { border-radius: var(--radius-sm); }
314
+ .alt-btn:focus-visible::after, .alt-crumbs button:focus-visible::after { opacity: 1; bottom: calc(-1 * var(--wall, 0px) - 1px); }
315
+ .alt-btn:active::after, .alt-btn[data-loading]::after { bottom: 0; }
313
316
  .alt-btn-label { display: inline-flex; align-items: center; gap: var(--space-2); transition: opacity var(--dur-travel-1) var(--ease-rise); }
314
317
  .alt-btn-label > svg { flex: none; margin-inline: -2px; }
315
318
  .alt-btn[data-variant="primary"] {
@@ -705,7 +708,7 @@ body { margin: 0; background: var(--surface-ground); color: var(--ink); font-fam
705
708
  .alt-avatar[data-presence] .alt-avatar-face { outline: 2px solid var(--alt-presence); outline-offset: 1px; }
706
709
  .alt-avatar-lamp {
707
710
  --b-lamp: var(--positive);
708
- position: absolute; z-index: 2; right: -1px; bottom: -1px; width: max(8px, calc(var(--size) * 0.24)); height: max(8px, calc(var(--size) * 0.24)); border-radius: var(--radius-full);
711
+ position: absolute; z-index: 2; right: 0; bottom: 0; width: max(8px, calc(var(--size) * 0.24)); height: max(8px, calc(var(--size) * 0.24)); border-radius: var(--radius-full);
709
712
  background: var(--b-lamp); box-shadow: 0 0 0 2px var(--surface-ground); transform: translateY(calc(-1 * var(--lift)));
710
713
  }
711
714
  .alt-avatar[data-status="idle"] .alt-avatar-lamp { --b-lamp: var(--caution); }
@@ -713,15 +716,18 @@ body { margin: 0; background: var(--surface-ground); color: var(--ink); font-fam
713
716
  .alt-avatar[data-interactive] { cursor: pointer; outline: none; }
714
717
  .alt-avatar[data-interactive]:focus-visible .alt-avatar-face { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
715
718
  .alt-avatar-group { isolation: isolate; display: inline-flex; align-items: flex-end; padding: calc(var(--size, 36px) * 0.32) 0 calc(var(--size, 36px) * 0.1); }
716
- .alt-avatar-group > .alt-avatar { isolation: auto; margin-left: calc(var(--size) * -0.22); }
717
- .alt-avatar-group > .alt-avatar:first-child { margin-left: 0; }
719
+ .alt-avatar-group .alt-avatar { isolation: auto; }
720
+ /* Faces overlap by a fifth. An avatar that waited for its data sits inside its loading wrapper (display: contents at
721
+ rest), so the overlap is counted from the group's own children either way, imprints included. */
722
+ .alt-avatar-group > .alt-avatar:not(:first-child),
723
+ .alt-avatar-group > :is(.alt-await, .alt-await-wait):not(:first-child) :is(.alt-avatar, .alt-avatar-imprint) { margin-left: calc(var(--size, 36px) * -0.22); }
718
724
  /* Overlapping faces are cut out of each other with a ring of ground; higher faces sit in front. */
719
- .alt-avatar-group > .alt-avatar .alt-avatar-face { box-shadow: 0 0 0 2px var(--surface-ground), inset 0 0 0 1px var(--alt-ring); }
720
- .alt-avatar-group > .alt-avatar[data-status="offline"] .alt-avatar-face { box-shadow: 0 0 0 2px var(--surface-ground), inset 0 0 0 1px var(--alt-ring), inset 0 1px 2px var(--alt-cast); }
721
- .alt-avatar-group > .alt-avatar[data-status="active"] .alt-avatar-face { z-index: 3; }
722
- .alt-avatar-group > .alt-avatar[data-status="idle"] .alt-avatar-face { z-index: 2; }
723
- .alt-avatar-group > .alt-avatar:not([data-tint="more"]):hover { --hr: 0.32; }
724
- .alt-avatar-group > .alt-avatar:not([data-tint="more"]):hover .alt-avatar-face { z-index: 5; }
725
+ .alt-avatar-group .alt-avatar .alt-avatar-face { box-shadow: 0 0 0 2px var(--surface-ground), inset 0 0 0 1px var(--alt-ring); }
726
+ .alt-avatar-group .alt-avatar[data-status="offline"] .alt-avatar-face { box-shadow: 0 0 0 2px var(--surface-ground), inset 0 0 0 1px var(--alt-ring), inset 0 1px 2px var(--alt-cast); }
727
+ .alt-avatar-group .alt-avatar[data-status="active"] .alt-avatar-face { z-index: 3; }
728
+ .alt-avatar-group .alt-avatar[data-status="idle"] .alt-avatar-face { z-index: 2; }
729
+ .alt-avatar-group .alt-avatar:not([data-tint="more"]):hover { --hr: 0.32; }
730
+ .alt-avatar-group .alt-avatar:not([data-tint="more"]):hover .alt-avatar-face { z-index: 5; }
725
731
  .alt-avatar-group .alt-avatar-lamp { display: none; }
726
732
 
727
733
  /* ── Skeleton: an imprint of the content to come, pressed into the page. Light passes over it while it waits; when the
@@ -745,6 +751,38 @@ body { margin: 0; background: var(--surface-ground); color: var(--ink); font-fam
745
751
  }
746
752
  @keyframes alt-rise-out { from { transform: translateY(calc(100% * var(--alt-travel))); opacity: 0.25; } }
747
753
 
754
+ /* ── Suspense: imprints that come with the component. A component given a promise (Avatar's name or src, a Badge's or
755
+ a card title's text, a DataTable's rows) holds its own Suspense boundary, and its fallback is its own imprint, drawn
756
+ from the same props and tokens, so it's the size and shape of what's coming. Card and CardContent hold one for
757
+ anything of yours that suspends inside them. An imprint waits dur-grace before it shows, so quick loads show nothing;
758
+ when content lands it rises 8px out of its imprint as the imprint fills in behind it, within the attention budget.
759
+ Containers just let their content rise. In a RevealGroup, content that lands early stays an imprint until the rest
760
+ arrive. At rest the wrapper is display: contents: it adds nothing to layout or to your selectors' parent chain. ── */
761
+ .alt-await-wait { display: block; animation: alt-onset var(--dur-travel-1) ease-out var(--dur-grace) backwards; }
762
+ .alt-await-wait[data-inline] { display: inline-block; vertical-align: middle; line-height: 0; }
763
+ .alt-await, .alt-await > .alt-await-face { display: contents; }
764
+ .alt-await[data-phase] { display: block; position: relative; isolation: isolate; }
765
+ .alt-await[data-phase] > .alt-await-face { display: block; }
766
+ .alt-await[data-phase][data-inline], .alt-await[data-phase][data-inline] > .alt-await-face { display: inline-block; vertical-align: middle; line-height: 0; }
767
+ .alt-await-imprint { position: absolute; inset: 0; z-index: -1; }
768
+ .alt-await[data-phase="held"] > .alt-await-face { position: absolute; inset: 0 0 auto; visibility: hidden; }
769
+ .alt-await-held { display: block; }
770
+ .alt-await-held[data-early] { animation: alt-onset var(--dur-travel-1) ease-out var(--alt-grace-left, 0ms) backwards; }
771
+ .alt-await[data-inline] > .alt-await-held { display: inline-block; vertical-align: middle; line-height: 0; }
772
+ .alt-await[data-phase="reveal"] > .alt-await-face { --alt-from-y: 8px; animation: alt-emerge var(--dur-travel-3) var(--ease-rise) var(--alt-rise-delay, 0ms) backwards; }
773
+ .alt-await[data-phase="reveal"] > .alt-await-imprint { animation: alt-await-fill var(--dur-travel-3) linear var(--alt-rise-delay, 0ms) forwards; }
774
+ @keyframes alt-await-fill { to { opacity: 0; } }
775
+ /* Text imprints are as tall as the text they stand for: one line-height of the surrounding type per line. */
776
+ .alt-await-lines { display: flex; flex-direction: column; justify-content: space-around; height: calc(var(--lines, 1) * 1lh); max-width: 100%; }
777
+ .alt-await-lines > .alt-skeleton { flex: none; height: 0.6em; }
778
+ .alt-await-card { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-5); }
779
+ .alt-await-card-body { font: 400 15px/23px var(--font-sans); }
780
+ .alt-avatar-imprint { display: inline-block; vertical-align: middle; }
781
+ .alt-badge-imprint { display: inline-block; vertical-align: middle; width: 64px; height: 22px; }
782
+ .alt-badge-imprint[data-variant="count"] { width: 22px; border-radius: var(--radius-full); }
783
+ .alt-check-imprint { display: block; width: 22px; height: 22px; border-radius: var(--radius-xs); }
784
+ .alt-cell-imprint { display: block; height: 0.6em; margin: calc((1lh - 0.6em) / 2) 0; }
785
+
748
786
  /* ── Alert: a sheet laid on the page. Status that informs is a thin tinted sheet resting on the ground — you can see
749
787
  its paper edge and the contact shadow under it — and it never floats. Status that asks you to act is a raised
750
788
  card. Dismissing lifts the sheet off the page and the gap closes behind it. Several alerts make an AlertStack:
@@ -942,6 +980,9 @@ body { margin: 0; background: var(--surface-ground); color: var(--ink); font-fam
942
980
  }
943
981
  .alt-dialog-head { display: flex; flex-direction: column; gap: 6px; padding-right: var(--space-8); }
944
982
  .alt-dialog-title { margin: 0; font: 600 19px/26px var(--font-sans); letter-spacing: -0.01em; }
983
+ /* A title that takes focus (initialFocus="title") isn't a control: no ring after a click, the usual ring from the keyboard. */
984
+ .alt-dialog-title:focus { outline: none; }
985
+ .alt-dialog-title:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; border-radius: var(--radius-xs); }
945
986
  .alt-dialog-desc { margin: 0; font: 400 14px/21px var(--font-sans); color: var(--ink-muted); }
946
987
  .alt-dialog-body { margin-top: var(--space-5); font: 400 15px/23px var(--font-sans); }
947
988
  .alt-dialog-footer { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-6); }
@@ -1134,6 +1175,31 @@ body { margin: 0; background: var(--surface-ground); color: var(--ink); font-fam
1134
1175
  .alt-drop[data-drop="armed"] { background: var(--surface-well); box-shadow: var(--well); border-color: var(--ink-muted); }
1135
1176
  .alt-drop[data-drop="over"] { background: var(--accent-soft); box-shadow: var(--trench); border-style: solid; border-color: var(--accent-ink); color: var(--ink); transform: scale(calc(1 - 0.01 * var(--alt-travel))); }
1136
1177
 
1178
+ /* ── EmptyState: an opening the first record will fill. Nothing here yet isn't a problem (Alert) or a settled
1179
+ container (Card): it's a space waiting to receive, so it's sunk, a well cut into the page with a dashed rim, the
1180
+ same below-ground signal as an empty field. Its actions are ordinary raised buttons standing in it. Pointing at
1181
+ the primary action deepens the well to a trench and darkens the rim: feedforward for where the record will land.
1182
+ It sinks in when it appears (flush to well, dur-travel-1, ease-sink), the inverse of content arriving lifted,
1183
+ and it never pulses. ── */
1184
+ .alt-empty {
1185
+ display: flex; flex-direction: column; align-items: center; gap: var(--space-2); padding: var(--space-8) var(--space-6); text-align: center;
1186
+ border: 1.5px dashed var(--line-control); border-radius: var(--radius-lg); background: var(--surface-well); box-shadow: var(--well);
1187
+ color: var(--ink-muted); font: 400 15px/23px var(--font-sans);
1188
+ animation: alt-empty-sink var(--dur-travel-1) var(--ease-sink);
1189
+ transition: box-shadow var(--dur-travel-1) var(--ease-sink), border-color var(--dur-travel-1);
1190
+ }
1191
+ @keyframes alt-empty-sink { from { background-color: var(--surface-ground); box-shadow: var(--elev-0); } }
1192
+ .alt-empty-icon { display: inline-flex; margin-bottom: var(--space-1); color: var(--ink-muted); }
1193
+ .alt-empty-icon > svg { width: 24px; height: 24px; }
1194
+ .alt-empty-title { margin: 0; font: 600 19px/26px var(--font-sans); letter-spacing: -0.01em; color: var(--ink); text-wrap: balance; }
1195
+ .alt-empty-desc { margin: 0; max-width: 46ch; color: var(--ink-muted); text-wrap: pretty; }
1196
+ .alt-empty-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); margin-top: var(--space-3); }
1197
+ .alt-empty:has(.alt-empty-actions > :first-child:is(:hover, :focus-visible):not(:disabled)) { box-shadow: var(--trench); border-color: var(--ink-muted); }
1198
+ .alt-empty[data-size="sm"] { gap: var(--space-1); padding: var(--space-5) var(--space-4); border-radius: var(--radius-md); font: 400 14px/20px var(--font-sans); }
1199
+ .alt-empty[data-size="sm"] .alt-empty-icon > svg { width: 18px; height: 18px; }
1200
+ .alt-empty[data-size="sm"] .alt-empty-title { font: 500 14px/20px var(--font-sans); letter-spacing: 0; }
1201
+ .alt-empty[data-size="sm"] .alt-empty-actions { margin-top: var(--space-2); }
1202
+
1137
1203
  /* ── SortableList: pick up → level 3 in your hand; the landing slot opens as a well; release → it lands. ── */
1138
1204
  .alt-sort { position: relative; display: flex; flex-direction: column; }
1139
1205
  .alt-sort-item {
@@ -1586,6 +1652,7 @@ body { margin: 0; background: var(--surface-ground); color: var(--ink); font-fam
1586
1652
  --alt-ring: var(--ink); --alt-cast: transparent; --alt-cast-deep: transparent; --alt-wall-in: var(--ink-muted); --alt-lip: transparent; --alt-rim-held: var(--ink);
1587
1653
  }
1588
1654
  .alt-btn, .alt-kbd, .alt-crumbs button { --wall-c: var(--ink); }
1655
+ .alt-empty { border-style: solid; }
1589
1656
  }
1590
1657
  [data-contrast="more"], [data-contrast="more"] [data-theme] {
1591
1658
  --elev-0: inset 0 0 0 0 #0000, 0 0 0 0 #0000, 0 0 0 0 #0000, 0 0 0 0 #0000;
@@ -1600,17 +1667,19 @@ body { margin: 0; background: var(--surface-ground); color: var(--ink); font-fam
1600
1667
  --alt-ring: var(--ink); --alt-cast: transparent; --alt-cast-deep: transparent; --alt-wall-in: var(--ink-muted); --alt-lip: transparent; --alt-rim-held: var(--ink);
1601
1668
  }
1602
1669
  [data-contrast="more"] .alt-btn, [data-contrast="more"] .alt-kbd, [data-contrast="more"] .alt-crumbs button { --wall-c: var(--ink); }
1670
+ [data-contrast="more"] .alt-empty { border-style: solid; }
1603
1671
 
1604
1672
  /* Forced colours (Windows High Contrast) drop every box-shadow, which erases elevation entirely
1605
1673
  (measured: the ladder renders as blank). Height falls back to border weight and style. */
1606
1674
  @media (forced-colors: active) {
1675
+ .alt-skeleton { outline: 1px dashed CanvasText; outline-offset: -1px; }
1607
1676
  .alt-surface[data-level="1"], .alt-surface[data-level="2"], .alt-card, .alt-btn:not([data-variant="ghost"]), .alt-tabs-indicator,
1608
1677
  .alt-switch-thumb, .alt-slider-thumb, .alt-kbd, .alt-avatar-face, .alt-sort-item, .alt-pile-top, .alt-pile-sheet, .alt-scroll { border: 1px solid CanvasText; }
1609
1678
  .alt-surface[data-level="3"], .alt-surface[data-level="4"], .alt-pop, .alt-tip, .alt-toast, .alt-sort-item[data-held], [data-held] { border: 2px solid CanvasText; }
1610
1679
  .alt-surface[data-level="5"], .alt-dialog { border: 4px double CanvasText; }
1611
1680
  .alt-input:has(input:not(:placeholder-shown)), .alt-input:has(textarea:not(:placeholder-shown)) { border-style: solid !important; }
1612
1681
  .alt-input[data-invalid] { border: 2px dashed CanvasText !important; }
1613
- .alt-surface[data-level="-1"], .alt-input, .alt-tabs-list, .alt-sort-slot, .alt-drop[data-drop="armed"] { border: 1px dashed CanvasText; }
1682
+ .alt-surface[data-level="-1"], .alt-input, .alt-tabs-list, .alt-sort-slot, .alt-drop[data-drop="armed"], .alt-empty { border: 1px dashed CanvasText; }
1614
1683
  .alt-surface[data-level="-2"], .alt-switch, .alt-slider-track, .alt-progress, [data-status="failed"], .alt-drop[data-drop="over"] { border: 2px dashed CanvasText; }
1615
1684
  [data-status="pending"] { border: 2px dotted CanvasText; }
1616
1685
  .alt-progress-fill, .alt-slider-fill, .alt-switch-fill, .alt-check-fill { background: Highlight; }