@port60/template-kit 0.16.0 → 0.18.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/package.json +9 -3
- package/src/commands/dev.mjs +3 -1
- package/src/vendor/contract/v1/content-model.json +372 -76
- package/src/vendor/contract/v1/context.json +60 -3
- package/src/vendor/contract/v1/islands.json +50 -0
- package/src/vendor/contract/v1/manifest.schema.json +132 -25
- package/src/vendor/contract/v1/sections.json +18 -6
- package/src/vendor/validator/focus.mjs +49 -0
- package/src/vendor/validator/platform-base.css +294 -0
- package/src/vendor/validator/preview.mjs +45 -7
- package/src/vendor/validator/validate.mjs +16 -1
- package/starter/manifest.json +7 -2
- package/starter/sections/homeHero.liquid +9 -8
|
@@ -2579,6 +2579,300 @@ textarea.p60-form-control {
|
|
|
2579
2579
|
border-color: var(--primary-strong);
|
|
2580
2580
|
}
|
|
2581
2581
|
|
|
2582
|
+
/* ---- Volunteer sign-up widget (components/VolunteerWidget.tsx) ----
|
|
2583
|
+
Reuses the donate card vocabulary (.donate-card/.donate-head/.field/.donate-submit/.donate-success)
|
|
2584
|
+
so a themed donate card themes this too; vol-* covers what is specific to asking for help. */
|
|
2585
|
+
.vol-search input,
|
|
2586
|
+
.vol-form input:not([type='checkbox']):not([type='radio']),
|
|
2587
|
+
.vol-form select,
|
|
2588
|
+
.vol-form textarea {
|
|
2589
|
+
width: 100%;
|
|
2590
|
+
border: 1.5px solid var(--line);
|
|
2591
|
+
border-radius: 12px;
|
|
2592
|
+
padding: 0.7rem 0.85rem;
|
|
2593
|
+
font: inherit;
|
|
2594
|
+
line-height: 1.45;
|
|
2595
|
+
color: var(--ink);
|
|
2596
|
+
background: var(--surface);
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
.vol-search input:focus,
|
|
2600
|
+
.vol-form input:not([type='checkbox']):not([type='radio']):focus,
|
|
2601
|
+
.vol-form select:focus,
|
|
2602
|
+
.vol-form textarea:focus {
|
|
2603
|
+
outline: none;
|
|
2604
|
+
border-color: var(--primary);
|
|
2605
|
+
box-shadow: 0 0 0 3px var(--primary-soft);
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
/* The steps live in a body of ONE height (owner rule: the hero widget must not grow and jump
|
|
2609
|
+
between steps). Lists scroll inside it; a step shorter than the body leaves quiet space. */
|
|
2610
|
+
.vol-body {
|
|
2611
|
+
display: flex;
|
|
2612
|
+
flex-direction: column;
|
|
2613
|
+
gap: 1rem;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
.vol-body--list {
|
|
2617
|
+
height: 32rem;
|
|
2618
|
+
overflow: hidden;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
.vol-body--form {
|
|
2622
|
+
min-height: 32rem;
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
.vol-list {
|
|
2626
|
+
display: grid;
|
|
2627
|
+
gap: 0.6rem;
|
|
2628
|
+
align-content: start;
|
|
2629
|
+
flex: 1 1 auto;
|
|
2630
|
+
min-height: 0;
|
|
2631
|
+
overflow-y: auto;
|
|
2632
|
+
padding-right: 0.2rem;
|
|
2633
|
+
scrollbar-gutter: stable;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
.vol-body > .vol-form,
|
|
2637
|
+
.vol-body > .vol-chosen,
|
|
2638
|
+
.vol-body > .vol-search {
|
|
2639
|
+
flex: none;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
.vol-group {
|
|
2643
|
+
display: grid;
|
|
2644
|
+
gap: 0.5rem;
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
.vol-group-label {
|
|
2648
|
+
margin: 0.2rem 0 0;
|
|
2649
|
+
font-size: 0.78rem;
|
|
2650
|
+
font-weight: 700;
|
|
2651
|
+
letter-spacing: 0.06em;
|
|
2652
|
+
text-transform: uppercase;
|
|
2653
|
+
color: var(--muted);
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
.vol-option {
|
|
2657
|
+
display: flex;
|
|
2658
|
+
align-items: center;
|
|
2659
|
+
justify-content: space-between;
|
|
2660
|
+
gap: 0.8rem;
|
|
2661
|
+
width: 100%;
|
|
2662
|
+
text-align: start;
|
|
2663
|
+
border: 1.5px solid var(--line);
|
|
2664
|
+
background: var(--surface);
|
|
2665
|
+
color: var(--ink);
|
|
2666
|
+
border-radius: 12px;
|
|
2667
|
+
padding: 0.85rem 1rem;
|
|
2668
|
+
font: inherit;
|
|
2669
|
+
cursor: pointer;
|
|
2670
|
+
transition: border-color 0.16s ease, background 0.16s ease;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
.vol-option:hover,
|
|
2674
|
+
.vol-option:focus-visible {
|
|
2675
|
+
border-color: var(--primary);
|
|
2676
|
+
background: var(--primary-soft);
|
|
2677
|
+
outline: none;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
.vol-option-main {
|
|
2681
|
+
display: grid;
|
|
2682
|
+
gap: 0.15rem;
|
|
2683
|
+
min-width: 0;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
.vol-option-summary {
|
|
2687
|
+
font-size: 0.88rem;
|
|
2688
|
+
color: var(--muted);
|
|
2689
|
+
line-height: 1.35;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
.vol-option-meta {
|
|
2693
|
+
display: grid;
|
|
2694
|
+
gap: 0.2rem;
|
|
2695
|
+
justify-items: end;
|
|
2696
|
+
flex-shrink: 0;
|
|
2697
|
+
text-align: end;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
.vol-needed {
|
|
2701
|
+
font-size: 0.8rem;
|
|
2702
|
+
font-weight: 700;
|
|
2703
|
+
color: var(--primary-strong);
|
|
2704
|
+
white-space: nowrap;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
.vol-needed.is-full {
|
|
2708
|
+
color: var(--muted);
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
.vol-shift-count {
|
|
2712
|
+
font-size: 0.78rem;
|
|
2713
|
+
color: var(--muted);
|
|
2714
|
+
white-space: nowrap;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
.vol-chosen {
|
|
2718
|
+
display: flex;
|
|
2719
|
+
align-items: center;
|
|
2720
|
+
justify-content: space-between;
|
|
2721
|
+
gap: 0.8rem;
|
|
2722
|
+
padding: 0.7rem 0.9rem;
|
|
2723
|
+
border-radius: 12px;
|
|
2724
|
+
background: var(--primary-soft);
|
|
2725
|
+
font-size: 0.92rem;
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
.vol-chosen-shift {
|
|
2729
|
+
display: block;
|
|
2730
|
+
color: var(--muted);
|
|
2731
|
+
font-size: 0.85rem;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
.vol-form {
|
|
2735
|
+
display: grid;
|
|
2736
|
+
gap: 0.9rem;
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
.vol-optional {
|
|
2740
|
+
font-style: normal;
|
|
2741
|
+
font-weight: 500;
|
|
2742
|
+
color: var(--muted);
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
.vol-availability {
|
|
2746
|
+
border: 0;
|
|
2747
|
+
padding: 0;
|
|
2748
|
+
margin: 0;
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
.vol-availability legend {
|
|
2752
|
+
font-weight: 600;
|
|
2753
|
+
font-size: 0.9rem;
|
|
2754
|
+
line-height: 1.4;
|
|
2755
|
+
padding: 0;
|
|
2756
|
+
margin-bottom: 0.4rem;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
/* The When row is a select or a chip row; both sit on the same box height so the panel never
|
|
2760
|
+
changes height when the choice above changes. */
|
|
2761
|
+
.vol-form select,
|
|
2762
|
+
.vol-chip {
|
|
2763
|
+
box-sizing: border-box;
|
|
2764
|
+
min-height: 3.1rem;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
.vol-form .field > span {
|
|
2768
|
+
line-height: 1.4;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
.vol-chips {
|
|
2772
|
+
display: flex;
|
|
2773
|
+
flex-wrap: wrap;
|
|
2774
|
+
gap: 0.4rem;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
.vol-summary-needed {
|
|
2778
|
+
color: var(--primary-strong);
|
|
2779
|
+
font-weight: 700;
|
|
2780
|
+
text-transform: capitalize;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
.vol-chip {
|
|
2784
|
+
flex: 1 1 0;
|
|
2785
|
+
border: 1.5px solid var(--line);
|
|
2786
|
+
background: var(--surface);
|
|
2787
|
+
color: var(--ink);
|
|
2788
|
+
border-radius: 999px;
|
|
2789
|
+
/* The same vertical rhythm as a select in this form, so the When row never changes height. */
|
|
2790
|
+
padding: 0.7rem 0.4rem;
|
|
2791
|
+
font: inherit;
|
|
2792
|
+
font-size: 0.86rem;
|
|
2793
|
+
line-height: 1.45;
|
|
2794
|
+
font-weight: 600;
|
|
2795
|
+
cursor: pointer;
|
|
2796
|
+
white-space: nowrap;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
.vol-chip.selected {
|
|
2800
|
+
border-color: var(--primary);
|
|
2801
|
+
background: var(--primary);
|
|
2802
|
+
color: #fff;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
|
|
2806
|
+
.vol-why {
|
|
2807
|
+
text-align: center;
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
.vol-form select {
|
|
2811
|
+
appearance: none;
|
|
2812
|
+
-webkit-appearance: none;
|
|
2813
|
+
padding-right: 2.4rem;
|
|
2814
|
+
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23555' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
|
|
2815
|
+
background-repeat: no-repeat;
|
|
2816
|
+
background-position: right 0.9rem center;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
.vol-form select:disabled {
|
|
2820
|
+
opacity: 0.6;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
.vol-summary {
|
|
2824
|
+
margin: -0.3rem 0 0;
|
|
2825
|
+
font-size: 0.86rem;
|
|
2826
|
+
line-height: 1.4;
|
|
2827
|
+
min-height: calc(0.86rem * 1.4 * 2);
|
|
2828
|
+
color: var(--muted);
|
|
2829
|
+
display: -webkit-box;
|
|
2830
|
+
-webkit-line-clamp: 2;
|
|
2831
|
+
-webkit-box-orient: vertical;
|
|
2832
|
+
overflow: hidden;
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
.vol-field-error {
|
|
2836
|
+
display: block;
|
|
2837
|
+
font-style: normal;
|
|
2838
|
+
font-size: 0.8rem;
|
|
2839
|
+
line-height: 1.3;
|
|
2840
|
+
color: var(--danger, #b3261e);
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
.vol-form .field.has-error input,
|
|
2844
|
+
.vol-form .field.has-error select {
|
|
2845
|
+
border-color: var(--danger, #b3261e);
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
.vol-signed-in {
|
|
2849
|
+
margin: 0;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
.vol-extra {
|
|
2853
|
+
display: grid;
|
|
2854
|
+
gap: 0.9rem;
|
|
2855
|
+
padding: 0.9rem;
|
|
2856
|
+
border: 1px dashed var(--line);
|
|
2857
|
+
border-radius: 12px;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
.vol-extra .p60-form-heading {
|
|
2861
|
+
margin: 0;
|
|
2862
|
+
font-size: 1rem;
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
@media (max-width: 560px) {
|
|
2866
|
+
.vol-option {
|
|
2867
|
+
flex-direction: column;
|
|
2868
|
+
align-items: stretch;
|
|
2869
|
+
}
|
|
2870
|
+
.vol-option-meta {
|
|
2871
|
+
justify-items: start;
|
|
2872
|
+
text-align: start;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2582
2876
|
/* ---- Members area (shell + sections; see components/members/) ----
|
|
2583
2877
|
Desktop: left rail + content. Small screens: the rail becomes a horizontal, scrollable pill
|
|
2584
2878
|
strip above the content — same registry, no separate mobile markup. */
|
|
@@ -17,6 +17,7 @@ import islandRegistry from '../contract/v1/islands.json' with { type: 'json' };
|
|
|
17
17
|
import contextContract from '../contract/v1/context.json' with { type: 'json' };
|
|
18
18
|
import { resolveFixtureArt } from './fixture-art.mjs';
|
|
19
19
|
import { buildSiteFixture, applyPreviewContent } from './site-context.mjs';
|
|
20
|
+
import { normaliseFocus, previewActions, withResolvedActions, applyFocus } from './focus.mjs';
|
|
20
21
|
|
|
21
22
|
// Fixture imagery resolved for the SEALED studio render (p60fixture: refs become inline-SVG data
|
|
22
23
|
// URIs the network-dead CSP can show). The dev preview may instead resolve them to the platform
|
|
@@ -143,6 +144,25 @@ function islandSkeleton(name, ctx = {}, fx = STUDIO_FX) {
|
|
|
143
144
|
<label class="newsletter-label">Email address</label><div class="newsletter-fields"><input class="newsletter-email" type="email" disabled><button class="newsletter-submit" type="button" disabled>Join our newsletter</button></div>
|
|
144
145
|
<label class="newsletter-consent"><input type="checkbox" disabled><span>Email me about our work and appeals.</span></label>
|
|
145
146
|
</form>`;
|
|
147
|
+
case 'primary_action_widget':
|
|
148
|
+
// What this island becomes follows the preview's focus (fixtures.focus, from ?focus= on the
|
|
149
|
+
// dev server): the volunteer sign-up, nothing, or the default, the donation widget.
|
|
150
|
+
if (fixtures.focus === 'volunteer') {
|
|
151
|
+
return islandSkeleton('volunteer_signup', ctx, fx).replace('data-p60-preview-island="volunteer_signup"', 'data-p60-preview-island="primary_action_widget"');
|
|
152
|
+
}
|
|
153
|
+
if (fixtures.focus === 'none') {
|
|
154
|
+
return `<div class="p60-preview-empty" data-p60-preview-island="primary_action_widget">${previewNote(name)}<p>No widget here: this charity leads with buttons only (site.focus is 'none').</p></div>`;
|
|
155
|
+
}
|
|
156
|
+
return islandSkeleton('donation_widget', ctx, fx).replace('data-p60-preview-island="donation_widget"', 'data-p60-preview-island="primary_action_widget"');
|
|
157
|
+
case 'volunteer_signup':
|
|
158
|
+
return `<section class="donate-card vol-card" data-p60-preview-island="volunteer_signup">
|
|
159
|
+
${previewNote(name)}
|
|
160
|
+
<header class="donate-head"><h2>Volunteer with us</h2><p class="donate-sub">Where would you like to help?</p></header>
|
|
161
|
+
<div class="vol-list">
|
|
162
|
+
<div class="vol-group"><p class="vol-group-label">General</p><button class="vol-option" type="button" disabled><span class="vol-option-main"><strong>Help out where needed</strong><span class="vol-option-summary">A few hours a month, wherever the team needs a hand.</span></span><span class="vol-option-meta"><span class="vol-needed">Everyone welcome</span></span></button></div>
|
|
163
|
+
<div class="vol-group"><p class="vol-group-label">Events</p><button class="vol-option" type="button" disabled><span class="vol-option-main"><strong>Spring fair stewards</strong><span class="vol-option-summary">Welcome visitors and keep the day running.</span></span><span class="vol-option-meta"><span class="vol-needed">6 volunteers needed</span><span class="vol-shift-count">3 shifts</span></span></button></div>
|
|
164
|
+
</div>
|
|
165
|
+
</section>`;
|
|
146
166
|
case 'language_switch':
|
|
147
167
|
return `<label class="language-switch" data-p60-preview-island="language_switch">${previewNote(name)}<span class="language-switch-label">Language</span><select class="language-switch-select" disabled><option>English</option><option>Cymraeg</option><option>العربية</option></select></label>`;
|
|
148
168
|
case 'search':
|
|
@@ -348,18 +368,27 @@ const SURFACES = {
|
|
|
348
368
|
/** The routed dev preview's surface names (plus 'home'). */
|
|
349
369
|
export const PREVIEW_SURFACES = ['home', ...Object.keys(SURFACES)];
|
|
350
370
|
|
|
351
|
-
function surfaceBar(active) {
|
|
371
|
+
function surfaceBar(active, focus = 'donate') {
|
|
372
|
+
const withFocus = (href) => (focus === 'donate' ? href : `${href}${href.includes('?') ? '&' : '?'}focus=${focus}`);
|
|
352
373
|
const link = (name, href) =>
|
|
353
|
-
`<a href="${href}"${name === active ? ' style="font-weight:700;text-decoration:underline"' : ''}>${name}</a>`;
|
|
374
|
+
`<a href="${withFocus(href)}"${name === active ? ' style="font-weight:700;text-decoration:underline"' : ''}>${name}</a>`;
|
|
354
375
|
const links = [
|
|
355
376
|
link('home', '/'), link('events', '/events'), link('event', '/events?event=fixture'),
|
|
356
377
|
link('services', '/services'), link('service', '/services?service=fixture'),
|
|
357
378
|
link('donate', '/donate'), link('articles', '/articles'), link('article', '/articles/fixture'),
|
|
358
379
|
link('campaigns', '/campaigns'), link('campaign', '/campaigns/fixture'), link('course', '/courses'),
|
|
359
|
-
`<a href="/model" style="margin-left:auto;font-weight:700">site.content model →</a>`,
|
|
380
|
+
`<a href="${withFocus('/model')}" style="margin-left:auto;font-weight:700">site.content model →</a>`,
|
|
360
381
|
];
|
|
382
|
+
// What leads: the charity's switch, here as three links, so a developer sees the hero widget as
|
|
383
|
+
// the donation widget, as the volunteer sign-up, or absent, with the buttons following each time.
|
|
384
|
+
const focusLink = (value, label) =>
|
|
385
|
+
`<a href="${value === 'donate' ? '/' : `/?focus=${value}`}"${value === focus ? ' style="font-weight:700;text-decoration:underline"' : ''}>${label}</a>`;
|
|
386
|
+
const focusLinks = [focusLink('donate', 'giving'), focusLink('volunteer', 'volunteering'), focusLink('none', 'buttons only')];
|
|
361
387
|
return `<nav class="p60-preview-surfaces" aria-label="Preview surfaces" style="position:sticky;top:0;z-index:99;display:flex;gap:12px;flex-wrap:wrap;padding:8px 14px;font:12px/1.4 system-ui,sans-serif;background:#0b1220;color:#e6e9f2;opacity:.94">
|
|
362
388
|
<strong style="letter-spacing:.06em;text-transform:uppercase;font-size:10px">Surfaces</strong>${links.join('')}
|
|
389
|
+
<span style="flex-basis:100%;height:0"></span>
|
|
390
|
+
<strong style="letter-spacing:.06em;text-transform:uppercase;font-size:10px">Leads with</strong>${focusLinks.join('')}
|
|
391
|
+
<span style="opacity:.7">the hero widget becomes the donation widget, the volunteer sign-up, or nothing; the buttons follow</span>
|
|
363
392
|
</nav>`;
|
|
364
393
|
}
|
|
365
394
|
|
|
@@ -409,12 +438,17 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
409
438
|
// imagery to the platform CDN instead of inline-SVG art (the dev-richer half of the split).
|
|
410
439
|
const artOptions = options.fixtureImageBase ? { imageBase: options.fixtureImageBase } : null;
|
|
411
440
|
let fx = artOptions ? resolveFixtureArt(contextContract.fixtures, artOptions) : STUDIO_FX;
|
|
441
|
+
// What leads (docs/volunteering.md 'Site focus'): the dev server passes ?focus=; the studio
|
|
442
|
+
// renders the platform default. The island skeletons read it from the fixtures they are handed.
|
|
443
|
+
const focus = normaliseFocus(options.focus);
|
|
444
|
+
const actions = previewActions(focus);
|
|
445
|
+
fx = { ...fx, focus };
|
|
412
446
|
// The one content tree (content model v1): about composed from this manifest's declared
|
|
413
447
|
// sections, dev preview-content overlaid when the kit passes it (validated there), imagery
|
|
414
448
|
// resolved exactly like the rest of the fixtures.
|
|
415
|
-
const site = resolveFixtureArt(
|
|
449
|
+
const site = applyFocus(resolveFixtureArt(
|
|
416
450
|
applyPreviewContent(buildSiteFixture(manifest), options.previewContent ?? null),
|
|
417
|
-
artOptions ?? {});
|
|
451
|
+
artOptions ?? {}), actions);
|
|
418
452
|
const brand = site.brand ?? fx.brand;
|
|
419
453
|
// With a content override, the TREE is the source of truth for every fixture view: the routed
|
|
420
454
|
// platform skeletons and island skeletons re-derive their slices from the overridden site, so
|
|
@@ -470,8 +504,12 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
470
504
|
const entry = catalogueByType.get(type);
|
|
471
505
|
const source = files[`sections/${type}.liquid`];
|
|
472
506
|
if (!entry || source == null) continue;
|
|
507
|
+
const sample = artOptions ? resolveFixtureArt(entry.sample ?? {}, artOptions) : resolveFixtureArt(entry.sample ?? {});
|
|
473
508
|
const context = {
|
|
474
|
-
|
|
509
|
+
// The home hero carries the resolved actions exactly as the platform hands them over, so a
|
|
510
|
+
// developer sees the widget AND the button that pairs with it. The catalogue sample's
|
|
511
|
+
// givingStyle is dropped here: in the preview the focus decides the style.
|
|
512
|
+
section: type === 'homeHero' ? withResolvedActions({ ...sample, givingStyle: undefined, actionStyle: undefined }, actions) : sample,
|
|
475
513
|
brand,
|
|
476
514
|
site,
|
|
477
515
|
...(fx.sections?.[type] ?? {})
|
|
@@ -569,7 +607,7 @@ export async function renderStudioPreview(files, options = {}) {
|
|
|
569
607
|
<style>${themeCss}</style>
|
|
570
608
|
</head>
|
|
571
609
|
<body ${attrs}>
|
|
572
|
-
${options.surface ? surfaceBar(surface) : ''}
|
|
610
|
+
${options.surface ? surfaceBar(surface, focus) : ''}
|
|
573
611
|
${bodyHtml}
|
|
574
612
|
${runtime ? `<script>${runtime}\np60Behaviors.initBehaviors();</script>` : ''}
|
|
575
613
|
</body>
|
|
@@ -99,7 +99,8 @@ export async function validateArtifact(files) {
|
|
|
99
99
|
locations: () => sections.has('locations'),
|
|
100
100
|
newsletter: () => islands.has('newsletter_signup'),
|
|
101
101
|
i18n: () => islands.has('language_switch'),
|
|
102
|
-
search: () => islands.has('search')
|
|
102
|
+
search: () => islands.has('search'),
|
|
103
|
+
volunteering: () => islands.has('volunteer_signup') || islands.has('primary_action_widget')
|
|
103
104
|
};
|
|
104
105
|
for (const capability of manifest?.requiresCapabilities ?? []) {
|
|
105
106
|
if (!capabilitySurface[capability]?.()) {
|
|
@@ -108,6 +109,20 @@ export async function validateArtifact(files) {
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
// Site focus honesty (docs/volunteering.md): a template that claims it can lead with
|
|
113
|
+
// volunteering must give the volunteer sign-up a way into the hero, either the
|
|
114
|
+
// primary_action_widget island (which becomes the sign-up when volunteering leads) or the
|
|
115
|
+
// volunteer_signup island placed directly.
|
|
116
|
+
{
|
|
117
|
+
const focusKinds = new Set(manifest?.supports?.focus ?? []);
|
|
118
|
+
if (focusKinds.has('volunteer')) {
|
|
119
|
+
const liquidSource = Object.entries(files).filter(([path]) => path.endsWith('.liquid')).map(([, s]) => s).join('\n');
|
|
120
|
+
if (!/island\s+['"]primary_action_widget['"]/.test(liquidSource) && !/island\s+['"]volunteer_signup['"]/.test(liquidSource)) {
|
|
121
|
+
errors.push("manifest: supports.focus lists 'volunteer' but no section places {% island 'primary_action_widget' %} (or 'volunteer_signup'), so the site could never lead with volunteering");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
111
126
|
// Markup and attributes, NEVER code — the machine-enforced JavaScript ban over every liquid
|
|
112
127
|
// source (docs/template-behaviours.md). Behaviour is engine-owned; a template wanting motion
|
|
113
128
|
// declares supports.behaviors and uses the data-p60-* grammar.
|
package/starter/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"format": "port60-liquid@1",
|
|
5
5
|
"label": "Starter",
|
|
6
6
|
"description": "The reference template for the Port60 dialect, the developer docs' worked example and the base to copy when building your own. 1.1.0 adds the layout slot: the template owns the header, navigation and footer chrome.",
|
|
@@ -22,13 +22,18 @@
|
|
|
22
22
|
"donation_widget",
|
|
23
23
|
"member_menu",
|
|
24
24
|
"hero_carousel",
|
|
25
|
-
"map"
|
|
25
|
+
"map",
|
|
26
|
+
"primary_action_widget"
|
|
26
27
|
],
|
|
27
28
|
"layout": true,
|
|
28
29
|
"heroImagery": true,
|
|
29
30
|
"behaviors": [
|
|
30
31
|
"reveal",
|
|
31
32
|
"carousel"
|
|
33
|
+
],
|
|
34
|
+
"focus": [
|
|
35
|
+
"donate",
|
|
36
|
+
"volunteer"
|
|
32
37
|
]
|
|
33
38
|
},
|
|
34
39
|
"settings": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
{% endif %}
|
|
32
32
|
{% assign heroFraming = section.photoFraming | default: 'fill' %}
|
|
33
33
|
{% if heroCount > 0 and heroFraming == 'whole' %}
|
|
34
|
-
{% if section.givingStyle == 'button' %}
|
|
34
|
+
{% if section.actionStyle == 'button' or section.givingStyle == 'button' %}
|
|
35
35
|
<section class="lq-homehero lq-homehero--vista{% if heroCount > 1 %} lq-homehero--carousel{% endif %}">
|
|
36
36
|
<div class="lq-vista">
|
|
37
37
|
<img class="lq-vista-sizer" src="{{ section.images.first.imageUrl }}" alt="{{ section.images.first.alt }}" style="object-position: {{ heroFocus }}" fetchpriority="high" decoding="async">
|
|
@@ -63,12 +63,13 @@
|
|
|
63
63
|
<h1>{{ section.title | default: site.brand.tagline | default: site.brand.name }}</h1>
|
|
64
64
|
{% if section.lead %}<p class="lq-homehero-lead">{{ section.lead }}</p>{% endif %}
|
|
65
65
|
<div class="lq-homehero-actions">
|
|
66
|
-
<a class="button button-primary" href="#donate">{{ section.primaryLabel | default: 'Donate now' }}</a>
|
|
66
|
+
{% if section.action or section.primaryLabel %}<a class="button button-primary" href="{{ section.primaryHref | default: section.action.href | default: '#donate' }}">{{ section.primaryLabel | default: section.action.label | default: 'Donate now' }}</a>{% endif %}
|
|
67
67
|
<a class="button" href="{{ section.secondaryHref | default: '/about' }}">{{ section.secondaryLabel | default: 'Our story' }}</a>
|
|
68
68
|
</div>
|
|
69
69
|
</div>
|
|
70
70
|
<aside id="donate">
|
|
71
|
-
{
|
|
71
|
+
{%- comment -%} The site focus decides which widget leads (section.primary); this island becomes it: the donation widget or the volunteer sign-up. {%- endcomment -%}
|
|
72
|
+
{% island 'primary_action_widget' %}
|
|
72
73
|
</aside>
|
|
73
74
|
</div>
|
|
74
75
|
</section>
|
|
@@ -97,17 +98,17 @@
|
|
|
97
98
|
<h1>{{ section.title | default: site.brand.tagline | default: site.brand.name }}</h1>
|
|
98
99
|
{% if section.lead %}<p class="lq-homehero-lead">{{ section.lead }}</p>{% endif %}
|
|
99
100
|
<div class="lq-homehero-actions">
|
|
100
|
-
{% if section.givingStyle == 'button' %}
|
|
101
|
-
<a class="button button-primary" href="{{ section.primaryHref | default: '/donate' }}">{{ section.primaryLabel | default: 'Donate now' }}</a>
|
|
101
|
+
{% if section.actionStyle == 'button' or section.givingStyle == 'button' %}
|
|
102
|
+
{% if section.action or section.primaryLabel %}<a class="button button-primary" href="{{ section.primaryHref | default: section.action.href | default: '/donate' }}">{{ section.primaryLabel | default: section.action.label | default: 'Donate now' }}</a>{% endif %}
|
|
102
103
|
{% else %}
|
|
103
|
-
<a class="button button-primary" href="#donate">{{ section.primaryLabel | default: 'Donate now' }}</a>
|
|
104
|
+
{% if section.action or section.primaryLabel %}<a class="button button-primary" href="{{ section.primaryHref | default: section.action.href | default: '#donate' }}">{{ section.primaryLabel | default: section.action.label | default: 'Donate now' }}</a>{% endif %}
|
|
104
105
|
{% endif %}
|
|
105
106
|
<a class="button" href="{{ section.secondaryHref | default: '/about' }}">{{ section.secondaryLabel | default: 'Our story' }}</a>
|
|
106
107
|
</div>
|
|
107
108
|
</div>
|
|
108
|
-
{% unless section.givingStyle == 'button' %}
|
|
109
|
+
{% unless section.actionStyle == 'button' or section.givingStyle == 'button' %}
|
|
109
110
|
<aside id="donate">
|
|
110
|
-
{% island '
|
|
111
|
+
{% island 'primary_action_widget' %}
|
|
111
112
|
</aside>
|
|
112
113
|
{% endunless %}
|
|
113
114
|
</div>
|