ferst-core 0.9.6 → 0.9.8
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/bin/cms-config.mjs +4 -47
- package/bin/ferst-modules.mjs +1 -1
- package/components/recipes/FeatureCards.astro +12 -9
- package/components/recipes/Form.astro +1 -1
- package/components/recipes/MediaCards.astro +9 -5
- package/components/recipes/Stats.astro +7 -5
- package/components/recipes/Tiles.astro +12 -9
- package/lib/git-base.mjs +62 -0
- package/lib/gridColumns.ts +22 -0
- package/package.json +1 -1
package/bin/cms-config.mjs
CHANGED
|
@@ -60,53 +60,10 @@ export function repoSlug() {
|
|
|
60
60
|
return '';
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
* The platform git-proxy origin (CMS `base_url` / `api_root`) for THIS build:
|
|
68
|
-
* 1. `FERST_GIT_BASE` env — explicit override, always wins.
|
|
69
|
-
* 2. committed `ferst-site.json` `gitBase` — an explicit per-site pin (e.g. a
|
|
70
|
-
* custom proxy); a BYO/seeded site can hard-bind its origin this way.
|
|
71
|
-
* 3. otherwise DERIVE from the deploy environment: the production branch talks to
|
|
72
|
-
* the prod git-proxy, every other (preview/dev) branch talks to the dev
|
|
73
|
-
* git-proxy. So a templated repo self-configures per environment on deploy — a
|
|
74
|
-
* dev-branch build points its CMS at the dev worker and main at prod — with NO
|
|
75
|
-
* per-client config. (The production branch defaults to `main`; override via
|
|
76
|
-
* `FERST_PROD_BRANCH` env or `ferst-site.json` `productionBranch`.)
|
|
77
|
-
* This restores the "site respects the environment it's deployed in" binding the old
|
|
78
|
-
* per-branch site config gave, without a per-client file to maintain.
|
|
79
|
-
*/
|
|
80
|
-
export function resolveGitBase(cwd = process.cwd(), branch = deploymentBranch()) {
|
|
81
|
-
const fromEnv = env('FERST_GIT_BASE');
|
|
82
|
-
if (fromEnv) return fromEnv;
|
|
83
|
-
let prodBranch = env('FERST_PROD_BRANCH') || 'main';
|
|
84
|
-
try {
|
|
85
|
-
const site = JSON.parse(readFileSync(resolve(cwd, 'ferst-site.json'), 'utf8'));
|
|
86
|
-
if (site && typeof site.gitBase === 'string' && site.gitBase.trim()) return site.gitBase.trim();
|
|
87
|
-
if (!env('FERST_PROD_BRANCH') && site && typeof site.productionBranch === 'string' && site.productionBranch.trim()) {
|
|
88
|
-
prodBranch = site.productionBranch.trim();
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
/* no ferst-site.json (e.g. the template repo itself) — derive from the branch */
|
|
92
|
-
}
|
|
93
|
-
return branch === prodBranch ? PROD_GIT_BASE : DEV_GIT_BASE;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** The deployment branch: CF Pages env → current git branch → main. */
|
|
97
|
-
export function deploymentBranch() {
|
|
98
|
-
const cf = env('CF_PAGES_BRANCH');
|
|
99
|
-
if (cf) return cf;
|
|
100
|
-
try {
|
|
101
|
-
const git = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
102
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
103
|
-
}).toString().trim();
|
|
104
|
-
if (git && git !== 'HEAD') return git;
|
|
105
|
-
} catch {
|
|
106
|
-
/* not a git checkout — fall through */
|
|
107
|
-
}
|
|
108
|
-
return 'main';
|
|
109
|
-
}
|
|
63
|
+
// The git-proxy origin lives in lib/git-base.mjs, which components can import: this file is a
|
|
64
|
+
// build-time script whose module scope needs a real filesystem (TEMPLATE_PATH).
|
|
65
|
+
export { PROD_GIT_BASE, DEV_GIT_BASE, resolveGitBase, deploymentBranch } from '../lib/git-base.mjs';
|
|
66
|
+
import { resolveGitBase, deploymentBranch } from '../lib/git-base.mjs';
|
|
110
67
|
|
|
111
68
|
/** Ferst's managed GitHub org: its sites are on the Basic plan. */
|
|
112
69
|
const MANAGED_OWNER = 'myai4webdev';
|
package/bin/ferst-modules.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { readFileSync, realpathSync } from 'node:fs';
|
|
13
13
|
import { resolve } from 'node:path';
|
|
14
14
|
import { pathToFileURL } from 'node:url';
|
|
15
|
-
import { resolveGitBase } from '
|
|
15
|
+
import { resolveGitBase } from '../lib/git-base.mjs';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* True when THIS file is run as a CLI (not imported). Each bin needs its own copy:
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* link. Composes the Card, Icon, Heading and Button primitives.
|
|
6
6
|
*/
|
|
7
7
|
import Heading from '../blocks/Heading.astro';
|
|
8
|
+
import { gridColumns } from '../../lib/gridColumns';
|
|
8
9
|
import Card from '../Card.astro';
|
|
9
10
|
import Icon from '../Icon.astro';
|
|
10
11
|
import Button from '../Button.astro';
|
|
@@ -24,7 +25,7 @@ const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | '
|
|
|
24
25
|
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
25
26
|
// Never reserve more columns than there are cards (a lone card in a 3-col grid
|
|
26
27
|
// would strand two empty cells); `data-single` centres a solitary card.
|
|
27
|
-
const cols =
|
|
28
|
+
const { tablet, desktop: cols } = gridColumns(cards.length, columns); // never a short last row
|
|
28
29
|
---
|
|
29
30
|
|
|
30
31
|
<section class="b-feature-cards">
|
|
@@ -34,7 +35,7 @@ const cols = Math.min(columns, Math.max(cards.length, 1)) as 1 | 2 | 3 | 4;
|
|
|
34
35
|
{intro && <p class="b-feature-cards__intro" set:html={inlineMarkdown(intro)} />}
|
|
35
36
|
</div>
|
|
36
37
|
)}
|
|
37
|
-
<div class="b-feature-cards__grid" data-cols={cols} data-single={cards.length === 1 ? 'true' : undefined}>
|
|
38
|
+
<div class="b-feature-cards__grid" data-cols={cols} data-tablet={tablet} data-single={cards.length === 1 ? 'true' : undefined}>
|
|
38
39
|
{cards.map((card) => (
|
|
39
40
|
<Card>
|
|
40
41
|
{card.icon && isIconKey(card.icon) && (
|
|
@@ -79,17 +80,19 @@ const cols = Math.min(columns, Math.max(cards.length, 1)) as 1 | 2 | 3 | 4;
|
|
|
79
80
|
grid-template-columns: 1fr; /* mobile-first */
|
|
80
81
|
gap: 1.5rem;
|
|
81
82
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.b-feature-cards__grid[data-
|
|
83
|
+
/* Never a short last row (lib/gridColumns): twos on tablet for an even count; on desktop
|
|
84
|
+
everything in one row, or even rows. Phones stay one column. */
|
|
85
|
+
@media (min-width: 40em) {
|
|
86
|
+
.b-feature-cards__grid[data-tablet='2'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
87
|
+
}
|
|
88
|
+
@media (min-width: 60em) {
|
|
89
|
+
.b-feature-cards__grid[data-cols='2'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
90
|
+
.b-feature-cards__grid[data-cols='3'] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
91
|
+
.b-feature-cards__grid[data-cols='4'] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
86
92
|
}
|
|
87
93
|
/* A solitary card is a centred feature, not a full-width slab. */
|
|
88
94
|
.b-feature-cards__grid[data-single] { max-width: 26rem; margin-inline: auto;
|
|
89
95
|
}
|
|
90
|
-
@media (min-width: 64em) {
|
|
91
|
-
.b-feature-cards__grid[data-cols='4'] { grid-template-columns: repeat(4, 1fr); }
|
|
92
|
-
}
|
|
93
96
|
.b-feature-cards__icon {
|
|
94
97
|
color: var(--gold);
|
|
95
98
|
margin-bottom: 0.5rem;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import ContactForm from './ContactForm.astro';
|
|
9
9
|
import { loadFormDefinition } from '../../lib/forms';
|
|
10
|
-
import { resolveGitBase } from '../../
|
|
10
|
+
import { resolveGitBase } from '../../lib/git-base.mjs';
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
form: string;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* when `href` is set. Token-driven, equal-height.
|
|
10
10
|
*/
|
|
11
11
|
import Icon from '../Icon.astro';
|
|
12
|
+
import { gridColumns } from '../../lib/gridColumns';
|
|
12
13
|
import { isIconKey } from '../../lib/icons';
|
|
13
14
|
import Heading from '../blocks/Heading.astro';
|
|
14
15
|
|
|
@@ -31,7 +32,7 @@ interface Props {
|
|
|
31
32
|
}
|
|
32
33
|
const { title, intro, columns = 3, ratio = '16/9', cards = [] } = Astro.props;
|
|
33
34
|
// Never reserve more columns than there are cards; `data-single` centres a lone one.
|
|
34
|
-
const cols =
|
|
35
|
+
const { tablet, desktop: cols } = gridColumns(cards.length, columns); // never a short last row
|
|
35
36
|
---
|
|
36
37
|
|
|
37
38
|
<section class="b-media-cards">
|
|
@@ -41,7 +42,7 @@ const cols = Math.min(columns, Math.max(cards.length, 1)) as 1 | 2 | 3;
|
|
|
41
42
|
{intro && <p class="b-media-cards__intro" set:html={inlineMarkdown(intro)} />}
|
|
42
43
|
</div>
|
|
43
44
|
)}
|
|
44
|
-
<div class="b-media-cards__grid" data-cols={cols} data-single={cards.length === 1 ? 'true' : undefined}>
|
|
45
|
+
<div class="b-media-cards__grid" data-cols={cols} data-tablet={tablet} data-single={cards.length === 1 ? 'true' : undefined}>
|
|
45
46
|
{cards.map((card) => {
|
|
46
47
|
// Surface a mailto:/tel: as visible, copyable text — a plain mailto does
|
|
47
48
|
// nothing on a device with no mail app, so the address must be readable too.
|
|
@@ -104,12 +105,15 @@ const cols = Math.min(columns, Math.max(cards.length, 1)) as 1 | 2 | 3;
|
|
|
104
105
|
}
|
|
105
106
|
/* A solitary card is a centred feature, not a full-width slab. */
|
|
106
107
|
.b-media-cards__grid[data-single] { max-width: 28rem; margin-inline: auto; }
|
|
108
|
+
/* Never a short last row (lib/gridColumns): twos on tablet for an even count; on desktop
|
|
109
|
+
everything in one row, or even rows. Phones stay one column. */
|
|
107
110
|
@media (min-width: 40em) {
|
|
108
|
-
.b-media-cards__grid[data-
|
|
109
|
-
.b-media-cards__grid[data-cols='3'] { grid-template-columns: repeat(2, 1fr); }
|
|
111
|
+
.b-media-cards__grid[data-tablet='2'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
110
112
|
}
|
|
111
113
|
@media (min-width: 60em) {
|
|
112
|
-
.b-media-cards__grid[data-cols='
|
|
114
|
+
.b-media-cards__grid[data-cols='2'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
115
|
+
.b-media-cards__grid[data-cols='3'] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
116
|
+
.b-media-cards__grid[data-cols='4'] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
113
117
|
}
|
|
114
118
|
.b-media-cards__card {
|
|
115
119
|
position: relative;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* at any count and in either theme. Labels are width-capped so a long caption
|
|
10
10
|
* wraps to a tidy block rather than a full-width line. */
|
|
11
11
|
import Heading from '../blocks/Heading.astro';
|
|
12
|
+
import { gridColumns } from '../../lib/gridColumns';
|
|
12
13
|
|
|
13
14
|
interface Item { value: string; label: string; description?: string }
|
|
14
15
|
interface Props {
|
|
@@ -25,7 +26,7 @@ const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | '
|
|
|
25
26
|
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
26
27
|
// Cap the column count at the item count — one stat should never sit in a
|
|
27
28
|
// two-column grid (half empty). Falls back to 1 for an empty list.
|
|
28
|
-
const cols =
|
|
29
|
+
const { tablet, desktop: cols } = gridColumns(items.length, columns); // never a short last row
|
|
29
30
|
---
|
|
30
31
|
|
|
31
32
|
<section class="b-stats" data-bg={background}>
|
|
@@ -36,7 +37,7 @@ const cols = Math.min(columns, Math.max(items.length, 1)) as 1 | 2 | 3 | 4;
|
|
|
36
37
|
{intro && <p class="b-stats__intro">{intro}</p>}
|
|
37
38
|
</div>
|
|
38
39
|
)}
|
|
39
|
-
<div class="b-stats__grid" data-cols={cols}>
|
|
40
|
+
<div class="b-stats__grid" data-cols={cols} data-tablet={tablet}>
|
|
40
41
|
{items.map((item) => (
|
|
41
42
|
<div class="b-stats__item ferst-tile">
|
|
42
43
|
<span class="b-stats__value">{item.value}</span>
|
|
@@ -75,12 +76,13 @@ const cols = Math.min(columns, Math.max(items.length, 1)) as 1 | 2 | 3 | 4;
|
|
|
75
76
|
width: 100%;
|
|
76
77
|
margin-inline: auto;
|
|
77
78
|
}
|
|
79
|
+
/* Never a short last row (lib/gridColumns): twos on tablet for an even count; on desktop
|
|
80
|
+
everything in one row, or even rows. Phones stay one column. */
|
|
78
81
|
@media (min-width: 40em) {
|
|
79
|
-
.b-stats__grid[data-
|
|
80
|
-
.b-stats__grid[data-cols='3'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
81
|
-
.b-stats__grid[data-cols='4'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
82
|
+
.b-stats__grid[data-tablet='2'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
82
83
|
}
|
|
83
84
|
@media (min-width: 60em) {
|
|
85
|
+
.b-stats__grid[data-cols='2'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
84
86
|
.b-stats__grid[data-cols='3'] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
85
87
|
.b-stats__grid[data-cols='4'] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
86
88
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* Composes the Icon primitive.
|
|
15
15
|
*/
|
|
16
16
|
import Heading from '../blocks/Heading.astro';
|
|
17
|
+
import { gridColumns } from '../../lib/gridColumns';
|
|
17
18
|
import Icon from '../Icon.astro';
|
|
18
19
|
import { isIconKey } from '../../lib/icons';
|
|
19
20
|
|
|
@@ -43,7 +44,7 @@ const ItemHeading = title ? 'h3' : 'h2';
|
|
|
43
44
|
const { align: blockAlign, headingAlign } = Astro.props as { align?: 'start' | 'center' | 'end'; headingAlign?: 'start' | 'center' | 'end' };
|
|
44
45
|
const headAlign = headingAlign ?? blockAlign ?? 'center';
|
|
45
46
|
// Never reserve more columns than there are tiles; `data-single` centres a lone one.
|
|
46
|
-
const cols =
|
|
47
|
+
const { tablet, desktop: cols } = gridColumns(tiles.length, columns); // never a short last row
|
|
47
48
|
---
|
|
48
49
|
|
|
49
50
|
<section class="b-tiles">
|
|
@@ -53,7 +54,7 @@ const cols = Math.min(columns, Math.max(tiles.length, 1)) as 1 | 2 | 3 | 4;
|
|
|
53
54
|
{intro && <p class="b-tiles__intro" set:html={inlineMarkdown(intro)} />}
|
|
54
55
|
</div>
|
|
55
56
|
)}
|
|
56
|
-
<div class="b-tiles__grid" data-cols={cols} data-single={tiles.length === 1 ? 'true' : undefined} data-variant={variant} data-spotlight={spotlight ? 'true' : 'false'}>
|
|
57
|
+
<div class="b-tiles__grid" data-cols={cols} data-tablet={tablet} data-single={tiles.length === 1 ? 'true' : undefined} data-variant={variant} data-spotlight={spotlight ? 'true' : 'false'}>
|
|
57
58
|
{tiles.map((tile) => {
|
|
58
59
|
const Tag = tile.href ? 'a' : 'div';
|
|
59
60
|
// A picture tile: an image with no words, shown WHOLE as a real <img> (posters,
|
|
@@ -114,13 +115,15 @@ const cols = Math.min(columns, Math.max(tiles.length, 1)) as 1 | 2 | 3 | 4;
|
|
|
114
115
|
}
|
|
115
116
|
/* A solitary tile is a centred feature, not a full-width slab. */
|
|
116
117
|
.b-tiles__grid[data-single] { max-width: 26rem; margin-inline: auto; }
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.b-tiles__grid[data-
|
|
121
|
-
}
|
|
122
|
-
@media (min-width:
|
|
123
|
-
.b-tiles__grid[data-cols='
|
|
118
|
+
/* Never a short last row (lib/gridColumns): twos on tablet for an even count; on desktop
|
|
119
|
+
everything in one row, or even rows. Phones stay one column. */
|
|
120
|
+
@media (min-width: 40em) {
|
|
121
|
+
.b-tiles__grid[data-tablet='2'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
122
|
+
}
|
|
123
|
+
@media (min-width: 60em) {
|
|
124
|
+
.b-tiles__grid[data-cols='2'] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
125
|
+
.b-tiles__grid[data-cols='3'] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
126
|
+
.b-tiles__grid[data-cols='4'] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
/* Base tile — radius/stroke/shadow all from the global surface tokens. */
|
package/lib/git-base.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform git-proxy origin for this build (the CMS `base_url`, and where the Form block
|
|
3
|
+
* reads a form's fields). Kept apart from bin/cms-config.mjs so components can import it: that
|
|
4
|
+
* file is a build-time script whose module scope needs a real filesystem, which an on-request
|
|
5
|
+
* renderer (Cloudflare Workers) doesn't have. Nothing here runs at import time.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
function env(name) {
|
|
12
|
+
const v = typeof process !== 'undefined' ? process.env?.[name] : undefined;
|
|
13
|
+
return v && v.trim() ? v.trim() : '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const PROD_GIT_BASE = 'https://signin.ferst.co.uk';
|
|
17
|
+
export const DEV_GIT_BASE = 'https://signin-dev.ferst.co.uk';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The platform git-proxy origin (CMS `base_url` / `api_root`) for THIS build:
|
|
21
|
+
* 1. `FERST_GIT_BASE` env — explicit override, always wins.
|
|
22
|
+
* 2. committed `ferst-site.json` `gitBase` — an explicit per-site pin (e.g. a
|
|
23
|
+
* custom proxy); a BYO/seeded site can hard-bind its origin this way.
|
|
24
|
+
* 3. otherwise DERIVE from the deploy environment: the production branch talks to
|
|
25
|
+
* the prod git-proxy, every other (preview/dev) branch talks to the dev
|
|
26
|
+
* git-proxy. So a templated repo self-configures per environment on deploy — a
|
|
27
|
+
* dev-branch build points its CMS at the dev worker and main at prod — with NO
|
|
28
|
+
* per-client config. (The production branch defaults to `main`; override via
|
|
29
|
+
* `FERST_PROD_BRANCH` env or `ferst-site.json` `productionBranch`.)
|
|
30
|
+
* This restores the "site respects the environment it's deployed in" binding the old
|
|
31
|
+
* per-branch site config gave, without a per-client file to maintain.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveGitBase(cwd = process.cwd(), branch = deploymentBranch()) {
|
|
34
|
+
const fromEnv = env('FERST_GIT_BASE');
|
|
35
|
+
if (fromEnv) return fromEnv;
|
|
36
|
+
let prodBranch = env('FERST_PROD_BRANCH') || 'main';
|
|
37
|
+
try {
|
|
38
|
+
const site = JSON.parse(readFileSync(resolve(cwd, 'ferst-site.json'), 'utf8'));
|
|
39
|
+
if (site && typeof site.gitBase === 'string' && site.gitBase.trim()) return site.gitBase.trim();
|
|
40
|
+
if (!env('FERST_PROD_BRANCH') && site && typeof site.productionBranch === 'string' && site.productionBranch.trim()) {
|
|
41
|
+
prodBranch = site.productionBranch.trim();
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
/* no ferst-site.json (e.g. the template repo itself) — derive from the branch */
|
|
45
|
+
}
|
|
46
|
+
return branch === prodBranch ? PROD_GIT_BASE : DEV_GIT_BASE;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The deployment branch: CF Pages env → current git branch → main. */
|
|
50
|
+
export function deploymentBranch() {
|
|
51
|
+
const cf = env('CF_PAGES_BRANCH');
|
|
52
|
+
if (cf) return cf;
|
|
53
|
+
try {
|
|
54
|
+
const git = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
55
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
56
|
+
}).toString().trim();
|
|
57
|
+
if (git && git !== 'HEAD') return git;
|
|
58
|
+
} catch {
|
|
59
|
+
/* not a git checkout — fall through */
|
|
60
|
+
}
|
|
61
|
+
return 'main';
|
|
62
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many columns a grid of cards or numbers uses, so the last row is never short.
|
|
3
|
+
* - **Desktop:** everything in one row when it fits within `max`; otherwise the widest column
|
|
4
|
+
* count up to `max` that divides the items evenly; failing that, `max`.
|
|
5
|
+
* - **Tablet:** twos for an even count, one column for an odd one (an odd count sits in one row
|
|
6
|
+
* on desktop and one column below it, never 2 + 1).
|
|
7
|
+
* - **Phones:** always one column (the recipes' base style).
|
|
8
|
+
*/
|
|
9
|
+
export interface GridColumns { tablet: 1 | 2; desktop: number }
|
|
10
|
+
|
|
11
|
+
export function gridColumns(count: number, max: number): GridColumns {
|
|
12
|
+
if (count <= 1) return { tablet: 1, desktop: 1 };
|
|
13
|
+
let desktop = max;
|
|
14
|
+
if (count <= max) desktop = count;
|
|
15
|
+
else {
|
|
16
|
+
for (let c = max; c >= 2; c--) {
|
|
17
|
+
if (count % c === 0) { desktop = c; break; }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const tablet = desktop > 1 && count % 2 === 0 ? 2 : 1;
|
|
21
|
+
return { tablet, desktop };
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ferst-core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ferst Core — the shared, brand-agnostic client-site system: Astro components, layouts, content schemas, the layered config resolver, and a neutral styling architecture that every client overrides.",
|
|
6
6
|
"license": "BUSL-1.1",
|