sitevision-cli 0.5.0-beta.0 → 0.6.0-beta.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/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { render } from 'ink';
4
4
  import { Text, Box } from 'ink';
5
5
  import meow from 'meow';
@@ -11,7 +11,8 @@ import { promptYesNo } from './utils/password-prompt.js';
11
11
  import { checkForUpdate } from './utils/version-check.js';
12
12
  import { isFirstRun, markFirstRunComplete, getLastSeenVersion, setLastSeenVersion, } from './utils/config.js';
13
13
  import { WelcomeScreen } from './components/WelcomeScreen.js';
14
- import { printBranding } from './utils/branding.js';
14
+ import { AnimatedLogo } from './components/AnimatedLogo.js';
15
+ import { printBranding, BIG_LOGO_WIDTH } from './utils/branding.js';
15
16
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
16
17
  const cli = meow(`
17
18
  Usage
@@ -84,6 +85,13 @@ function printMasthead(version) {
84
85
  `${spaces(gap)}${DIM}${right}${RESET}${spaces(padding)}${CYAN}│${RESET}`);
85
86
  console.log(`${CYAN}╰${border}╯${RESET}`);
86
87
  }
88
+ // Play the one-shot animated wordmark and resolve once it finishes.
89
+ async function playIntro() {
90
+ await new Promise(resolve => {
91
+ const app = render(_jsx(AnimatedLogo, { onDone: () => app.unmount() }));
92
+ app.waitUntilExit().then(() => resolve(), () => resolve());
93
+ });
94
+ }
87
95
  async function main() {
88
96
  // On the very first run we show a dedicated welcome screen instead of the
89
97
  // masthead, so the branding is the moment. Only when stdin is a TTY — the
@@ -95,12 +103,20 @@ async function main() {
95
103
  // silently rather than claiming an update happened.
96
104
  const lastSeen = getLastSeenVersion();
97
105
  const isUpdate = !firstRun && lastSeen !== undefined && lastSeen !== pkg.version;
106
+ // On the plain interactive `svc` (no command), play the animated wordmark
107
+ // instead of the static masthead — but only when stdout is wide enough for
108
+ // the art and stdin is a TTY (so it doesn't run in CI / piped input).
109
+ const wantsIntro = !firstRun &&
110
+ !isUpdate &&
111
+ !commandName &&
112
+ Boolean(process.stdin.isTTY) &&
113
+ (process.stdout.columns ?? 0) >= BIG_LOGO_WIDTH;
98
114
  if (!firstRun) {
99
115
  if (isUpdate) {
100
116
  printBranding();
101
117
  console.log(`\x1b[32m\n ✨ Updated to v${pkg.version}\x1b[0m \x1b[2m(from v${lastSeen})\x1b[0m\n`);
102
118
  }
103
- else {
119
+ else if (!wantsIntro) {
104
120
  printMasthead(pkg.version);
105
121
  }
106
122
  // Record the current version so the banner shows once per upgrade.
@@ -133,8 +149,12 @@ async function main() {
133
149
  app.waitUntilExit().then(() => resolve(), () => resolve());
134
150
  });
135
151
  }
136
- // If no command, show interactive menu
152
+ // If no command, show interactive menu (with the animated intro first when
153
+ // the terminal can fit it).
137
154
  if (!commandName) {
155
+ if (wantsIntro) {
156
+ await playIntro();
157
+ }
138
158
  render(_jsx(App, { project: project }));
139
159
  return;
140
160
  }
@@ -0,0 +1,9 @@
1
+ interface Props {
2
+ onDone: () => void;
3
+ }
4
+ /**
5
+ * One-shot startup flair: wipes the big wordmark in left-to-right while a
6
+ * rainbow gradient drifts across it, then calls `onDone`. Purely decorative.
7
+ */
8
+ export declare function AnimatedLogo({ onDone }: Props): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Fragment, useEffect, useRef, useState } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { BIG_LOGO, BIG_LOGO_WIDTH } from '../utils/branding.js';
5
+ const FRAME_MS = 45;
6
+ const REVEAL_COLS_PER_FRAME = 9; // how fast the wipe sweeps left → right
7
+ const HOLD_FRAMES = 20; // frames to keep cycling colours once fully revealed
8
+ const REVEAL_FRAMES = Math.ceil(BIG_LOGO_WIDTH / REVEAL_COLS_PER_FRAME);
9
+ const TOTAL_FRAMES = REVEAL_FRAMES + HOLD_FRAMES;
10
+ // Convert HSL (h in degrees, s/l in 0..1) to a #rrggbb string for ink/chalk.
11
+ function hslToHex(h, s, l) {
12
+ const hue = h / 360;
13
+ const a = s * Math.min(l, 1 - l);
14
+ const channel = (n) => {
15
+ const scaled = hue * 12;
16
+ const k = (n + scaled) % 12;
17
+ const offset = a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
18
+ const value = l - offset;
19
+ return Math.round(255 * value)
20
+ .toString(16)
21
+ .padStart(2, '0');
22
+ };
23
+ return `#${channel(0)}${channel(8)}${channel(4)}`;
24
+ }
25
+ function buildSpans(line, y, frame, reveal) {
26
+ const spans = [];
27
+ for (const [x, char] of [...line].entries()) {
28
+ const hidden = x >= reveal;
29
+ const blank = char === ' ' || hidden;
30
+ // Moving diagonal rainbow: hue depends on column + row + time, quantised
31
+ // so neighbouring characters share a colour and runs stay long.
32
+ const col = x * 1.6;
33
+ const row = y * 6;
34
+ const time = frame * 7;
35
+ const stepped = Math.round((col + row + time) / 8) * 8;
36
+ const hue = blank ? undefined : stepped % 360;
37
+ // Shadow characters sit darker than the solid blocks for a bit of depth.
38
+ const color = hue === undefined
39
+ ? undefined
40
+ : hslToHex(hue, 0.95, char === '░' ? 0.32 : 0.58);
41
+ const text = hidden ? ' ' : char;
42
+ const last = spans.at(-1);
43
+ if (last && last.color === color) {
44
+ last.text += text;
45
+ }
46
+ else {
47
+ spans.push({ text, color });
48
+ }
49
+ }
50
+ return spans;
51
+ }
52
+ /**
53
+ * One-shot startup flair: wipes the big wordmark in left-to-right while a
54
+ * rainbow gradient drifts across it, then calls `onDone`. Purely decorative.
55
+ */
56
+ export function AnimatedLogo({ onDone }) {
57
+ const [frame, setFrame] = useState(0);
58
+ const intervalRef = useRef(undefined);
59
+ useEffect(() => {
60
+ intervalRef.current = setInterval(() => {
61
+ setFrame(current => current + 1);
62
+ }, FRAME_MS);
63
+ return () => {
64
+ clearInterval(intervalRef.current);
65
+ };
66
+ }, []);
67
+ // Stop the loop and notify the parent exactly once, when the last frame is
68
+ // reached. Kept out of the setFrame updater so that updater stays pure.
69
+ useEffect(() => {
70
+ if (frame >= TOTAL_FRAMES) {
71
+ clearInterval(intervalRef.current);
72
+ onDone();
73
+ }
74
+ }, [frame, onDone]);
75
+ const reveal = frame >= REVEAL_FRAMES
76
+ ? BIG_LOGO_WIDTH
77
+ : (frame + 1) * REVEAL_COLS_PER_FRAME;
78
+ return (_jsx(Box, { flexDirection: "column", padding: 1, children: BIG_LOGO.map((line, y) => (_jsx(Text, { children: buildSpans(line, y, frame, reveal).map((span, index) => (_jsx(Fragment, { children: span.color ? (_jsx(Text, { color: span.color, children: span.text })) : (_jsx(Text, { children: span.text })) }, index))) }, y))) }));
79
+ }
@@ -5,6 +5,15 @@
5
5
  */
6
6
  export declare const LOGO: string[];
7
7
  export declare const AUTHOR = "Rasmus S\u00F6derstr\u00F6m";
8
+ /**
9
+ * Big block-shadow "Sitevision CLI" wordmark, used by the animated startup
10
+ * intro (see components/AnimatedLogo). It's ~120 columns wide, so callers
11
+ * should only render it when the terminal is at least that wide — otherwise it
12
+ * wraps and looks broken.
13
+ */
14
+ export declare const BIG_LOGO: string[];
15
+ /** Display width of the widest BIG_LOGO line. */
16
+ export declare const BIG_LOGO_WIDTH: number;
8
17
  /**
9
18
  * Print the logo + author line straight to stdout (non-interactive), mirroring
10
19
  * how the masthead is printed. Used for the update banner.
@@ -17,6 +17,24 @@ export const LOGO = [
17
17
  ' └──────────┘ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄',
18
18
  ];
19
19
  export const AUTHOR = 'Rasmus Söderström';
20
+ /**
21
+ * Big block-shadow "Sitevision CLI" wordmark, used by the animated startup
22
+ * intro (see components/AnimatedLogo). It's ~120 columns wide, so callers
23
+ * should only render it when the terminal is at least that wide — otherwise it
24
+ * wraps and looks broken.
25
+ */
26
+ export const BIG_LOGO = [
27
+ ' █████████ ███ █████ ███ ███ █████████ █████ █████',
28
+ ' ███░░░░░███ ░░░ ░░███ ░░░ ░░░ ███░░░░░███░░███ ░░███ ',
29
+ '░███ ░░░ ████ ███████ ██████ █████ █████ ████ █████ ████ ██████ ████████ ███ ░░░ ░███ ░███ ',
30
+ '░░█████████ ░░███ ░░░███░ ███░░███░░███ ░░███ ░░███ ███░░ ░░███ ███░░███░░███░░███ ░███ ░███ ░███ ',
31
+ ' ░░░░░░░░███ ░███ ░███ ░███████ ░███ ░███ ░███ ░░█████ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ',
32
+ ' ███ ░███ ░███ ░███ ███░███░░░ ░░███ ███ ░███ ░░░░███ ░███ ░███ ░███ ░███ ░███ ░░███ ███ ░███ █ ░███ ',
33
+ '░░█████████ █████ ░░█████ ░░██████ ░░█████ █████ ██████ █████░░██████ ████ █████ ░░█████████ ███████████ █████',
34
+ ' ░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░░░░░ ░░░░░░░░░░░ ░░░░░ ',
35
+ ];
36
+ /** Display width of the widest BIG_LOGO line. */
37
+ export const BIG_LOGO_WIDTH = Math.max(...BIG_LOGO.map(line => line.length));
20
38
  /**
21
39
  * Print the logo + author line straight to stdout (non-interactive), mirroring
22
40
  * how the masthead is printed. Used for the update banner.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sitevision-cli",
3
- "version": "0.5.0-beta.0",
3
+ "version": "0.6.0-beta.0",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "svc": "dist/cli.js"