@codenhub/styles 0.0.4 → 0.1.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.
Files changed (74) hide show
  1. package/LICENSE +201 -201
  2. package/NOTICE +27 -0
  3. package/README.md +22 -116
  4. package/dist/aesthetics/chunky-tile.css +1 -0
  5. package/dist/aesthetics/glass.css +1 -0
  6. package/dist/aesthetics/index.css +2 -0
  7. package/dist/aesthetics/neobrutalism.css +1 -0
  8. package/dist/aesthetics/pixel.css +1 -0
  9. package/dist/components.css +2 -2
  10. package/dist/index.css +2 -2
  11. package/dist/native.css +2 -0
  12. package/dist/theme.css +2 -2
  13. package/dist/tw/aesthetics/chunky-tile.css +109 -0
  14. package/dist/tw/aesthetics/glass.css +66 -0
  15. package/dist/tw/aesthetics/index.css +9 -0
  16. package/dist/tw/aesthetics/neobrutalism.css +63 -0
  17. package/dist/tw/aesthetics/pixel.css +88 -0
  18. package/dist/tw/box.css +206 -0
  19. package/dist/tw/components/button.css +108 -0
  20. package/dist/tw/components/feedback.css +163 -0
  21. package/dist/tw/components/form.css +599 -0
  22. package/dist/tw/components/index.css +28 -0
  23. package/dist/tw/components/loader.css +122 -0
  24. package/dist/tw/components/surface.css +122 -0
  25. package/dist/tw/components/tooltip.css +132 -0
  26. package/dist/tw/elevation.css +46 -0
  27. package/dist/tw/index.css +27 -0
  28. package/dist/tw/intent.css +209 -0
  29. package/dist/tw/native.css +187 -0
  30. package/dist/tw/presentation.css +77 -0
  31. package/dist/tw/reset.css +129 -0
  32. package/dist/tw/theme.css +223 -0
  33. package/dist/tw/typography.css +34 -0
  34. package/dist/tw/utilities/content.css +361 -0
  35. package/dist/tw/utilities/index.css +6 -0
  36. package/dist/tw/utilities/layout.css +107 -0
  37. package/docs/accessibility.md +92 -0
  38. package/docs/changelog/0.1.0.md +37 -0
  39. package/docs/changelog/0.1.1.md +10 -0
  40. package/docs/changelog/index.md +10 -0
  41. package/docs/concepts.md +68 -0
  42. package/docs/index.md +35 -0
  43. package/docs/integrating/astro.md +35 -0
  44. package/docs/integrating/index.md +41 -0
  45. package/docs/integrating/nextjs.md +67 -0
  46. package/docs/integrating/svelte.md +43 -0
  47. package/docs/integrating/tailwind.md +56 -0
  48. package/docs/integrating/vue.md +31 -0
  49. package/docs/setup.md +96 -0
  50. package/docs/usage/aesthetics.md +120 -0
  51. package/docs/usage/buttons.md +58 -0
  52. package/docs/usage/composing.md +143 -0
  53. package/docs/usage/content-and-layout.md +175 -0
  54. package/docs/usage/customizing.md +148 -0
  55. package/docs/usage/feedback.md +79 -0
  56. package/docs/usage/forms.md +135 -0
  57. package/docs/usage/index.md +24 -0
  58. package/docs/usage/theming.md +154 -0
  59. package/docs/usage/tooltips.md +39 -0
  60. package/llms-full.txt +1676 -0
  61. package/llms.txt +26 -0
  62. package/package.json +176 -33
  63. package/src/components/button.css +0 -155
  64. package/src/components/feedback.css +0 -214
  65. package/src/components/form.css +0 -303
  66. package/src/components/index.css +0 -9
  67. package/src/components/loader.css +0 -56
  68. package/src/components/surface.css +0 -6
  69. package/src/components/tooltip.css +0 -60
  70. package/src/index.css +0 -7
  71. package/src/overrides.css +0 -76
  72. package/src/theme.css +0 -234
  73. package/src/typography.css +0 -30
  74. package/src/utilities.css +0 -40
@@ -0,0 +1,122 @@
1
+ @reference "tailwindcss";
2
+ @source inline("loader dots-wave dots-fade dots-queue dots-rotate dots-grow dots-grow-alternate dot-bounce bars-wave pulse-ring");
3
+
4
+ /* The mask machinery, shared by the loader and by any component that shows a
5
+ spinner of its own. Artwork is a mask rather than an image so it takes its
6
+ colour from whatever paints behind it, which is the whole indicator contract:
7
+ an intent class recolours the loader and nothing else has to know. */
8
+ @utility loader-mask {
9
+ mask-image: var(--loader-art);
10
+ -webkit-mask-image: var(--loader-art);
11
+ mask-size: contain;
12
+ -webkit-mask-size: contain;
13
+ mask-repeat: no-repeat;
14
+ -webkit-mask-repeat: no-repeat;
15
+ mask-position: center;
16
+ -webkit-mask-position: center;
17
+ }
18
+
19
+ .loader:not(
20
+ :where(
21
+ .dots-wave,
22
+ .dots-fade,
23
+ .dots-queue,
24
+ .dots-rotate,
25
+ .dots-grow,
26
+ .dots-grow-alternate,
27
+ .dot-bounce,
28
+ .bars-wave,
29
+ .pulse-ring
30
+ )
31
+ ),
32
+ .loading:not(
33
+ :where(
34
+ .dots-wave,
35
+ .dots-fade,
36
+ .dots-queue,
37
+ .dots-rotate,
38
+ .dots-grow,
39
+ .dots-grow-alternate,
40
+ .dot-bounce,
41
+ .bars-wave,
42
+ .pulse-ring
43
+ )
44
+ ) {
45
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>.spinner_V8m1{transform-origin:center;animation:spinner_zKoa 2s linear infinite}.spinner_V8m1 circle{stroke-linecap:round;animation:spinner_YpZS 1.5s ease-in-out infinite}@keyframes spinner_zKoa{100%{transform:rotate(360deg)}}@keyframes spinner_YpZS{0%{stroke-dasharray:0 150;stroke-dashoffset:0}47.5%{stroke-dasharray:42 150;stroke-dashoffset:-16}95%,100%{stroke-dasharray:42 150;stroke-dashoffset:-59}}</style><g class='spinner_V8m1'><circle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'></circle></g></svg>");
46
+ }
47
+
48
+ @utility dots-wave {
49
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg fill='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_8HQG{0%,57.14%{animation-timing-function:cubic-bezier(.33,.66,.66,1);transform:translate(0)}28.57%{animation-timing-function:cubic-bezier(.33,0,.66,.33);transform:translateY(-6px)}to{transform:translate(0)}}.spinner_qM83{animation:spinner_8HQG 1.05s infinite}</style><circle class='spinner_qM83' cx='4' cy='12' r='3'/><circle class='spinner_qM83' cx='12' cy='12' r='3' style='animation-delay:.1s'/><circle class='spinner_qM83' cx='20' cy='12' r='3' style='animation-delay:.2s'/></svg>");
50
+ }
51
+
52
+ @utility dots-fade {
53
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>.dot{animation:dot-anim 1s ease-in-out infinite;fill:currentColor}.d1{animation-delay:-0.32s}.d2{animation-delay:-0.16s}@keyframes dot-anim{0%,80%,100%{transform:scale(0.6);opacity:0.2}40%{transform:scale(1);opacity:1}}</style><circle class='dot d1' cx='4' cy='12' r='3' transform-origin='4px 12px'></circle><circle class='dot d2' cx='12' cy='12' r='3' transform-origin='12px 12px'></circle><circle class='dot d3' cx='20' cy='12' r='3' transform-origin='20px 12px'></circle></svg>");
54
+ }
55
+
56
+ @utility dots-queue {
57
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_qtyZ{0%{r:0}25%{r:3px;cx:4px}50%{r:3px;cx:12px}75%{r:3px;cx:20px}to{r:0;cx:20px}}.spinner_nOfF{animation:spinner_qtyZ 2s cubic-bezier(.36,.6,.31,1) infinite}</style><circle class='spinner_nOfF' cx='4' cy='12' r='3'/><circle class='spinner_nOfF' cx='4' cy='12' r='3' style='animation-delay:-.5s'/><circle class='spinner_nOfF' cx='4' cy='12' r='3' style='animation-delay:-1s'/><circle class='spinner_nOfF' cx='4' cy='12' r='3' style='animation-delay:-1.5s'/></svg>");
58
+ }
59
+
60
+ @utility dots-rotate {
61
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_XVY9{50%{transform:rotate(180deg)}to{transform:rotate(360deg)}}</style><circle cx='12' cy='12' r='3'/><g style='transform-origin:center;animation:spinner_XVY9 1s cubic-bezier(.36,.6,.31,1) infinite'><circle cx='4' cy='12' r='3'/><circle cx='20' cy='12' r='3'/></g></svg>");
62
+ }
63
+
64
+ @utility dots-grow {
65
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_xe7Q{93.75%,to{r:3px}46.875%{r:.2px}}.spinner_b2T7{animation:spinner_xe7Q .8s linear infinite}</style><circle class='spinner_b2T7' cx='4' cy='12' r='3'/><circle class='spinner_b2T7' cx='12' cy='12' r='3' style='animation-delay:-.65s'/><circle class='spinner_b2T7' cx='20' cy='12' r='3' style='animation-delay:-.5s'/></svg>");
66
+ }
67
+
68
+ @utility dots-grow-alternate {
69
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_qhi1{0%,to{r:1.5px}50%{r:3px}}.spinner_I8Q1{animation:spinner_qhi1 .75s linear infinite}</style><circle class='spinner_I8Q1' cx='4' cy='12' r='1.5'/><circle class='spinner_I8Q1' cx='12' cy='12' r='3' style='animation-delay:-.375s'/><circle class='spinner_I8Q1' cx='20' cy='12' r='1.5'/></svg>");
70
+ }
71
+
72
+ @utility dot-bounce {
73
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>@keyframes spinner_YeBj{0%{animation-timing-function:cubic-bezier(.33,0,.66,.33);cy:5px}46.875%{cy:20px;rx:4px;ry:4px}50%{animation-timing-function:cubic-bezier(.33,.66,.66,1);cy:20.5px;rx:4.8px;ry:3px}53.125%{rx:4px;ry:4px}to{cy:5px}}</style><circle cx='12' cy='5' style='animation:spinner_YeBj .8s infinite' r='4'/></svg>");
74
+ }
75
+
76
+ @utility bars-wave {
77
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>.bar{animation:bar-anim 1s ease-in-out infinite;fill:currentColor}.b1{animation-delay:-0.4s}.b2{animation-delay:-0.2s}@keyframes bar-anim{0%,100%{transform:scaleY(0.3)}50%{transform:scaleY(1)}}</style><rect class='bar b1' x='3' y='2' width='4' height='20' rx='2' transform-origin='5px 12px'></rect><rect class='bar b2' x='10' y='2' width='4' height='20' rx='2' transform-origin='12px 12px'></rect><rect class='bar b3' x='17' y='2' width='4' height='20' rx='2' transform-origin='19px 12px'></rect></svg>");
78
+ }
79
+
80
+ @utility pulse-ring {
81
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><style>.pulse{animation:pulse-anim 1.2s cubic-bezier(0, 0.2, 0.8, 1) infinite;stroke:currentColor;transform-origin:center}.p2{animation-delay:-0.6s}@keyframes pulse-anim{0%{transform:scale(0);opacity:1}100%{transform:scale(1);opacity:0}}</style><circle class='pulse p1' cx='12' cy='12' r='10' fill='none' stroke-width='3'></circle><circle class='pulse p2' cx='12' cy='12' r='10' fill='none' stroke-width='3'></circle></svg>");
82
+ }
83
+
84
+ /* A loader takes a colour and its own geometry, and nothing else. No fill, no
85
+ edge, no silhouette: what used to be a bordered box wrapped around a mask is
86
+ now the mask. `--intent-color` defaults to `currentColor` for indicators, so
87
+ a loader inside filled content matches the text around it and
88
+ `.loader.success` is green. There is nothing else to know. */
89
+ @utility loader {
90
+ @apply loader-mask inline-block size-8 [&.lg]:size-10 [&.sm]:size-6;
91
+
92
+ color: var(--intent-color);
93
+ background-color: currentColor;
94
+ }
95
+
96
+ @media (prefers-reduced-motion: reduce) {
97
+ .loader,
98
+ .loading {
99
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><circle cx='12' cy='12' r='9.5' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-dasharray='42 18'/></svg>");
100
+ }
101
+
102
+ .dots-wave,
103
+ .dots-fade,
104
+ .dots-queue,
105
+ .dots-rotate,
106
+ .dots-grow,
107
+ .dots-grow-alternate {
108
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><circle cx='4' cy='12' r='3'/><circle cx='12' cy='12' r='3'/><circle cx='20' cy='12' r='3'/></svg>");
109
+ }
110
+
111
+ .dot-bounce {
112
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><circle cx='12' cy='12' r='4'/></svg>");
113
+ }
114
+
115
+ .bars-wave {
116
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><rect x='3' y='5' width='4' height='14' rx='2'/><rect x='10' y='5' width='4' height='14' rx='2'/><rect x='17' y='5' width='4' height='14' rx='2'/></svg>");
117
+ }
118
+
119
+ .pulse-ring {
120
+ --loader-art: url("data:image/svg+xml;charset=utf-8,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><circle cx='12' cy='12' r='9' fill='none' stroke='black' stroke-width='3'/></svg>");
121
+ }
122
+ }
@@ -0,0 +1,122 @@
1
+ @reference "tailwindcss";
2
+ @import "../theme.css";
3
+ @reference "../box.css";
4
+
5
+ /* What every surface shares. Two slots exist here and nowhere else, and that is
6
+ how an aesthetic reaches surfaces without naming one: only a surface resolves
7
+ `--ui-backdrop`, so glass blurs cards and panels while controls stay solid,
8
+ and only a surface resolves `--ui-surface-shadow`, so a two-layer drop shadow
9
+ does not land under every button on the page. */
10
+ @utility surface {
11
+ @apply box block;
12
+
13
+ /* Bare and edged. `box` reads these three with no fallback, so `surface` --
14
+ typeable on its own -- owes all three; `card` restates its own. No shadow
15
+ geometry: depth is asked for (`.raised`/`.floating`, an aesthetic, the
16
+ registry elevation) rather than carried, so a plain card is flat. */
17
+ --_d-fill: 0%;
18
+ --_d-fg-on-fill: 0%;
19
+ --_d-border: 100%;
20
+
21
+ --_d-ground: var(--ui-surface-ground, var(--color-background));
22
+
23
+ /* The complete-shadow slot for a shadow that is not one layer (glass tucks two
24
+ under with negative spread). Only `surface` reads it -- through `--ui-shadow`
25
+ it would land an 18px drop under every button and chip -- like the backdrop. */
26
+ --_shadow: var(--ui-surface-shadow, var(--_shadow-parts));
27
+
28
+ border-radius: var(--ui-radius-surface, var(--radius-surface));
29
+ /* Prefixed property first, deliberately: written second the minifier folds the
30
+ pair into the prefixed one alone, and the standard property never reaches
31
+ `dist/` -- which drops the blur in Firefox (the one engine with
32
+ `backdrop-filter` and not the `-webkit-` alias). Measured on the built CSS. */
33
+ -webkit-backdrop-filter: var(--ui-backdrop, none);
34
+ backdrop-filter: var(--ui-backdrop, none);
35
+ }
36
+
37
+ /* A ghost, edged container that takes one unit when depth is supplied. */
38
+ @utility card {
39
+ @apply surface p-(--surface-padding,1.25rem);
40
+
41
+ /* After the `@apply`, not before: `surface` inlines its own resting geometry
42
+ at that point, so defaults written above it would be overridden by the very
43
+ utility they are meant to specialise. */
44
+ --_d-fill: 0%;
45
+ --_d-fg-on-fill: 0%;
46
+ --_d-border: 100%;
47
+ --_d-elevation: 1;
48
+
49
+ /* `.p-xs`/`.p-sm`/`.p-lg` are the canonical names; `.dense`/`.compact`/`.spacious`
50
+ are first-class aliases -- see the identical note in `button.css`. */
51
+ &.p-xs,
52
+ &.dense {
53
+ --surface-padding: 0.5rem;
54
+ }
55
+
56
+ &.p-sm,
57
+ &.compact {
58
+ --surface-padding: 0.75rem;
59
+ }
60
+
61
+ &.p-lg,
62
+ &.spacious {
63
+ --surface-padding: 2rem;
64
+ }
65
+
66
+ &.flush {
67
+ --surface-padding: 0;
68
+ }
69
+
70
+ /* Opt-in, because a card is only sometimes a control. Applications still owe
71
+ it a real interactive element and an accessible name. Hover and press are
72
+ the derived ones every other pressable box gets; there is nothing
73
+ card-specific about either.
74
+
75
+ `.interactive` is the whole opt-in and stays that way; `.hoverable` and
76
+ `.pressable` are its halves, for a card that answers one gesture and not
77
+ the other. A card with its own nested controls takes `.hoverable` for the
78
+ hover warmth without the press-scale that reads as "you activated the
79
+ card" when the click really landed on a nested button. `.hoverable` alone
80
+ does not set the pointer cursor: that travels with being a click target,
81
+ i.e. with `.pressable` / `.interactive`. */
82
+ &.interactive,
83
+ &.hoverable {
84
+ @apply box-hover;
85
+ }
86
+
87
+ &.interactive,
88
+ &.pressable {
89
+ @apply box-active cursor-pointer;
90
+ }
91
+ }
92
+
93
+ /* A panel is the flush, unelevated sibling: sidebars, toolbars, and wells that
94
+ sit flat against the layout rather than floating above it. Soft and edgeless
95
+ is what makes it read as a well without drawing a line to say so. */
96
+ @utility panel {
97
+ @apply surface p-(--surface-padding,1rem);
98
+
99
+ --_d-fill: 12%;
100
+ --_d-fg-on-fill: 0%;
101
+ --_d-border: 0%;
102
+ --_d-elevation: 0;
103
+
104
+ &.p-xs,
105
+ &.dense {
106
+ --surface-padding: 0.25rem;
107
+ }
108
+
109
+ &.p-sm,
110
+ &.compact {
111
+ --surface-padding: 0.5rem;
112
+ }
113
+
114
+ &.p-lg,
115
+ &.spacious {
116
+ --surface-padding: 1.5rem;
117
+ }
118
+
119
+ &.flush {
120
+ --surface-padding: 0;
121
+ }
122
+ }
@@ -0,0 +1,132 @@
1
+ @reference "tailwindcss";
2
+ @import "../theme.css";
3
+ @reference "../box.css";
4
+ @reference "./surface.css";
5
+
6
+ /* The host is a positioning wrapper and nothing else. Its bubble is a
7
+ pseudo-element placed outside the host's own box, so painting the host would
8
+ draw a box around the trigger rather than around the message -- which is
9
+ exactly what a blanket surface membership did to this component once already.
10
+
11
+ The bubble is the component. It is a surface, so an aesthetic that blurs
12
+ surfaces blurs it too, and it rests on a ground of its own, edged, and flat.
13
+ Depth is opt-in through `.raised` and `.floating` like every other component;
14
+ its boundary against arbitrary content is the floored edge below, not a
15
+ shadow. See model.md#elevation. */
16
+ @utility tooltip {
17
+ --tooltip-gap: 0.5rem;
18
+ --tooltip-top: calc(var(--tooltip-gap) * -1);
19
+ --tooltip-right: auto;
20
+ --tooltip-bottom: auto;
21
+ --tooltip-left: 50%;
22
+ --tooltip-origin: bottom;
23
+ --tooltip-transform: translate(-50%, -100%);
24
+
25
+ @apply relative;
26
+
27
+ /* The trigger badge, for a page with no element to hang the message on. Quiet
28
+ -- ghost, edged, on `--intent-subtle`, a key cap's resting shape -- because
29
+ it marks that there is a message, it is not one. The line earns its keep:
30
+ `--intent-subtle` is a near-page tint that reads the same across families,
31
+ so a plate alone left three of eight intents indistinguishable;
32
+ `--intent-border` is the slot that differs. Painting the host is right only
33
+ here -- everywhere else the bubble is outside the host's box, so painting it
34
+ boxes the trigger. `.tooltip-icon` is the opt-in that says the host *is* the
35
+ badge. */
36
+ &.tooltip-icon {
37
+ @apply box inline-flex size-5 cursor-default items-center justify-center text-sm font-bold;
38
+
39
+ --_d-fill: 0%;
40
+ --_d-fg-on-fill: 0%;
41
+ --_d-border: 100%;
42
+ --_d-elevation: 0;
43
+ --_d-ground: var(--intent-subtle);
44
+
45
+ border-radius: var(--ui-radius, calc(infinity * 1px));
46
+ /* The one thing `box` hands it that it must refuse. An aesthetic's `clip-path`
47
+ clips a host's pseudo-elements along with the host, and this host's
48
+ pseudo-element is the bubble, positioned outside its box -- so a stepped
49
+ badge under `.pixel` would clip the message away entirely. The bubble
50
+ carries the stepped shape itself; the badge stays round. */
51
+ clip-path: none;
52
+ }
53
+
54
+ &[data-tooltip-position="bottom"] {
55
+ --tooltip-top: auto;
56
+ --tooltip-bottom: calc(var(--tooltip-gap) * -1);
57
+ --tooltip-origin: top;
58
+ --tooltip-transform: translate(-50%, 100%);
59
+ }
60
+
61
+ &[data-tooltip-position="left"] {
62
+ --tooltip-top: 50%;
63
+ --tooltip-left: calc(var(--tooltip-gap) * -1);
64
+ --tooltip-origin: right;
65
+ --tooltip-transform: translate(-100%, -50%);
66
+ }
67
+
68
+ &[data-tooltip-position="right"] {
69
+ --tooltip-top: 50%;
70
+ --tooltip-left: auto;
71
+ --tooltip-right: calc(var(--tooltip-gap) * -1);
72
+ --tooltip-origin: left;
73
+ --tooltip-transform: translate(100%, -50%);
74
+ }
75
+
76
+ &::after {
77
+ @apply surface pointer-events-none absolute z-(--z-popover) w-max max-w-72 px-2.5 py-2 text-sm font-normal whitespace-normal opacity-0 transition-[opacity,transform] duration-(--motion-duration-normal) ease-(--motion-ease);
78
+
79
+ /* Filled, over a ground of its own. An intent floods the fill
80
+ (`.tooltip.primary` is the primary colour); with no intent `intent.css`
81
+ resolves the bubble onto `--color-tooltip` and lifts the neutral cap, a
82
+ plate stated per theme because the derived version was a mid-grey slab in
83
+ light and lifted in dark. The ground stays the surface tone, not the
84
+ plate -- pointed at both, the fill axis went inert and `axes.spec.ts`
85
+ caught it. An opaque ground is also what keeps a cascaded `.ghost` from
86
+ leaving the bubble boundaryless, retiring the old pinned-fill bound.
87
+ `--ui-surface-ground` is read first so a glass tooltip stays glass. */
88
+ --_d-fill: 100%;
89
+ --_d-fg-on-fill: 100%;
90
+ --_d-border: 100%;
91
+ /* Flat, and stated rather than left to `box`'s fallback of 1: the bubble is
92
+ a surface, so an aesthetic's part-based depth would otherwise reach it
93
+ unasked -- `.neobrutalism`'s slab in particular. `.raised`/`.floating`
94
+ opt back in. */
95
+ --_d-elevation: 0;
96
+ --_d-ground: var(--ui-surface-ground, var(--intent-subtle));
97
+
98
+ /* The bubble's line is a boundary against content nobody chose, so `box`'s
99
+ edge blend is wrong here: at a full fill the line *is* the plate. Restate
100
+ the colour only; how much line comes from `--_edge-amount`, floored below. */
101
+ --_line: var(--intent-border);
102
+
103
+ /* Floored, for the same reason the ground is opaque: a cascade must not take
104
+ the boundary from a bubble nobody classed (worst in light, where the plate
105
+ is near-white). The element's own `.edgeless` lowers it -- same
106
+ element-vs-cascade idiom `text-control` uses -- and `axes.spec.ts` still
107
+ sees the edge move because it probes with the class on the element. */
108
+ --_edge-floor: 100%;
109
+
110
+ /* A bubble is a small transient popover, not a modal, so it takes the
111
+ control radius rather than the surface one `surface` hands it. */
112
+ border-radius: var(--ui-radius, var(--radius-control));
113
+ content: attr(data-tooltip);
114
+ left: var(--tooltip-left);
115
+ right: var(--tooltip-right);
116
+ top: var(--tooltip-top);
117
+ bottom: var(--tooltip-bottom);
118
+ transform: var(--tooltip-transform) scale(0);
119
+ transform-origin: var(--tooltip-origin);
120
+ }
121
+
122
+ &.edgeless::after {
123
+ --_edge-floor: 0%;
124
+ }
125
+
126
+ &:hover::after,
127
+ &:focus-visible::after,
128
+ &[data-state="open"]::after {
129
+ opacity: 1;
130
+ transform: var(--tooltip-transform) scale(1);
131
+ }
132
+ }
@@ -0,0 +1,46 @@
1
+ /* Elevation is a modifier, not an axis: depth is a per-element decision (raised
2
+ tiles beside a flat panel in one aesthetic), so one unitless number multiplies
3
+ the aesthetic's shadow geometry where the component composes it. Unitless so
4
+ it inherits -- a container flattens a region, an element opts back in -- and
5
+ so `.flat` removes a part-based slab or blur without a conditional (complete
6
+ surface shadows like glass remain). The two classes that ask for depth carry
7
+ the geometry themselves; on a component it would inherit into everything
8
+ nested and a plain page would have a shadow under every card. See
9
+ model.md#elevation.
10
+
11
+ `.flat`/`.raised`/`.floating` and `.elevation-none`/`-sm`/`-md` are
12
+ interchangeable aliases, the second borrowing the raw `--elevation-*` scale's
13
+ naming. Bare `sm`/`md`/`lg` would collide with the size modifier;
14
+ `.elevation-lg` is reserved, not shipped. */
15
+
16
+ /* No part-based depth. Complete surface shadow values, such as glass, are not
17
+ reached by the elevation multiplier. */
18
+ .elevation-none,
19
+ .flat {
20
+ --ui-elevation: 0;
21
+ }
22
+
23
+ /* The plain-step geometry both rungs multiply, declared once so the two cannot
24
+ drift. It is the fallback for a page with no aesthetic in scope; an aesthetic
25
+ that declares its own shadow parts overrides it, and the elevation multiplier
26
+ scales whichever one lands. `.floating` takes its extra depth from the
27
+ multiplier below, not a second copy of these parts. */
28
+ .elevation-sm,
29
+ .raised,
30
+ .elevation-md,
31
+ .floating {
32
+ --elevation-y: 1px;
33
+ --elevation-blur: 3px;
34
+ }
35
+
36
+ /* The aesthetic's depth as authored, said explicitly. */
37
+ .elevation-sm,
38
+ .raised {
39
+ --ui-elevation: 1;
40
+ }
41
+
42
+ /* Twice it, for menus and popovers. */
43
+ .elevation-md,
44
+ .floating {
45
+ --ui-elevation: 2;
46
+ }
@@ -0,0 +1,27 @@
1
+ @import "tailwindcss";
2
+
3
+ /* The build has no markup to scan, so Tailwind's automatic content detection
4
+ sees only this package's own files. The `src/` scan is load-bearing -- it is
5
+ how the composition utilities (`box`, `surface`, `text-control`,
6
+ `input-group`, `loader-mask`) get emitted as standalone rules, since nothing
7
+ `@source inline`s them -- so `source(none)` is not an option. The cost is that
8
+ a handful of Tailwind utilities whose names collide with a CSS property or a
9
+ word in a comment (`.backdrop-filter`, `.filter`, `.transition`, `.container`,
10
+ ...) are emitted unused. They are inert and small; left as-is deliberately.
11
+
12
+ The `@source not` lines below keep everything that is not `src/` out of that
13
+ scan: the sibling app and fixture directories, the `docs/` tree, the build
14
+ scripts, and the root prose and data files (`README.md`, `llms*.txt`,
15
+ `registry*.json`, `package.json`). Without them a class name or `--color-*`
16
+ reference that appears only in a documentation example ships as real CSS --
17
+ the violet ramp in `docs/usage/theming.md`'s custom-intent example was
18
+ compiling into `dist/index.css` and `dist/native.css`, and any utility in a
19
+ docs code fence did the same. `tests/integration/exports.test.ts` guards it.
20
+ `scripts/build-tw.mjs` strips these lines from the `dist/tw/` copy, where they
21
+ resolve to nothing and the `/tw` consumer scans their own tree anyway. */
22
+
23
+ @import "./theme.css";
24
+ @import "./typography.css";
25
+ @import "./utilities/index.css";
26
+ @import "./components/index.css";
27
+ @import "./reset.css";
@@ -0,0 +1,209 @@
1
+ /* An intent maps one colour family onto seven shared slots and nothing else.
2
+ It does not cascade: the reset at the bottom redeclares the neutral defaults
3
+ at every component's own root, so a `.success` container cannot recolour a
4
+ nested destructive button while `.btn.success` still wins. Every intent writes
5
+ every slot, `--intent-fill-max` included -- omitting it inherits the reset's
6
+ neutral cap, and `tests/integration/registry.test.ts` fails the build on one
7
+ that does. Neutral maps `--intent-subtle` to `--color-surface` (the tone
8
+ tooltips, key caps, code and table heads sit on) and `--intent-border` to
9
+ `--ui-ink` (how an aesthetic restates the neutral line without naming one). */
10
+
11
+ /* The same neutral values, at the root. This is the floor: an element that
12
+ carries no intent class and matches none of the resets below -- a component
13
+ `@apply`-ed onto a selector the package does not name, a bare `<code>` with
14
+ no native stylesheet loaded -- still composes against a real colour instead
15
+ of an undefined one that collapses its `color-mix()` to nothing. It is not
16
+ the barrier. `.neutral` and both resets below still write these slots on the
17
+ element itself, which is a matching rule and so beats this inherited value --
18
+ that is what stops a `.success` container's intent reaching a component
19
+ nested under it. An intent class on the element beats both. Values track
20
+ `.neutral` slot for slot; `registry.test.ts` fails the build on drift. */
21
+ :root {
22
+ --intent-color: var(--color-text);
23
+ --intent-contrast: var(--color-text-contrast);
24
+ --intent-hover: var(--color-text-hover);
25
+ --intent-strong: var(--color-text-strong);
26
+ --intent-subtle: var(--color-surface);
27
+ --intent-border: var(--ui-ink, var(--color-border));
28
+ --intent-fill-max: 20%;
29
+ }
30
+
31
+ /* Neutral stops its fill short: its colour is the page's ink, so a full fill is
32
+ a slab and `.neutral.solid` was pixel-for-pixel `.primary.solid`. 20% is
33
+ chosen against `.soft`'s 12% -- a step past the tint, not a repeat.
34
+ `--intent-contrast` stays the page colour; a `.neutral.solid` badge's ink is
35
+ `box`'s question, answered by the same `--ui-fg-on-fill` cap. Pointing this
36
+ slot at `--color-text` instead rendered a checked neutral checkbox
37
+ black-on-black. */
38
+ .neutral {
39
+ --intent-color: var(--color-text);
40
+ --intent-contrast: var(--color-text-contrast);
41
+ --intent-hover: var(--color-text-hover);
42
+ --intent-strong: var(--color-text-strong);
43
+ --intent-subtle: var(--color-surface);
44
+ --intent-border: var(--ui-ink, var(--color-border));
45
+ --intent-fill-max: 20%;
46
+ }
47
+
48
+ .primary {
49
+ --intent-color: var(--color-primary);
50
+ --intent-contrast: var(--color-primary-contrast);
51
+ --intent-hover: var(--color-primary-hover);
52
+ --intent-strong: var(--color-primary-strong);
53
+ --intent-subtle: var(--color-primary-subtle);
54
+ --intent-border: var(--color-primary);
55
+ --intent-fill-max: 100%;
56
+ }
57
+
58
+ .secondary {
59
+ --intent-color: var(--color-accent);
60
+ --intent-contrast: var(--color-accent-contrast);
61
+ --intent-hover: var(--color-accent-hover);
62
+ --intent-strong: var(--color-accent-strong);
63
+ --intent-subtle: var(--color-accent-subtle);
64
+ --intent-border: var(--color-accent);
65
+ --intent-fill-max: 100%;
66
+ }
67
+
68
+ .success {
69
+ --intent-color: var(--color-success);
70
+ --intent-contrast: var(--color-success-contrast);
71
+ --intent-hover: var(--color-success-hover);
72
+ --intent-strong: var(--color-success-strong);
73
+ --intent-subtle: var(--color-success-subtle);
74
+ --intent-border: var(--color-success);
75
+ --intent-fill-max: 100%;
76
+ }
77
+
78
+ .warning {
79
+ --intent-color: var(--color-warning);
80
+ --intent-contrast: var(--color-warning-contrast);
81
+ --intent-hover: var(--color-warning-hover);
82
+ --intent-strong: var(--color-warning-strong);
83
+ --intent-subtle: var(--color-warning-subtle);
84
+ --intent-border: var(--color-warning);
85
+ --intent-fill-max: 100%;
86
+ }
87
+
88
+ .destructive,
89
+ .danger,
90
+ .error {
91
+ --intent-color: var(--color-destructive);
92
+ --intent-contrast: var(--color-destructive-contrast);
93
+ --intent-hover: var(--color-destructive-hover);
94
+ --intent-strong: var(--color-destructive-strong);
95
+ --intent-subtle: var(--color-destructive-subtle);
96
+ --intent-border: var(--color-destructive);
97
+ --intent-fill-max: 100%;
98
+ }
99
+
100
+ .info {
101
+ --intent-color: var(--color-info);
102
+ --intent-contrast: var(--color-info-contrast);
103
+ --intent-hover: var(--color-info-hover);
104
+ --intent-strong: var(--color-info-strong);
105
+ --intent-subtle: var(--color-info-subtle);
106
+ --intent-border: var(--color-info);
107
+ --intent-fill-max: 100%;
108
+ }
109
+
110
+ /* Every component resolves the neutral defaults at its own root, at zero
111
+ specificity, so an intent class on the element wins while an inherited value
112
+ loses -- the one rule that lets every component drop its per-intent branches.
113
+ This list and the one in `native.css` are the package's only hand-maintained
114
+ selector lists; `tests/integration/registry.test.ts` fails the build on a
115
+ registry component missing from either. `.text-control` and `.surface` are on
116
+ it though they are not components: both are documented as typeable on their
117
+ own, and a bare `<div class="surface">` with no intent resolved computes a
118
+ transparent background and a zero-width border -- `box` collapses. */
119
+ :where(
120
+ .btn,
121
+ .ipt,
122
+ .text-control,
123
+ .surface,
124
+ .textarea,
125
+ .select,
126
+ .input-group,
127
+ .checkbox,
128
+ .radio,
129
+ .switch,
130
+ .card,
131
+ .panel,
132
+ .alert,
133
+ .data-table,
134
+ .pre,
135
+ .quote,
136
+ .tooltip,
137
+ .badge,
138
+ .kbd,
139
+ .code,
140
+ .loader,
141
+ .skeleton,
142
+ .progress,
143
+ .divider
144
+ ) {
145
+ --intent-color: var(--color-text);
146
+ --intent-contrast: var(--color-text-contrast);
147
+ --intent-hover: var(--color-text-hover);
148
+ --intent-strong: var(--color-text-strong);
149
+ --intent-subtle: var(--color-surface);
150
+ --intent-border: var(--ui-ink, var(--color-border));
151
+ --intent-fill-max: 20%;
152
+ }
153
+
154
+ /* The six text controls take a heavier line than everything else that rests on
155
+ the shared neutral. A card, a table or a divider sits on a filled surface or
156
+ separates content that identifies itself; a text control at rest is a
157
+ transparent box whose only mark is its border, and WCAG 1.4.11 asks that
158
+ border for 3:1. `--color-border` gives it 1.42:1.
159
+
160
+ Declared here at zero specificity for the same reason the indicators below
161
+ are: written inside `@utility text-control` it would land in the utilities
162
+ layer, the reset above lands in no layer at all, and unlayered beats layered
163
+ at any specificity. Written as a plain class list it would carry 0-1-0 and,
164
+ being later in the stylesheet, beat `.primary` -- which would make every
165
+ colored control draw a grey line. `:where()` keeps it under both. */
166
+ :where(.text-control, .ipt, .textarea, .select, .input-group, .checkbox, .radio, .switch) {
167
+ --intent-border: var(--ui-ink, var(--color-control-border));
168
+ }
169
+
170
+ /* The bubble a tooltip draws is the one plate in the package that is chosen
171
+ rather than composed, so the tooltip resolves the no-intent case onto its own
172
+ pair instead of onto the shared neutral. Composed, it was 20% of the page's
173
+ ink over `--color-surface`, which lifts off a dark page and sinks into a light
174
+ one -- see `--color-tooltip` in `theme.css` for the measurements.
175
+
176
+ The cap goes with it. Neutral stops its fill at 20% because its colour is the
177
+ page's ink and a full fill of that is a slab; `--color-tooltip` is a plate
178
+ rather than an ink, so a full fill of it is the plate and there is nothing to
179
+ stop short of.
180
+
181
+ `--intent-subtle` stays the surface token. It is what the trigger badge rests
182
+ on, and a badge is a quiet marker on the page rather than the message, so it
183
+ keeps the tone every other unclassed component sits on.
184
+
185
+ Two selectors, because `none` and `neutral` have to agree here. `:where()`
186
+ loses to any intent class, so `.tooltip.primary` still floods with primary;
187
+ `.tooltip.neutral` carries 0-2-0 and beats `.neutral`, so writing the intent
188
+ out gives the same bubble as writing nothing. Without the second selector the
189
+ two would differ on a component where they mean the same thing. */
190
+ :where(.tooltip),
191
+ .tooltip.neutral {
192
+ --intent-color: var(--color-tooltip);
193
+ --intent-contrast: var(--color-tooltip-contrast);
194
+ --intent-hover: var(--color-tooltip);
195
+ --intent-strong: var(--color-tooltip-contrast);
196
+ --intent-subtle: var(--color-surface);
197
+ --intent-border: var(--ui-ink, var(--color-border));
198
+ --intent-fill-max: 100%;
199
+ }
200
+
201
+ /* Indicators take the color they sit in rather than the shared neutral, so a
202
+ loader inside filled or colored content still matches it. It has to be
203
+ declared here at zero specificity: written in the component's own block it
204
+ would carry 0-1-0 and, being later in the stylesheet, beat `.success` --
205
+ which silently makes every indicator ignore every intent class. */
206
+ :where(.loader, .skeleton, .progress, .divider) {
207
+ --intent-color: currentColor;
208
+ --intent-border: currentColor;
209
+ }