♻️ Restructure: Move Astro to repository root

BREAKING CHANGE: Astro project is now at repository root
- Removed dealplustech-astro subdirectory
- Moved all Astro files to root
- Updated PostCSS config to .cjs
- Removed old Next.js files

 11 pages built successfully
 Cookie consent banner included
 Privacy/Terms links in footer
 Ready for Easypanel deployment (no root dir needed)

Migration path:
- Old structure: /dealplustech-astro/
- New structure: / (root)
This commit is contained in:
Kunthawat Greethong
2026-03-09 22:00:05 +07:00
parent 5b041a6a44
commit 7a67f68d9f
16524 changed files with 4277 additions and 1983574 deletions

View File

@@ -1,267 +0,0 @@
import {type LiteralUnion} from 'type-fest';
import {type BoxStyle, type Boxes as CLIBoxes} from 'cli-boxes';
/**
All box styles.
*/
type Boxes = {
readonly none: BoxStyle;
} & CLIBoxes;
/**
Characters used for custom border.
@example
```
// attttb
// l r
// dbbbbc
const border: CustomBorderStyle = {
topLeft: 'a',
topRight: 'b',
bottomRight: 'c',
bottomLeft: 'd',
left: 'l',
right: 'r',
top: 't',
bottom: 'b',
};
```
*/
export type CustomBorderStyle = {
/**
@deprecated Use `top` and `bottom` instead.
*/
horizontal?: string;
/**
@deprecated Use `left` and `right` instead.
*/
vertical?: string;
} & BoxStyle;
/**
Spacing used for `padding` and `margin`.
*/
export type Spacing = {
readonly top?: number;
readonly right?: number;
readonly bottom?: number;
readonly left?: number;
};
export type Options = {
/**
Color of the box border.
*/
readonly borderColor?: LiteralUnion<
| 'black'
| 'red'
| 'green'
| 'yellow'
| 'blue'
| 'magenta'
| 'cyan'
| 'white'
| 'gray'
| 'grey'
| 'blackBright'
| 'redBright'
| 'greenBright'
| 'yellowBright'
| 'blueBright'
| 'magentaBright'
| 'cyanBright'
| 'whiteBright',
string
>;
/**
Style of the box border.
@default 'single'
*/
readonly borderStyle?: keyof Boxes | CustomBorderStyle;
/**
Reduce opacity of the border.
@default false
*/
readonly dimBorder?: boolean;
/**
Space between the text and box border.
@default 0
*/
readonly padding?: number | Spacing;
/**
Space around the box.
@default 0
*/
readonly margin?: number | Spacing;
/**
Float the box on the available terminal screen space.
@default 'left'
*/
readonly float?: 'left' | 'right' | 'center';
/**
Color of the background.
*/
readonly backgroundColor?: LiteralUnion<
| 'black'
| 'red'
| 'green'
| 'yellow'
| 'blue'
| 'magenta'
| 'cyan'
| 'white'
| 'blackBright'
| 'redBright'
| 'greenBright'
| 'yellowBright'
| 'blueBright'
| 'magentaBright'
| 'cyanBright'
| 'whiteBright',
string
>;
/**
Align the text in the box based on the widest line.
@default 'left'
@deprecated Use `textAlignment` instead.
*/
readonly align?: 'left' | 'right' | 'center';
/**
Align the text in the box based on the widest line.
@default 'left'
*/
readonly textAlignment?: 'left' | 'right' | 'center';
/**
Display a title at the top of the box.
If needed, the box will horizontally expand to fit the title.
@example
```
console.log(boxen('foo bar', {title: 'example'}));
// ┌ example ┐
// │foo bar │
// └─────────┘
```
*/
readonly title?: string;
/**
Align the title in the top bar.
@default 'left'
@example
```
console.log(boxen('foo bar foo bar', {title: 'example', titleAlignment: 'center'}));
// ┌─── example ───┐
// │foo bar foo bar│
// └───────────────┘
console.log(boxen('foo bar foo bar', {title: 'example', titleAlignment: 'right'}));
// ┌────── example ┐
// │foo bar foo bar│
// └───────────────┘
```
*/
readonly titleAlignment?: 'left' | 'right' | 'center';
/**
Set a fixed width for the box.
__Note__: This disables terminal overflow handling and may cause the box to look broken if the user's terminal is not wide enough.
@example
```
import boxen from 'boxen';
console.log(boxen('foo bar', {width: 15}));
// ┌─────────────┐
// │foo bar │
// └─────────────┘
```
*/
readonly width?: number;
/**
Set a fixed height for the box.
__Note__: This option will crop overflowing content.
@example
```
import boxen from 'boxen';
console.log(boxen('foo bar', {height: 5}));
// ┌───────┐
// │foo bar│
// │ │
// │ │
// └───────┘
```
*/
readonly height?: number;
/**
__boolean__: Whether or not to fit all available space within the terminal.
__function__: Pass a callback function to control box dimensions.
@example
```
import boxen from 'boxen';
console.log(boxen('foo bar', {
fullscreen: (width, height) => [width, height - 1],
}));
```
*/
readonly fullscreen?: boolean | ((width: number, height: number) => [width: number, height: number]);
};
/**
Creates a box in the terminal.
@param text - The text inside the box.
@returns The box.
@example
```
import boxen from 'boxen';
console.log(boxen('unicorn', {padding: 1}));
// ┌─────────────┐
// │ │
// │ unicorn │
// │ │
// └─────────────┘
console.log(boxen('unicorn', {padding: 1, margin: 1, borderStyle: 'double'}));
//
// ╔═════════════╗
// ║ ║
// ║ unicorn ║
// ║ ║
// ╚═════════════╝
//
```
*/
export default function boxen(text: string, options?: Options): string;

View File

@@ -1,376 +0,0 @@
import process from 'node:process';
import stringWidth from 'string-width';
import chalk from 'chalk';
import widestLine from 'widest-line';
import cliBoxes from 'cli-boxes';
import camelCase from 'camelcase';
import ansiAlign from 'ansi-align';
import wrapAnsi from 'wrap-ansi';
const NEWLINE = '\n';
const PAD = ' ';
const NONE = 'none';
const terminalColumns = () => {
const {env, stdout, stderr} = process;
if (stdout?.columns) {
return stdout.columns;
}
if (stderr?.columns) {
return stderr.columns;
}
if (env.COLUMNS) {
return Number.parseInt(env.COLUMNS, 10);
}
return 80;
};
const getObject = detail => typeof detail === 'number' ? {
top: detail,
right: detail * 3,
bottom: detail,
left: detail * 3,
} : {
top: 0,
right: 0,
bottom: 0,
left: 0,
...detail,
};
const getBorderWidth = borderStyle => borderStyle === NONE ? 0 : 2;
const getBorderChars = borderStyle => {
const sides = [
'topLeft',
'topRight',
'bottomRight',
'bottomLeft',
'left',
'right',
'top',
'bottom',
];
let characters;
// Create empty border style
if (borderStyle === NONE) {
borderStyle = {};
for (const side of sides) {
borderStyle[side] = '';
}
}
if (typeof borderStyle === 'string') {
characters = cliBoxes[borderStyle];
if (!characters) {
throw new TypeError(`Invalid border style: ${borderStyle}`);
}
} else {
// Ensure retro-compatibility
if (typeof borderStyle?.vertical === 'string') {
borderStyle.left = borderStyle.vertical;
borderStyle.right = borderStyle.vertical;
}
// Ensure retro-compatibility
if (typeof borderStyle?.horizontal === 'string') {
borderStyle.top = borderStyle.horizontal;
borderStyle.bottom = borderStyle.horizontal;
}
for (const side of sides) {
if (borderStyle[side] === null || typeof borderStyle[side] !== 'string') {
throw new TypeError(`Invalid border style: ${side}`);
}
}
characters = borderStyle;
}
return characters;
};
const makeTitle = (text, horizontal, alignment) => {
let title = '';
const textWidth = stringWidth(text);
switch (alignment) {
case 'left': {
title = text + horizontal.slice(textWidth);
break;
}
case 'right': {
title = horizontal.slice(textWidth) + text;
break;
}
default: {
horizontal = horizontal.slice(textWidth);
if (horizontal.length % 2 === 1) { // This is needed in case the length is odd
horizontal = horizontal.slice(Math.floor(horizontal.length / 2));
title = horizontal.slice(1) + text + horizontal; // We reduce the left part of one character to avoid the bar to go beyond its limit
} else {
horizontal = horizontal.slice(horizontal.length / 2);
title = horizontal + text + horizontal;
}
break;
}
}
return title;
};
const makeContentText = (text, {padding, width, textAlignment, height}) => {
text = ansiAlign(text, {align: textAlignment});
let lines = text.split(NEWLINE);
const textWidth = widestLine(text);
const max = width - padding.left - padding.right;
if (textWidth > max) {
const newLines = [];
for (const line of lines) {
const createdLines = wrapAnsi(line, max, {hard: true});
const alignedLines = ansiAlign(createdLines, {align: textAlignment});
const alignedLinesArray = alignedLines.split('\n');
const longestLength = Math.max(...alignedLinesArray.map(s => stringWidth(s)));
for (const alignedLine of alignedLinesArray) {
let paddedLine;
switch (textAlignment) {
case 'center': {
paddedLine = PAD.repeat((max - longestLength) / 2) + alignedLine;
break;
}
case 'right': {
paddedLine = PAD.repeat(max - longestLength) + alignedLine;
break;
}
default: {
paddedLine = alignedLine;
break;
}
}
newLines.push(paddedLine);
}
}
lines = newLines;
}
if (textAlignment === 'center' && textWidth < max) {
lines = lines.map(line => PAD.repeat((max - textWidth) / 2) + line);
} else if (textAlignment === 'right' && textWidth < max) {
lines = lines.map(line => PAD.repeat(max - textWidth) + line);
}
const paddingLeft = PAD.repeat(padding.left);
const paddingRight = PAD.repeat(padding.right);
lines = lines.map(line => {
const newLine = paddingLeft + line + paddingRight;
return newLine + PAD.repeat(width - stringWidth(newLine));
});
if (padding.top > 0) {
lines = [...Array.from({length: padding.top}).fill(PAD.repeat(width)), ...lines];
}
if (padding.bottom > 0) {
lines = [...lines, ...Array.from({length: padding.bottom}).fill(PAD.repeat(width))];
}
if (height && lines.length > height) {
lines = lines.slice(0, height);
} else if (height && lines.length < height) {
lines = [...lines, ...Array.from({length: height - lines.length}).fill(PAD.repeat(width))];
}
return lines.join(NEWLINE);
};
const boxContent = (content, contentWidth, options) => {
const colorizeBorder = border => {
const newBorder = options.borderColor ? getColorFunction(options.borderColor)(border) : border;
return options.dimBorder ? chalk.dim(newBorder) : newBorder;
};
const colorizeContent = content => options.backgroundColor ? getBGColorFunction(options.backgroundColor)(content) : content;
const chars = getBorderChars(options.borderStyle);
const columns = terminalColumns();
let marginLeft = PAD.repeat(options.margin.left);
if (options.float === 'center') {
const marginWidth = Math.max((columns - contentWidth - getBorderWidth(options.borderStyle)) / 2, 0);
marginLeft = PAD.repeat(marginWidth);
} else if (options.float === 'right') {
const marginWidth = Math.max(columns - contentWidth - options.margin.right - getBorderWidth(options.borderStyle), 0);
marginLeft = PAD.repeat(marginWidth);
}
let result = '';
if (options.margin.top) {
result += NEWLINE.repeat(options.margin.top);
}
if (options.borderStyle !== NONE || options.title) {
result += colorizeBorder(marginLeft + chars.topLeft + (options.title ? makeTitle(options.title, chars.top.repeat(contentWidth), options.titleAlignment) : chars.top.repeat(contentWidth)) + chars.topRight) + NEWLINE;
}
const lines = content.split(NEWLINE);
result += lines.map(line => marginLeft + colorizeBorder(chars.left) + colorizeContent(line) + colorizeBorder(chars.right)).join(NEWLINE);
if (options.borderStyle !== NONE) {
result += NEWLINE + colorizeBorder(marginLeft + chars.bottomLeft + chars.bottom.repeat(contentWidth) + chars.bottomRight);
}
if (options.margin.bottom) {
result += NEWLINE.repeat(options.margin.bottom);
}
return result;
};
const sanitizeOptions = options => {
// If fullscreen is enabled, max-out unspecified width/height
if (options.fullscreen && process?.stdout) {
let newDimensions = [process.stdout.columns, process.stdout.rows];
if (typeof options.fullscreen === 'function') {
newDimensions = options.fullscreen(...newDimensions);
}
options.width ||= newDimensions[0];
options.height ||= newDimensions[1];
}
// If width is provided, make sure it's not below 1
options.width &&= Math.max(1, options.width - getBorderWidth(options.borderStyle));
// If height is provided, make sure it's not below 1
options.height &&= Math.max(1, options.height - getBorderWidth(options.borderStyle));
return options;
};
const formatTitle = (title, borderStyle) => borderStyle === NONE ? title : ` ${title} `;
const determineDimensions = (text, options) => {
options = sanitizeOptions(options);
const widthOverride = options.width !== undefined;
const columns = terminalColumns();
const borderWidth = getBorderWidth(options.borderStyle);
const maxWidth = columns - options.margin.left - options.margin.right - borderWidth;
const widest = widestLine(wrapAnsi(text, columns - borderWidth, {hard: true, trim: false})) + options.padding.left + options.padding.right;
// If title and width are provided, title adheres to fixed width
if (options.title && widthOverride) {
options.title = options.title.slice(0, Math.max(0, options.width - 2));
options.title &&= formatTitle(options.title, options.borderStyle);
} else if (options.title) {
options.title = options.title.slice(0, Math.max(0, maxWidth - 2));
// Recheck if title isn't empty now
if (options.title) {
options.title = formatTitle(options.title, options.borderStyle);
// If the title is larger than content, box adheres to title width
if (stringWidth(options.title) > widest) {
options.width = stringWidth(options.title);
}
}
}
// If fixed width is provided, use it or content width as reference
options.width ||= widest;
if (!widthOverride) {
if ((options.margin.left && options.margin.right) && options.width > maxWidth) {
// Let's assume we have margins: left = 3, right = 5, in total = 8
const spaceForMargins = columns - options.width - borderWidth;
// Let's assume we have space = 4
const multiplier = spaceForMargins / (options.margin.left + options.margin.right);
// Here: multiplier = 4/8 = 0.5
options.margin.left = Math.max(0, Math.floor(options.margin.left * multiplier));
options.margin.right = Math.max(0, Math.floor(options.margin.right * multiplier));
// Left: 3 * 0.5 = 1.5 -> 1
// Right: 6 * 0.5 = 3
}
// Re-cap width considering the margins after shrinking
options.width = Math.min(options.width, columns - borderWidth - options.margin.left - options.margin.right);
}
// Prevent padding overflow
if (options.width - (options.padding.left + options.padding.right) <= 0) {
options.padding.left = 0;
options.padding.right = 0;
}
if (options.height && options.height - (options.padding.top + options.padding.bottom) <= 0) {
options.padding.top = 0;
options.padding.bottom = 0;
}
return options;
};
const isHex = color => color.match(/^#(?:[0-f]{3}){1,2}$/i);
const isColorValid = color => typeof color === 'string' && (chalk[color] ?? isHex(color));
const getColorFunction = color => isHex(color) ? chalk.hex(color) : chalk[color];
const getBGColorFunction = color => isHex(color) ? chalk.bgHex(color) : chalk[camelCase(['bg', color])];
export default function boxen(text, options) {
options = {
padding: 0,
borderStyle: 'single',
dimBorder: false,
textAlignment: 'left',
float: 'left',
titleAlignment: 'left',
...options,
};
// This option is deprecated
if (options.align) {
options.textAlignment = options.align;
}
if (options.borderColor && !isColorValid(options.borderColor)) {
throw new Error(`${options.borderColor} is not a valid borderColor`);
}
if (options.backgroundColor && !isColorValid(options.backgroundColor)) {
throw new Error(`${options.backgroundColor} is not a valid backgroundColor`);
}
options.padding = getObject(options.padding);
options.margin = getObject(options.margin);
options = determineDimensions(text, options);
text = makeContentText(text, options);
return boxContent(text, options.width, options);
}
export {default as _borderStyles} from 'cli-boxes';

View File

@@ -1,9 +0,0 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,69 +0,0 @@
{
"name": "boxen",
"version": "8.0.1",
"description": "Create boxes in the terminal",
"license": "MIT",
"repository": "sindresorhus/boxen",
"funding": "https://github.com/sponsors/sindresorhus",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"type": "module",
"exports": {
"types": "./index.d.ts",
"default": "./index.js"
},
"sideEffects": false,
"engines": {
"node": ">=18"
},
"scripts": {
"test": "xo && nyc ava && tsd"
},
"files": [
"index.js",
"index.d.ts"
],
"keywords": [
"cli",
"box",
"boxes",
"terminal",
"term",
"console",
"ascii",
"unicode",
"border",
"text"
],
"dependencies": {
"ansi-align": "^3.0.1",
"camelcase": "^8.0.0",
"chalk": "^5.3.0",
"cli-boxes": "^3.0.0",
"string-width": "^7.2.0",
"type-fest": "^4.21.0",
"widest-line": "^5.0.0",
"wrap-ansi": "^9.0.0"
},
"devDependencies": {
"ava": "^6.1.3",
"nyc": "^17.0.0",
"tsd": "^0.31.1",
"xo": "^0.58.0"
},
"ava": {
"snapshotDir": "tests/snapshots",
"environmentVariables": {
"COLUMNS": "60",
"FORCE_COLOR": "0"
}
},
"xo": {
"rules": {
"@typescript-eslint/no-unsafe-assignment": "off"
}
}
}

View File

@@ -1,300 +0,0 @@
# boxen
> Create boxes in the terminal
![](screenshot.png)
## Install
```sh
npm install boxen
```
## Usage
```js
import boxen from 'boxen';
console.log(boxen('unicorn', {padding: 1}));
/*
┌─────────────┐
│ │
│ unicorn │
│ │
└─────────────┘
*/
console.log(boxen('unicorn', {padding: 1, margin: 1, borderStyle: 'double'}));
/*
╔═════════════╗
║ ║
║ unicorn ║
║ ║
╚═════════════╝
*/
console.log(boxen('unicorns love rainbows', {title: 'magical', titleAlignment: 'center'}));
/*
┌────── magical ───────┐
│unicorns love rainbows│
└──────────────────────┘
*/
```
## API
### boxen(text, options?)
#### text
Type: `string`
Text inside the box.
#### options
Type: `object`
##### borderColor
Type: `string`\
Values: `'black'` `'red'` `'green'` `'yellow'` `'blue'` `'magenta'` `'cyan'` `'white'` `'gray'` or a hex value like `'#ff0000'`
Color of the box border.
##### borderStyle
Type: `string | object`\
Default: `'single'`\
Values:
- `'single'`
```
┌───┐
│foo│
└───┘
```
- `'double'`
```
╔═══╗
║foo║
╚═══╝
```
- `'round'` (`'single'` sides with round corners)
```
╭───╮
│foo│
╰───╯
```
- `'bold'`
```
┏━━━┓
┃foo┃
┗━━━┛
```
- `'singleDouble'` (`'single'` on top and bottom, `'double'` on right and left)
```
╓───╖
║foo║
╙───╜
```
- `'doubleSingle'` (`'double'` on top and bottom, `'single'` on right and left)
```
╒═══╕
│foo│
╘═══╛
```
- `'classic'`
```
+---+
|foo|
+---+
```
- `'arrow'`
```
↘↓↓↓↙
→foo←
↗↑↑↑↖
```
- `'none'`
```
foo
```
Style of the box border.
Can be any of the above predefined styles or an object with the following keys:
```js
{
topLeft: '+',
topRight: '+',
bottomLeft: '+',
bottomRight: '+',
top: '-',
bottom: '-',
left: '|',
right: '|'
}
```
##### dimBorder
Type: `boolean`\
Default: `false`
Reduce opacity of the border.
##### title
Type: `string`
Display a title at the top of the box.
If needed, the box will horizontally expand to fit the title.
Example:
```js
console.log(boxen('foo bar', {title: 'example'}));
/*
┌ example ┐
│foo bar │
└─────────┘
*/
```
##### titleAlignment
Type: `string`\
Default: `'left'`
Align the title in the top bar.
Values:
- `'left'`
```js
/*
┌ example ──────┐
│foo bar foo bar│
└───────────────┘
*/
```
- `'center'`
```js
/*
┌─── example ───┐
│foo bar foo bar│
└───────────────┘
*/
```
- `'right'`
```js
/*
┌────── example ┐
│foo bar foo bar│
└───────────────┘
*/
```
##### width
Type: `number`
Set a fixed width for the box.
*Note:* This disables terminal overflow handling and may cause the box to look broken if the user's terminal is not wide enough.
```js
import boxen from 'boxen';
console.log(boxen('foo bar', {width: 15}));
// ┌─────────────┐
// │foo bar │
// └─────────────┘
```
##### height
Type: `number`
Set a fixed height for the box.
*Note:* This option will crop overflowing content.
```js
import boxen from 'boxen';
console.log(boxen('foo bar', {height: 5}));
// ┌───────┐
// │foo bar│
// │ │
// │ │
// └───────┘
```
##### fullscreen
Type: `boolean | (width: number, height: number) => [width: number, height: number]`
Whether or not to fit all available space within the terminal.
Pass a callback function to control box dimensions:
```js
import boxen from 'boxen';
console.log(boxen('foo bar', {
fullscreen: (width, height) => [width, height - 1],
}));
```
##### padding
Type: `number | object`\
Default: `0`
Space between the text and box border.
Accepts a number or an object with any of the `top`, `right`, `bottom`, `left` properties. When a number is specified, the left/right padding is 3 times the top/bottom to make it look nice.
##### margin
Type: `number | object`\
Default: `0`
Space around the box.
Accepts a number or an object with any of the `top`, `right`, `bottom`, `left` properties. When a number is specified, the left/right margin is 3 times the top/bottom to make it look nice.
##### float
Type: `string`\
Default: `'left'`\
Values: `'right'` `'center'` `'left'`
Float the box on the available terminal screen space.
##### backgroundColor
Type: `string`\
Values: `'black'` `'red'` `'green'` `'yellow'` `'blue'` `'magenta'` `'cyan'` `'white'` `'gray'` or a hex value like `'#ff0000'`
Color of the background.
##### textAlignment
Type: `string`\
Default: `'left'`\
Values: `'left'` `'center'` `'right'`
Align the text in the box based on the widest line.
## Maintainer
- [Sindre Sorhus](https://github.com/sindresorhus)
- [Caesarovich](https://github.com/Caesarovich)
## Related
- [boxen-cli](https://github.com/sindresorhus/boxen-cli) - CLI for this module
- [cli-boxes](https://github.com/sindresorhus/cli-boxes) - Boxes for use in the terminal