@gemboss/website-ui 0.5.0 → 0.6.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/AGENTS.md +30 -3
- package/CHANGELOG.md +55 -0
- package/README.md +7 -7
- package/bin/lib/check.mjs +30 -0
- package/css/components.css +8 -0
- package/css/footer.css +3 -0
- package/css/forms.css +154 -0
- package/css/ratings.css +78 -0
- package/css/tokens.css +20 -0
- package/demo/assets/avatar-sample.svg +1 -0
- package/demo/forms.html +505 -0
- package/demo/index.html +1 -0
- package/docs/components.md +143 -5
- package/docs/markup-contract.md +66 -4
- package/docs/theming.md +4 -3
- package/js/website.js +2 -2
- package/js/website.mjs +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +5 -0
- package/system.json +10 -1
- package/templates/footer.html +15 -0
- package/templates/sections/form.html +42 -0
- package/templates/sections/testimonials.html +58 -0
- package/website.css +266 -1
- package/website.min.css +1 -1
package/docs/components.md
CHANGED
|
@@ -116,8 +116,9 @@ Don't: more than three night bands on a page (the closing CTA does not count), o
|
|
|
116
116
|
|
|
117
117
|
40px tall, the action radius (8, 12 in `soft`), 14px/600, sentence case, the action colour as fill: ink, or the
|
|
118
118
|
brand's accent with `--gw-on-accent` words under `data-gw-action="accent"` (one colour on the whole site). Variants: `--secondary` (hairline, transparent), `--ghost`
|
|
119
|
-
(no frame), `--on-night` (light fill for a dark surface that is not a `.gw-band--night`), `--
|
|
120
|
-
|
|
119
|
+
(no frame), `--on-night` (light fill for a dark surface that is not a `.gw-band--night`), `--danger` (0.6.0: the
|
|
120
|
+
danger fill with `--gw-on-danger` words, for the action of a page that destroys something: delete a workspace, erase
|
|
121
|
+
my data), `--sm` (36px, with a 44px hit area), `--lg` (48px, 16px type), `--block` (full width). On touch screens every button is at least 44px tall and
|
|
121
122
|
its label may wrap. Inside a night band the default button turns light by itself.
|
|
122
123
|
|
|
123
124
|
```html
|
|
@@ -146,6 +147,9 @@ button.setAttribute('aria-busy', 'true'); // the request leaves
|
|
|
146
147
|
button.removeAttribute('aria-busy'); // the answer came
|
|
147
148
|
```
|
|
148
149
|
|
|
150
|
+
`--danger` is never a second primary beside `.gw-btn` and never a way to make an action louder: it marks what cannot
|
|
151
|
+
be undone, and the words around it say what will be lost.
|
|
152
|
+
|
|
149
153
|
Don't: a fill of your own (the accent comes only from the action profile, set on `<html>`), a gradient or a wash; a
|
|
150
154
|
raised or inset shadow; pill-shaped buttons; uppercase; two filled buttons side by side (one
|
|
151
155
|
`.gw-btn` per view, the second is `--secondary` or a link); a `<div>` with a click handler instead of `<a>`/`<button>`;
|
|
@@ -196,8 +200,9 @@ groups with the same name on one page; the old `role="group"` with `aria-pressed
|
|
|
196
200
|
|
|
197
201
|
### Badges `.gw-badge`
|
|
198
202
|
|
|
199
|
-
Small status, a pill, 12px. Variants: `--accent` (a test, the recommended plan), `--success
|
|
200
|
-
a coloured dot, ink words, because the state hues fail 4.5:1 as
|
|
203
|
+
Small status, a pill, 12px. Variants: `--accent` (a test, the recommended plan), `--success`, `--warning` and
|
|
204
|
+
`--danger` (0.6.0, an error: "Payment failed") (state: a coloured dot, ink words, because the state hues fail 4.5:1 as
|
|
205
|
+
small text on a tint).
|
|
201
206
|
|
|
202
207
|
```html
|
|
203
208
|
<span class="gw-badge gw-badge--success">Live</span>
|
|
@@ -356,8 +361,11 @@ hero pulled up under the header.
|
|
|
356
361
|
| `.gw-footer__brand` | the brand and one sentence, full width on phones |
|
|
357
362
|
| `.gw-footer__col`, `__heading` | a `<nav>` column: a sentence-case heading and a `<ul>` of links |
|
|
358
363
|
| `.gw-footer__bottom` | copyright, legal links, social links; alone (no grid above), it drops its top rule |
|
|
364
|
+
| `.gw-footer__newsletter` | 0.6.0, optional: a `<form>` in the brand block, 24px under its sentence: a `.gw-field` with its label, a `.gw-form-inline` row (the email field and a `--secondary` button) and the note |
|
|
359
365
|
|
|
360
|
-
Full markup: `templates/footer.html`.
|
|
366
|
+
Full markup: `templates/footer.html`. The newsletter note says how often you write and how to stop, and links to the
|
|
367
|
+
privacy policy. Don't: uppercase column headings; a newsletter form that stores nothing; a consent box ticked for the
|
|
368
|
+
reader; a filled primary button in the footer (the closing CTA above it is the page's action).
|
|
361
369
|
|
|
362
370
|
## Sections
|
|
363
371
|
|
|
@@ -594,6 +602,127 @@ visible on the page, with the same words:
|
|
|
594
602
|
</script>
|
|
595
603
|
```
|
|
596
604
|
|
|
605
|
+
## Forms (0.6.0)
|
|
606
|
+
|
|
607
|
+
A public site asks as little as it can. One field beats a form (the home page's "What do you sell?", the audit's store
|
|
608
|
+
URL), and a form keeps two or three fields; a question with a good guess is a choice the reader confirms, with the
|
|
609
|
+
guess chosen. The look is the dashboard's field family (`@gemboss/ui` `gemboss-field`, `ui-select`, `ui-textarea`,
|
|
610
|
+
`ui-check`, `ui-help`, `ui-error`) at the website's sizes. See every state in `demo/forms.html`; a short form is
|
|
611
|
+
`templates/sections/form.html`, the footer newsletter `templates/footer.html`.
|
|
612
|
+
|
|
613
|
+
| class | for |
|
|
614
|
+
|---|---|
|
|
615
|
+
| `.gw-field` | one field: the label, the control, the error, the hint, 8px apart |
|
|
616
|
+
| `.gw-field__label` | the `<label for>` above the control (14px/600, ink); also the `<legend>` of a group |
|
|
617
|
+
| `.gw-field__error` | under the control: what to do, in words, after a 16px icon, in `--gw-danger` (14px/500); empty, it takes no room |
|
|
618
|
+
| `.gw-field__hint` | under the error: a short help line (14px, muted) |
|
|
619
|
+
| `.gw-input` | a line of text (`type` email, url, tel, text, search, password…): 40px tall, 44px on touch, 16px words, the action radius (8, 12 in `soft`), a 1px `--gw-line-control` border (3:1) on the surface |
|
|
620
|
+
| `.gw-select` | a choice from a list (`<select>`), the same box with a drawn caret |
|
|
621
|
+
| `.gw-textarea` | a few lines, the same box from 96px tall, resizes down the page |
|
|
622
|
+
| `.gw-choices` | a group of checkboxes or radios: a `<fieldset>`, its question in `<legend class="gw-field__label">`, one choice per row |
|
|
623
|
+
| `.gw-check`, `.gw-radio` | a `<label>` around one `<input type="checkbox">` or `type="radio"` and its words: the row is the target (32px, 44px on touch); chosen, the box takes the accent |
|
|
624
|
+
| `.gw-check__count` | a filter's count at the end of its row, a small neutral pill |
|
|
625
|
+
| `.gw-form-inline` | one field and its button on one line (a newsletter, "email me the guide"); below 480px, or in a narrow box, each takes a full line |
|
|
626
|
+
| `.gw-form` | a short form: fields 24px apart, the actions 32px below, 560px at most; on a `.gw-card` or inside one |
|
|
627
|
+
|
|
628
|
+
States: hover darkens the border; focus is the kit's accent ring (2px) with an ink border; read-only
|
|
629
|
+
(`readonly`) sits on the soft ground; disabled (`disabled`) is faded, always with its reason in the hint; invalid
|
|
630
|
+
(`aria-invalid="true"`) takes the danger border and tint, and its `.gw-field__error` says why. No success state: say
|
|
631
|
+
what happened after sending, in a `role="status"` line.
|
|
632
|
+
|
|
633
|
+
```html
|
|
634
|
+
<div class="gw-field">
|
|
635
|
+
<label class="gw-field__label" for="email">Work email</label>
|
|
636
|
+
<input class="gw-input" id="email" name="email" type="email" autocomplete="email" required
|
|
637
|
+
aria-invalid="true" aria-describedby="email-error email-hint">
|
|
638
|
+
<p class="gw-field__error" id="email-error">Enter your work email, like name@example.com.</p>
|
|
639
|
+
<p class="gw-field__hint" id="email-hint">We reply to this address.</p>
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
<fieldset class="gw-choices">
|
|
643
|
+
<legend class="gw-field__label">Industry</legend>
|
|
644
|
+
<label class="gw-check"><input type="checkbox" name="industry" value="beauty" checked><span>Beauty</span><span class="gw-check__count">96</span></label>
|
|
645
|
+
<label class="gw-check"><input type="checkbox" name="industry" value="home"><span>Home</span><span class="gw-check__count">41</span></label>
|
|
646
|
+
</fieldset>
|
|
647
|
+
|
|
648
|
+
<div class="gw-field">
|
|
649
|
+
<label class="gw-field__label" for="news-email">Product news by email</label>
|
|
650
|
+
<div class="gw-form-inline">
|
|
651
|
+
<input class="gw-input" id="news-email" name="email" type="email" autocomplete="email" required aria-describedby="news-email-error news-email-note">
|
|
652
|
+
<button class="gw-btn gw-btn--secondary" type="submit">Subscribe</button>
|
|
653
|
+
</div>
|
|
654
|
+
<p class="gw-field__error" id="news-email-error"></p>
|
|
655
|
+
<p class="gw-field__hint" id="news-email-note">Once a month. Unsubscribe in one click. <a href="/privacy">Privacy policy</a></p>
|
|
656
|
+
</div>
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
**Validation is the page's.** The kit ships no validator and no form script: the page (or its server) decides what is
|
|
660
|
+
wrong and writes the words. On send, set `aria-invalid="true"` on each field in error and its message in the
|
|
661
|
+
`.gw-field__error`, then move the focus to the first one: a screen reader reads its label and its error (through
|
|
662
|
+
`aria-describedby`, the error named first). Keep the empty error element in the markup, so the description is already
|
|
663
|
+
wired. Clear both when the field is right again, on the next send or as the reader fixes it. While the form is sent,
|
|
664
|
+
`aria-busy="true"` on its button; when the answer comes, a `role="status"` line (`.gw-note`) says what happened
|
|
665
|
+
("Sent. We'll reply to name@example.com."). Use `novalidate` on the `<form>` when the page writes its own messages, so
|
|
666
|
+
the browser's bubbles do not speak over them.
|
|
667
|
+
|
|
668
|
+
Words: the label names the answer ("Work email", not "Enter your email"); a hint only when the answer is not obvious;
|
|
669
|
+
an error says what to do, not what went wrong ("Enter the whole address, like name@example.com", not "Invalid
|
|
670
|
+
input"); optional fields say "(optional)" in their label, required ones need no mark.
|
|
671
|
+
|
|
672
|
+
Don't: a placeholder as the only label; a field across a wide screen (keep a form in a `.gw-form` or a narrow
|
|
673
|
+
column); a form split into steps to start something free; a consent box ticked in advance or required to get
|
|
674
|
+
something else; an error told by its colour alone; `disabled` on a field without its reason; a filled primary button
|
|
675
|
+
in a footer form.
|
|
676
|
+
|
|
677
|
+
## Ratings and testimonials (0.6.0)
|
|
678
|
+
|
|
679
|
+
Proof from people who are not the brand: the store's score and its reviews. **Every rating and every testimonial
|
|
680
|
+
links to the page it comes from** (the store's review page, the review itself) and copies only what that page shows:
|
|
681
|
+
the score and the count on the day you copy them, the reviewer's own words (cut a long one with an ellipsis, never
|
|
682
|
+
reword it), their name and store as they wrote them. No made-up review, no score you averaged yourself, no source you
|
|
683
|
+
cannot link. `check` warns about a badge or a card without a link (`rating-source`). See them in `demo/forms.html`;
|
|
684
|
+
the section is `templates/sections/testimonials.html`.
|
|
685
|
+
|
|
686
|
+
| class | for |
|
|
687
|
+
|---|---|
|
|
688
|
+
| `.gw-stars` | a value from 0 to 5 drawn as five 16px stars in ink, the last one cut where the value ends (4.5 shows half a star); `role="img"`, `aria-label="4.9 out of 5"`, `style="--gw-stars: 4.9"` |
|
|
689
|
+
| `.gw-testimonials` | the wall: a `<ul role="list">` of cards, one column on phones, two from 768px, three from 1080px; the cards of a row share its height (no masonry) |
|
|
690
|
+
| `.gw-testimonial` (with `.gw-card`, on a `<figure>`) | one review: stars, `__quote` (a `<blockquote>`), `__by` (a `<figcaption>`: the optional `__avatar`, the name in `<b>`, the store and the link to the review) |
|
|
691
|
+
| `.gw-rating-badge` (on an `<a href>`) | the store's score: `__icon` (the store's mark, or the generic bag), `__body` with `__score` (the number and the stars) and `__count` (the reviews and where) |
|
|
692
|
+
|
|
693
|
+
```html
|
|
694
|
+
<a class="gw-rating-badge" href="https://apps.shopify.com/your-app/reviews">
|
|
695
|
+
<span class="gw-rating-badge__icon" aria-hidden="true"><svg …></svg></span>
|
|
696
|
+
<span class="gw-rating-badge__body">
|
|
697
|
+
<span class="gw-rating-badge__score"><span aria-hidden="true">4.9</span><span class="gw-stars" role="img" aria-label="4.9 out of 5" style="--gw-stars: 4.9"></span></span>
|
|
698
|
+
<span class="gw-rating-badge__count">1,234 reviews on the Shopify App Store</span>
|
|
699
|
+
</span>
|
|
700
|
+
</a>
|
|
701
|
+
|
|
702
|
+
<ul class="gw-testimonials" role="list">
|
|
703
|
+
<li>
|
|
704
|
+
<figure class="gw-card gw-testimonial">
|
|
705
|
+
<span class="gw-stars" role="img" aria-label="5 out of 5" style="--gw-stars: 5"></span>
|
|
706
|
+
<blockquote class="gw-testimonial__quote"><p>The reviewer's exact words.</p></blockquote>
|
|
707
|
+
<figcaption class="gw-testimonial__by">
|
|
708
|
+
<img class="gw-testimonial__avatar" src="/img/reviewer.webp" alt="" width="40" height="40" loading="lazy">
|
|
709
|
+
<span><b>Name</b>Store · <a href="https://apps.shopify.com/your-app/reviews?…">Shopify App Store</a></span>
|
|
710
|
+
</figcaption>
|
|
711
|
+
</figure>
|
|
712
|
+
</li>
|
|
713
|
+
</ul>
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
The stars' value is written twice on purpose, once for the drawing (`--gw-stars`) and once in words (`aria-label`),
|
|
717
|
+
and `check` reports them when they disagree, when the value is not a number from 0 to 5, or when the name is missing
|
|
718
|
+
(`stars-markup`). A number shown beside the stars is `aria-hidden="true"`, so it is not read twice. In another language
|
|
719
|
+
the label is written for its reader ("4,9 trên 5"); the number stays the same. The badge's icon is the generic bag of
|
|
720
|
+
the template, or the store's own mark only where that store's brand rules allow it; the stars stay ink on every brand.
|
|
721
|
+
|
|
722
|
+
Don't: a rating or a quote without its link; a quote longer than three sentences (link to the rest); a star row with
|
|
723
|
+
no value in words; stars in a colour of your own; a "4.9" with no count and no source; a photo of the reviewer they did
|
|
724
|
+
not make public with the review; masonry or a carousel that moves by itself.
|
|
725
|
+
|
|
597
726
|
## Motion
|
|
598
727
|
|
|
599
728
|
| class | for |
|
|
@@ -603,6 +732,15 @@ visible on the page, with the same words:
|
|
|
603
732
|
Put it on a block (a grid, a list, a window), not on each card in a grid. Don't use it on the hero or anything in
|
|
604
733
|
the first screen, and don't animate anything but `transform` and `opacity`.
|
|
605
734
|
|
|
735
|
+
## Additions to the markup contract in 0.6.0
|
|
736
|
+
|
|
737
|
+
Forms: `.gw-field` (`__label`, `__error`, `__hint`), `.gw-input`, `.gw-select`, `.gw-textarea`, `.gw-choices`, `.gw-check`
|
|
738
|
+
and `.gw-radio` (`.gw-check__count`), `.gw-form-inline`, `.gw-form`, the states `readonly`, `disabled` and
|
|
739
|
+
`aria-invalid="true"`, `.gw-footer__newsletter`, `.gw-btn--danger` and `.gw-badge--danger`. Ratings: `.gw-stars` with its
|
|
740
|
+
`--gw-stars` input, `.gw-testimonials`, `.gw-testimonial` (`__quote`, `__by`, `__avatar`), `.gw-rating-badge` (`__icon`,
|
|
741
|
+
`__body`, `__score`, `__count`). Tokens: `--gw-danger`, `--gw-danger-soft`, `--gw-on-danger`, `--gw-line-control`. No
|
|
742
|
+
script, no hook and no event: the page owns validation. No markup or class of 0.5.0 changes.
|
|
743
|
+
|
|
606
744
|
## Additions to the markup contract in 0.5.0
|
|
607
745
|
|
|
608
746
|
The blog parts: `.gw-breadcrumbs`, `.gw-article-meta` (`__avatar`), `.gw-article` (`__head`, `__hero`), `.gw-toc` with
|
package/docs/markup-contract.md
CHANGED
|
@@ -43,7 +43,7 @@ prose tables (0.5.0). It comes in two entries built from one source
|
|
|
43
43
|
to the page (Escape, the outside click, the busy-button guard, the system theme, the scroll) are installed once.
|
|
44
44
|
- **`destroy(root = document)`** takes back what `init` wired under `root`: its listeners and its marks. An open
|
|
45
45
|
mega menu or mobile menu is closed first, so an unmounted menu cannot leave the page locked.
|
|
46
|
-
- **`version`**: `'0.
|
|
46
|
+
- **`version`**: `'0.6.0'`. The classic script exposes the three as `window.gwWebsite`.
|
|
47
47
|
- **`data-gw-ready`**: on each wired element, the parts wired on it (`mega`, `menu`, `theme`, `reveal`, `header`,
|
|
48
48
|
`segmented`, `toc`, `table`). It is a mark for people and tests, not a switch: markup copied from a live page, attribute and all,
|
|
49
49
|
is still wired.
|
|
@@ -204,6 +204,63 @@ Script behaviour:
|
|
|
204
204
|
- A framework that renders the prose's table itself (JSX, not HTML from the CMS) should call `destroy(prose)`
|
|
205
205
|
before it re-renders that part, as for every element the script changed.
|
|
206
206
|
|
|
207
|
+
## Forms (0.6.0)
|
|
208
|
+
|
|
209
|
+
CSS only: the script does not touch a form. The page (or its server) owns validation and the words.
|
|
210
|
+
|
|
211
|
+
```html
|
|
212
|
+
<form class="gw-form" action="…" method="post" novalidate> <!-- or .gw-card.gw-form; novalidate when the page writes its own messages -->
|
|
213
|
+
<div class="gw-field">
|
|
214
|
+
<label class="gw-field__label" for="ID">Label</label>
|
|
215
|
+
<input class="gw-input" id="ID" … aria-describedby="ID-error ID-hint" aria-invalid?="true"> <!-- or select.gw-select, textarea.gw-textarea -->
|
|
216
|
+
<p class="gw-field__error" id="ID-error">What to do</p> <!-- empty until the page fills it; empty = no room -->
|
|
217
|
+
<p class="gw-field__hint" id="ID-hint">Help</p> <!-- optional -->
|
|
218
|
+
</div>
|
|
219
|
+
<fieldset class="gw-choices">
|
|
220
|
+
<legend class="gw-field__label">Question</legend>
|
|
221
|
+
<label class="gw-check"><input type="checkbox" …><span>Words</span><span class="gw-check__count">12</span></label> <!-- or .gw-radio with type="radio"; __count optional -->
|
|
222
|
+
</fieldset>
|
|
223
|
+
<div class="gw-field"> <!-- one field and its button -->
|
|
224
|
+
<label class="gw-field__label" for="ID2">Label</label>
|
|
225
|
+
<div class="gw-form-inline"><input class="gw-input" id="ID2" …><button class="gw-btn" type="submit">Action</button></div>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="gw-actions"><button class="gw-btn" type="submit">Send</button><p class="gw-note" role="status"></p></div>
|
|
228
|
+
</form>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- The error and the hint are children of `.gw-field` (or `.gw-choices`); `aria-describedby` names the error first.
|
|
232
|
+
- States are attributes, never classes: `aria-invalid="true"` (danger border and tint; on a checkbox or radio, a
|
|
233
|
+
danger box while unchecked), `readonly`, `disabled`. Hover and focus are the browser's.
|
|
234
|
+
- `.gw-check` / `.gw-radio` wrap exactly one `<input>` and its words, so the whole row is the target.
|
|
235
|
+
- `.gw-footer__newsletter` is a `<form>` in `.gw-footer__brand`, after the brand's sentence.
|
|
236
|
+
|
|
237
|
+
## Ratings (0.6.0)
|
|
238
|
+
|
|
239
|
+
```html
|
|
240
|
+
<span class="gw-stars" role="img" aria-label="N out of 5" style="--gw-stars: N"></span> <!-- N from 0 to 5, the same in both -->
|
|
241
|
+
|
|
242
|
+
<a class="gw-rating-badge" href="SOURCE">
|
|
243
|
+
<span class="gw-rating-badge__icon" aria-hidden="true"><svg|img></span>
|
|
244
|
+
<span class="gw-rating-badge__body">
|
|
245
|
+
<span class="gw-rating-badge__score"><span aria-hidden="true">N</span><span class="gw-stars" …></span></span>
|
|
246
|
+
<span class="gw-rating-badge__count">COUNT reviews on STORE</span>
|
|
247
|
+
</span>
|
|
248
|
+
</a>
|
|
249
|
+
|
|
250
|
+
<ul class="gw-testimonials" role="list">
|
|
251
|
+
<li><figure class="gw-card gw-testimonial">
|
|
252
|
+
<span class="gw-stars" …></span>
|
|
253
|
+
<blockquote class="gw-testimonial__quote"><p>Words</p></blockquote>
|
|
254
|
+
<figcaption class="gw-testimonial__by"><img class="gw-testimonial__avatar" alt="" …>?<span><b>Name</b>Store · <a href="SOURCE">Where</a></span></figcaption>
|
|
255
|
+
</figure></li>
|
|
256
|
+
</ul>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
- `--gw-stars` is the one `--gw-` variable a page writes, on the `.gw-stars` element itself: an input, not a token.
|
|
260
|
+
`check` accepts it there and reports a value outside 0 to 5, a missing `role="img"` or name, and a name that does not
|
|
261
|
+
carry the same number (`stars-markup`), and a `.gw-rating-badge` or `.gw-testimonial` without an `<a href>`
|
|
262
|
+
(`rating-source`, a warning).
|
|
263
|
+
|
|
207
264
|
## Brand profiles (0.3.0)
|
|
208
265
|
|
|
209
266
|
- `data-gw-action` and `data-gw-radius` sit on `<html>` only, with a name from `system.json` `profiles` (`ink` or
|
|
@@ -247,13 +304,17 @@ Script behaviour:
|
|
|
247
304
|
|
|
248
305
|
| class | what |
|
|
249
306
|
|---|---|
|
|
250
|
-
| `.gw-btn` (+ `--secondary`, `--ghost`, `--on-night`, `--sm`, `--lg`, `--block`) | actions; a rectangle with the action radius, the action colour as fill (ink, or the accent under `data-gw-action="accent"`); `:active` pressed, `[aria-busy="true"]` loading (script above) |
|
|
307
|
+
| `.gw-btn` (+ `--secondary`, `--ghost`, `--on-night`, `--danger` (0.6.0), `--sm`, `--lg`, `--block`) | actions; a rectangle with the action radius, the action colour as fill (ink, or the accent under `data-gw-action="accent"`); `:active` pressed, `[aria-busy="true"]` loading (script above) |
|
|
251
308
|
| `.gw-link` (+ `--arrow`) | inline text link in the accent; `--arrow` adds → that moves 2px on hover |
|
|
252
309
|
| `.gw-label` | the label above a heading: 13px muted, 8px accent square before it |
|
|
253
310
|
| `.gw-dim` | the muted first sentence inside a headline |
|
|
254
311
|
| `.gw-chip` (+ `.is-active`) | selectable token, pill |
|
|
255
312
|
| `.gw-segmented` (+ `--block`) | one choice out of two to four (script above) |
|
|
256
|
-
| `.gw-badge` (+ `--accent`, `--success`, `--warning`) | small status, pill |
|
|
313
|
+
| `.gw-badge` (+ `--accent`, `--success`, `--warning`, `--danger` (0.6.0)) | small status, pill |
|
|
314
|
+
| `.gw-field` (`__label`, `__error`, `__hint`), `.gw-input`, `.gw-select`, `.gw-textarea` | a field and its controls; states by attribute (forms above, 0.6.0) |
|
|
315
|
+
| `.gw-choices`, `.gw-check`, `.gw-radio` (`.gw-check__count`) | a group of checkboxes or radios, a row per choice (0.6.0) |
|
|
316
|
+
| `.gw-form-inline`, `.gw-form` | one field and its button on a line; a short form's stack (0.6.0) |
|
|
317
|
+
| `.gw-stars`, `.gw-rating-badge` (`__icon`, `__body`, `__score`, `__count`) | a 0 to 5 value; the store's score, a link to its reviews (ratings above, 0.6.0) |
|
|
257
318
|
| `.gw-card` (+ `--soft`, `--link`) | white box, 1px hairline, the card radius; `--link` makes the whole card an `<a>` |
|
|
258
319
|
| `.gw-card__media` (+ `--wide`, `--square`) | the card's picture, first child, edge to edge on top; 16:9, 1.91:1 or 1:1 (0.5.0) |
|
|
259
320
|
| `.gw-callout` (+ `--note`, `--tip`, `--warning`, `__label`) | a note, a tip or a warning in an article, `role="note"`, its kind in words (0.5.0) |
|
|
@@ -285,6 +346,7 @@ Script behaviour:
|
|
|
285
346
|
| `.gw-showcase` | night band with a large screen and two or three points |
|
|
286
347
|
| `.gw-steps` (`<ol>`) | numbered steps, how it works |
|
|
287
348
|
| `.gw-quote` | one real testimonial with name, role, company |
|
|
349
|
+
| `.gw-testimonials` (`.gw-card.gw-testimonial`: `__quote`, `__by`, `__avatar`) | a wall of short reviews, each linked to its source (0.6.0) |
|
|
288
350
|
| `.gw-pricing` (`.gw-plan`, `--featured`) | plan cards; the featured one has a hairline in the accent, not a fill |
|
|
289
351
|
| `.gw-cta` | closing call to action band |
|
|
290
|
-
| `.gw-footer` (`__grid`, `__col`, `__heading`, `__bottom`) | footer columns + bottom row |
|
|
352
|
+
| `.gw-footer` (`__grid`, `__col`, `__heading`, `__bottom`, `__newsletter`) | footer columns + bottom row; an optional newsletter form in the brand block (0.6.0) |
|
package/docs/theming.md
CHANGED
|
@@ -57,7 +57,7 @@ needs no markup, so a GemBoss page, and every page written for 0.2.0, stays exac
|
|
|
57
57
|
| knob | profiles | what changes |
|
|
58
58
|
|---|---|---|
|
|
59
59
|
| action colour, `data-gw-action` on `<html>` | `ink` (GemBoss, default), `accent` | `accent`: the primary `.gw-btn` is filled with `--gw-accent`, its words are `--gw-on-accent`, hover and pressed are `--gw-accent-strong`. In dark mode and night bands it takes the `-dark` family. Secondary, ghost and on-night buttons do not change. |
|
|
60
|
-
| radius, `data-gw-radius` on `<html>` | `tight` (GemBoss, default): 8 action, 12 card, 16 panel, 20 mega menu; `soft` (GemPages): 12, 20, 24, 24 | every button, control, card, window, table, FAQ, plan, quote and the mega menu. Chips and
|
|
60
|
+
| radius, `data-gw-radius` on `<html>` | `tight` (GemBoss, default): 8 action, 12 card, 16 panel, 20 mega menu; `soft` (GemPages): 12, 20, 24, 24 | every button, control (fields, lists, the rating badge's icon), card, window, table, FAQ, plan, quote and the mega menu. Chips, badges and a filter's count stay pills; a checkbox keeps its 4px. |
|
|
61
61
|
| header, a class on the `<header>` | flat (default), `.gw-header--float` | from 1080px the bar becomes an inset card: the panel radius of the profile (16 tight, 24 soft), the surface at 85% over a 10px blur, one hairline, the raised shadow, 16px from the top. Same bar, same mega menu, same script; below 1080px it is the flat bar, so the phone menu is unchanged. Solid for a visitor who asks for less transparency, and in a browser that cannot blur. |
|
|
62
62
|
|
|
63
63
|
Why the action colour is a mode and not a colour of its own: the button stays one colour on the whole site, and that
|
|
@@ -71,10 +71,11 @@ the panels; in `tight` that is the 16px the header was designed with.
|
|
|
71
71
|
## What you may not change
|
|
72
72
|
|
|
73
73
|
- **Neutrals**: `--gw-bg`, `--gw-bg-soft`, `--gw-surface`, `--gw-ink`, `--gw-ink-2`, `--gw-muted`, `--gw-line`,
|
|
74
|
-
`--gw-line-strong`, and the night colours. No cream or grey page, no brand-coloured ink.
|
|
74
|
+
`--gw-line-strong`, `--gw-line-control` (a form control's border, 3:1), and the night colours. No cream or grey page, no brand-coloured ink.
|
|
75
75
|
- **The action tokens**: `--gw-action`, `--gw-action-hover`, `--gw-on-action`. Choose the `accent` profile instead;
|
|
76
76
|
a colour set here is not checked and does not follow dark mode or night bands.
|
|
77
|
-
- **State**: `--gw-success`, `--gw-warning` and their tints. Green
|
|
77
|
+
- **State**: `--gw-success`, `--gw-warning`, `--gw-danger` and their tints (`--gw-on-danger` too). Green, amber and red
|
|
78
|
+
mean state on every GemCommerce site; the stars of a rating stay ink.
|
|
78
79
|
- **Scales and shapes**: type sizes and weights, spacing, the radius tokens (`--gw-radius-*`: choose a profile), shadows,
|
|
79
80
|
widths, the header height, the font family. No pill buttons, no bigger shadows, no other font.
|
|
80
81
|
- **Kit classes**: never restyle a `.gw-*` class. If the kit cannot do what your brand needs, that is a gap
|
package/js/website.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/*! @gemboss/website-ui 0.
|
|
1
|
+
/*! @gemboss/website-ui 0.6.0 website.js | GENERATED by scripts/build.mjs from js/website.mjs (the source, with its comments): edit that file | spec: docs/markup-contract.md | load with defer */
|
|
2
2
|
(function () {
|
|
3
3
|
'use strict';
|
|
4
4
|
if (window.gwWebsite) return;
|
|
5
5
|
|
|
6
|
-
var version = '0.
|
|
6
|
+
var version = '0.6.0';
|
|
7
7
|
var api = { version: version, init: init, destroy: destroy };
|
|
8
8
|
var doc, root, mine, fine, sys, wide, live = [], megaOpen = null, megaTimer = 0, queued = false;
|
|
9
9
|
var READY = 'data-gw-ready';
|
package/js/website.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// the export line and the // comments, plus auto-init): edit this file, then run the build. Keep comments on lines of
|
|
7
7
|
// their own, so the build can leave them out.
|
|
8
8
|
|
|
9
|
-
var version = '0.
|
|
9
|
+
var version = '0.6.0';
|
|
10
10
|
var api = { version: version, init: init, destroy: destroy };
|
|
11
11
|
// Page state. Set up by the first init(), never at import: a server can import this file.
|
|
12
12
|
var doc, root, mine, fine, sys, wide, live = [], megaOpen = null, megaTimer = 0, queued = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemboss/website-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The GemBoss website kit: tokens, header, footer, sections and the rules (AGENTS.md) that GemCommerce public websites share, plus a CLI that checks a site, reports kit gaps and upgrades.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
package/skill/SKILL.md
CHANGED
|
@@ -20,6 +20,11 @@ the kit and its rules are how the page ends up looking designed. Compose from th
|
|
|
20
20
|
- An article page (a blog post, a guide) is `KIT/templates/sections/article.html`: breadcrumbs, the meta row, a
|
|
21
21
|
contents list from the article's h2s, the prose, callouts, the FAQ last. Never a callout told by its colour
|
|
22
22
|
alone (it keeps its label: Note, Tip, Warning). The blog index is `KIT/templates/sections/post-list.html`.
|
|
23
|
+
- A form asks as little as it can: one field beats a form, a form keeps two or three fields
|
|
24
|
+
(`KIT/templates/sections/form.html`; one field and its button: `.gw-form-inline`). Label above, the error in
|
|
25
|
+
words under it with `aria-invalid="true"`; the page decides when, the kit draws it.
|
|
26
|
+
- Ratings and testimonials (`KIT/templates/sections/testimonials.html`): only real ones, each linked to the page it
|
|
27
|
+
comes from, the reviewer's words exactly; stars carry their value in words (`aria-label="4.9 out of 5"`).
|
|
23
28
|
- Changed page: keep its kit classes; change words, order and choice of sections.
|
|
24
29
|
- Site CSS only for what the kit cannot know (an illustration, a product mock-up), with the kit's tokens and
|
|
25
30
|
scales, classes in the site's own prefix. Never style a `.gw-*` class, never edit `node_modules`.
|
package/system.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"muted": "#686b71",
|
|
12
12
|
"line": "#e5e7ea",
|
|
13
13
|
"line-strong": "#d5d8dd",
|
|
14
|
+
"line-control": "#85888e",
|
|
14
15
|
"action": "#1b1c1e",
|
|
15
16
|
"action-hover": "#000000",
|
|
16
17
|
"on-action": "#ffffff",
|
|
@@ -22,6 +23,9 @@
|
|
|
22
23
|
"success-soft": "#e3f1df",
|
|
23
24
|
"warning": "#9a6700",
|
|
24
25
|
"warning-soft": "#fbf1dc",
|
|
26
|
+
"danger": "#b42318",
|
|
27
|
+
"danger-soft": "#fff0e9",
|
|
28
|
+
"on-danger": "#ffffff",
|
|
25
29
|
"night": "#0c0e14",
|
|
26
30
|
"night-2": "#141722",
|
|
27
31
|
"on-night": "#e3e4e8",
|
|
@@ -36,6 +40,7 @@
|
|
|
36
40
|
"muted": "#9a9ca6",
|
|
37
41
|
"line": "#2a2b33",
|
|
38
42
|
"line-strong": "#383944",
|
|
43
|
+
"line-control": "#6a6d78",
|
|
39
44
|
"action": "#f2f3f5",
|
|
40
45
|
"action-hover": "#ffffff",
|
|
41
46
|
"on-action": "#0d0d12",
|
|
@@ -47,6 +52,9 @@
|
|
|
47
52
|
"success-soft": "#132a22",
|
|
48
53
|
"warning": "#e0b341",
|
|
49
54
|
"warning-soft": "#2b2410",
|
|
55
|
+
"danger": "#ff6b5a",
|
|
56
|
+
"danger-soft": "#2e1714",
|
|
57
|
+
"on-danger": "#0d0d12",
|
|
50
58
|
"night": "#0a0b10",
|
|
51
59
|
"night-2": "#12141c",
|
|
52
60
|
"on-night": "#e3e4e8",
|
|
@@ -62,7 +70,8 @@
|
|
|
62
70
|
"roles": {
|
|
63
71
|
"action": "The thing you click: ink, or the brand's accent when the site chose the accent action profile (profiles.action).",
|
|
64
72
|
"accent": "Links, focus rings, active state, a chart line, the 8px label square; the primary button only through the accent action profile. Never body text, never words in a heading, never a section band.",
|
|
65
|
-
"success/warning": "State only. Their -soft tints are grounds: a tip or a warning callout, a success badge.",
|
|
73
|
+
"success/warning/danger": "State only. Their -soft tints are grounds: a tip or a warning callout, a success or an error badge, a field marked invalid. danger (0.6.0, gap #28) is the dashboard's --gemboss-danger family: the words of a field's error and the fill of a destructive button (on-danger on it).",
|
|
74
|
+
"line-control": "The border of a form control (a field, a checkbox, a radio), 0.6.0: it must reach 3:1 on bg, bg-soft and surface, light and dark, where line and line-strong are hairlines that do not. Like the dashboard's --gemboss-field-line.",
|
|
66
75
|
"wash": "The hero wash and illustrations only."
|
|
67
76
|
}
|
|
68
77
|
},
|
package/templates/footer.html
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
Footer: one per site, the same on every page. A brand block, three to five link columns, a bottom row.
|
|
3
3
|
Limits: column headings are one or two words in sentence case; four to seven links per column; the bottom row
|
|
4
4
|
holds the copyright, the legal links and the social links, nothing else. Replace every [bracketed] placeholder.
|
|
5
|
+
The newsletter form in the brand block is optional (0.6.0): keep it only when it is wired to a list that keeps the
|
|
6
|
+
address and sends what the note promises; otherwise delete the <form>. The note says how often and how to stop, and
|
|
7
|
+
links to the privacy policy. No box ticked for the reader, no "No thanks, I hate saving money". On send: the page
|
|
8
|
+
sets aria-invalid="true" and the error's words, or aria-busy on the button and then a role="status" line.
|
|
5
9
|
-->
|
|
6
10
|
<footer class="gw-footer">
|
|
7
11
|
<div class="gw-wrap">
|
|
@@ -9,6 +13,17 @@
|
|
|
9
13
|
<div class="gw-footer__brand">
|
|
10
14
|
<a class="gw-brand" href="/"><img class="gw-brand__mark" src="[/logo.svg]" alt="" width="26" height="26"><span class="gw-brand__name">[Brand]</span></a>
|
|
11
15
|
<p>[One sentence on what the brand does for the reader.]</p>
|
|
16
|
+
<form class="gw-footer__newsletter" action="[/newsletter endpoint]" method="post" novalidate>
|
|
17
|
+
<div class="gw-field">
|
|
18
|
+
<label class="gw-field__label" for="footer-email">[Product news by email]</label>
|
|
19
|
+
<div class="gw-form-inline">
|
|
20
|
+
<input class="gw-input" id="footer-email" name="email" type="email" autocomplete="email" required aria-describedby="footer-email-error footer-email-note">
|
|
21
|
+
<button class="gw-btn gw-btn--secondary" type="submit">[Subscribe]</button>
|
|
22
|
+
</div>
|
|
23
|
+
<p class="gw-field__error" id="footer-email-error"></p>
|
|
24
|
+
<p class="gw-field__hint" id="footer-email-note">[How often you write.] Unsubscribe in one click. <a href="[/privacy]">Privacy policy</a></p>
|
|
25
|
+
</div>
|
|
26
|
+
</form>
|
|
12
27
|
</div>
|
|
13
28
|
<nav class="gw-footer__col" aria-label="[Column heading]">
|
|
14
29
|
<h2 class="gw-footer__heading">[Column heading]</h2>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Form: a short form beside the reason to send it (a contact form, "get the guide", a demo request). Ask as little as
|
|
3
|
+
you can: one field beats a form, and a form keeps two or three fields. A question with a good guess is a choice the
|
|
4
|
+
reader confirms (a radio group with the guess checked), not a box to fill.
|
|
5
|
+
- Every field: its <label for> above the control, the error right under it, then the hint; aria-describedby names
|
|
6
|
+
the error first, then the hint. Mark the optional fields "(optional)" in the label; the placeholder is never the label.
|
|
7
|
+
- Validation is the page's: on send, set aria-invalid="true" on each wrong field and write in its .gw-field__error
|
|
8
|
+
what to do ("Enter your work email, like name@example.com"), then move the focus to the first one. The kit adds the
|
|
9
|
+
icon and the colour; the words say it, never the colour alone. Keep the empty error element in the markup.
|
|
10
|
+
- While it is sent: aria-busy="true" on the button. When the answer comes: say what happened in words in the
|
|
11
|
+
role="status" line ("Sent. We'll reply to name@example.com.").
|
|
12
|
+
- Consent boxes start unchecked, say exactly what the reader gets, and are never required to get something else.
|
|
13
|
+
- One field and its button (a newsletter, "email me the guide"): the .gw-form-inline row, see templates/footer.html.
|
|
14
|
+
Limits: three fields, one .gw-btn; no field that the team does not read.
|
|
15
|
+
-->
|
|
16
|
+
<section class="gw-section" aria-labelledby="[form-h]">
|
|
17
|
+
<div class="gw-wrap gw-split">
|
|
18
|
+
<div>
|
|
19
|
+
<p class="gw-label">[Contact]</p>
|
|
20
|
+
<h2 id="[form-h]"><span class="gw-dim">[The reader's question.]</span> [What sending this gets them.]</h2>
|
|
21
|
+
<p class="gw-lead">[One line: who answers and how.]</p>
|
|
22
|
+
</div>
|
|
23
|
+
<form class="gw-card gw-form" action="[/endpoint]" method="post" novalidate>
|
|
24
|
+
<div class="gw-field">
|
|
25
|
+
<label class="gw-field__label" for="[form-email]">[Work email]</label>
|
|
26
|
+
<input class="gw-input" id="[form-email]" name="email" type="email" autocomplete="email" required aria-describedby="[form-email]-error">
|
|
27
|
+
<p class="gw-field__error" id="[form-email]-error"></p>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="gw-field">
|
|
30
|
+
<label class="gw-field__label" for="[form-message]">[What are you building?]</label>
|
|
31
|
+
<textarea class="gw-textarea" id="[form-message]" name="message" rows="3" required aria-describedby="[form-message]-error [form-message]-hint"></textarea>
|
|
32
|
+
<p class="gw-field__error" id="[form-message]-error"></p>
|
|
33
|
+
<p class="gw-field__hint" id="[form-message]-hint">[A sentence or two is enough.]</p>
|
|
34
|
+
</div>
|
|
35
|
+
<label class="gw-check"><input type="checkbox" name="[news]" value="yes"><span>[Also send me product news, once a month]</span></label>
|
|
36
|
+
<div class="gw-actions">
|
|
37
|
+
<button class="gw-btn" type="submit">[Send the request]</button>
|
|
38
|
+
<p class="gw-note" role="status"></p>
|
|
39
|
+
</div>
|
|
40
|
+
</form>
|
|
41
|
+
</div>
|
|
42
|
+
</section>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Testimonials: a rating badge and a wall of short reviews. Every one comes from a page anyone can open, and links to
|
|
3
|
+
it: the badge to the store's review page, each card to its review. Copy only what that page shows: the score and
|
|
4
|
+
the count on the day you copy them (update them when they change), the reviewer's words exactly (cut a long one
|
|
5
|
+
with an ellipsis, never reword it), their name and store as they wrote them. No review you wrote, no score you
|
|
6
|
+
averaged yourself, no source you cannot link: leave the card out. `check` warns about a badge or a card without
|
|
7
|
+
its link (rating-source).
|
|
8
|
+
- Stars: style="--gw-stars: N" and aria-label="N out of 5", the same number (0 to 5, one decimal). A number you also
|
|
9
|
+
show beside them is aria-hidden="true", so it is not read twice.
|
|
10
|
+
- The badge's icon: the generic bag below, or the store's own mark only where its brand rules allow it.
|
|
11
|
+
- Avatars are optional: a photo the reviewer made public with the review, alt="" (the name is beside it).
|
|
12
|
+
Limits: three or six cards; one to three sentences each; one badge.
|
|
13
|
+
-->
|
|
14
|
+
<section class="gw-section gw-section--soft" aria-labelledby="[reviews-h]">
|
|
15
|
+
<div class="gw-wrap">
|
|
16
|
+
<div class="gw-section__head">
|
|
17
|
+
<p class="gw-label">[Reviews]</p>
|
|
18
|
+
<h2 id="[reviews-h]"><span class="gw-dim">[What merchants say.]</span> [In their own words.]</h2>
|
|
19
|
+
<a class="gw-rating-badge" href="[https://apps.shopify.com/your-app/reviews]">
|
|
20
|
+
<span class="gw-rating-badge__icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 8h14l-1 12H6z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></span>
|
|
21
|
+
<span class="gw-rating-badge__body">
|
|
22
|
+
<span class="gw-rating-badge__score"><span aria-hidden="true">[4.9]</span><span class="gw-stars" role="img" aria-label="[4.9] out of 5" style="--gw-stars: 4.9"></span></span>
|
|
23
|
+
<span class="gw-rating-badge__count">[1,234] reviews on the [Shopify App Store]</span>
|
|
24
|
+
</span>
|
|
25
|
+
</a>
|
|
26
|
+
</div>
|
|
27
|
+
<ul class="gw-testimonials" role="list">
|
|
28
|
+
<li>
|
|
29
|
+
<figure class="gw-card gw-testimonial">
|
|
30
|
+
<span class="gw-stars" role="img" aria-label="[5] out of 5" style="--gw-stars: 5"></span>
|
|
31
|
+
<blockquote class="gw-testimonial__quote"><p>[The reviewer's exact words.]</p></blockquote>
|
|
32
|
+
<figcaption class="gw-testimonial__by">
|
|
33
|
+
<img class="gw-testimonial__avatar" src="[/img/reviewer.webp]" alt="" width="40" height="40" loading="lazy">
|
|
34
|
+
<span><b>[Name]</b>[Store] · <a href="[https://link to the review]">[Shopify App Store]</a></span>
|
|
35
|
+
</figcaption>
|
|
36
|
+
</figure>
|
|
37
|
+
</li>
|
|
38
|
+
<li>
|
|
39
|
+
<figure class="gw-card gw-testimonial">
|
|
40
|
+
<span class="gw-stars" role="img" aria-label="[5] out of 5" style="--gw-stars: 5"></span>
|
|
41
|
+
<blockquote class="gw-testimonial__quote"><p>[The reviewer's exact words.]</p></blockquote>
|
|
42
|
+
<figcaption class="gw-testimonial__by">
|
|
43
|
+
<span><b>[Name]</b>[Store] · <a href="[https://link to the review]">[Shopify App Store]</a></span>
|
|
44
|
+
</figcaption>
|
|
45
|
+
</figure>
|
|
46
|
+
</li>
|
|
47
|
+
<li>
|
|
48
|
+
<figure class="gw-card gw-testimonial">
|
|
49
|
+
<span class="gw-stars" role="img" aria-label="[5] out of 5" style="--gw-stars: 5"></span>
|
|
50
|
+
<blockquote class="gw-testimonial__quote"><p>[The reviewer's exact words.]</p></blockquote>
|
|
51
|
+
<figcaption class="gw-testimonial__by">
|
|
52
|
+
<span><b>[Name]</b>[Store] · <a href="[https://link to the review]">[Shopify App Store]</a></span>
|
|
53
|
+
</figcaption>
|
|
54
|
+
</figure>
|
|
55
|
+
</li>
|
|
56
|
+
</ul>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|