@at-flux/astroflare 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -2
- package/package.json +5 -1
- package/scripts/link-local.mjs +197 -0
- package/src/components/Modal.astro +33 -18
- package/src/components/Section.astro +27 -0
package/README.md
CHANGED
|
@@ -33,8 +33,9 @@ import { forms } from '@at-flux/astroflare/core';
|
|
|
33
33
|
|
|
34
34
|
### Components (Astro)
|
|
35
35
|
|
|
36
|
-
- `Modal.astro` — Headless modal using native `<dialog>` and `<app-modal>` web component
|
|
36
|
+
- `Modal.astro` — Headless modal using native `<dialog>` and `<app-modal>` web component (`class` applies to the panel)
|
|
37
37
|
- `ModalTrigger.astro` — Trigger that opens a modal by ID using `<modal-trigger>` web component
|
|
38
|
+
- `Section.astro` — Page section with optional `narrow` and `contentOnly` (inner width wrapper without outer padding)
|
|
38
39
|
- `ThemeToggle.astro` — Dark/light mode toggle using `<theme-toggle>` web component
|
|
39
40
|
- `ClientRouterLoadingSpinner.astro` — Loading spinner for Astro view transitions
|
|
40
41
|
|
|
@@ -47,7 +48,28 @@ import { forms } from '@at-flux/astroflare/core';
|
|
|
47
48
|
|
|
48
49
|
## Usage
|
|
49
50
|
|
|
50
|
-
### Local
|
|
51
|
+
### Local checkout without changing package.json or lockfile
|
|
52
|
+
|
|
53
|
+
Keep **`@at-flux/astroflare`** on a normal semver range in `package.json` and run **`pnpm install`** so the lockfile records the registry version. Then overlay the install with a symlink (only under `node_modules`):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pnpm exec astroflare-link-local link /absolute/or/relative/path/to/astroflare/packages/astroflare
|
|
57
|
+
# or
|
|
58
|
+
ASTROFLARE_LOCAL_PATH=../../ts-libs/astroflare/packages/astroflare pnpm exec astroflare-link-local
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **`USE_LOCAL_ASTROFLARE=1`** (or `true` / `yes`) is supported only together with **`ASTROFLARE_LOCAL_PATH`** or **`USE_LOCAL_ASTROFLARE_PATH`** (path to `packages/astroflare`).
|
|
62
|
+
- **`unlink`** removes the overlay and runs **`pnpm install`** again so `node_modules` matches the lockfile:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pnpm exec astroflare-link-local unlink
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **`status`** shows whether an overlay is active.
|
|
69
|
+
|
|
70
|
+
Re-running **`pnpm install`** may replace the symlink with the store copy; run **`link`** again if that happens.
|
|
71
|
+
|
|
72
|
+
### Local Import (file: protocol) — alternative
|
|
51
73
|
|
|
52
74
|
```json
|
|
53
75
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@at-flux/astroflare",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Reusable headless components, styles, and utilities for Astro + Tailwind v4 + Cloudflare projects",
|
|
6
6
|
"author": "Taylor Siviter <taylor@siviter.xyz>",
|
|
@@ -49,10 +49,14 @@
|
|
|
49
49
|
"./components/*": "./src/components/*",
|
|
50
50
|
"./styles/*": "./src/styles/*"
|
|
51
51
|
},
|
|
52
|
+
"bin": {
|
|
53
|
+
"astroflare-link-local": "./scripts/link-local.mjs"
|
|
54
|
+
},
|
|
52
55
|
"files": [
|
|
53
56
|
"dist",
|
|
54
57
|
"src/components",
|
|
55
58
|
"src/styles",
|
|
59
|
+
"scripts/link-local.mjs",
|
|
56
60
|
"README.md"
|
|
57
61
|
],
|
|
58
62
|
"devDependencies": {
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Overlay @at-flux/astroflare in node_modules with a symlink to a local checkout.
|
|
4
|
+
* Does not change package.json or the lockfile — run `pnpm install` first so the
|
|
5
|
+
* registry version is recorded, then this replaces only the on-disk resolution.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { platform } from 'node:os';
|
|
11
|
+
|
|
12
|
+
const MARKER = '.astroflare-local-overlay.json';
|
|
13
|
+
|
|
14
|
+
function findProjectRoot(start = process.cwd()) {
|
|
15
|
+
let dir = resolve(start);
|
|
16
|
+
for (;;) {
|
|
17
|
+
const pkgPath = join(dir, 'package.json');
|
|
18
|
+
if (existsSync(pkgPath)) {
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
21
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
22
|
+
if (deps['@at-flux/astroflare']) {
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
/* continue */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const parent = dirname(dir);
|
|
30
|
+
if (parent === dir) {
|
|
31
|
+
console.error('astroflare-link-local: could not find a package.json that depends on @at-flux/astroflare');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
dir = parent;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function markerPath(projectRoot) {
|
|
39
|
+
return join(projectRoot, 'node_modules', MARKER);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function nmTarget(projectRoot) {
|
|
43
|
+
return join(projectRoot, 'node_modules', '@at-flux', 'astroflare');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveLocalPath(projectRoot, raw) {
|
|
47
|
+
if (!raw || typeof raw !== 'string') {
|
|
48
|
+
console.error(
|
|
49
|
+
'astroflare-link-local: pass a path to packages/astroflare, or set ASTROFLARE_LOCAL_PATH or USE_LOCAL_ASTROFLARE_PATH',
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const abs = isAbsolute(raw) ? raw : resolve(projectRoot, raw);
|
|
54
|
+
const pkgJson = join(abs, 'package.json');
|
|
55
|
+
if (!existsSync(pkgJson)) {
|
|
56
|
+
console.error(`astroflare-link-local: not a package directory (missing package.json): ${abs}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const { name } = JSON.parse(readFileSync(pkgJson, 'utf8'));
|
|
61
|
+
if (name !== '@at-flux/astroflare') {
|
|
62
|
+
console.error(`astroflare-link-local: expected name @at-flux/astroflare, got ${name}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.error('astroflare-link-local: could not read local package.json', e);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
return abs;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function symlinkType() {
|
|
73
|
+
return platform() === 'win32' ? 'junction' : 'dir';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function cmdLink(projectRoot, localAbs) {
|
|
77
|
+
const target = nmTarget(projectRoot);
|
|
78
|
+
const fluxScope = dirname(target);
|
|
79
|
+
|
|
80
|
+
rmSync(target, { recursive: true, force: true });
|
|
81
|
+
mkdirSync(fluxScope, { recursive: true });
|
|
82
|
+
symlinkSync(localAbs, target, symlinkType());
|
|
83
|
+
|
|
84
|
+
mkdirSync(join(projectRoot, 'node_modules'), { recursive: true });
|
|
85
|
+
writeFileSync(
|
|
86
|
+
markerPath(projectRoot),
|
|
87
|
+
`${JSON.stringify({ localPath: localAbs, linkedAt: new Date().toISOString() }, null, 2)}\n`,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
console.log(`astroflare-link-local: linked\n ${target}\n → ${localAbs}`);
|
|
91
|
+
console.log('astroflare-link-local: package.json and lockfile unchanged. Run `pnpm install` again only if you need to restore the registry copy (or use `unlink`).');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function cmdUnlink(projectRoot, runInstall) {
|
|
95
|
+
const target = nmTarget(projectRoot);
|
|
96
|
+
const marker = markerPath(projectRoot);
|
|
97
|
+
|
|
98
|
+
rmSync(target, { recursive: true, force: true });
|
|
99
|
+
rmSync(marker, { force: true });
|
|
100
|
+
|
|
101
|
+
if (runInstall) {
|
|
102
|
+
const r = spawnSync('pnpm', ['install'], { cwd: projectRoot, stdio: 'inherit', shell: true });
|
|
103
|
+
if (r.error) {
|
|
104
|
+
console.error('astroflare-link-local: pnpm install failed:', r.error.message);
|
|
105
|
+
process.exit(r.status ?? 1);
|
|
106
|
+
}
|
|
107
|
+
if (r.status !== 0) {
|
|
108
|
+
process.exit(r.status ?? 1);
|
|
109
|
+
}
|
|
110
|
+
console.log('astroflare-link-local: restored @at-flux/astroflare from the lockfile.');
|
|
111
|
+
} else {
|
|
112
|
+
console.log('astroflare-link-local: removed overlay. Run `pnpm install` to restore node_modules.');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function cmdStatus(projectRoot) {
|
|
117
|
+
const marker = markerPath(projectRoot);
|
|
118
|
+
const target = nmTarget(projectRoot);
|
|
119
|
+
if (!existsSync(marker)) {
|
|
120
|
+
console.log('astroflare-link-local: no local overlay (registry / store layout).');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const data = JSON.parse(readFileSync(marker, 'utf8'));
|
|
125
|
+
console.log('astroflare-link-local: overlay active');
|
|
126
|
+
console.log(` marker: ${marker}`);
|
|
127
|
+
console.log(` local: ${data.localPath}`);
|
|
128
|
+
if (existsSync(target)) {
|
|
129
|
+
console.log(` node_modules: ${target}`);
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
console.log('astroflare-link-local: marker present but invalid');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function localPathFromEnv() {
|
|
137
|
+
return process.env.ASTROFLARE_LOCAL_PATH || process.env.USE_LOCAL_ASTROFLARE_PATH || null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function useLocalAstroflareEnv() {
|
|
141
|
+
const v = process.env.USE_LOCAL_ASTROFLARE;
|
|
142
|
+
return v === '1' || v === 'true' || v === 'yes';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const argv = process.argv.slice(2);
|
|
146
|
+
|
|
147
|
+
if (argv[0] === 'help' || argv[0] === '-h' || argv[0] === '--help') {
|
|
148
|
+
console.log(`Usage:
|
|
149
|
+
astroflare-link-local [link] <path-to-packages/astroflare>
|
|
150
|
+
astroflare-link-local # uses ASTROFLARE_LOCAL_PATH or USE_LOCAL_ASTROFLARE_PATH (required)
|
|
151
|
+
USE_LOCAL_ASTROFLARE=1 must be paired with a path via ASTROFLARE_LOCAL_PATH or USE_LOCAL_ASTROFLARE_PATH
|
|
152
|
+
|
|
153
|
+
astroflare-link-local unlink # remove symlink; run pnpm install
|
|
154
|
+
astroflare-link-local unlink --no-install
|
|
155
|
+
astroflare-link-local status
|
|
156
|
+
|
|
157
|
+
Symlinks node_modules/@at-flux/astroflare → local checkout. Does not edit package.json or lockfile.`);
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const projectRoot = findProjectRoot();
|
|
162
|
+
|
|
163
|
+
if (argv[0] === 'unlink') {
|
|
164
|
+
const noInstall = argv.includes('--no-install');
|
|
165
|
+
cmdUnlink(projectRoot, !noInstall);
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (argv[0] === 'status') {
|
|
170
|
+
cmdStatus(projectRoot);
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let rawPath;
|
|
175
|
+
if (argv[0] === 'link') {
|
|
176
|
+
rawPath = argv[1] ?? localPathFromEnv();
|
|
177
|
+
} else if (argv[0]) {
|
|
178
|
+
rawPath = argv[0];
|
|
179
|
+
} else {
|
|
180
|
+
rawPath = localPathFromEnv();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (useLocalAstroflareEnv() && !rawPath) {
|
|
184
|
+
console.error(
|
|
185
|
+
'astroflare-link-local: USE_LOCAL_ASTROFLARE is set; also set ASTROFLARE_LOCAL_PATH or USE_LOCAL_ASTROFLARE_PATH (or pass a path).',
|
|
186
|
+
);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!rawPath) {
|
|
191
|
+
console.error('astroflare-link-local: missing path. Pass <path>, or set ASTROFLARE_LOCAL_PATH / USE_LOCAL_ASTROFLARE_PATH.');
|
|
192
|
+
console.error('Run: astroflare-link-local --help');
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const localAbs = resolveLocalPath(projectRoot, rawPath);
|
|
197
|
+
cmdLink(projectRoot, localAbs);
|
|
@@ -1,44 +1,59 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* Headless modal
|
|
4
|
-
*
|
|
3
|
+
* Headless modal using native `<dialog>`.
|
|
4
|
+
* `class` applies to the modal panel (card). Dialog element keeps backdrop/transparent shell.
|
|
5
5
|
*/
|
|
6
6
|
interface Props {
|
|
7
7
|
id: string;
|
|
8
|
+
/** Panel (card) classes — surface, border, radius, etc. */
|
|
8
9
|
class?: string;
|
|
9
10
|
backdropClass?: string;
|
|
11
|
+
/** Extra panel classes merged after `class`. */
|
|
10
12
|
panelClass?: string;
|
|
13
|
+
closeButtonClass?: string;
|
|
14
|
+
/** Scrollable inner wrapper around the default slot. */
|
|
15
|
+
contentClass?: string;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
const {
|
|
14
19
|
id,
|
|
15
20
|
class: className = '',
|
|
16
|
-
backdropClass = 'backdrop:bg-black/80',
|
|
21
|
+
backdropClass = 'backdrop:bg-black/80 bg-transparent p-0 w-full h-full max-w-none max-h-none',
|
|
17
22
|
panelClass = '',
|
|
23
|
+
closeButtonClass = 'absolute top-3 right-6 z-50 flex h-10 w-10 items-center justify-center rounded-full opacity-80 transition-opacity hover:opacity-100',
|
|
24
|
+
contentClass = 'relative overflow-y-auto max-h-[calc(100dvh-2rem)]',
|
|
18
25
|
} = Astro.props;
|
|
19
26
|
---
|
|
20
27
|
|
|
21
28
|
<app-modal>
|
|
22
|
-
<dialog
|
|
23
|
-
|
|
24
|
-
class:list={['bg-transparent p-0 w-full h-full max-w-none max-h-none', backdropClass, className]}
|
|
25
|
-
>
|
|
26
|
-
<div class='flex items-center justify-center min-h-full p-0 md:p-4'>
|
|
29
|
+
<dialog id={id} class:list={[backdropClass]}>
|
|
30
|
+
<div class='flex min-h-full items-center justify-center p-0 md:p-4'>
|
|
27
31
|
<div
|
|
28
|
-
class:list={[
|
|
32
|
+
class:list={[
|
|
33
|
+
'relative max-h-[100dvh] w-[100dvw] max-w-4xl overflow-hidden outline-none',
|
|
34
|
+
className,
|
|
35
|
+
panelClass,
|
|
36
|
+
]}
|
|
29
37
|
role='dialog'
|
|
30
38
|
>
|
|
31
|
-
<button
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
<button class:list={[closeButtonClass]} type='button' aria-label='Close modal'>
|
|
40
|
+
<svg
|
|
41
|
+
width='20'
|
|
42
|
+
height='20'
|
|
43
|
+
viewBox='0 0 24 24'
|
|
44
|
+
fill='none'
|
|
45
|
+
stroke='currentColor'
|
|
46
|
+
stroke-width='2'
|
|
47
|
+
stroke-linecap='round'
|
|
48
|
+
stroke-linejoin='round'
|
|
49
|
+
aria-hidden='true'
|
|
50
|
+
>
|
|
51
|
+
<path d='M18 6L6 18'></path>
|
|
52
|
+
<path d='M6 6L18 18'></path>
|
|
38
53
|
</svg>
|
|
39
54
|
</button>
|
|
40
55
|
|
|
41
|
-
<div class=
|
|
56
|
+
<div class:list={[contentClass]}>
|
|
42
57
|
<slot />
|
|
43
58
|
</div>
|
|
44
59
|
</div>
|
|
@@ -52,7 +67,7 @@ const {
|
|
|
52
67
|
const dialog = this.querySelector<HTMLDialogElement>('dialog');
|
|
53
68
|
if (!dialog) return;
|
|
54
69
|
|
|
55
|
-
const closeBtn = this.querySelector<HTMLButtonElement>(
|
|
70
|
+
const closeBtn = this.querySelector<HTMLButtonElement>('button[aria-label="Close modal"]');
|
|
56
71
|
closeBtn?.addEventListener('click', () => dialog.close());
|
|
57
72
|
}
|
|
58
73
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
id?: string;
|
|
4
|
+
class?: string;
|
|
5
|
+
narrow?: boolean;
|
|
6
|
+
/** Inner max-width wrapper only — no section padding (e.g. modal body). */
|
|
7
|
+
contentOnly?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { id, class: className = '', narrow = false, contentOnly = false } = Astro.props;
|
|
11
|
+
|
|
12
|
+
const innerClass = ['mx-auto w-full', narrow ? 'max-w-4xl' : 'max-w-6xl'];
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
contentOnly ? (
|
|
17
|
+
<div class:list={innerClass}>
|
|
18
|
+
<slot />
|
|
19
|
+
</div>
|
|
20
|
+
) : (
|
|
21
|
+
<section id={id} class:list={['px-6 pt-10 pb-16 md:px-8 md:pt-16 md:pb-24', className]}>
|
|
22
|
+
<div class:list={innerClass}>
|
|
23
|
+
<slot />
|
|
24
|
+
</div>
|
|
25
|
+
</section>
|
|
26
|
+
)
|
|
27
|
+
}
|