@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 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 Import (file: protocol)
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.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 component using native <dialog>.
4
- * Provides no visual styling beyond structure -- consumers apply their own theme.
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
- id={id}
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={['relative overflow-hidden outline-none w-[100dvw] max-w-4xl max-h-[100dvh]', panelClass]}
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
- id={`${id}-close`}
33
- class='absolute top-3 right-6 w-10 h-10 rounded-full z-50 flex items-center justify-center'
34
- aria-label='Close modal'
35
- >
36
- <svg width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'>
37
- <path d='M18 6L6 18' /><path d='M6 6L18 18' />
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='relative overflow-y-auto max-h-[calc(100dvh-2rem)]'>
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>(`#${dialog.id}-close`);
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
+ }