feat: initial Pezkuwi Apps rebrand from polkadot-apps

Rebranded terminology:
- Polkadot → Pezkuwi
- Kusama → Dicle
- Westend → Zagros
- Rococo → PezkuwiChain
- Substrate → Bizinikiwi
- parachain → teyrchain

Custom logos with Kurdistan brand colors (#e6007a → #86e62a):
- bizinikiwi-hexagon.svg
- sora-bizinikiwi.svg
- hezscanner.svg
- heztreasury.svg
- pezkuwiscan.svg
- pezkuwistats.svg
- pezkuwiassembly.svg
- pezkuwiholic.svg
This commit is contained in:
2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 changed files with 329019 additions and 0 deletions
@@ -0,0 +1,89 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import React, { Suspense } from 'react';
import { ThemeProvider } from 'styled-components';
import { lightTheme } from '@pezkuwi/react-components';
import i18next from '@pezkuwi/react-components/i18n';
import Popup from './index.js';
function TestPopup () {
return (
<>
<h1>Test outside text</h1>
<Popup
value={
<div>
Test popup content
</div>
}
/>
</>
);
}
function renderPopup () {
return render(
<Suspense fallback='...'>
<ThemeProvider theme={lightTheme}>
<TestPopup />
</ThemeProvider>
</Suspense>
);
}
describe('Popup Component', () => {
beforeAll(async () => {
await i18next.changeLanguage('en');
});
// eslint-disable-next-line jest/expect-expect
it('opens and closes', async () => {
renderPopup();
await waitFor(async () => {
await expectPopupToBeClosed();
await togglePopup();
await expectPopupToBeOpen();
await togglePopup();
await expectPopupToBeClosed();
});
});
// eslint-disable-next-line jest/expect-expect
it('closes popup with outside click', async () => {
renderPopup();
await waitFor(async () => {
await expectPopupToBeClosed();
await togglePopup();
await expectPopupToBeOpen();
await clickOutside();
await expectPopupToBeClosed();
});
});
});
async function expectPopupToBeClosed () {
await new Promise((resolve) => setTimeout(resolve, 1000));
await screen.findByRole('button');
expect(screen.queryAllByText('Test popup content')).toHaveLength(0);
}
async function expectPopupToBeOpen () {
await new Promise((resolve) => setTimeout(resolve, 1000));
await screen.findByText('Test popup content');
}
async function togglePopup () {
fireEvent.click(await screen.findByTestId('popup-open'));
}
async function clickOutside () {
fireEvent.click(await screen.findByText('Test outside text'));
}
@@ -0,0 +1,105 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PopupWindowProps as Props } from './types.js';
import React from 'react';
import { createPortal } from 'react-dom';
import { usePopupWindow } from '@pezkuwi/react-hooks/usePopupWindow';
import { styled } from '../styled.js';
function PopupWindow ({ children, className = '', position, triggerRef, windowRef }: Props): React.ReactElement<Props> {
const { pointerStyle, renderCoords: { x, y } } = usePopupWindow(windowRef, triggerRef, position);
return createPortal(
<StyledDiv
className={`${className} ${pointerStyle}Pointer ${position}Position`}
data-testid='popup-window'
ref={windowRef}
style={
(x && y && {
transform: `translate3d(${x}px, ${y}px, 0)`,
zIndex: 1000
}) || undefined
}
>
{children}
</StyledDiv>,
document.body
);
}
const StyledDiv = styled.div`
background-color: var(--bg-menu);
border: 1px solid #d4d4d5;
border-radius: 4px;
box-shadow: 0 2px 4px 0 rgb(34 36 38 / 12%), 0 2px 10px 0 rgb(34 36 38 / 15%);
color: var(--color-text);
left: 0;
margin: 0.7rem 0;
padding: 0;
position: absolute;
top: 0;
z-index: -1;
&.leftPosition {
&::before {
left: unset;
right: 0.75rem;
}
}
&.rightPosition {
&::before {
left: 0.75rem;
right: unset;
}
}
&::before {
background-color: var(--bg-menu);
bottom: -0.5rem;
box-shadow: 1px 1px 0 0 #bababc;
content: '';
height: 1rem;
position: absolute;
right: 50%;
top: unset;
width: 1rem;
transform: rotate(45deg);
z-index: 2;
}
&.bottomPointer::before {
box-shadow: -1px -1px 0 0 #bababc;
top: -0.5rem;
bottom: unset;
}
.ui.text.menu .item {
color: var(--color-text) !important;
text-align: left;
&.disabled {
opacity: 0.3;
}
}
& > *:not(.ui--Menu) {
margin-left: 1rem;
margin-right: 1rem;
}
& > *:first-child:not(.ui--Menu) {
margin-top: 1rem;
}
& > *:last-child:not(.ui--Menu) {
margin-bottom: 1rem;
}
`;
export default React.memo(PopupWindow);
@@ -0,0 +1,82 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PopupProps } from './types.js';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useOutsideClick, useTheme, useToggle } from '@pezkuwi/react-hooks';
import Button from '../Button/index.js';
import { styled } from '../styled.js';
import PopupWindow from './PopupWindow.js';
function Popup ({ children, className = '', closeOnScroll, isDisabled, onCloseAction, position = 'left', value }: PopupProps) {
const { themeClassName } = useTheme();
const [isOpen, toggleIsOpen, setIsOpen] = useToggle(false);
const triggerRef = useRef<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const closeWindow = useCallback(
() => setIsOpen(false),
[setIsOpen]
);
const refs = useMemo(
() => [triggerRef, dropdownRef],
[triggerRef, dropdownRef]
);
useOutsideClick(refs, closeWindow);
useEffect(() => {
if (closeOnScroll) {
document.addEventListener('scroll', closeWindow, true);
}
return () => document.removeEventListener('scroll', closeWindow, true);
}, [closeOnScroll, closeWindow, setIsOpen]);
useEffect(() => {
if (!isOpen && onCloseAction) {
onCloseAction();
}
}, [isOpen, onCloseAction]);
return (
<StyledDiv className={`${className} ui--Popup ${themeClassName}`}>
{isOpen && (
<PopupWindow
position={position}
triggerRef={triggerRef}
windowRef={dropdownRef}
>
{value}
</PopupWindow>
)}
<div
data-testid='popup-open'
onClick={toggleIsOpen}
ref={triggerRef}
>
{children ?? (
<Button
className={isOpen ? 'isOpen' : ''}
icon='ellipsis-v'
isDisabled={isDisabled}
isReadOnly={false}
/>
)}
</div>
</StyledDiv>
);
}
const StyledDiv = styled.div`
display: inline-flex;
flex-direction: column;
justify-content: center;
position: relative;
`;
export default React.memo(Popup);
@@ -0,0 +1,33 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type React from 'react';
export interface ElementPosition {
x: number,
y: number,
width: number,
height: number,
}
export type HorizontalPosition = 'left' | 'middle' | 'right'
export type VerticalPosition = 'top' | 'bottom'
export interface PopupWindowProps {
className?: string;
children?: React.ReactNode;
windowRef: React.RefObject<HTMLDivElement>;
triggerRef: React.RefObject<HTMLDivElement>;
position: HorizontalPosition;
}
export interface PopupProps {
isDisabled?: boolean;
className?: string;
closeOnScroll?: boolean;
value?: React.ReactNode;
children?: React.ReactNode;
position?: HorizontalPosition;
onCloseAction?: () => void;
}
@@ -0,0 +1,71 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { WindowSize } from '@pezkuwi/react-hooks/ctx/types';
import type { ElementPosition, HorizontalPosition, VerticalPosition } from './types.js';
interface Coords {
x: number;
y: number;
}
// 0.8rem
const POINTER_OFFSET = 14 * 0.8;
// we don't want our popup window to cover apps menu
const MENU_BAR_HEIGHT = 56;
// adds 1rem margin between browsers window and popup
const FIXED_VERTICAL_OFFSET_MARGIN = 14;
export function getPosition (triggerPosition: ElementPosition, positionX: HorizontalPosition, positionY: VerticalPosition, windowPosition: ElementPosition, scrollY: number, windowSize: WindowSize): Coords {
const globalX = triggerPosition.x + (triggerPosition.width / 2);
const globalY = triggerPosition.y + scrollY + (triggerPosition.height / 2);
return {
x: globalX - getHorizontalOffset(windowPosition.width, positionX),
y: fitsInView(positionY, triggerPosition, windowPosition.height, windowSize, scrollY)
? globalY + getVerticalOffset(triggerPosition.height, positionY, windowPosition.height)
: getFixedVerticalPosition(scrollY, positionY, windowSize, windowPosition.height)
};
}
function getHorizontalOffset (popupWindowWidth: number, position: HorizontalPosition): number {
if (position === 'left') {
return popupWindowWidth - POINTER_OFFSET;
}
if (position === 'right') {
return POINTER_OFFSET;
}
return (popupWindowWidth / 2);
}
function fitsInView (positionY: VerticalPosition, trigger: ElementPosition, popupWindowHeight: number, windowSize: WindowSize, scrollY: number): boolean {
const { height: triggerHeight, y: triggerY } = trigger;
if (positionY === 'bottom') {
return windowSize.height - triggerHeight - triggerY - FIXED_VERTICAL_OFFSET_MARGIN > popupWindowHeight;
}
return scrollY < MENU_BAR_HEIGHT
? triggerY - (MENU_BAR_HEIGHT - scrollY) > popupWindowHeight
: triggerY > popupWindowHeight;
}
function getVerticalOffset (triggerHeight: number, position: VerticalPosition, windowHeight: number): number {
if (position === 'bottom') {
return triggerHeight / 2;
}
return (triggerHeight / 2 + windowHeight + POINTER_OFFSET) * -1;
}
function getFixedVerticalPosition (scrollY: number, position: VerticalPosition, windowSize: WindowSize, popupWindowHeight: number): number {
if (position === 'bottom') {
return scrollY + windowSize.height - popupWindowHeight - FIXED_VERTICAL_OFFSET_MARGIN;
}
return scrollY < MENU_BAR_HEIGHT
? MENU_BAR_HEIGHT
: scrollY;
}