@gemboss/website-ui 0.5.0 → 0.7.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.
package/css/forms.css ADDED
@@ -0,0 +1,154 @@
1
+ /* forms (0.6.0, gap #28): a field (label, control, error, hint), the controls, a group of choices, the inline row and
2
+ the form stack. A public site asks as little as it can: one field beats a form, and a form keeps two or three fields
3
+ (AGENTS.md section 5). The look is the dashboard's field family (@gemboss/ui gemboss-field, ui-select, ui-textarea,
4
+ ui-check, ui-help, ui-error) at the website's sizes: as tall as a button (40px, 44px on touch), 16px words (a phone
5
+ does not zoom the page), the action radius of the site's profile, and a 1px border that reaches 3:1
6
+ (--gw-line-control). The page owns validation: it sets aria-invalid="true" and writes the message; the kit draws it. */
7
+
8
+ /* a field: the label above the control, the error and then the hint under it, 8px apart */
9
+ .gw-field { display: grid; gap: 8px; min-width: 0; }
10
+ .gw-field__label { display: block; color: var(--gw-ink); font-size: 14px; font-weight: 600; line-height: 1.5; }
11
+ /* two classes, so a context that styles its paragraphs (the footer's brand block) does not move or recolour them */
12
+ :is(.gw-field, .gw-choices) > :is(.gw-field__hint, .gw-field__error) { margin: 0; font-size: 14px; font-weight: 400; line-height: 1.5; }
13
+ :is(.gw-field, .gw-choices) > .gw-field__hint { color: var(--gw-muted); }
14
+ /* the error: an icon and words, in the danger colour; never the colour alone. Empty, it takes no room, so a page can
15
+ keep it in the markup (named by aria-describedby) and fill it when the visitor sends the form. */
16
+ :is(.gw-field, .gw-choices) > .gw-field__error { display: flex; gap: 6px; color: var(--gw-danger); font-weight: 500; }
17
+ :is(.gw-field, .gw-choices) > .gw-field__error:empty { display: none; }
18
+ .gw-field__error::before {
19
+ content: "";
20
+ flex: none;
21
+ width: 16px;
22
+ height: 16px;
23
+ margin-top: 2px;
24
+ margin-top: calc((1lh - 16px) / 2);
25
+ background: currentColor;
26
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/%3E%3C/svg%3E") center / 16px no-repeat;
27
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/%3E%3C/svg%3E") center / 16px no-repeat;
28
+ }
29
+
30
+ /* the controls: one box for a line of text, a choice from a list and a few lines. Full width of their field. */
31
+ .gw-input,
32
+ .gw-select,
33
+ .gw-textarea {
34
+ display: block;
35
+ width: 100%;
36
+ min-width: 0;
37
+ height: var(--gw-button-height);
38
+ margin: 0;
39
+ padding: 0 12px;
40
+ border: 1px solid var(--gw-line-control);
41
+ border-radius: var(--gw-radius-action);
42
+ background-color: var(--gw-surface);
43
+ color: var(--gw-ink);
44
+ font-size: 16px;
45
+ line-height: 1.5;
46
+ -webkit-appearance: none;
47
+ appearance: none;
48
+ }
49
+ :is(.gw-input, .gw-textarea)::placeholder { color: var(--gw-muted); opacity: 1; }
50
+ /* a list: the caret is drawn with the muted ink (two gradients, so it follows dark mode), with room for it */
51
+ .gw-select {
52
+ padding-right: 40px;
53
+ background-image: linear-gradient(45deg, transparent 50%, var(--gw-muted) 50%), linear-gradient(135deg, var(--gw-muted) 50%, transparent 50%);
54
+ background-position: calc(100% - 20px) 50%, calc(100% - 15px) 50%;
55
+ background-size: 5px 5px;
56
+ background-repeat: no-repeat;
57
+ cursor: pointer;
58
+ }
59
+ .gw-textarea { height: auto; min-height: 96px; padding-block: 8px; resize: vertical; }
60
+ /* the states, in this order: hover, focus, read-only, invalid, disabled. Focus is the kit's accent ring. */
61
+ :is(.gw-input, .gw-select, .gw-textarea):hover { border-color: var(--gw-muted); }
62
+ :is(.gw-input, .gw-select, .gw-textarea):focus-visible { border-color: var(--gw-ink); outline: 2px solid var(--gw-accent); outline-offset: 1px; }
63
+ /* read-only: still focusable and readable, visibly not editable (the dashboard's) */
64
+ :is(.gw-input, .gw-textarea)[readonly] { border-color: var(--gw-line-strong); background-color: var(--gw-bg-soft); cursor: default; }
65
+ :is(.gw-input, .gw-select, .gw-textarea)[aria-invalid="true"] { border-color: var(--gw-danger); background-color: var(--gw-danger-soft); }
66
+ :is(.gw-input, .gw-select, .gw-textarea):disabled { opacity: 0.48; cursor: not-allowed; }
67
+
68
+ /* a group of choices: a <fieldset> whose <legend> is the question, one choice per row */
69
+ .gw-choices { min-width: 0; margin: 0; padding: 0; border: 0; }
70
+ .gw-choices > legend { margin-bottom: 4px; padding: 0; }
71
+ .gw-choices > :is(.gw-check, .gw-radio) { display: flex; }
72
+ .gw-choices > :is(.gw-field__hint, .gw-field__error) { margin-top: 4px; }
73
+
74
+ /* a checkbox or a radio: the <label> is the target (32px, 44px on touch), the box sits on the first line of words */
75
+ .gw-check,
76
+ .gw-radio {
77
+ display: inline-flex;
78
+ align-items: flex-start;
79
+ gap: 8px;
80
+ max-width: 100%;
81
+ padding-block: 4px;
82
+ color: var(--gw-ink);
83
+ font-size: 16px;
84
+ line-height: 1.5;
85
+ overflow-wrap: break-word;
86
+ cursor: pointer;
87
+ }
88
+ :is(.gw-check, .gw-radio) > :not(input) { min-width: 0; }
89
+ :is(.gw-check, .gw-radio) > input {
90
+ display: inline-grid;
91
+ flex: none;
92
+ place-content: center;
93
+ width: 18px;
94
+ height: 18px;
95
+ margin: 3px 0 0;
96
+ border: 1px solid var(--gw-line-control);
97
+ border-radius: var(--gw-radius-4);
98
+ background: var(--gw-surface);
99
+ -webkit-appearance: none;
100
+ appearance: none;
101
+ cursor: inherit;
102
+ }
103
+ .gw-radio > input { border-radius: var(--gw-radius-chip); }
104
+ :is(.gw-check, .gw-radio):hover > input { border-color: var(--gw-muted); }
105
+ /* chosen: the accent, as every check mark of the kit; the mark in the words-on-accent colour */
106
+ .gw-check > input:checked { border-color: var(--gw-accent); background: var(--gw-accent); }
107
+ .gw-check > input:checked::before {
108
+ content: "";
109
+ width: 14px;
110
+ height: 14px;
111
+ background: var(--gw-on-accent);
112
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.4l2.9 2.8L12.5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 14px no-repeat;
113
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.4l2.9 2.8L12.5 5' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 14px no-repeat;
114
+ }
115
+ .gw-radio > input:checked { border: 5px solid var(--gw-accent); }
116
+ :is(.gw-check, .gw-radio) > input[aria-invalid="true"]:not(:checked) { border-color: var(--gw-danger); }
117
+ :is(.gw-check, .gw-radio):has(> input:disabled) { opacity: 0.48; cursor: not-allowed; }
118
+ /* a filter's count: a small pill at the end of the row, the neutral badge's colours */
119
+ .gw-check__count {
120
+ flex: none;
121
+ min-width: 24px;
122
+ margin: 1px 0 0 auto;
123
+ padding: 2px 8px;
124
+ border-radius: var(--gw-radius-chip);
125
+ background: color-mix(in srgb, var(--gw-ink) 8%, transparent);
126
+ color: var(--gw-ink-2);
127
+ font-size: 12px;
128
+ font-weight: 500;
129
+ text-align: center;
130
+ font-variant-numeric: tabular-nums;
131
+ }
132
+
133
+ /* the inline row: one field and its button (a newsletter, "get the guide"). One line from 480px, the field taking the
134
+ room; below 480px, and in a box too narrow for both, the field and the button each take a full line. */
135
+ .gw-form-inline { display: flex; flex-wrap: wrap; gap: 8px; }
136
+ .gw-form-inline > :not(.gw-btn) { flex: 999 1 160px; min-width: 0; }
137
+ .gw-form-inline > .gw-btn { flex: 1 0 auto; }
138
+ @media (max-width: 479px) {
139
+ .gw-form-inline > :not(.gw-btn) { flex-basis: 100%; }
140
+ }
141
+
142
+ /* the form stack: fields 24px apart, the actions 32px below the last one, 560px at most (a field's width hints at its
143
+ answer: no field runs across a wide screen) */
144
+ .gw-form { display: grid; gap: 24px; max-width: 560px; }
145
+ /* the gap is the rhythm, also when the form is the card itself (a card spaces its own children otherwise) */
146
+ .gw-form > * { margin: 0; }
147
+ .gw-form > :is(.gw-actions, .gw-btn) { margin-top: 8px; }
148
+ .gw-form > .gw-btn { justify-self: start; }
149
+
150
+ /* touch: every control is at least 44px tall, a choice's row too */
151
+ @media (max-width: 768px), (pointer: coarse) {
152
+ .gw-input, .gw-select { height: var(--gw-tap-min); }
153
+ .gw-check, .gw-radio { padding-block: 10px; }
154
+ }
@@ -0,0 +1,78 @@
1
+ /* ratings (0.6.0, gap #56): stars, the testimonial wall and the rating badge. Every rating and every testimonial
2
+ links to the page it comes from, and says only what that page says (AGENTS.md section 6); `check` warns about one
3
+ without its link (rating-source). */
4
+
5
+ /* stars: a value from 0 to 5 (style="--gw-stars: 4.9") drawn as five 16px stars, the last one cut where the value
6
+ ends. A screen reader hears the value in words: role="img" and aria-label="4.9 out of 5" (the drawing itself is a
7
+ background, nothing to read). Ink on the strong hairline: a rating is proof, not a state or an action. */
8
+ .gw-stars {
9
+ display: inline-block;
10
+ flex: none;
11
+ width: 80px;
12
+ height: 16px;
13
+ vertical-align: middle;
14
+ background: linear-gradient(90deg, var(--gw-ink) calc(clamp(0, var(--gw-stars, 0), 5) * 20%), var(--gw-line-strong) 0);
15
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6L9.8 6.4L14.8 6.6L10.9 9.7L12.2 14.6L8 11.8L3.8 14.6L5.1 9.7L1.2 6.6L6.2 6.4Z' stroke='%23000' stroke-width='1' stroke-linejoin='round'/%3E%3C/svg%3E") 0 0 / 16px 16px repeat-x;
16
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6L9.8 6.4L14.8 6.6L10.9 9.7L12.2 14.6L8 11.8L3.8 14.6L5.1 9.7L1.2 6.6L6.2 6.4Z' stroke='%23000' stroke-width='1' stroke-linejoin='round'/%3E%3C/svg%3E") 0 0 / 16px 16px repeat-x;
17
+ }
18
+ /* a forced-colours screen drops backgrounds: draw the stars in its own text colours */
19
+ @media (forced-colors: active) {
20
+ .gw-stars { forced-color-adjust: none; background: linear-gradient(90deg, CanvasText calc(clamp(0, var(--gw-stars, 0), 5) * 20%), GrayText 0); }
21
+ }
22
+
23
+ /* the wall: short quotes in cards, one column on phones, two from 768px, three from 1080px. No masonry: the cards of
24
+ a row share its height, and each card's byline sits at its foot. */
25
+ .gw-testimonials {
26
+ display: grid;
27
+ grid-template-columns: minmax(0, 1fr);
28
+ gap: 16px;
29
+ margin: 0;
30
+ padding: 0;
31
+ list-style: none;
32
+ }
33
+ .gw-testimonials > li { display: flex; min-width: 0; }
34
+ @media (min-width: 768px) { .gw-testimonials { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; } }
35
+ @media (min-width: 1080px) { .gw-testimonials { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
36
+ /* a quote card (with .gw-card): stars, the reviewer's own words, who wrote them and where */
37
+ .gw-testimonial { display: flex; flex-direction: column; gap: 16px; width: 100%; margin: 0; }
38
+ .gw-testimonial > * { margin: 0; }
39
+ .gw-testimonial__quote,
40
+ .gw-testimonial__quote p { color: var(--gw-ink); font-size: 16px; line-height: 1.6; }
41
+ .gw-testimonial__quote > * + * { margin-top: 8px; }
42
+ .gw-testimonial > .gw-testimonial__by { display: flex; align-items: center; gap: 12px; margin-top: auto; color: var(--gw-muted); font-size: 14px; line-height: 1.5; }
43
+ .gw-testimonial__by b { display: block; color: var(--gw-ink); font-weight: 600; }
44
+ .gw-testimonial__by > :not(img) { min-width: 0; }
45
+ .gw-testimonial__avatar { flex: none; width: 40px; height: 40px; border-radius: var(--gw-radius-chip); background: var(--gw-bg-soft); object-fit: cover; }
46
+
47
+ /* the rating badge: one link to the store's review page, with the store's mark, the score, the stars and the count.
48
+ A small card that sits; the border darkens on hover, as a linked card's. */
49
+ .gw-rating-badge {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ gap: 12px;
53
+ max-width: 100%;
54
+ padding: 8px 16px 8px 8px;
55
+ border: 1px solid var(--gw-line);
56
+ border-radius: var(--gw-radius-card);
57
+ background: var(--gw-surface);
58
+ color: var(--gw-ink);
59
+ font-size: 13px;
60
+ line-height: 1.4;
61
+ vertical-align: middle;
62
+ }
63
+ a.gw-rating-badge:hover { border-color: var(--gw-line-strong); }
64
+ .gw-rating-badge__icon {
65
+ display: inline-flex;
66
+ flex: none;
67
+ align-items: center;
68
+ justify-content: center;
69
+ width: 40px;
70
+ height: 40px;
71
+ border-radius: var(--gw-radius-action);
72
+ background: var(--gw-bg-soft);
73
+ color: var(--gw-ink);
74
+ }
75
+ .gw-rating-badge__icon :is(svg, img) { width: 24px; height: 24px; }
76
+ .gw-rating-badge__body { display: grid; gap: 2px; min-width: 0; }
77
+ .gw-rating-badge__score { display: flex; align-items: center; gap: 8px; color: var(--gw-ink); font-size: 16px; font-weight: 600; font-variant-numeric: tabular-nums; }
78
+ .gw-rating-badge__count { color: var(--gw-muted); }
package/css/tokens.css CHANGED
@@ -15,6 +15,7 @@
15
15
  --gw-text-30: 30px;
16
16
  --gw-display-h1: clamp(40px, 4.6vw, 68px);
17
17
  --gw-display-h2: clamp(32px, 3.4vw, 52px);
18
+ --gw-display-wordmark: clamp(40px, 9vw, 128px);
18
19
  --gw-display-weight: 600;
19
20
  --gw-display-tracking: -0.03em;
20
21
  --gw-heading-weight: 650;
@@ -89,6 +90,7 @@
89
90
  --gw-muted: #686b71;
90
91
  --gw-line: #e5e7ea;
91
92
  --gw-line-strong: #d5d8dd;
93
+ --gw-line-control: #85888e;
92
94
  --gw-action: #1b1c1e;
93
95
  --gw-action-hover: #000000;
94
96
  --gw-on-action: #ffffff;
@@ -100,6 +102,9 @@
100
102
  --gw-success-soft: #e3f1df;
101
103
  --gw-warning: #9a6700;
102
104
  --gw-warning-soft: #fbf1dc;
105
+ --gw-danger: #b42318;
106
+ --gw-danger-soft: #fff0e9;
107
+ --gw-on-danger: #ffffff;
103
108
  --gw-night: #0c0e14;
104
109
  --gw-night-2: #141722;
105
110
  --gw-on-night: #e3e4e8;
@@ -116,6 +121,7 @@
116
121
  --gw-muted: #9a9ca6;
117
122
  --gw-line: #2a2b33;
118
123
  --gw-line-strong: #383944;
124
+ --gw-line-control: #6a6d78;
119
125
  --gw-action: #f2f3f5;
120
126
  --gw-action-hover: #ffffff;
121
127
  --gw-on-action: #0d0d12;
@@ -127,6 +133,9 @@
127
133
  --gw-success-soft: #132a22;
128
134
  --gw-warning: #e0b341;
129
135
  --gw-warning-soft: #2b2410;
136
+ --gw-danger: #ff6b5a;
137
+ --gw-danger-soft: #2e1714;
138
+ --gw-on-danger: #0d0d12;
130
139
  --gw-night: #0a0b10;
131
140
  --gw-night-2: #12141c;
132
141
  --gw-on-night: #e3e4e8;
@@ -143,6 +152,7 @@
143
152
  --gw-muted: #9a9ca6;
144
153
  --gw-line: #2a2b33;
145
154
  --gw-line-strong: #383944;
155
+ --gw-line-control: #6a6d78;
146
156
  --gw-action: #f2f3f5;
147
157
  --gw-action-hover: #ffffff;
148
158
  --gw-on-action: #0d0d12;
@@ -154,6 +164,9 @@
154
164
  --gw-success-soft: #132a22;
155
165
  --gw-warning: #e0b341;
156
166
  --gw-warning-soft: #2b2410;
167
+ --gw-danger: #ff6b5a;
168
+ --gw-danger-soft: #2e1714;
169
+ --gw-on-danger: #0d0d12;
157
170
  --gw-night: #0a0b10;
158
171
  --gw-night-2: #12141c;
159
172
  --gw-on-night: #e3e4e8;
@@ -169,6 +182,7 @@
169
182
  --gw-muted: #686b71;
170
183
  --gw-line: #e5e7ea;
171
184
  --gw-line-strong: #d5d8dd;
185
+ --gw-line-control: #85888e;
172
186
  --gw-action: #1b1c1e;
173
187
  --gw-action-hover: #000000;
174
188
  --gw-on-action: #ffffff;
@@ -180,6 +194,9 @@
180
194
  --gw-success-soft: #e3f1df;
181
195
  --gw-warning: #9a6700;
182
196
  --gw-warning-soft: #fbf1dc;
197
+ --gw-danger: #b42318;
198
+ --gw-danger-soft: #fff0e9;
199
+ --gw-on-danger: #ffffff;
183
200
  --gw-night: #0c0e14;
184
201
  --gw-night-2: #141722;
185
202
  --gw-on-night: #e3e4e8;
@@ -196,6 +213,7 @@
196
213
  --gw-muted: #9a9ca6;
197
214
  --gw-line: #2a2b33;
198
215
  --gw-line-strong: #383944;
216
+ --gw-line-control: #6a6d78;
199
217
  --gw-action: #f2f3f5;
200
218
  --gw-action-hover: #ffffff;
201
219
  --gw-on-action: #0d0d12;
@@ -207,6 +225,9 @@
207
225
  --gw-success-soft: #132a22;
208
226
  --gw-warning: #e0b341;
209
227
  --gw-warning-soft: #2b2410;
228
+ --gw-danger: #ff6b5a;
229
+ --gw-danger-soft: #2e1714;
230
+ --gw-on-danger: #0d0d12;
210
231
  }
211
232
  /* action profile "accent": data-gw-action="accent" on <html>. In night bands too, where the dark palette applies. */
212
233
  :root[data-gw-action="accent"],
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48"><rect width="48" height="48" fill="#e5e7ea"/><circle cx="24" cy="19" r="8" fill="#9a9ca6"/><path d="M9 44c1.6-8.4 7.8-13 15-13s13.4 4.6 15 13z" fill="#9a9ca6"/></svg>