This commit is contained in:
2026-04-04 00:32:01 +02:00
parent f9636d1464
commit d678560d60
39 changed files with 5013 additions and 0 deletions

View File

@@ -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 />}>

View File

@@ -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

View 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

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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
```

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}

View File

@@ -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!
[![Support me on Sociabuzz](https://img.shields.io/badge/Support%20Me-Sociabuzz-orange?style=for-the-badge&logo=buymeacoffee&logoColor=white)](https://sociabuzz.com/bagusindrayana/tribe)

View 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>;
}
}
}

View File

@@ -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();
});
});

View File

@@ -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;
}
}

View File

@@ -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>
);
}
}

View File

@@ -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/)*

View File

@@ -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%;
}

View File

@@ -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>
);
}
}

View File

@@ -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/)*

View File

@@ -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");
}

View File

@@ -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>
);
}
}

View File

@@ -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/)*

View File

@@ -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%;
}
}

View File

@@ -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>
);
}
}

View File

@@ -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/)*

View File

@@ -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;
}

View File

@@ -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>
);
}
}

View File

@@ -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/)*

View File

@@ -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>
`);
});
});

View File

@@ -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>;
}
}

View File

@@ -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/)*

View File

@@ -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>

View File

@@ -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';

View File

@@ -0,0 +1,3 @@
export function format(first?: string, middle?: string, last?: string): string {
return (first || '') + (middle ? ` ${middle}` : '') + (last ? ` ${last}` : '');
}

View File

@@ -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');
});
});

View File

@@ -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
},
],
};

View File

@@ -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"
]
}

View File

@@ -0,0 +1,4 @@
// Load Stencil components for browser tests
await import('./dist/ews-component/ews-component.esm.js');
export { };

View File

@@ -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' }],
},
},
},
],
},
});

View 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>
);
}