kkt
This commit is contained in:
@@ -19,6 +19,7 @@ import AuthLayout from "./layouts/AuthLayout";
|
||||
import LogoutPage from "./pages/account/Logout";
|
||||
import LoginPage from "./pages/account/Login";
|
||||
import RegisterPage from "./pages/account/Register";
|
||||
import { RetroSoundTest } from "./pages/test/sounds";
|
||||
|
||||
|
||||
export default function App() {
|
||||
@@ -39,6 +40,8 @@ export default function App() {
|
||||
<Route path="services/drone" element={< DroneServisSection />} />
|
||||
<Route path="services/web" element={<Downloader />} />
|
||||
|
||||
<Route path="test/sounds" element={<RetroSoundTest />} />
|
||||
|
||||
</Route>
|
||||
|
||||
<Route path="auth/" element={<AuthLayout />}>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# http://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
26
frontend/src/pages/test/ews-component-master/ews-component-master/.gitignore
vendored
Normal file
26
frontend/src/pages/test/ews-component-master/ews-component-master/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
dist/
|
||||
www/
|
||||
loader/
|
||||
|
||||
*~
|
||||
*.sw[mnpcod]
|
||||
*.log
|
||||
*.lock
|
||||
*.tmp
|
||||
*.tmp.*
|
||||
log.txt
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
.stencil/
|
||||
.idea/
|
||||
.vscode/
|
||||
.sass-cache/
|
||||
.versions/
|
||||
node_modules/
|
||||
$RECYCLE.BIN/
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
UserInterfaceState.xcuserstate
|
||||
.env
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"quoteProps": "consistent",
|
||||
"printWidth": 180,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,124 @@
|
||||
# Cara Menggunakan Komponen Secara Lokal
|
||||
|
||||
Ikuti langkah-langkah berikut untuk menggunakan `ews-card` dan `ews-hex-shape` di proyek lokal lain.
|
||||
|
||||
## 1. Build Library
|
||||
Pastikan library sudah di-build di direktori ini (`ews-component`):
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 2. Link Library (Development)
|
||||
Gunakan `npm link` agar perubahan di library ini langsung terlihat di proyek tujuan.
|
||||
|
||||
**Di direktori ini (`ews-component`):**
|
||||
```bash
|
||||
npm link
|
||||
```
|
||||
|
||||
**Di direktori proyek tujuan (misal: `ews-concept-new`):**
|
||||
```bash
|
||||
npm link ews-component
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Framework: Svelte / Vite / Vanilla JS
|
||||
Tambahkan loader di file entri utama (seperti `src/routes/+layout.svelte` atau `main.ts`):
|
||||
|
||||
```javascript
|
||||
import { defineCustomElements } from 'ews-component/loader';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
defineCustomElements();
|
||||
}
|
||||
```
|
||||
|
||||
### Framework: React
|
||||
Untuk React, panggil `defineCustomElements()` di file entri utama (`index.js` atau `App.js`):
|
||||
|
||||
```tsx
|
||||
import { useEffect } from 'react';
|
||||
import { defineCustomElements } from 'ews-component/loader';
|
||||
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
defineCustomElements();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ews-card>
|
||||
<div slot="header">React Card</div>
|
||||
<p>Konten di React</p>
|
||||
</ews-card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Framework: Vue (Vite)
|
||||
Untuk Vue 3 dengan Vite, cara paling stabil adalah mengimport komponen secara langsung (menghindari error "Constructor not found"):
|
||||
|
||||
1. Konfigurasi `vite.config.ts`:
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag.startsWith('ews-')
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
2. Register komponen di `main.ts` atau di component yang membutuhkan:
|
||||
```typescript
|
||||
// Mengimport dan register secara eksplisit (lebih stabil untuk development/npm link)
|
||||
import 'ews-component/components/ews-card';
|
||||
import 'ews-component/components/ews-hex-shape';
|
||||
import 'ews-component/components/ews-stripe-bar';
|
||||
```
|
||||
|
||||
Atau jika ingin loader otomatis (namun kadang terkendala `npm link`):
|
||||
```typescript
|
||||
import { defineCustomElements } from 'ews-component/loader';
|
||||
defineCustomElements();
|
||||
```
|
||||
|
||||
### Plain HTML (Tanpa Bundler)
|
||||
Jika ingin menggunakan langsung di file HTML tanpa build tool:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>EWS Component Demo</title>
|
||||
<!-- Load component bundle (sesuaikan path ke node_modules jika lokal) -->
|
||||
<script type="module" src="./node_modules/ews-component/dist/ews-component/ews-component.esm.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<ews-card>
|
||||
<div slot="header">Vanilla HTML</div>
|
||||
<p>Berjalan langsung di browser.</p>
|
||||
</ews-card>
|
||||
|
||||
<ews-hex-shape color="#3498db" size="120"></ews-hex-shape>
|
||||
|
||||
<ews-stripe-bar color="red" loop="true" duration="10"></ews-stripe-bar>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Alternatif: Install Langsung
|
||||
Jika tidak ingin menggunakan `link`, Anda bisa menginstall langsung dari path folder:
|
||||
```bash
|
||||
npm install ../path/to/ews-component
|
||||
```
|
||||
1768
frontend/src/pages/test/ews-component-master/ews-component-master/package-lock.json
generated
Normal file
1768
frontend/src/pages/test/ews-component-master/ews-component-master/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "ews-component",
|
||||
"version": "0.0.1",
|
||||
"description": "Stencil Component Starter",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/types/index.d.ts",
|
||||
"collection": "dist/collection/collection-manifest.json",
|
||||
"collection:main": "dist/collection/index.js",
|
||||
"unpkg": "dist/ews-component/ews-component.esm.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/ews-component/ews-component.esm.js",
|
||||
"require": "./dist/ews-component/ews-component.cjs.js"
|
||||
},
|
||||
"./components": {
|
||||
"import": "./dist/components/index.js",
|
||||
"types": "./dist/components/index.d.ts"
|
||||
},
|
||||
"./components/*": {
|
||||
"import": "./dist/components/*.js",
|
||||
"types": "./dist/components/*.d.ts"
|
||||
},
|
||||
"./loader": {
|
||||
"import": "./loader/index.js",
|
||||
"require": "./loader/index.cjs",
|
||||
"types": "./loader/index.d.ts"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bagusindrayana/ews-component"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"loader/"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "stencil build",
|
||||
"start": "stencil build --dev --watch --serve",
|
||||
"test": "stencil-test",
|
||||
"test:watch": "stencil-test --watch",
|
||||
"generate": "stencil generate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stencil/core": "^4.27.1",
|
||||
"@stencil/vitest": "^1.8.3",
|
||||
"@types/node": "^22.13.5",
|
||||
"@vitest/browser-playwright": "^4.0.0",
|
||||
"vitest": "^4.0.0"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
# EWS Component Library
|
||||
|
||||
> A collection of StencilJS web components for the EWS project.
|
||||
|
||||
This library contains reusable web components such as layout managers, charts, and UI elements designed for high-performance and framework-agnostic usage.
|
||||
|
||||
## Installation
|
||||
|
||||
To use `ews-component` in your project, install it via npm:
|
||||
|
||||
```bash
|
||||
npm install ews-component
|
||||
```
|
||||
|
||||
## Available Components
|
||||
|
||||
- `ews-card`: A versatile card component for displaying content.
|
||||
- `ews-hex-grid`: A grid layout with hexagonal cells.
|
||||
- `ews-hex-shape`: Individual hexagonal shape component.
|
||||
- `ews-rib-layout`: A responsive "ribcage" layout for hierarchical data.
|
||||
- `ews-stripe-bar`: A striped status or progress bar.
|
||||
|
||||
## Local Development (StencilJS)
|
||||
|
||||
To start developing components locally, clone this repository and follow these steps:
|
||||
|
||||
1. **Install dependencies**:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Start development server**:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
This will start a local dev server with hot-reloading.
|
||||
|
||||
3. **Build for production**:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
4. **Run tests**:
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Framework Integration
|
||||
|
||||
Since these are standard Web Components, they work in any framework (React, Vue, Angular, Svelte) or with no framework at all.
|
||||
|
||||
### Lazy Loading (Universal)
|
||||
|
||||
Include the loader script in your HTML:
|
||||
|
||||
```html
|
||||
<script type="module" src="https://unpkg.com/ews-component/dist/ews-component/ews-component.esm.js"></script>
|
||||
<ews-rib-layout max-branches="4">
|
||||
<!-- Your content here -->
|
||||
</ews-rib-layout>
|
||||
```
|
||||
|
||||
### Direct Import (React/Vite/NextJS)
|
||||
|
||||
```tsx
|
||||
import { defineCustomElements } from 'ews-component/loader';
|
||||
|
||||
defineCustomElements();
|
||||
|
||||
// Use in your component
|
||||
<ews-stripe-bar percent={75} status="active"></ews-stripe-bar>
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For more detailed information on specific components, please refer to the documentation in each component's directory or the official [StencilJS documentation](https://stenciljs.com/docs/introduction).
|
||||
|
||||
|
||||
## Contributing & Adding New Components
|
||||
|
||||
To maintain consistency, please follow these steps when adding a new component:
|
||||
|
||||
1. **Generate Component**:
|
||||
Use the Stencil CLI to scaffold your component:
|
||||
```bash
|
||||
npm run generate
|
||||
```
|
||||
*Input the name with `ews-` prefix (e.g., `ews-new-button`).*
|
||||
|
||||
2. **Naming & Directory**:
|
||||
- **Folder**: `src/components/ews-[name]/`
|
||||
- **Tag Name**: `ews-[name]`
|
||||
- **Class Name**: `Ews[Name]` (PascalCase)
|
||||
|
||||
3. **Code Style Guidelines**:
|
||||
- **TypeScript & TSX**: Always use TypeScript/TSX for component logic.
|
||||
- **Styling**: Use a dedicated CSS file (`[name].css`). Prefix all classes with `ews-` (e.g., `.ews-card`) to avoid global style collisions.
|
||||
- **Reactivity**: Use `@Prop()`, `@State()`, and `@Event()` decorators for state management and communication.
|
||||
- **Documentation**: Write clear JSDoc comments for props and events; Stencil will automatically update the component's `readme.md`.
|
||||
|
||||
|
||||
## Support Me!
|
||||
[](https://sociabuzz.com/bagusindrayana/tribe)
|
||||
394
frontend/src/pages/test/ews-component-master/ews-component-master/src/components.d.ts
vendored
Normal file
394
frontend/src/pages/test/ews-component-master/ews-component-master/src/components.d.ts
vendored
Normal file
@@ -0,0 +1,394 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This is an autogenerated file created by the Stencil compiler.
|
||||
* It contains typing information for all components that exist in this project.
|
||||
*/
|
||||
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
|
||||
export namespace Components {
|
||||
interface EwsCard {
|
||||
/**
|
||||
* Custom color for border and content (red or orange)
|
||||
*/
|
||||
"color"?: string;
|
||||
/**
|
||||
* Additional CSS classes to apply to the card wrapper
|
||||
* @default ''
|
||||
*/
|
||||
"customClass": string;
|
||||
/**
|
||||
* Inline style
|
||||
*/
|
||||
"customStyle"?: string;
|
||||
}
|
||||
interface EwsHexGrid {
|
||||
/**
|
||||
* Additional CSS class for the container.
|
||||
* @default ''
|
||||
*/
|
||||
"customClass": string;
|
||||
/**
|
||||
* Gap between hex cells in pixels.
|
||||
* @default 4
|
||||
*/
|
||||
"gap": number;
|
||||
/**
|
||||
* Height of each hex cell in pixels.
|
||||
*/
|
||||
"hexHeight": number;
|
||||
/**
|
||||
* Width of each hex cell in pixels.
|
||||
*/
|
||||
"hexWidth": number;
|
||||
/**
|
||||
* Hex orientation variant: 'pointy' or 'flat'.
|
||||
* @default 'pointy'
|
||||
*/
|
||||
"variant": 'pointy' | 'flat';
|
||||
}
|
||||
interface EwsHexShape {
|
||||
/**
|
||||
* Whether to clip content within the hex shape.
|
||||
* @default false
|
||||
*/
|
||||
"clipContent": boolean;
|
||||
/**
|
||||
* The color variant of the hex shape.
|
||||
* @default 'orange'
|
||||
*/
|
||||
"color": string;
|
||||
/**
|
||||
* Additional CSS classes for the component.
|
||||
* @default ''
|
||||
*/
|
||||
"customClass": string;
|
||||
/**
|
||||
* Whether the hex shape has a flat top.
|
||||
* @default true
|
||||
*/
|
||||
"flatTop": boolean;
|
||||
/**
|
||||
* The padding inside the hex shape for content.
|
||||
* @default 10
|
||||
*/
|
||||
"paddingContent": number;
|
||||
}
|
||||
interface EwsRibLayout {
|
||||
/**
|
||||
* Optional renderer function for the connector content.
|
||||
*/
|
||||
"connectorRenderer"?: (item: any, props: { side: 'left' | 'right'; branchIndex: number; index: number; delay: number }) => any;
|
||||
/**
|
||||
* Function to get the href for a node. If provided, nodes will be rendered as <a> tags.
|
||||
*/
|
||||
"getHref"?: (item: any) => string;
|
||||
/**
|
||||
* Array of items to be displayed in the rib cage layout.
|
||||
* @default []
|
||||
*/
|
||||
"items": any[];
|
||||
/**
|
||||
* Maximum number of branches to display. If not provided, it defaults to 5 (responsive).
|
||||
*/
|
||||
"maxBranches"?: number;
|
||||
/**
|
||||
* Optional renderer function for the node content.
|
||||
*/
|
||||
"nodeRenderer"?: (item: any, props: { side: 'left' | 'right'; branchIndex: number; index: number; delay: number }) => any;
|
||||
}
|
||||
interface EwsStripeBar {
|
||||
/**
|
||||
* @default ''
|
||||
*/
|
||||
"color": string;
|
||||
/**
|
||||
* @default 10
|
||||
*/
|
||||
"duration": number;
|
||||
/**
|
||||
* @default false
|
||||
*/
|
||||
"loop": boolean;
|
||||
/**
|
||||
* @default ''
|
||||
*/
|
||||
"orientation": string;
|
||||
/**
|
||||
* @default false
|
||||
*/
|
||||
"reverse": boolean;
|
||||
/**
|
||||
* @default '30px'
|
||||
*/
|
||||
"size": string;
|
||||
}
|
||||
interface MyComponent {
|
||||
/**
|
||||
* The first name
|
||||
*/
|
||||
"first": string;
|
||||
/**
|
||||
* The last name
|
||||
*/
|
||||
"last": string;
|
||||
/**
|
||||
* The middle name
|
||||
*/
|
||||
"middle": string;
|
||||
}
|
||||
}
|
||||
export interface EwsCardCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLEwsCardElement;
|
||||
}
|
||||
declare global {
|
||||
interface HTMLEwsCardElementEventMap {
|
||||
"toggle": void;
|
||||
}
|
||||
interface HTMLEwsCardElement extends Components.EwsCard, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLEwsCardElementEventMap>(type: K, listener: (this: HTMLEwsCardElement, ev: EwsCardCustomEvent<HTMLEwsCardElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLEwsCardElementEventMap>(type: K, listener: (this: HTMLEwsCardElement, ev: EwsCardCustomEvent<HTMLEwsCardElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLEwsCardElement: {
|
||||
prototype: HTMLEwsCardElement;
|
||||
new (): HTMLEwsCardElement;
|
||||
};
|
||||
interface HTMLEwsHexGridElement extends Components.EwsHexGrid, HTMLStencilElement {
|
||||
}
|
||||
var HTMLEwsHexGridElement: {
|
||||
prototype: HTMLEwsHexGridElement;
|
||||
new (): HTMLEwsHexGridElement;
|
||||
};
|
||||
interface HTMLEwsHexShapeElement extends Components.EwsHexShape, HTMLStencilElement {
|
||||
}
|
||||
var HTMLEwsHexShapeElement: {
|
||||
prototype: HTMLEwsHexShapeElement;
|
||||
new (): HTMLEwsHexShapeElement;
|
||||
};
|
||||
interface HTMLEwsRibLayoutElement extends Components.EwsRibLayout, HTMLStencilElement {
|
||||
}
|
||||
var HTMLEwsRibLayoutElement: {
|
||||
prototype: HTMLEwsRibLayoutElement;
|
||||
new (): HTMLEwsRibLayoutElement;
|
||||
};
|
||||
interface HTMLEwsStripeBarElement extends Components.EwsStripeBar, HTMLStencilElement {
|
||||
}
|
||||
var HTMLEwsStripeBarElement: {
|
||||
prototype: HTMLEwsStripeBarElement;
|
||||
new (): HTMLEwsStripeBarElement;
|
||||
};
|
||||
interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement {
|
||||
}
|
||||
var HTMLMyComponentElement: {
|
||||
prototype: HTMLMyComponentElement;
|
||||
new (): HTMLMyComponentElement;
|
||||
};
|
||||
interface HTMLElementTagNameMap {
|
||||
"ews-card": HTMLEwsCardElement;
|
||||
"ews-hex-grid": HTMLEwsHexGridElement;
|
||||
"ews-hex-shape": HTMLEwsHexShapeElement;
|
||||
"ews-rib-layout": HTMLEwsRibLayoutElement;
|
||||
"ews-stripe-bar": HTMLEwsStripeBarElement;
|
||||
"my-component": HTMLMyComponentElement;
|
||||
}
|
||||
}
|
||||
declare namespace LocalJSX {
|
||||
interface EwsCard {
|
||||
/**
|
||||
* Custom color for border and content (red or orange)
|
||||
*/
|
||||
"color"?: string;
|
||||
/**
|
||||
* Additional CSS classes to apply to the card wrapper
|
||||
* @default ''
|
||||
*/
|
||||
"customClass"?: string;
|
||||
/**
|
||||
* Inline style
|
||||
*/
|
||||
"customStyle"?: string;
|
||||
/**
|
||||
* Emitted when the card toggles open/close state
|
||||
*/
|
||||
"onToggle"?: (event: EwsCardCustomEvent<void>) => void;
|
||||
}
|
||||
interface EwsHexGrid {
|
||||
/**
|
||||
* Additional CSS class for the container.
|
||||
* @default ''
|
||||
*/
|
||||
"customClass"?: string;
|
||||
/**
|
||||
* Gap between hex cells in pixels.
|
||||
* @default 4
|
||||
*/
|
||||
"gap"?: number;
|
||||
/**
|
||||
* Height of each hex cell in pixels.
|
||||
*/
|
||||
"hexHeight"?: number;
|
||||
/**
|
||||
* Width of each hex cell in pixels.
|
||||
*/
|
||||
"hexWidth"?: number;
|
||||
/**
|
||||
* Hex orientation variant: 'pointy' or 'flat'.
|
||||
* @default 'pointy'
|
||||
*/
|
||||
"variant"?: 'pointy' | 'flat';
|
||||
}
|
||||
interface EwsHexShape {
|
||||
/**
|
||||
* Whether to clip content within the hex shape.
|
||||
* @default false
|
||||
*/
|
||||
"clipContent"?: boolean;
|
||||
/**
|
||||
* The color variant of the hex shape.
|
||||
* @default 'orange'
|
||||
*/
|
||||
"color"?: string;
|
||||
/**
|
||||
* Additional CSS classes for the component.
|
||||
* @default ''
|
||||
*/
|
||||
"customClass"?: string;
|
||||
/**
|
||||
* Whether the hex shape has a flat top.
|
||||
* @default true
|
||||
*/
|
||||
"flatTop"?: boolean;
|
||||
/**
|
||||
* The padding inside the hex shape for content.
|
||||
* @default 10
|
||||
*/
|
||||
"paddingContent"?: number;
|
||||
}
|
||||
interface EwsRibLayout {
|
||||
/**
|
||||
* Optional renderer function for the connector content.
|
||||
*/
|
||||
"connectorRenderer"?: (item: any, props: { side: 'left' | 'right'; branchIndex: number; index: number; delay: number }) => any;
|
||||
/**
|
||||
* Function to get the href for a node. If provided, nodes will be rendered as <a> tags.
|
||||
*/
|
||||
"getHref"?: (item: any) => string;
|
||||
/**
|
||||
* Array of items to be displayed in the rib cage layout.
|
||||
* @default []
|
||||
*/
|
||||
"items"?: any[];
|
||||
/**
|
||||
* Maximum number of branches to display. If not provided, it defaults to 5 (responsive).
|
||||
*/
|
||||
"maxBranches"?: number;
|
||||
/**
|
||||
* Optional renderer function for the node content.
|
||||
*/
|
||||
"nodeRenderer"?: (item: any, props: { side: 'left' | 'right'; branchIndex: number; index: number; delay: number }) => any;
|
||||
}
|
||||
interface EwsStripeBar {
|
||||
/**
|
||||
* @default ''
|
||||
*/
|
||||
"color"?: string;
|
||||
/**
|
||||
* @default 10
|
||||
*/
|
||||
"duration"?: number;
|
||||
/**
|
||||
* @default false
|
||||
*/
|
||||
"loop"?: boolean;
|
||||
/**
|
||||
* @default ''
|
||||
*/
|
||||
"orientation"?: string;
|
||||
/**
|
||||
* @default false
|
||||
*/
|
||||
"reverse"?: boolean;
|
||||
/**
|
||||
* @default '30px'
|
||||
*/
|
||||
"size"?: string;
|
||||
}
|
||||
interface MyComponent {
|
||||
/**
|
||||
* The first name
|
||||
*/
|
||||
"first"?: string;
|
||||
/**
|
||||
* The last name
|
||||
*/
|
||||
"last"?: string;
|
||||
/**
|
||||
* The middle name
|
||||
*/
|
||||
"middle"?: string;
|
||||
}
|
||||
|
||||
interface EwsCardAttributes {
|
||||
"customClass": string;
|
||||
"color": string;
|
||||
"customStyle": string;
|
||||
}
|
||||
interface EwsHexGridAttributes {
|
||||
"customClass": string;
|
||||
"variant": 'pointy' | 'flat';
|
||||
"hexWidth": number;
|
||||
"hexHeight": number;
|
||||
"gap": number;
|
||||
}
|
||||
interface EwsHexShapeAttributes {
|
||||
"customClass": string;
|
||||
"color": string;
|
||||
"flatTop": boolean;
|
||||
"clipContent": boolean;
|
||||
"paddingContent": number;
|
||||
}
|
||||
interface EwsRibLayoutAttributes {
|
||||
"maxBranches": number;
|
||||
}
|
||||
interface EwsStripeBarAttributes {
|
||||
"color": string;
|
||||
"orientation": string;
|
||||
"loop": boolean;
|
||||
"reverse": boolean;
|
||||
"duration": number;
|
||||
"size": string;
|
||||
}
|
||||
interface MyComponentAttributes {
|
||||
"first": string;
|
||||
"middle": string;
|
||||
"last": string;
|
||||
}
|
||||
|
||||
interface IntrinsicElements {
|
||||
"ews-card": Omit<EwsCard, keyof EwsCardAttributes> & { [K in keyof EwsCard & keyof EwsCardAttributes]?: EwsCard[K] } & { [K in keyof EwsCard & keyof EwsCardAttributes as `attr:${K}`]?: EwsCardAttributes[K] } & { [K in keyof EwsCard & keyof EwsCardAttributes as `prop:${K}`]?: EwsCard[K] };
|
||||
"ews-hex-grid": Omit<EwsHexGrid, keyof EwsHexGridAttributes> & { [K in keyof EwsHexGrid & keyof EwsHexGridAttributes]?: EwsHexGrid[K] } & { [K in keyof EwsHexGrid & keyof EwsHexGridAttributes as `attr:${K}`]?: EwsHexGridAttributes[K] } & { [K in keyof EwsHexGrid & keyof EwsHexGridAttributes as `prop:${K}`]?: EwsHexGrid[K] };
|
||||
"ews-hex-shape": Omit<EwsHexShape, keyof EwsHexShapeAttributes> & { [K in keyof EwsHexShape & keyof EwsHexShapeAttributes]?: EwsHexShape[K] } & { [K in keyof EwsHexShape & keyof EwsHexShapeAttributes as `attr:${K}`]?: EwsHexShapeAttributes[K] } & { [K in keyof EwsHexShape & keyof EwsHexShapeAttributes as `prop:${K}`]?: EwsHexShape[K] };
|
||||
"ews-rib-layout": Omit<EwsRibLayout, keyof EwsRibLayoutAttributes> & { [K in keyof EwsRibLayout & keyof EwsRibLayoutAttributes]?: EwsRibLayout[K] } & { [K in keyof EwsRibLayout & keyof EwsRibLayoutAttributes as `attr:${K}`]?: EwsRibLayoutAttributes[K] } & { [K in keyof EwsRibLayout & keyof EwsRibLayoutAttributes as `prop:${K}`]?: EwsRibLayout[K] };
|
||||
"ews-stripe-bar": Omit<EwsStripeBar, keyof EwsStripeBarAttributes> & { [K in keyof EwsStripeBar & keyof EwsStripeBarAttributes]?: EwsStripeBar[K] } & { [K in keyof EwsStripeBar & keyof EwsStripeBarAttributes as `attr:${K}`]?: EwsStripeBarAttributes[K] } & { [K in keyof EwsStripeBar & keyof EwsStripeBarAttributes as `prop:${K}`]?: EwsStripeBar[K] };
|
||||
"my-component": Omit<MyComponent, keyof MyComponentAttributes> & { [K in keyof MyComponent & keyof MyComponentAttributes]?: MyComponent[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as `attr:${K}`]?: MyComponentAttributes[K] } & { [K in keyof MyComponent & keyof MyComponentAttributes as `prop:${K}`]?: MyComponent[K] };
|
||||
}
|
||||
}
|
||||
export { LocalJSX as JSX };
|
||||
declare module "@stencil/core" {
|
||||
export namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
"ews-card": LocalJSX.IntrinsicElements["ews-card"] & JSXBase.HTMLAttributes<HTMLEwsCardElement>;
|
||||
"ews-hex-grid": LocalJSX.IntrinsicElements["ews-hex-grid"] & JSXBase.HTMLAttributes<HTMLEwsHexGridElement>;
|
||||
"ews-hex-shape": LocalJSX.IntrinsicElements["ews-hex-shape"] & JSXBase.HTMLAttributes<HTMLEwsHexShapeElement>;
|
||||
"ews-rib-layout": LocalJSX.IntrinsicElements["ews-rib-layout"] & JSXBase.HTMLAttributes<HTMLEwsRibLayoutElement>;
|
||||
"ews-stripe-bar": LocalJSX.IntrinsicElements["ews-stripe-bar"] & JSXBase.HTMLAttributes<HTMLEwsStripeBarElement>;
|
||||
"my-component": LocalJSX.IntrinsicElements["my-component"] & JSXBase.HTMLAttributes<HTMLMyComponentElement>;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { render, h, describe, it, expect } from '@stencil/vitest';
|
||||
|
||||
describe('ews-card', () => {
|
||||
it('renders', async () => {
|
||||
const { root } = await render(<ews-card></ews-card>);
|
||||
await expect(root).toBeDefined();
|
||||
await expect(root.querySelector('.ews-card')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
.ews-card {
|
||||
--ews-card-color: var(--orange, #fa0);
|
||||
--ews-card-radius: var(--gutter-size, 8px);
|
||||
--ews-card-border-width: 3px;
|
||||
background-color: black;
|
||||
transition: 0.3s;
|
||||
|
||||
border-radius: var(--ews-card-radius);
|
||||
border-style: solid;
|
||||
border-width: var(--ews-card-border-width);
|
||||
border-color: var(--ews-card-color);
|
||||
}
|
||||
|
||||
.ews-card.ews-card-red {
|
||||
--ews-card-color: var(--red, #f23);
|
||||
}
|
||||
|
||||
.ews-card-header {
|
||||
/* padding: 6px; */
|
||||
color: var(--ews-card-color);
|
||||
position: relative;
|
||||
border-radius: 10px 10px 0px 0px;
|
||||
border-bottom: var(--ews-card-border-width) solid var(--ews-card-color);
|
||||
}
|
||||
|
||||
.ews-card-header .ews-card-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ews-card-header button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ews-card-footer {
|
||||
/* padding: 6px; */
|
||||
color: var(--ews-card-color);
|
||||
position: relative;
|
||||
border-radius: 0px 0px 10px 10px;
|
||||
border-top: var(--ews-card-border-width) solid var(--ews-card-color);
|
||||
}
|
||||
|
||||
.ews-card-footer .ews-card-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ews-card-content {
|
||||
color: var(--ews-card-color);
|
||||
}
|
||||
|
||||
|
||||
.ews-card-content::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
background-color: rgb(61, 61, 61);
|
||||
}
|
||||
|
||||
.ews-card-content::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: rgb(61, 61, 61);
|
||||
}
|
||||
|
||||
.ews-card-content::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
background-color: var(--red);
|
||||
}
|
||||
|
||||
.ews-card-content tbody {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
.ews-card-float {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.ews-card-float .ews-card-content {
|
||||
display: block;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.ews-card-close-button {
|
||||
font-size: 24px;
|
||||
color: #e60003;
|
||||
padding: 2px 4px;
|
||||
background-color: black !important;
|
||||
right: 10px !important;
|
||||
top: 10px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.ews-card {
|
||||
--ews-card-border-width: 1px;
|
||||
}
|
||||
|
||||
.ews-card-float .ews-card-content {
|
||||
display: none;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.ews-card-float.open .ews-card-content {
|
||||
display: block;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.ews-card-float {
|
||||
margin: auto;
|
||||
right: 0.25rem;
|
||||
left: 0.25rem;
|
||||
}
|
||||
|
||||
.ews-card-float .ews-card-header {
|
||||
border-bottom: unset;
|
||||
}
|
||||
|
||||
.ews-card-header {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Component, Prop, State, Event, EventEmitter, Element, h } from '@stencil/core';
|
||||
|
||||
@Component({
|
||||
tag: 'ews-card',
|
||||
styleUrl: 'ews-card.css',
|
||||
shadow: false,
|
||||
})
|
||||
export class EwsCard {
|
||||
@Element() el: HTMLElement;
|
||||
|
||||
/**
|
||||
* Additional CSS classes to apply to the card wrapper
|
||||
*/
|
||||
@Prop() customClass: string = '';
|
||||
|
||||
|
||||
/**
|
||||
* Custom color for border and content (red or orange)
|
||||
*/
|
||||
@Prop() color?: string;
|
||||
|
||||
/**
|
||||
* Inline style
|
||||
*/
|
||||
@Prop() customStyle?: string;
|
||||
|
||||
/**
|
||||
* Tracks whether the card content is toggled open
|
||||
*/
|
||||
@State() open: boolean = false;
|
||||
|
||||
@State() hasHeader: boolean = false;
|
||||
@State() hasFooter: boolean = false;
|
||||
|
||||
/**
|
||||
* Emitted when the card toggles open/close state
|
||||
*/
|
||||
@Event() toggle: EventEmitter<void>;
|
||||
|
||||
componentWillLoad() {
|
||||
this.checkSlots();
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
this.checkSlots();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkSlots();
|
||||
}
|
||||
|
||||
private checkSlots() {
|
||||
if (!this.el) return;
|
||||
const headerSlot = !!this.el.querySelector('[slot="header"]');
|
||||
const footerSlot = !!this.el.querySelector('[slot="footer"]');
|
||||
|
||||
if (this.hasHeader !== headerSlot) {
|
||||
this.hasHeader = headerSlot;
|
||||
}
|
||||
if (this.hasFooter !== footerSlot) {
|
||||
this.hasFooter = footerSlot;
|
||||
}
|
||||
}
|
||||
|
||||
private handleToggle = () => {
|
||||
this.open = !this.open;
|
||||
this.toggle.emit();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
class={`ews-card ${this.customClass} ews-card-${this.color} ${this.open ? 'open' : ''}`.trim()}
|
||||
style={this.customStyle ? { style: this.customStyle } : {}}
|
||||
>
|
||||
{this.hasHeader && (
|
||||
<div
|
||||
class="ews-card-header"
|
||||
onClick={this.handleToggle}
|
||||
>
|
||||
<slot name="header" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="ews-card-content">
|
||||
<slot />
|
||||
<slot name="content" />
|
||||
</div>
|
||||
|
||||
{this.hasFooter && (
|
||||
<div class="ews-card-footer">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# ews-card
|
||||
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ------------- | -------------- | --------------------------------------------------- | -------- | ----------- |
|
||||
| `color` | `color` | Custom color for border and content (red or orange) | `string` | `undefined` |
|
||||
| `customClass` | `custom-class` | Additional CSS classes to apply to the card wrapper | `string` | `''` |
|
||||
| `customStyle` | `custom-style` | Inline style | `string` | `undefined` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description | Type |
|
||||
| -------- | ---------------------------------------------- | ------------------- |
|
||||
| `toggle` | Emitted when the card toggles open/close state | `CustomEvent<void>` |
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -0,0 +1,240 @@
|
||||
/* =============================================
|
||||
HEXAGONAL GRID STYLES
|
||||
============================================= */
|
||||
|
||||
/* --- Shared hex clip-path (flat-top orientation) --- */
|
||||
.ews-hex-clip {
|
||||
clip-path: polygon(24.96% 100%,
|
||||
0% 50%,
|
||||
24.96% 0%,
|
||||
74.87% 0%,
|
||||
99.84% 50%,
|
||||
74.87% 100%);
|
||||
}
|
||||
|
||||
/* --- Shared hex clip-path (pointy-top / rotated 90°) --- */
|
||||
.ews-hex-clip-pointy {
|
||||
clip-path: polygon(0% 25.13%,
|
||||
50% 0%,
|
||||
100% 25.13%,
|
||||
100% 74.87%,
|
||||
50% 100%,
|
||||
0% 74.87%);
|
||||
}
|
||||
|
||||
/* ---- 1. Basic Flat Hex Grid ---- */
|
||||
:host .ews-hex-grid-flat {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-cell-flat) {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 70px;
|
||||
aspect-ratio: 584 / 507;
|
||||
clip-path: polygon(24.96% 100%, 0% 50%, 24.96% 0%, 74.87% 0%, 99.84% 50%, 74.87% 100%);
|
||||
background-color: rgba(255, 170, 0, 0.08);
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.25s ease, transform 0.2s ease;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-cell-flat:hover) {
|
||||
background-color: rgba(255, 170, 0, 0.18);
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-cell-flat.ews-hex-danger) {
|
||||
background-color: rgba(255, 34, 51, 0.15);
|
||||
box-shadow: 0 0 12px 2px rgba(255, 34, 51, 0.3);
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-cell-flat.ews-hex-warn) {
|
||||
background-color: rgba(255, 170, 0, 0.15);
|
||||
box-shadow: 0 0 10px 2px rgba(255, 170, 0, 0.25);
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-cell-flat.ews-hex-safe) {
|
||||
background-color: rgba(0, 200, 80, 0.12);
|
||||
box-shadow: 0 0 8px 1px rgba(0, 200, 80, 0.2);
|
||||
}
|
||||
|
||||
.ews-hex-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
color: var(--orange);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---- 2. Honeycomb Offset Grid ---- */
|
||||
.ews-hex-honeycomb {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
:host .ews-hex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
:host .ews-hex-row-offset {
|
||||
margin-left: calc(72px / 2 + 2px);
|
||||
margin-top: -14px;
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-hive) {
|
||||
position: relative;
|
||||
width: 72px;
|
||||
height: 83px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-hive.ews-hex-danger) {
|
||||
background-color: rgba(255, 34, 51, 0.18);
|
||||
filter: drop-shadow(0 0 8px rgba(255, 34, 51, 0.5));
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-hive.ews-hex-warn) {
|
||||
background-color: rgba(255, 170, 0, 0.18);
|
||||
filter: drop-shadow(0 0 6px rgba(255, 170, 0, 0.4));
|
||||
}
|
||||
|
||||
.ews-hex-hive-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1px;
|
||||
text-align: center;
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
/* ---- 3. Animated Status Hex Cells ---- */
|
||||
::slotted(.ews-hex-status-cell) {
|
||||
position: relative;
|
||||
width: 90px;
|
||||
height: 104px;
|
||||
clip-path: polygon(0% 25.13%, 50% 0%, 100% 25.13%, 100% 74.87%, 50% 100%, 0% 74.87%);
|
||||
background-color: rgba(255, 170, 0, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-status-cell:hover) {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-status-cell.ews-hex-danger) {
|
||||
background-color: rgba(255, 34, 51, 0.12);
|
||||
filter: drop-shadow(0 0 10px rgba(255, 34, 51, 0.45));
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-status-cell.ews-hex-warn) {
|
||||
background-color: rgba(255, 170, 0, 0.12);
|
||||
filter: drop-shadow(0 0 8px rgba(255, 170, 0, 0.4));
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-status-cell.ews-hex-caution) {
|
||||
background-color: rgba(255, 255, 0, 0.08);
|
||||
filter: drop-shadow(0 0 6px rgba(255, 255, 0, 0.25));
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-status-cell.ews-hex-safe) {
|
||||
background-color: rgba(0, 200, 80, 0.08);
|
||||
filter: drop-shadow(0 0 6px rgba(0, 200, 80, 0.2));
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-status-cell.ews-hex-pulse) {
|
||||
animation: hexPulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes hexPulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
filter: drop-shadow(0 0 8px rgba(255, 34, 51, 0.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
filter: drop-shadow(0 0 22px rgba(255, 34, 51, 0.85));
|
||||
}
|
||||
}
|
||||
|
||||
.ews-hex-status-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 2px;
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
/* ---- 4. Hex with Strip Decoration ---- */
|
||||
::slotted(.ews-hex-stripe-cell) {
|
||||
position: relative;
|
||||
width: 110px;
|
||||
height: 127px;
|
||||
clip-path: polygon(0% 25.13%, 50% 0%, 100% 25.13%, 100% 74.87%, 50% 100%, 0% 74.87%);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(255, 170, 0, 0.05);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-stripe-cell:hover) {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
::slotted(.ews-hex-stripe-cell.ews-hex-danger) {
|
||||
background-color: rgba(255, 34, 51, 0.08);
|
||||
}
|
||||
|
||||
.ews-hex-stripe-bg {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ews-hex-stripe-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 1px;
|
||||
color: var(--orange);
|
||||
background-color: rgba(0, 0, 0, 0.55);
|
||||
padding: 6px 10px;
|
||||
clip-path: polygon(0% 25.13%, 50% 0%, 100% 25.13%, 100% 74.87%, 50% 100%, 0% 74.87%);
|
||||
width: 88%;
|
||||
height: 88%;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { Component, Host, h, Prop, Element, Watch, State } from '@stencil/core';
|
||||
|
||||
@Component({
|
||||
tag: 'ews-hex-grid',
|
||||
styleUrl: 'ews-hex-grid.css',
|
||||
shadow: true,
|
||||
})
|
||||
export class EwsHexGrid {
|
||||
/**
|
||||
* Additional CSS class for the container.
|
||||
*/
|
||||
@Prop() customClass: string = '';
|
||||
|
||||
/**
|
||||
* Hex orientation variant: 'pointy' or 'flat'.
|
||||
*/
|
||||
@Prop() variant: 'pointy' | 'flat' = 'pointy';
|
||||
|
||||
/**
|
||||
* Width of each hex cell in pixels.
|
||||
*/
|
||||
@Prop() hexWidth: number;
|
||||
|
||||
/**
|
||||
* Height of each hex cell in pixels.
|
||||
*/
|
||||
@Prop() hexHeight: number;
|
||||
|
||||
/**
|
||||
* Gap between hex cells in pixels.
|
||||
*/
|
||||
@Prop() gap: number = 4;
|
||||
|
||||
@Element() el: HTMLElement;
|
||||
|
||||
@State() containerHeight: string = 'auto';
|
||||
|
||||
private ro: ResizeObserver;
|
||||
private mo: MutationObserver;
|
||||
|
||||
componentDidLoad() {
|
||||
this.setupLayout();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.ro) this.ro.disconnect();
|
||||
if (this.mo) this.mo.disconnect();
|
||||
}
|
||||
|
||||
@Watch('variant')
|
||||
@Watch('hexWidth')
|
||||
@Watch('hexHeight')
|
||||
@Watch('gap')
|
||||
onPropChange() {
|
||||
this.layout();
|
||||
}
|
||||
|
||||
private setupLayout() {
|
||||
// Delay initial layout to next tick to ensure styles are computed
|
||||
setTimeout(() => this.layout(), 0);
|
||||
|
||||
this.ro = new ResizeObserver(() => this.layout());
|
||||
this.ro.observe(this.el);
|
||||
|
||||
const slot = this.el.shadowRoot.querySelector('slot');
|
||||
if (slot) {
|
||||
slot.addEventListener('slotchange', () => this.layout());
|
||||
}
|
||||
|
||||
this.mo = new MutationObserver(() => this.layout());
|
||||
this.mo.observe(this.el, { childList: true });
|
||||
}
|
||||
|
||||
private layout() {
|
||||
const container = this.el.shadowRoot.querySelector('.ews-hex-honeycomb') as HTMLElement;
|
||||
if (!container) return;
|
||||
|
||||
const containerWidth = container.clientWidth;
|
||||
if (!containerWidth) return;
|
||||
|
||||
const slot = container.querySelector('slot') as HTMLSlotElement;
|
||||
if (!slot) return;
|
||||
|
||||
const childElements = slot.assignedElements() as HTMLElement[];
|
||||
if (childElements.length === 0) return;
|
||||
|
||||
const isFlat = this.variant === 'flat';
|
||||
const w = this.hexWidth ?? (isFlat ? 83 : 72);
|
||||
const h = this.hexHeight ?? (isFlat ? 72 : 83);
|
||||
const gap = this.gap;
|
||||
|
||||
if (!isFlat) {
|
||||
// Pointy (Variant 1)
|
||||
const rowOffsetTop = gap + -20;
|
||||
const itemFullWidth = w + gap;
|
||||
|
||||
let maxCols = Math.floor((containerWidth + gap) / itemFullWidth);
|
||||
if (maxCols < 1) maxCols = 1;
|
||||
|
||||
let isOffset = false;
|
||||
let currentCol = 0;
|
||||
let currentRow = 0;
|
||||
|
||||
for (let i = 0; i < childElements.length; i++) {
|
||||
const child = childElements[i];
|
||||
const colsInThisRow = isOffset ? Math.max(1, maxCols - 1) : maxCols;
|
||||
|
||||
let x = currentCol * itemFullWidth;
|
||||
if (isOffset) {
|
||||
x += w / 2 + gap / 2;
|
||||
}
|
||||
|
||||
const y = currentRow * (h + rowOffsetTop);
|
||||
|
||||
child.style.position = 'absolute';
|
||||
child.style.left = `${x}px`;
|
||||
child.style.top = `${y}px`;
|
||||
child.style.margin = '0';
|
||||
child.style.width = `${w}px`;
|
||||
child.style.height = `${h}px`;
|
||||
|
||||
currentCol++;
|
||||
if (currentCol >= colsInThisRow) {
|
||||
currentCol = 0;
|
||||
isOffset = !isOffset;
|
||||
currentRow++;
|
||||
}
|
||||
}
|
||||
|
||||
let totalHeight = 0;
|
||||
if (currentCol > 0) {
|
||||
totalHeight = currentRow * (h + rowOffsetTop) + h;
|
||||
} else {
|
||||
totalHeight = (currentRow - 1) * (h + rowOffsetTop) + h;
|
||||
}
|
||||
this.containerHeight = `${totalHeight}px`;
|
||||
} else {
|
||||
// Flat (Variant 2)
|
||||
const colAdvanceX = w * 0.75 + gap;
|
||||
const rowAdvanceY = h + gap;
|
||||
|
||||
let maxCols = Math.floor((containerWidth - w) / colAdvanceX) + 1;
|
||||
if (containerWidth < w) maxCols = 1;
|
||||
|
||||
let currentCol = 0;
|
||||
let currentRow = 0;
|
||||
let maxBottom = 0;
|
||||
|
||||
for (let i = 0; i < childElements.length; i++) {
|
||||
const child = childElements[i];
|
||||
|
||||
let x = currentCol * colAdvanceX;
|
||||
let y = currentRow * rowAdvanceY;
|
||||
|
||||
// Offset odd columns down
|
||||
if (currentCol % 2 === 1) {
|
||||
y += rowAdvanceY / 2;
|
||||
}
|
||||
|
||||
child.style.position = 'absolute';
|
||||
child.style.left = `${x}px`;
|
||||
child.style.top = `${y}px`;
|
||||
child.style.margin = '0';
|
||||
child.style.width = `${w}px`;
|
||||
child.style.height = `${h}px`;
|
||||
|
||||
const bottom = y + h;
|
||||
if (bottom > maxBottom) maxBottom = bottom;
|
||||
|
||||
currentCol++;
|
||||
if (currentCol >= maxCols) {
|
||||
currentCol = 0;
|
||||
currentRow++;
|
||||
}
|
||||
}
|
||||
|
||||
this.containerHeight = `${maxBottom}px`;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Host>
|
||||
<div
|
||||
class={`ews-hex-honeycomb ${this.customClass}`.trim()}
|
||||
style={{ position: 'relative', display: 'block', height: this.containerHeight }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# ews-hex-grid
|
||||
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ------------- | -------------- | -------------------------------------------- | -------------------- | ----------- |
|
||||
| `customClass` | `custom-class` | Additional CSS class for the container. | `string` | `''` |
|
||||
| `gap` | `gap` | Gap between hex cells in pixels. | `number` | `4` |
|
||||
| `hexHeight` | `hex-height` | Height of each hex cell in pixels. | `number` | `undefined` |
|
||||
| `hexWidth` | `hex-width` | Width of each hex cell in pixels. | `number` | `undefined` |
|
||||
| `variant` | `variant` | Hex orientation variant: 'pointy' or 'flat'. | `"flat" \| "pointy"` | `'pointy'` |
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -0,0 +1,84 @@
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ews-hex-shape {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 0.866 / 1;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='115' height='133' viewBox='0 0 115 133' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M115 99.1859L57.5 132.25L7.62939e-06 99.1859L7.62939e-06 33.0641L57.5 3.05176e-05L115 33.0641L115 99.1859Z' fill='%23fa0'/%3E%3Cpath d='M113.69 98.4426L57.6307 130.678L1.57149 98.4426L1.57149 33.9776L57.6307 1.74199L113.69 33.9776L113.69 98.4426Z' fill='black'/%3E%3Cpath d='M111.071 97.0671L57.6309 127.796L4.1913 97.0671L4.1913 35.6145L57.6309 4.88519L111.071 35.6145L111.071 97.0671Z' fill='%23fa0'/%3E%3C/svg%3E%0A");
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
--polygon-shape: polygon(0% 25.13%,
|
||||
/* top-left point */
|
||||
50% 0%,
|
||||
/* top center point */
|
||||
100% 25.13%,
|
||||
/* top-right point */
|
||||
100% 74.87%,
|
||||
/* bottom-right point */
|
||||
50% 100%,
|
||||
/* bottom center point */
|
||||
0% 74.87%
|
||||
/* bottom-left point */
|
||||
);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.ews-hex-shape.clip-content {
|
||||
overflow: hidden;
|
||||
clip-path: var(--polygon-shape);
|
||||
}
|
||||
|
||||
.ews-hex-shape.clip-content .inner-content {
|
||||
--ews-hex-padding: 10px;
|
||||
width: calc(100% - var(--ews-hex-padding));
|
||||
height: calc(100% - var(--ews-hex-padding));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
clip-path: var(--polygon-shape);
|
||||
}
|
||||
|
||||
.ews-hex-shape.flat-top {
|
||||
aspect-ratio: 1.1547 / 1;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='584' height='507' viewBox='0 0 584 507' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M145.77 507L0 253.5L145.77 -3.05176e-05H437.28L583.05 253.5L437.28 507H145.77Z' fill='%23E60003'/%3E%3Cpath d='M149.046 500.648L6.92932 253.5L149.046 6.35181H433.253L575.37 253.5L433.253 500.648H149.046Z' fill='black'/%3E%3Cpath d='M155.007 492L18 253.5L155.007 15H428.993L566 253.5L428.993 492H155.007Z' fill='%23E60003'/%3E%3C/svg%3E%0A");
|
||||
--polygon-shape: polygon(24.96% 100%,
|
||||
/* 145.77/584, 507/507 */
|
||||
0% 50%,
|
||||
/* 0/584, 253.5/507 */
|
||||
24.96% 0%,
|
||||
/* 145.77/584, 0/507 */
|
||||
74.87% 0%,
|
||||
/* 437.28/584, 0/507 */
|
||||
99.84% 50%,
|
||||
/* 583.05/584, 253.5/507 */
|
||||
74.87% 100%
|
||||
/* 437.28/584, 507/507 */
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
.ews-hex-shape.orange {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='115' height='133' viewBox='0 0 115 133' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M115 99.1859L57.5 132.25L7.62939e-06 99.1859L7.62939e-06 33.0641L57.5 3.05176e-05L115 33.0641L115 99.1859Z' fill='%23fa0'/%3E%3Cpath d='M113.69 98.4426L57.6307 130.678L1.57149 98.4426L1.57149 33.9776L57.6307 1.74199L113.69 33.9776L113.69 98.4426Z' fill='black'/%3E%3Cpath d='M111.071 97.0671L57.6309 127.796L4.1913 97.0671L4.1913 35.6145L57.6309 4.88519L111.071 35.6145L111.071 97.0671Z' fill='%23fa0'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
|
||||
.ews-hex-shape.orange.flat-top {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='584' height='507' viewBox='0 0 584 507' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M145.77 507L0 253.5L145.77 -3.05176e-05H437.28L583.05 253.5L437.28 507H145.77Z' fill='%23fa0'/%3E%3Cpath d='M149.046 500.648L6.92932 253.5L149.046 6.35181H433.253L575.37 253.5L433.253 500.648H149.046Z' fill='black'/%3E%3Cpath d='M155.007 492L18 253.5L155.007 15H428.993L566 253.5L428.993 492H155.007Z' fill='%23fa0'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
|
||||
.ews-hex-shape.red {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='115' height='133' viewBox='0 0 115 133' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M115 99.1859L57.5 132.25L7.62939e-06 99.1859L7.62939e-06 33.0641L57.5 3.05176e-05L115 33.0641L115 99.1859Z' fill='%23E60003'/%3E%3Cpath d='M113.69 98.4426L57.6307 130.678L1.57149 98.4426L1.57149 33.9776L57.6307 1.74199L113.69 33.9776L113.69 98.4426Z' fill='black'/%3E%3Cpath d='M111.071 97.0671L57.6309 127.796L4.1913 97.0671L4.1913 35.6145L57.6309 4.88519L111.071 35.6145L111.071 97.0671Z' fill='%23E60003'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
|
||||
.ews-hex-shape.red.flat-top {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='584' height='507' viewBox='0 0 584 507' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M145.77 507L0 253.5L145.77 -3.05176e-05H437.28L583.05 253.5L437.28 507H145.77Z' fill='%23E60003'/%3E%3Cpath d='M149.046 500.648L6.92932 253.5L149.046 6.35181H433.253L575.37 253.5L433.253 500.648H149.046Z' fill='black'/%3E%3Cpath d='M155.007 492L18 253.5L155.007 15H428.993L566 253.5L428.993 492H155.007Z' fill='%23E60003'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Component, Host, h, Prop } from '@stencil/core';
|
||||
|
||||
@Component({
|
||||
tag: 'ews-hex-shape',
|
||||
styleUrl: 'ews-hex-shape.css',
|
||||
shadow: true,
|
||||
})
|
||||
export class EwsHexShape {
|
||||
/**
|
||||
* Additional CSS classes for the component.
|
||||
*/
|
||||
@Prop() customClass: string = '';
|
||||
|
||||
/**
|
||||
* The color variant of the hex shape.
|
||||
*/
|
||||
@Prop() color: string = 'orange';
|
||||
|
||||
/**
|
||||
* Whether the hex shape has a flat top.
|
||||
*/
|
||||
@Prop() flatTop: boolean = true;
|
||||
|
||||
/**
|
||||
* Whether to clip content within the hex shape.
|
||||
*/
|
||||
@Prop() clipContent: boolean = false;
|
||||
|
||||
/**
|
||||
* The padding inside the hex shape for content.
|
||||
*/
|
||||
@Prop() paddingContent: number = 10;
|
||||
|
||||
render() {
|
||||
const classes = {
|
||||
'ews-hex-shape': true,
|
||||
'flat-top': this.flatTop,
|
||||
'clip-content': this.clipContent,
|
||||
[this.color]: !!this.color,
|
||||
[this.customClass]: !!this.customClass,
|
||||
};
|
||||
|
||||
return (
|
||||
<Host>
|
||||
<div class={classes}>
|
||||
<div
|
||||
class="inner-content"
|
||||
style={{ '--ews-hex-padding': `${this.paddingContent}px` }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# ews-hex-shape
|
||||
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ---------------- | ----------------- | --------------------------------------------- | --------- | ---------- |
|
||||
| `clipContent` | `clip-content` | Whether to clip content within the hex shape. | `boolean` | `false` |
|
||||
| `color` | `color` | The color variant of the hex shape. | `string` | `'orange'` |
|
||||
| `customClass` | `custom-class` | Additional CSS classes for the component. | `string` | `''` |
|
||||
| `flatTop` | `flat-top` | Whether the hex shape has a flat top. | `boolean` | `true` |
|
||||
| `paddingContent` | `padding-content` | The padding inside the hex shape for content. | `number` | `10` |
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -0,0 +1,276 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ews-rib-layout {
|
||||
display: inline-flex;
|
||||
height: auto;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ews-rib-layout__branch {
|
||||
position: relative;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.ews-rib-layout__branch {
|
||||
padding-top: 2.5rem;
|
||||
padding-bottom: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ews-rib-layout__spine {
|
||||
position: absolute;
|
||||
height: auto;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0.25rem;
|
||||
transform: translateX(-50%);
|
||||
z-index: 0;
|
||||
background-color: var(--orange, #FC8416);
|
||||
}
|
||||
|
||||
.ews-rib-layout__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.ews-rib-layout__node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.ews-rib-layout__node--left {
|
||||
flex-grow: 1;
|
||||
justify-content: flex-end;
|
||||
padding-right: 0;
|
||||
grid-column-start: 1;
|
||||
}
|
||||
|
||||
.ews-rib-layout__node--right {
|
||||
justify-content: flex-start;
|
||||
padding-left: 0;
|
||||
grid-column-start: 2;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.ews-rib-layout__node-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-wrapper {
|
||||
width: 6rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-wrapper--left {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-wrapper--right {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-line {
|
||||
height: 2px;
|
||||
width: 6rem;
|
||||
z-index: 0;
|
||||
background-color: var(--orange, #FC8416);
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-text {
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
z-index: 10;
|
||||
color: var(--orange, #FC8416);
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-text--left {
|
||||
left: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ews-rib-layout__connector-text--right {
|
||||
right: 0.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ews-rib-node {
|
||||
--bg-url: url('data:image/svg+xml,<svg width="500" height="100" viewBox="0 0 500 100" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M500 0H37.5414L-1.63437e-05 100H462.459L500 0Z" fill="%2300FF80"/><path d="M168.5 0H37.1618L30.5 19H161.838L168.5 0Z" fill="%23FC8416"/></svg>');
|
||||
background-image: var(--bg-url);
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 6rem;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 1.5rem;
|
||||
margin-right: -0.5rem;
|
||||
}
|
||||
|
||||
.parent-node {
|
||||
transform: translateX(0px);
|
||||
rotate: -21deg !important;
|
||||
transition: all 0.2s ease-in-out;
|
||||
margin-left: 0px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.parent-node.flip {
|
||||
margin-left: -0.5rem;
|
||||
rotate: 21deg !important;
|
||||
}
|
||||
|
||||
.ews-rib-layout__node--left:hover .parent-node {
|
||||
cursor: pointer;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
.ews-rib-node.danger {
|
||||
--bg-url: url('data:image/svg+xml,<svg width="500" height="100" viewBox="0 0 500 100" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M500 0H37.5414L-1.63437e-05 100H462.459L500 0Z" fill="%23E60003"/><path d="M168.5 0H37.1618L30.5 19H161.838L168.5 0Z" fill="%23FC8416"/></svg>');
|
||||
}
|
||||
|
||||
.ews-rib-node.flip {
|
||||
--bg-url: url('data:image/svg+xml,<svg width="500" height="100" viewBox="0 0 500 100" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 0H462.459L500 100H37.5415L0 0Z" fill="%2300FF80"/><path d="M0 0H131.338L138 19H6.66178L0 0Z" fill="%23FC8416"/></svg>');
|
||||
}
|
||||
|
||||
.ews-rib-layout__node--right:hover .parent-node {
|
||||
cursor: pointer;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.ews-rib-node.flip.danger {
|
||||
--bg-url: url('data:image/svg+xml,<svg width="500" height="100" viewBox="0 0 500 100" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 0H462.459L500 100H37.5415L0 0Z" fill="%23E60003"/><path d="M0 0H131.338L138 19H6.66178L0 0Z" fill="%23FC8416"/></svg>');
|
||||
}
|
||||
|
||||
.ews-rib-node.slide-fade-in {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
animation: slideFadeIn 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
.ews-rib-node-flip.slide-fade-in {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
animation: slideFadeInFlip 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
.slide-fade-in {
|
||||
animation: slideFadeIn 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translateX(-15px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-fade-in-flip {
|
||||
animation: slideFadeInFlip 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideFadeInFlip {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translateX(15px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animation-delay-5 {
|
||||
animation-delay: 500ms;
|
||||
}
|
||||
|
||||
.line-node {
|
||||
width: 0%;
|
||||
animation: slideWidth 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideWidth {
|
||||
0% {
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
width: 6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.line-central {
|
||||
height: 0%;
|
||||
animation: slideHeight 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideHeight {
|
||||
0% {
|
||||
height: 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { Component, Host, h, Prop, State, Listen } from '@stencil/core';
|
||||
|
||||
@Component({
|
||||
tag: 'ews-rib-layout',
|
||||
styleUrl: 'ews-rib-layout.css',
|
||||
shadow: true,
|
||||
})
|
||||
export class EwsRibLayout {
|
||||
/**
|
||||
* Array of items to be displayed in the rib cage layout.
|
||||
*/
|
||||
@Prop() items: any[] = [];
|
||||
|
||||
/**
|
||||
* Function to get the href for a node. If provided, nodes will be rendered as <a> tags.
|
||||
*/
|
||||
@Prop() getHref?: (item: any) => string;
|
||||
|
||||
/**
|
||||
* Optional renderer function for the node content.
|
||||
*/
|
||||
@Prop() nodeRenderer?: (item: any, props: { side: 'left' | 'right'; branchIndex: number; index: number; delay: number }) => any;
|
||||
|
||||
/**
|
||||
* Optional renderer function for the connector content.
|
||||
*/
|
||||
@Prop() connectorRenderer?: (item: any, props: { side: 'left' | 'right'; branchIndex: number; index: number; delay: number }) => any;
|
||||
|
||||
/**
|
||||
* Maximum number of branches to display. If not provided, it defaults to 5 (responsive).
|
||||
*/
|
||||
@Prop() maxBranches?: number;
|
||||
|
||||
@State() branchCount: number = 5;
|
||||
@State() windowWidth: number = 0;
|
||||
|
||||
componentWillLoad() {
|
||||
this.handleResize();
|
||||
}
|
||||
|
||||
@Listen('resize', { target: 'window' })
|
||||
handleResize() {
|
||||
this.windowWidth = typeof window !== 'undefined' ? window.innerWidth : 0;
|
||||
this.branchCount = this.getBranchCount(this.windowWidth);
|
||||
}
|
||||
|
||||
private getBranchCount(width: number): number {
|
||||
let count = 5;
|
||||
if (width < 768) count = 1;
|
||||
else if (width < 1024) count = 2;
|
||||
else if (width < 1300) count = 4;
|
||||
|
||||
if (this.maxBranches && this.maxBranches > 0) {
|
||||
return Math.min(count, this.maxBranches);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private get chunkedItems() {
|
||||
if (!this.items || this.items.length === 0) return [];
|
||||
const count = Math.max(1, this.branchCount);
|
||||
const result = [];
|
||||
const itemsPerBranch = Math.ceil(this.items.length / count);
|
||||
|
||||
for (let i = 0; i < this.items.length; i += itemsPerBranch) {
|
||||
result.push(this.items.slice(i, i + itemsPerBranch));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private renderNodeContent(item: any, props: { side: 'left' | 'right'; branchIndex: number; index: number; delay: number }) {
|
||||
if (this.nodeRenderer) {
|
||||
const content = this.nodeRenderer(item, props);
|
||||
|
||||
// Handle HTMLElement return (common in plain JS)
|
||||
if (content instanceof HTMLElement) {
|
||||
return <div class="node-content-wrapper" ref={el => {
|
||||
if (el) {
|
||||
el.innerHTML = '';
|
||||
el.appendChild(content);
|
||||
}
|
||||
}}></div>;
|
||||
}
|
||||
|
||||
// Handle string return (simple HTML)
|
||||
if (typeof content === 'string') {
|
||||
return <div class="node-content-wrapper" innerHTML={content}></div>;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// Default rendering if no renderer is provided
|
||||
const isDanger = item.type === 'danger';
|
||||
return (
|
||||
<div class={`ews-rib-node ${props.side === 'right' ? 'flip' : ''} ${isDanger ? 'danger' : ''}`}>
|
||||
<span>{item.label || item.name || item.value || 'Node'}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderConnectorContent(item: any, props: { side: 'left' | 'right'; branchIndex: number; index: number; delay: number }) {
|
||||
if (this.connectorRenderer) {
|
||||
const content = this.connectorRenderer(item, props);
|
||||
|
||||
const renderWrapper = (inner) => (
|
||||
<div class={`ews-rib-layout__connector-text ews-rib-layout__connector-text--${props.side} fade-in animation-delay-5`}>
|
||||
{inner}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (content instanceof HTMLElement) {
|
||||
return renderWrapper(<div ref={el => {
|
||||
if (el) {
|
||||
el.innerHTML = '';
|
||||
el.appendChild(content);
|
||||
}
|
||||
}}></div>);
|
||||
}
|
||||
|
||||
if (typeof content === 'string') {
|
||||
return renderWrapper(<div innerHTML={content}></div>);
|
||||
}
|
||||
|
||||
return renderWrapper(content);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const chunked = this.chunkedItems;
|
||||
|
||||
return (
|
||||
<Host>
|
||||
<div class="ews-rib-layout">
|
||||
{chunked.map((branchItems, branchIndex) => (
|
||||
<div class="ews-rib-layout__branch" key={branchIndex}>
|
||||
{/* Central Spine */}
|
||||
<div
|
||||
class="ews-rib-layout__spine line-central"
|
||||
style={{ animationDelay: `${branchIndex * 200}ms` }}
|
||||
></div>
|
||||
|
||||
<div class="ews-rib-layout__grid">
|
||||
{branchItems.map((item, index) => {
|
||||
const side = index % 2 === 0 ? 'left' : 'right';
|
||||
const delay = (branchIndex + 1) * (index + 1) * 10;
|
||||
const href = this.getHref?.(item);
|
||||
const Tag = href ? 'a' : 'div';
|
||||
|
||||
return (
|
||||
<Tag
|
||||
href={href}
|
||||
class={`ews-rib-layout__node ${side === 'left' ? 'ews-rib-layout__node--left node' : 'ews-rib-layout__node--right node-flip'}`}
|
||||
key={index}
|
||||
>
|
||||
{side === 'left' ? (
|
||||
[
|
||||
<div class="ews-rib-layout__node-content parent-node">
|
||||
{this.renderNodeContent(item, { side, branchIndex, index, delay })}
|
||||
</div>,
|
||||
<div class="ews-rib-layout__connector-wrapper ews-rib-layout__connector-wrapper--left line">
|
||||
<div class="ews-rib-layout__connector-line line-node" style={{ animationDelay: `${delay}ms` }}></div>
|
||||
{this.renderConnectorContent(item, { side, branchIndex, index, delay })}
|
||||
</div>,
|
||||
]
|
||||
) : (
|
||||
[
|
||||
<div class="ews-rib-layout__connector-wrapper ews-rib-layout__connector-wrapper--right">
|
||||
<div class="ews-rib-layout__connector-line line-node" style={{ animationDelay: `${delay}ms` }}></div>
|
||||
{this.renderConnectorContent(item, { side, branchIndex, index, delay })}
|
||||
</div>,
|
||||
<div class="parent-node flip ews-rib-layout__node-content ews-rib-layout__node-content--right">
|
||||
{this.renderNodeContent(item, { side, branchIndex, index, delay })}
|
||||
</div>,
|
||||
]
|
||||
)}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# ews-rib-layout
|
||||
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ------------------- | -------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ----------- |
|
||||
| `connectorRenderer` | -- | Optional renderer function for the connector content. | `(item: any, props: { side: "left" \| "right"; branchIndex: number; index: number; delay: number; }) => any` | `undefined` |
|
||||
| `getHref` | -- | Function to get the href for a node. If provided, nodes will be rendered as <a> tags. | `(item: any) => string` | `undefined` |
|
||||
| `items` | -- | Array of items to be displayed in the rib cage layout. | `any[]` | `[]` |
|
||||
| `maxBranches` | `max-branches` | Maximum number of branches to display. If not provided, it defaults to 5 (responsive). | `number` | `undefined` |
|
||||
| `nodeRenderer` | -- | Optional renderer function for the node content. | `(item: any, props: { side: "left" \| "right"; branchIndex: number; index: number; delay: number; }) => any` | `undefined` |
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -0,0 +1,219 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.host-wrapper {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Strip Bar Styles */
|
||||
.ews-stripe-wrapper {
|
||||
width: max(200vw, 2000px);
|
||||
height: 30px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin: 0px !important;
|
||||
padding: 0px !important;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ews-stripe-wrapper.vertical {
|
||||
height: 100%;
|
||||
width: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ews-stripe-bar {
|
||||
width: max(200vw, 2000px);
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
margin-right: 0px !important;
|
||||
margin-left: 0px !important;
|
||||
--ews-stripe-color: var(--orange, #fa0);
|
||||
--ews-stripe-size: 15px;
|
||||
--ews-glow-color: rgba(255, 94, 0, 0.8);
|
||||
--ews-glow-size: 3px;
|
||||
background-image: repeating-linear-gradient(-45deg,
|
||||
var(--ews-glow-color) calc(-1 * var(--ews-glow-size)),
|
||||
var(--ews-stripe-color) 0,
|
||||
var(--ews-stripe-color) calc(var(--ews-stripe-size) - var(--ews-glow-size) / 2),
|
||||
var(--ews-glow-color) calc(var(--ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(var(--ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(2 * var(--ews-stripe-size)),
|
||||
var(--ews-glow-color) calc(2 * var(--ews-stripe-size) - var(--ews-glow-size)));
|
||||
background-size: 47px 47px;
|
||||
}
|
||||
|
||||
.ews-stripe-bar.red {
|
||||
--ews-stripe-color: var(--red, #f23);
|
||||
--ews-stripe-size: 15px;
|
||||
--ews-glow-color: rgba(255, 17, 0, 0.8);
|
||||
--ews-glow-size: 3px;
|
||||
}
|
||||
|
||||
.ews-stripe-bar.vertical {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Include the legacy combined classes just in case */
|
||||
.ews-stripe-bar-red {
|
||||
width: max(200vw, 2000px);
|
||||
height: 30px;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
--ews-stripe-color: var(--red, #f23);
|
||||
--ews-stripe-size: 15px;
|
||||
--ews-glow-color: rgba(255, 17, 0, 0.8);
|
||||
--ews-glow-size: 3px;
|
||||
background-image: repeating-linear-gradient(-45deg,
|
||||
var(--ews-glow-color) calc(-1 * var(--ews-glow-size)),
|
||||
var(--ews-stripe-color) 0,
|
||||
var(--ews-stripe-color) calc(var(--ews-stripe-size) - var(--ews-glow-size) / 2),
|
||||
var(--ews-glow-color) calc(var(--ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(var(--ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(2 * var(--ews-stripe-size)),
|
||||
var(--ews-glow-color) calc(2 * var(--ews-stripe-size) - var(--ews-glow-size)));
|
||||
}
|
||||
|
||||
.ews-stripe-bar-vertical {
|
||||
height: max(2000px, 200vh);
|
||||
transform: translate3d(0, 0, 0);
|
||||
--ews-stripe-color: var(--orange, #fa0);
|
||||
--ews-stripe-size: 15px;
|
||||
--ews-glow-color: rgba(255, 94, 0, 0.8);
|
||||
--ews-glow-size: 3px;
|
||||
background-image: repeating-linear-gradient(45deg,
|
||||
var(--ews-glow-color) calc(-1 * var(--ews-glow-size)),
|
||||
var(--ews-stripe-color) 0,
|
||||
var(--ews-stripe-color) calc(var(--ews-stripe-size) - var(--ews-glow-size) / 2),
|
||||
var(--ews-glow-color) calc(var(--ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(var(--ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(2 * var(--ews-stripe-size)),
|
||||
var(--ews-glow-color) calc(2 * var(--ews-stripe-size) - var(--ews-glow-size)));
|
||||
}
|
||||
|
||||
.ews-stripe-bar-red-vertical {
|
||||
height: max(2000px, 200vh);
|
||||
transform: translate3d(0, 0, 0);
|
||||
--ews-stripe-color: var(--red, #f23);
|
||||
--ews-stripe-size: 15px;
|
||||
--ews-glow-color: rgba(255, 17, 0, 0.8);
|
||||
--ews-glow-size: 3px;
|
||||
background-image: repeating-linear-gradient(45deg,
|
||||
var(--ews-glow-color) calc(-1 * var(--ews-glow-size)),
|
||||
var(--ews-stripe-color) 0,
|
||||
var(--ews-stripe-color) calc(var(--ews-stripe-size) - var(--ews-glow-size) / 2),
|
||||
var(--ews-glow-color) calc(var(--ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(var(--ews-stripe-size) + var(--ews-glow-size) / 2),
|
||||
transparent calc(2 * var(--ews-stripe-size)),
|
||||
var(--ews-glow-color) calc(2 * var(--ews-stripe-size) - var(--ews-glow-size)));
|
||||
}
|
||||
|
||||
.ews-stripe-wrapper-vertical {
|
||||
height: max(200vh, 2000px);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin: 0px !important;
|
||||
padding: 0px !important;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ews-stripe {
|
||||
background-color: black;
|
||||
width: 100vw;
|
||||
border-top: 1px solid var(--red, #f23);
|
||||
border-bottom: 1px solid var(--red, #f23);
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes loopStripVertical {
|
||||
from {
|
||||
background-position: 0px 0px;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: 0px calc(-42.4264px * 47);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes stripeAnimationVertical {
|
||||
from {
|
||||
background-position: 0px 0px;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: 0px calc(-42.4264px * 47);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes stripeAnimation {
|
||||
from {
|
||||
background-position: 0px 0px;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: calc(-42.4264px * 47) 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loopStrip {
|
||||
from {
|
||||
background-position: 0px 0px;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: calc(-42.4264px * 47) 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.loop-stripe-vertical {
|
||||
animation: stripeAnimationVertical 15s infinite linear;
|
||||
}
|
||||
|
||||
.loop-stripe-vertical.reverse {
|
||||
animation: stripeAnimationVertical 15s infinite linear reverse;
|
||||
}
|
||||
|
||||
.loop-stripe-vertical-reverse {
|
||||
animation: loopStripVertical 15s infinite linear reverse;
|
||||
}
|
||||
|
||||
.stripe-animation {
|
||||
animation: stripeAnimation 10s infinite linear;
|
||||
}
|
||||
|
||||
.stripe-animation-reverse {
|
||||
animation: stripeAnimation 10s infinite linear reverse;
|
||||
}
|
||||
|
||||
.loop-stripe {
|
||||
animation: stripeAnimation infinite linear;
|
||||
animation-duration: 10s;
|
||||
}
|
||||
|
||||
.loop-stripe.reverse {
|
||||
animation: loopStrip infinite linear reverse;
|
||||
}
|
||||
|
||||
.loop-stripe-reverse {
|
||||
animation: loopStrip infinite linear reverse;
|
||||
animation-duration: 10s;
|
||||
}
|
||||
|
||||
.anim-duration-5 {
|
||||
animation-duration: 5s !important;
|
||||
}
|
||||
|
||||
.anim-duration-10 {
|
||||
animation-duration: 10s !important;
|
||||
}
|
||||
|
||||
.anim-duration-20 {
|
||||
animation-duration: 20s !important;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Component, Host, h, Prop } from '@stencil/core';
|
||||
|
||||
@Component({
|
||||
tag: 'ews-stripe-bar',
|
||||
styleUrl: 'ews-stripe-bar.css',
|
||||
shadow: true,
|
||||
})
|
||||
export class EwsStripeBar {
|
||||
@Prop() color: string = '';
|
||||
@Prop() orientation: string = '';
|
||||
@Prop() loop: boolean = false;
|
||||
@Prop() reverse: boolean = false;
|
||||
@Prop() duration: number = 10;
|
||||
@Prop() size: string = '30px';
|
||||
|
||||
private getStripeClasses() {
|
||||
const loopStr = this.loop ? 'loop-stripe' : '';
|
||||
const orientationStr = this.orientation ? `-${this.orientation}` : '';
|
||||
const combinedStr = loopStr + orientationStr;
|
||||
|
||||
return [
|
||||
'ews-stripe-bar',
|
||||
this.color,
|
||||
this.orientation,
|
||||
combinedStr,
|
||||
this.reverse ? 'reverse' : '',
|
||||
`anim-duration-${this.duration}`
|
||||
].filter(c => c.trim() !== '').join(' ');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Host>
|
||||
<div style={{ overflow: 'hidden', height: '100%', width: '100%' }} class="host-wrapper">
|
||||
<div
|
||||
class={`ews-stripe-wrapper ${this.orientation}`}
|
||||
style={{ [this.orientation === 'vertical' ? 'width' : 'height']: this.size }}
|
||||
>
|
||||
<div class={this.getStripeClasses()}></div>
|
||||
<div class={this.getStripeClasses()}></div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# ews-stripe-bar
|
||||
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ------------- | ------------- | ----------- | --------- | -------- |
|
||||
| `color` | `color` | | `string` | `''` |
|
||||
| `duration` | `duration` | | `number` | `10` |
|
||||
| `loop` | `loop` | | `boolean` | `false` |
|
||||
| `orientation` | `orientation` | | `string` | `''` |
|
||||
| `reverse` | `reverse` | | `boolean` | `false` |
|
||||
| `size` | `size` | | `string` | `'30px'` |
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -0,0 +1,31 @@
|
||||
import { render, h, describe, it, expect } from '@stencil/vitest';
|
||||
|
||||
describe('my-component', () => {
|
||||
it('renders', async () => {
|
||||
const { root } = await render(<my-component></my-component>);
|
||||
await expect(root).toEqualHtml(`
|
||||
<my-component class="hydrated">
|
||||
<mock:shadow-root>
|
||||
<div>
|
||||
Hello, World! I'm
|
||||
</div>
|
||||
</mock:shadow-root>
|
||||
</my-component>
|
||||
`);
|
||||
});
|
||||
|
||||
it('renders with values', async () => {
|
||||
const { root } = await render(
|
||||
<my-component first="Stencil" middle="'Don't call me a framework'" last="JS"></my-component>,
|
||||
);
|
||||
await expect(root).toEqualHtml(`
|
||||
<my-component class="hydrated">
|
||||
<mock:shadow-root>
|
||||
<div>
|
||||
Hello, World! I'm Stencil 'Don't call me a framework' JS
|
||||
</div>
|
||||
</mock:shadow-root>
|
||||
</my-component>
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Component, Prop, h } from '@stencil/core';
|
||||
import { format } from '../../utils/utils';
|
||||
|
||||
@Component({
|
||||
tag: 'my-component',
|
||||
styleUrl: 'my-component.css',
|
||||
shadow: true,
|
||||
})
|
||||
export class MyComponent {
|
||||
/**
|
||||
* The first name
|
||||
*/
|
||||
@Prop() first: string;
|
||||
|
||||
/**
|
||||
* The middle name
|
||||
*/
|
||||
@Prop() middle: string;
|
||||
|
||||
/**
|
||||
* The last name
|
||||
*/
|
||||
@Prop() last: string;
|
||||
|
||||
private getText(): string {
|
||||
return format(this.first, this.middle, this.last);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>Hello, World! I'm {this.getText()}</div>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# my-component
|
||||
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| -------- | --------- | --------------- | -------- | ----------- |
|
||||
| `first` | `first` | The first name | `string` | `undefined` |
|
||||
| `last` | `last` | The last name | `string` | `undefined` |
|
||||
| `middle` | `middle` | The middle name | `string` | `undefined` |
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
@@ -0,0 +1,327 @@
|
||||
<!DOCTYPE html>
|
||||
<html dir="ltr" lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0" />
|
||||
<title>Stencil Component Starter</title>
|
||||
|
||||
<script type="module" src="/build/ews-component.esm.js"></script>
|
||||
<script nomodule src="/build/ews-component.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #555;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.example-section {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.example-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.example-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.example-label {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>EWS Component Examples</h1>
|
||||
|
||||
|
||||
|
||||
<!-- ews-card Examples -->
|
||||
<div class="example-section">
|
||||
<h2>ews-card</h2>
|
||||
<p>A collapsible card component with customizable colors and styling.</p>
|
||||
<div class="example-row">
|
||||
<div class="example-item">
|
||||
<ews-card color="red">
|
||||
<div slot="header">Red Card Header</div>
|
||||
<div slot="content">This is the content of a red card. Click the header to toggle.</div>
|
||||
</ews-card>
|
||||
<span class="example-label">Red color</span>
|
||||
</div>
|
||||
<div class="example-item">
|
||||
<ews-card color="orange">
|
||||
<div slot="header">Orange Card Header</div>
|
||||
<div slot="content">This is the content of an orange card.</div>
|
||||
</ews-card>
|
||||
<span class="example-label">Orange color</span>
|
||||
</div>
|
||||
<div class="example-item">
|
||||
<ews-card color="orange" style="max-width: 400px;">
|
||||
<div slot="header">
|
||||
<ews-stripe-bar></ews-stripe-bar>
|
||||
<div class="ews-card-text">
|
||||
<p style="padding: 2px; text-align: center;background-color: black;">WARNING</p>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="content">
|
||||
<div style="padding: 6px; text-align: center;">
|
||||
WARNING CONTENT NEED YOUR ATENTION
|
||||
</div>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<ews-stripe-bar reverse="true"></ews-stripe-bar>
|
||||
<div class="ews-card-text">
|
||||
<p style="padding: 2px; text-align: center;background-color: black;">WARNING</p>
|
||||
</div>
|
||||
</div>
|
||||
</ews-card>
|
||||
<span class="example-label">Custom</span>
|
||||
</div>
|
||||
<div class="example-item">
|
||||
<ews-card color="red" style="max-width: 400px;">
|
||||
<div slot="header">
|
||||
<ews-stripe-bar color="red" loop="true"></ews-stripe-bar>
|
||||
<div class="ews-card-text">
|
||||
<p style="padding: 2px; text-align: center;background-color: black;">DANGER</p>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="content">
|
||||
<div style="padding: 6px; text-align: center;">
|
||||
DANGER CONTENT NEED YOUR ATENTION
|
||||
</div>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<ews-stripe-bar color="red" loop="true" reverse="true"></ews-stripe-bar>
|
||||
<div class="ews-card-text">
|
||||
<p style="padding: 2px; text-align: center;background-color: black;">DANGER</p>
|
||||
</div>
|
||||
</div>
|
||||
</ews-card>
|
||||
<span class="example-label">Custom</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ews-hex-shape Examples -->
|
||||
<div class="example-section">
|
||||
<h2>ews-hex-shape</h2>
|
||||
<p>A hexagonal shape component with various configuration options.</p>
|
||||
<div class="example-row">
|
||||
<div class="example-item">
|
||||
<ews-hex-shape style="width: 150px;">
|
||||
<div style="padding: 20px; text-align: center; color: white;">Blue Hex</div>
|
||||
</ews-hex-shape>
|
||||
<span class="example-label">Default</span>
|
||||
</div>
|
||||
<div class="example-item">
|
||||
<ews-hex-shape flat-top="false" style="width: 150px;">
|
||||
<div style="padding: 20px; text-align: center; color: white;">Pointy Top</div>
|
||||
</ews-hex-shape>
|
||||
<span class="example-label">Pointy top</span>
|
||||
</div>
|
||||
|
||||
<div class="example-item">
|
||||
<ews-hex-shape color="red" style="width: 150px;"></ews-hex-shape>
|
||||
<span class="example-label">Red</span>
|
||||
</div>
|
||||
|
||||
<div class="example-item">
|
||||
<ews-hex-shape clip-content="true" style="width: 150px; height: 150px;">
|
||||
<div style="padding: 20px; text-align: center; color: white; background: rgba(0,0,0,0.3);">
|
||||
Clipped Content - This text is clipped within the hex boundary
|
||||
</div>
|
||||
</ews-hex-shape>
|
||||
<span class="example-label">Clipped content</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ews-stripe-bar Examples -->
|
||||
<div class="example-section">
|
||||
<h2>ews-stripe-bar</h2>
|
||||
<p>An animated stripe bar component with various customization options.</p>
|
||||
<div class="example-row">
|
||||
<div class="example-item">
|
||||
<ews-stripe-bar style="width: 200px;"></ews-stripe-bar>
|
||||
<span class="example-label">Default</span>
|
||||
</div>
|
||||
<div class="example-item">
|
||||
<ews-stripe-bar color="red" style="width: 200px;"></ews-stripe-bar>
|
||||
<span class="example-label">Red</span>
|
||||
</div>
|
||||
|
||||
<div class="example-item">
|
||||
<ews-stripe-bar color="red" loop="true" style="width: 200px;"></ews-stripe-bar>
|
||||
<span class="example-label">Animated</span>
|
||||
</div>
|
||||
|
||||
<div class="example-item">
|
||||
<ews-stripe-bar loop="true" reverse="true" style="width: 200px;"></ews-stripe-bar>
|
||||
<span class="example-label">Reverse</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="example-row">
|
||||
<div class="example-item">
|
||||
<ews-stripe-bar orientation="vertical" style="height: 200px;"></ews-stripe-bar>
|
||||
<span class="example-label">Default</span>
|
||||
</div>
|
||||
<div class="example-item">
|
||||
<ews-stripe-bar color="red" orientation="vertical" style="height: 200px;"></ews-stripe-bar>
|
||||
<span class="example-label">Red</span>
|
||||
</div>
|
||||
|
||||
<div class="example-item">
|
||||
<ews-stripe-bar color="red" orientation="vertical" loop="true" style="height: 200px;"></ews-stripe-bar>
|
||||
<span class="example-label">Animated</span>
|
||||
</div>
|
||||
|
||||
<div class="example-item">
|
||||
<ews-stripe-bar loop="true" orientation="vertical" reverse="true" style="height: 200px;"></ews-stripe-bar>
|
||||
<span class="example-label">Reverse</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="example-section">
|
||||
<h2>ews-hex-grid</h2>
|
||||
|
||||
<div class="example-row">
|
||||
<div class="example-item">
|
||||
<ews-hex-grid variant="pointy" gap="1" style="width: 300px;">
|
||||
<div class="ews-hex-hive">
|
||||
<ews-hex-shape flat-top="false">Cell 1</ews-hex-shape>
|
||||
</div>
|
||||
<div class="ews-hex-hive">
|
||||
<ews-hex-shape flat-top="false">Cell 2</ews-hex-shape>
|
||||
</div>
|
||||
<div class="ews-hex-hive">
|
||||
<ews-hex-shape flat-top="false">Cell 3</ews-hex-shape>
|
||||
</div>
|
||||
<div class="ews-hex-hive">
|
||||
<ews-hex-shape flat-top="false">Cell 4</ews-hex-shape>
|
||||
</div>
|
||||
<div class="ews-hex-hive">
|
||||
<ews-hex-shape flat-top="false">Cell 5</ews-hex-shape>
|
||||
</div>
|
||||
<div class="ews-hex-hive">
|
||||
<ews-hex-shape flat-top="false">Cell 6</ews-hex-shape>
|
||||
</div>
|
||||
<div class="ews-hex-hive">
|
||||
<ews-hex-shape flat-top="false">Cell 7</ews-hex-shape>
|
||||
</div>
|
||||
<div class="ews-hex-hive">
|
||||
<ews-hex-shape flat-top="false">Cell 8</ews-hex-shape>
|
||||
</div>
|
||||
<div class="ews-hex-hive">
|
||||
<ews-hex-shape flat-top="false">Cell 9</ews-hex-shape>
|
||||
</div>
|
||||
<div class="ews-hex-hive">
|
||||
<ews-hex-shape flat-top="false">Cell 10</ews-hex-shape>
|
||||
</div>
|
||||
</ews-hex-grid>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ews-rib-layout Examples -->
|
||||
<div class="example-section">
|
||||
<h2>ews-rib-layout</h2>
|
||||
<p>A complex rib cage layout with responsive branching and animated nodes.</p>
|
||||
<div style="padding: 40px 0; border-radius: 8px; margin-top: 15px; min-height: 600px; overflow-x: auto;">
|
||||
<ews-rib-layout id="rib-example"></ews-rib-layout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Example for ews-rib-layout with 50 items
|
||||
const ribLayout = document.getElementById('rib-example');
|
||||
|
||||
if (ribLayout) {
|
||||
// Generate 50 items
|
||||
const statuses = Array.from({ length: 50 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
title: `STN ${String.fromCharCode(65 + (i % 26))}${Math.floor(i / 26) + 1}`,
|
||||
networkCode: 'NET' + (i % 5),
|
||||
stationCode: 'STA' + i,
|
||||
type: i % 7 === 0 ? 'danger' : 'normal',
|
||||
value: Math.floor(Math.random() * 100)
|
||||
}));
|
||||
|
||||
ribLayout.items = statuses;
|
||||
|
||||
// Set href generator
|
||||
ribLayout.getHref = (item) =>
|
||||
`#/realtime?networkCode=${item.networkCode}&stationCode=${item.stationCode}`;
|
||||
|
||||
// Set node content renderer
|
||||
// Note: In plain JS, we can return a string or a DOM element if the component handles it,
|
||||
// but for Stencil JSX props, a function returning a VNode or DOM element is best.
|
||||
// Here we provide a simple renderer that leverages the CSS classes ported.
|
||||
ribLayout.nodeRenderer = (item, { side, delay }) => {
|
||||
console.log(item);
|
||||
const div = document.createElement('div');
|
||||
const isRight = side === 'right';
|
||||
const isDanger = item.type === 'danger';
|
||||
|
||||
// Match the classes from the Svelte snippet
|
||||
div.className = `slide-fade-in ews-rib-node ${isRight ? 'flip' : ''} ${isDanger ? 'danger' : ''} z-5 text-black text-xs font-bold`;
|
||||
div.style.animationDelay = `${delay}ms`;
|
||||
div.style.display = 'flex';
|
||||
div.style.alignItems = 'center';
|
||||
div.style.justifyContent = 'center';
|
||||
div.style.color = 'black';
|
||||
div.style.fontSize = '10px';
|
||||
div.innerHTML = `<span>${item.value}</span>`;
|
||||
|
||||
return div;
|
||||
};
|
||||
|
||||
// Set connector content renderer
|
||||
ribLayout.connectorRenderer = (item) => item.title;
|
||||
}
|
||||
|
||||
// Example of listening to ews-card toggle events
|
||||
document.querySelectorAll('ews-card').forEach(card => {
|
||||
card.addEventListener('toggle', () => {
|
||||
console.log('Card toggled!');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @fileoverview entry point for your component library
|
||||
*
|
||||
* This is the entry point for your component library. Use this file to export utilities,
|
||||
* constants or data structure that accompany your components.
|
||||
*
|
||||
* DO NOT use this file to export your components. Instead, use the recommended approaches
|
||||
* to consume components of this package as outlined in the `README.md`.
|
||||
*/
|
||||
|
||||
export { format } from './utils/utils';
|
||||
export type * from './components.d.ts';
|
||||
@@ -0,0 +1,3 @@
|
||||
export function format(first?: string, middle?: string, last?: string): string {
|
||||
return (first || '') + (middle ? ` ${middle}` : '') + (last ? ` ${last}` : '');
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { format } from './utils';
|
||||
|
||||
describe('format', () => {
|
||||
it('returns empty string for no names defined', () => {
|
||||
expect(format(undefined, undefined, undefined)).toEqual('');
|
||||
});
|
||||
|
||||
it('formats just first names', () => {
|
||||
expect(format('Joseph', undefined, undefined)).toEqual('Joseph');
|
||||
});
|
||||
|
||||
it('formats first and last names', () => {
|
||||
expect(format('Joseph', undefined, 'Publique')).toEqual('Joseph Publique');
|
||||
});
|
||||
|
||||
it('formats first, middle and last names', () => {
|
||||
expect(format('Joseph', 'Quincy', 'Publique')).toEqual('Joseph Quincy Publique');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Config } from '@stencil/core';
|
||||
|
||||
export const config: Config = {
|
||||
namespace: 'ews-component',
|
||||
outputTargets: [
|
||||
{
|
||||
type: 'dist',
|
||||
esmLoaderPath: '../loader',
|
||||
},
|
||||
{
|
||||
type: 'dist-custom-elements',
|
||||
customElementsExportBehavior: 'auto-define-custom-elements',
|
||||
externalRuntime: false,
|
||||
},
|
||||
{
|
||||
type: 'docs-readme',
|
||||
},
|
||||
{
|
||||
type: 'www',
|
||||
serviceWorker: null, // disable service workers
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"allowUnreachableCode": false,
|
||||
"declaration": false,
|
||||
"experimentalDecorators": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2022"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"module": "esnext",
|
||||
"target": "es2022",
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"jsx": "react",
|
||||
"jsxFactory": "h",
|
||||
"jsxFragmentFactory": "h.Fragment",
|
||||
"types": [
|
||||
"@stencil/vitest/globals"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Load Stencil components for browser tests
|
||||
await import('./dist/ews-component/ews-component.esm.js');
|
||||
|
||||
export { };
|
||||
@@ -0,0 +1,32 @@
|
||||
import { defineVitestConfig } from '@stencil/vitest/config';
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
|
||||
export default defineVitestConfig({
|
||||
stencilConfig: './stencil.config.ts',
|
||||
test: {
|
||||
projects: [
|
||||
// Unit tests - stencil environment for component logic
|
||||
{
|
||||
test: {
|
||||
name: 'unit',
|
||||
include: ['src/**/*.unit.test.{ts,tsx}'],
|
||||
environment: 'stencil',
|
||||
},
|
||||
},
|
||||
// Component browser tests - real browser via Playwright
|
||||
{
|
||||
test: {
|
||||
name: 'browser',
|
||||
include: ['src/**/*.cmp.test.{ts,tsx}'],
|
||||
setupFiles: ['./vitest-setup.ts'],
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: playwright(),
|
||||
headless: true,
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
299
frontend/src/pages/test/sounds.tsx
Normal file
299
frontend/src/pages/test/sounds.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
|
||||
export function RetroSoundTest() {
|
||||
const audioCtxRef = useRef<AudioContext | null>(null);
|
||||
const [isRepeating, setIsRepeating] = useState(false);
|
||||
const repeatTimeoutRef = useRef<number | null>(null);
|
||||
|
||||
const getAudioCtx = () => {
|
||||
if (!audioCtxRef.current) {
|
||||
audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
}
|
||||
if (audioCtxRef.current.state === 'suspended') {
|
||||
audioCtxRef.current.resume();
|
||||
}
|
||||
return audioCtxRef.current;
|
||||
};
|
||||
|
||||
const playSound = (freq: number, type: OscillatorType, duration: number, vol: number, fade: boolean = false) => {
|
||||
const ctx = getAudioCtx();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.type = type;
|
||||
osc.frequency.setValueAtTime(freq, ctx.currentTime);
|
||||
|
||||
gain.gain.setValueAtTime(vol, ctx.currentTime);
|
||||
if (fade) {
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + duration);
|
||||
}
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + duration);
|
||||
};
|
||||
|
||||
// White noise (for 8-bit style noise)
|
||||
const playNoise = (duration: number, vol: number, fade: boolean = false) => {
|
||||
const ctx = getAudioCtx();
|
||||
const bufferSize = ctx.sampleRate * duration;
|
||||
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
||||
const output = buffer.getChannelData(0);
|
||||
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
output[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
const noise = ctx.createBufferSource();
|
||||
noise.buffer = buffer;
|
||||
|
||||
const gain = ctx.createGain();
|
||||
gain.gain.setValueAtTime(vol, ctx.currentTime);
|
||||
if (fade) {
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + duration);
|
||||
}
|
||||
|
||||
noise.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
noise.start();
|
||||
};
|
||||
|
||||
// Play sound repeatedly with random delays
|
||||
const playRepeating = () => {
|
||||
// Play the sound
|
||||
playSound(1000, 'sawtooth', 0.08, 0.1);
|
||||
playNoise(0.09, 0.02);
|
||||
|
||||
// Schedule next play with random delay (0-1000ms)
|
||||
const randomDelay = Math.random() * 900;
|
||||
repeatTimeoutRef.current = window.setTimeout(playRepeating, randomDelay);
|
||||
};
|
||||
|
||||
// Toggle repeating sound
|
||||
const toggleRepeat = () => {
|
||||
if (isRepeating) {
|
||||
// Stop repeating
|
||||
if (repeatTimeoutRef.current !== null) {
|
||||
clearTimeout(repeatTimeoutRef.current);
|
||||
repeatTimeoutRef.current = null;
|
||||
}
|
||||
setIsRepeating(false);
|
||||
} else {
|
||||
// Start repeating
|
||||
setIsRepeating(true);
|
||||
playRepeating();
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (repeatTimeoutRef.current !== null) {
|
||||
clearTimeout(repeatTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Crew Bailout Alarm - 90s style aggressive buzzer
|
||||
const crewBailout = (durationSeconds: number) => {
|
||||
const ctx = getAudioCtx();
|
||||
const startTime = ctx.currentTime;
|
||||
const endTime = startTime + durationSeconds;
|
||||
|
||||
// Timing constants
|
||||
const beepDuration = 0.06; // 60ms ON
|
||||
const pauseDuration = 0.06; // 60ms OFF
|
||||
const cycleDuration = beepDuration + pauseDuration; // 120ms total ~ 8 beeps/sec
|
||||
|
||||
const oscillators: OscillatorNode[] = [];
|
||||
const gainNodes: GainNode[] = [];
|
||||
|
||||
let currentTime = startTime;
|
||||
|
||||
// Generate beeps for the entire duration
|
||||
while (currentTime < endTime) {
|
||||
// Create two oscillators for beating effect
|
||||
const osc1 = ctx.createOscillator();
|
||||
const osc2 = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc1.type = 'square';
|
||||
osc2.type = 'square';
|
||||
osc1.frequency.setValueAtTime(2850, currentTime);
|
||||
osc2.frequency.setValueAtTime(2855, currentTime); // 5Hz difference for beating
|
||||
|
||||
// Envelope
|
||||
const attackTime = 0.002; // 2ms attack
|
||||
const releaseTime = 0.01; // 10ms release
|
||||
|
||||
// Attack
|
||||
gain.gain.setValueAtTime(0, currentTime);
|
||||
gain.gain.linearRampToValueAtTime(0.15, currentTime + attackTime);
|
||||
|
||||
// Sustain
|
||||
gain.gain.setValueAtTime(0.15, currentTime + beepDuration - releaseTime);
|
||||
|
||||
// Release
|
||||
gain.gain.linearRampToValueAtTime(0, currentTime + beepDuration);
|
||||
|
||||
// Connect nodes
|
||||
osc1.connect(gain);
|
||||
osc2.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
|
||||
// Start and stop
|
||||
osc1.start(currentTime);
|
||||
osc2.start(currentTime);
|
||||
osc1.stop(currentTime + beepDuration);
|
||||
osc2.stop(currentTime + beepDuration);
|
||||
|
||||
// Store for cleanup
|
||||
oscillators.push(osc1, osc2);
|
||||
gainNodes.push(gain);
|
||||
|
||||
// Move to next beep
|
||||
currentTime += cycleDuration;
|
||||
}
|
||||
|
||||
// Cleanup after completion
|
||||
setTimeout(() => {
|
||||
oscillators.forEach(osc => osc.disconnect());
|
||||
gainNodes.forEach(gain => gain.disconnect());
|
||||
}, durationSeconds * 1000 + 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'monospace', maxWidth: '900px' }}>
|
||||
<h1>🔊 8-Bit & Industrial Buzzer Generator</h1>
|
||||
<p style={{ color: '#7f8c8d', fontSize: '14px', marginTop: '5px' }}>
|
||||
Harsh, digital, lo-fi sounds with noise and dissonance - NOT musical tones
|
||||
</p>
|
||||
|
||||
{/* Single Preset */}
|
||||
<div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#ecf0f1', borderRadius: '8px' }}>
|
||||
<h3 style={{ margin: '0 0 10px 0' }}>Preset Sound</h3>
|
||||
<button
|
||||
style={{ padding: '10px 20px', backgroundColor: '#7f8c8d', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold' }}
|
||||
onClick={() => {
|
||||
for (let i = 0; i < 8; i++) {
|
||||
setTimeout(() => {
|
||||
playSound(1800 + i * 50, 'square', 0.03, 0.08);
|
||||
playNoise(0.03, 0.015);
|
||||
}, i * 40);
|
||||
}
|
||||
}}
|
||||
>
|
||||
RISING BEEPS (sweep)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Crew Bailout Alarm - 90s Style */}
|
||||
<div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#c0392b', borderRadius: '8px' }}>
|
||||
<h3 style={{ margin: '0 0 10px 0', color: 'white' }}>🚨 Crew Bailout Alarm (90s)</h3>
|
||||
<p style={{ fontSize: '12px', color: 'white', margin: '0 0 10px 0' }}>
|
||||
Agresivní mechanický bzučák: 2850 Hz + 2855 Hz square, 8 beeps/sec
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#a93226',
|
||||
color: 'white',
|
||||
border: '2px solid white',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
onClick={() => crewBailout(2)}
|
||||
>
|
||||
⚠️ 2 sekundy
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#922b21',
|
||||
color: 'white',
|
||||
border: '2px solid white',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
onClick={() => crewBailout(5)}
|
||||
>
|
||||
🚨 5 sekund
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#7b241c',
|
||||
color: 'white',
|
||||
border: '2px solid white',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
onClick={() => crewBailout(10)}
|
||||
>
|
||||
💀 10 sekund
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Repeating Sound Toggle */}
|
||||
<div style={{ marginTop: '20px', padding: '15px', backgroundColor: isRepeating ? '#e74c3c' : '#27ae60', borderRadius: '8px' }}>
|
||||
<h3 style={{ margin: '0 0 10px 0', color: 'white' }}>🔁 Test</h3>
|
||||
<button
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: isRepeating ? '#c0392b' : '#229954',
|
||||
color: 'white',
|
||||
border: '2px solid white',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
onClick={toggleRepeat}
|
||||
>
|
||||
{isRepeating ? '⏹ STOP' : '▶ START'} Random Buzzer
|
||||
</button>
|
||||
<h3 style={{ margin: '0 0 10px 0', color: 'white' }}>Patlabor notify effect</h3>
|
||||
<button
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: isRepeating ? '#c0392b' : '#229954',
|
||||
color: 'white',
|
||||
border: '2px solid blue',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
onClick={() => playSound(380, 'square', 0.1, 0.1)}
|
||||
>
|
||||
Patlabor Notify
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Raw Noise Test */}
|
||||
<div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#2c3e50', borderRadius: '8px', color: 'white' }}>
|
||||
<h3 style={{ margin: '0 0 10px 0' }}>🎛️ Raw Sound Elements</h3>
|
||||
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
||||
{[320,1000].map(freq => (
|
||||
<button
|
||||
key={freq}
|
||||
style={{ padding: '8px 15px', backgroundColor: '#34495e', color: 'white', border: 'none', borderRadius: '4px' }}
|
||||
onClick={() => {
|
||||
playSound(freq, 'sawtooth', 2, 0.1);
|
||||
}}
|
||||
>
|
||||
{freq} Hz
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user