Add Pezkuwi SDK UI - Polkadot.js Apps clone

- Clone Polkadot.js Apps repository
- Update package.json with Pezkuwi branding
- Add Pezkuwi endpoint to production chains (wss://pezkuwichain.app:9944)
- Create comprehensive README for SDK UI
- Set up project structure with all packages

Next steps:
- Apply Kurdistan colors (Kesk, Sor, Zer, Spi + Black) to UI theme
- Replace logos with Pezkuwi branding
- Test build and deployment
This commit is contained in:
Claude
2025-11-14 00:55:17 +00:00
parent c48ded7ff2
commit c71ddb6e0d
5836 changed files with 324981 additions and 17 deletions
@@ -0,0 +1,39 @@
// Copyright 2017-2025 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { isString } from '@polkadot/util';
import Spinner from '../Spinner.js';
interface Props {
children?: React.ReactNode;
className?: string;
empty?: React.ReactNode | false;
emptySpinner?: React.ReactNode;
isEmpty: boolean;
noBodyTag?: boolean;
}
function Body ({ children, className = '', empty, emptySpinner, isEmpty, noBodyTag }: Props): React.ReactElement<Props> {
const bodyClassName = `${className} ui--Table-Body`;
return isEmpty
? (
<tbody className={bodyClassName}>
<tr>
<td colSpan={100}>{
isString(empty)
? <div className='empty'>{empty}</div>
: empty || <Spinner label={emptySpinner} />
}</td>
</tr>
</tbody>
)
: noBodyTag
? <>{children}</>
: <tbody className={bodyClassName}>{children}</tbody>;
}
export default React.memo(Body);
@@ -0,0 +1,48 @@
// Copyright 2017-2025 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@polkadot/util';
import React from 'react';
import { FormatBalance } from '@polkadot/react-query';
export interface Props {
children?: React.ReactNode;
className?: string;
colSpan?: number;
label?: React.ReactNode;
labelPost?: React.ReactNode;
rowSpan?: number;
value?: BN | null;
withLoading?: boolean;
}
function Balance ({ children, className = '', colSpan, label, labelPost, rowSpan, value, withLoading }: Props): React.ReactElement<Props> {
return (
<td
className={`${className} ui--Table-Column-Balance number`}
colSpan={colSpan}
rowSpan={rowSpan}
>
{value
? (
<FormatBalance
label={label}
labelPost={labelPost}
value={value}
/>
)
: withLoading && (
<FormatBalance
className='--tmp'
value={1}
/>
)
}
{children}
</td>
);
}
export default React.memo(Balance);
@@ -0,0 +1,60 @@
// Copyright 2017-2025 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import Icon from '../../Icon.js';
import { styled } from '../../styled.js';
export interface Props {
className?: string;
colSpan?: number;
rowSpan?: number;
isExpanded: boolean;
toggle: () => void;
}
function Expand ({ className = '', colSpan, isExpanded, rowSpan, toggle }: Props): React.ReactElement<Props> {
return (
<StyledTd
className={`${className} ui--Table-Column-Expand`}
colSpan={colSpan}
onClick={toggle}
rowSpan={rowSpan}
>
<div>
<Icon
icon={
isExpanded
? 'caret-up'
: 'caret-down'
}
/>
</div>
</StyledTd>
);
}
const StyledTd = styled.td`
&& {
box-sizing: content-box;
cursor: pointer;
min-width: 1.7rem;
padding-left: 0;
text-align: left;
width: 1.7rem;
> div {
align-items: center;
border: 1px solid var(--border-table);
border-radius: 4px;
box-sizing: border-box;
display: inline-flex;
height: 1.7rem;
justify-content: center;
width: 1.7rem;
}
}
`;
export default React.memo(Expand);
@@ -0,0 +1,54 @@
// Copyright 2017-2025 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback } from 'react';
import Icon from '../../Icon.js';
import { styled } from '../../styled.js';
export interface Props {
address: string;
className?: string;
colSpan?: number;
isFavorite: boolean;
rowSpan?: number;
toggle: (address: string) => void;
}
function Favorite ({ address, className = '', colSpan, isFavorite, rowSpan, toggle }: Props): React.ReactElement<Props> {
const onClick = useCallback(
() => toggle(address),
[address, toggle]
);
return (
<StyledTd
className={`${className} ui--Table-Column-Favorite`}
colSpan={colSpan}
onClick={onClick}
rowSpan={rowSpan}
>
<Icon
color={
isFavorite
? 'orange'
: 'gray'
}
icon='star'
/>
</StyledTd>
);
}
const StyledTd = styled.td`
&& {
box-sizing: content-box;
cursor: pointer;
min-width: 1rem;
padding-right: 0.35rem;
text-align: right;
width: 1rem;
}
`;
export default React.memo(Favorite);
@@ -0,0 +1,46 @@
// Copyright 2017-2025 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@polkadot/util';
import React from 'react';
import { formatNumber } from '@polkadot/util';
import { styled } from '../../styled.js';
export interface Props {
children?: React.ReactNode;
className?: string;
colSpan?: number;
rowSpan?: number;
value: BN | number;
}
function Id ({ children, className = '', colSpan, rowSpan, value }: Props): React.ReactElement<Props> {
return (
<StyledTd
className={`${className} ui--Table-Column-Id`}
colSpan={colSpan}
rowSpan={rowSpan}
>
<h2 className='--digits'>{formatNumber(value)}</h2>
{children}
</StyledTd>
);
}
// We want 5.5ch (which should be ok-ish for 5 decimals, i.e. 99,999), however
// we wrap the display in an h2 with max size text at ~1.3rem, so multiply it out
const WIDTH = `${(5.5 * 1.3).toFixed(3)}ch`;
const StyledTd = styled.td`
&& {
box-sizing: content-box;
min-width: ${WIDTH};
text-align: right;
white-space: nowrap;
width: ${WIDTH};
`;
export default React.memo(Id);
@@ -0,0 +1,36 @@
// Copyright 2017-2025 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import Balance from './Balance.js';
import Expand from './Expand.js';
import Favorite from './Favorite.js';
import Id from './Id.js';
interface Props {
children?: React.ReactNode;
className?: string;
}
function ColumnBase ({ children, className = '' }: Props): React.ReactElement<Props> {
return (
<td className={`${className} ui--Table-Column`}>
{children}
</td>
);
}
const Column = React.memo(ColumnBase) as unknown as typeof ColumnBase & {
Balance: typeof Balance,
Expand: typeof Expand,
Favorite: typeof Favorite,
Id: typeof Id
};
Column.Balance = Balance;
Column.Expand = Expand;
Column.Favorite = Favorite;
Column.Id = Id;
export default Column;
@@ -0,0 +1,42 @@
// Copyright 2017-2025 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { styled } from '../styled.js';
interface Props {
className?: string;
footer?: React.ReactNode;
isEmpty: boolean;
}
function Foot ({ className = '', footer, isEmpty }: Props): React.ReactElement<Props> | null {
if (!footer || isEmpty) {
return null;
}
return (
<StyledTfoot className={`${className} ui--Table-Foot`}>
{footer}
</StyledTfoot>
);
}
const StyledTfoot = styled.tfoot`
td {
color: var(--color-table-foot);
font: var(--font-sans);
font-weight: var(--font-weight-normal);
padding: 0.75rem 1rem 0.25rem;
text-align: right;
vertical-align: baseline;
white-space: nowrap;
}
tr {
background: var(--bg-page);
}
`;
export default React.memo(Foot);
@@ -0,0 +1,147 @@
// Copyright 2017-2025 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { styled } from '../styled.js';
type HeaderDef = [React.ReactNode?, string?, number?, (() => void)?];
interface Props {
children?: React.ReactNode;
className?: string;
filter?: React.ReactNode;
header?: (false | null | undefined | HeaderDef)[];
isEmpty: boolean;
}
function Head ({ children, className = '', filter, header, isEmpty }: Props): React.ReactElement<Props> | null {
if (!header?.length) {
return null;
}
return (
<StyledThead className={`${className} ui--Table-Head`}>
{filter && (
<tr className='filter'>
<th colSpan={100}>{filter}</th>
</tr>
)}
<tr>
{header.filter((h): h is HeaderDef => !!h).map(([label, className = 'default', colSpan = 1, onClick], index) =>
<th
className={className}
colSpan={colSpan}
key={index}
onClick={onClick}
>
{index === 0
? <h1>{label}</h1>
: !isEmpty && label && <label>{label}</label>
}
</th>
)}
</tr>
{children}
</StyledThead>
);
}
const StyledThead = styled.thead`
z-index: 1;
th {
background: var(--bg-table);
font: var(--font-sans);
font-weight: var(--font-weight-normal);
padding: 0.375rem 1rem;
text-align: right;
vertical-align: middle;
white-space: nowrap;
h1 {
display: table-cell;
vertical-align: middle;
.sub {
display: inline-block;
font-size: var(--font-size-base);
font-weight: var(--font-weight-normal);
opacity: var(--opacity-light);
padding-left: 1.5rem;
text-overflow: ellipsis;
vertical-align: middle;
}
}
> label {
margin: 0 !important;
}
&.address {
padding-left: 3rem;
text-align: left;
}
&.badge {
padding: 0;
}
&.expand,
&.number {
text-align: right;
}
&.isClickable {
cursor: pointer;
}
&.mini {
padding: 0 !important;
}
&.no-pad-left {
padding-left: 0.125rem;
}
&.no-pad-right {
padding-right: 0.125rem;
}
&.start {
text-align: left;
}
&.balances {
text-align: right;
padding-right: 2.25rem;
}
}
tr {
text-transform: lowercase;
&.filter {
.ui.input,
.ui.selection.dropdown {
background: transparent;
&:first-child {
margin-top: 0;
}
}
th {
padding: 0;
}
}
&:not(.filter) {
th {
color: var(--color-table-head);
}
}
}
`;
export default React.memo(Head);
@@ -0,0 +1,21 @@
// Copyright 2017-2025 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
interface Props {
children?: React.ReactNode;
className?: string;
}
function RowBase ({ children, className = '' }: Props): React.ReactElement<Props> {
return (
<tr className={`${className} ui--Table-Row`}>
{children}
</tr>
);
}
const Row = React.memo(RowBase) as unknown as typeof RowBase;
export default Row;
@@ -0,0 +1,552 @@
// Copyright 2017-2025 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { useWindowColumns } from '@polkadot/react-hooks';
import { styled } from '../styled.js';
import Column from './Column/index.js';
import Row from './Row/index.js';
import Body from './Body.js';
import Foot from './Foot.js';
import Head from './Head.js';
interface Props {
children?: React.ReactNode;
className?: string;
empty?: React.ReactNode | false;
emptySpinner?: React.ReactNode;
filter?: React.ReactNode;
footer?: React.ReactNode;
header?: ([React.ReactNode?, string?, number?, (() => void)?] | false | null | undefined)[];
headerChildren?: React.ReactNode;
isFixed?: boolean;
isInline?: boolean;
isSplit?: boolean;
legend?: React.ReactNode;
maxColumns?: 2 | 3;
noBodyTag?: boolean;
}
const COLUMN_INDEXES = {
2: [0, 1],
3: [0, 1, 2]
} as const;
function TableBase ({ children, className = '', empty, emptySpinner, filter, footer, header, headerChildren, isFixed, isInline, isSplit, legend, maxColumns, noBodyTag }: Props): React.ReactElement<Props> {
const numColumns = useWindowColumns(maxColumns);
const isArray = Array.isArray(children);
const isEmpty = !children || (isArray && children.length === 0);
const headerNode = (
<Head
filter={filter}
header={header}
isEmpty={isEmpty}
>
{headerChildren}
</Head>
);
if (isSplit && isArray && !isEmpty && (numColumns !== 1)) {
return (
<StyledDiv className={`${className} ui--Table isSplit`}>
{legend}
<table className='noMargin'>
{headerNode}
</table>
<div className='ui--Table-Split'>
{COLUMN_INDEXES[numColumns].map((column) => (
<div
className={`ui--Table-Split-${numColumns}`}
key={column}
>
<table className='noMargin'>
<tbody className='ui--Table-Body'>
{children.filter((_, i) => (i % numColumns) === column)}
</tbody>
</table>
</div>
))}
</div>
</StyledDiv>
);
}
return (
<StyledDiv className={`${className} ui--Table`}>
{legend}
<table className={`${(isFixed && !isEmpty) ? 'isFixed' : 'isNotFixed'} ${isInline ? 'isInline' : ''}`}>
{headerNode}
<Body
empty={empty}
emptySpinner={emptySpinner}
isEmpty={isEmpty}
noBodyTag={noBodyTag}
>
{children}
</Body>
<Foot
footer={footer}
isEmpty={isEmpty}
/>
</table>
</StyledDiv>
);
}
const BASE_BORDER = 0.125;
const BORDER_SIDE = `${BASE_BORDER * 1}rem solid var(--bg-page)`;
const BORDER_TOP = `${BASE_BORDER * 2}rem solid var(--bg-page)`;
const BORDER_RADIUS = `${BASE_BORDER * 4}rem`;
const StyledDiv = styled.div`
max-width: 100%;
width: 100%;
.ui--Table-Split {
display: flex;
flex-wrap: nowrap;
margin-bottom: 1.5rem;
> .ui--Table-Split-3 {
max-width: 33.3%;
min-width: 33.3%;
}
> .ui--Table-Split-2 {
max-width: 50%;
min-width: 50%;
}
}
table {
*border-collapse: collapse;
border-spacing: 0;
max-width: 100%;
overflow: hidden;
position: relative;
width: 100%;
z-index: 1;
&.isFixed {
table-layout: fixed;
}
&:not(.isInline):not(.noMargin) {
margin-bottom: 1.5rem;
}
&.isInline {
tbody tr td {
border-top-width: 1px;
padding: 0.25rem 0.75rem;
}
}
tr {
max-width: 100%;
width: 100%;
td,
&:not(.filter) th {
&:first-child {
padding-left: 1.5rem;
}
&:last-child {
padding-right: 0.75rem;
}
&.all {
width: 100%;
&:not(.overflow) {
word-break: break-word;
}
summary {
white-space: normal;
}
}
}
}
}
tbody, thead {
position: relative;
width: 100%;
tr {
width: 100%;
}
}
tbody {
position: relative;
td {
background: var(--bg-table);
padding: 0.5rem 1rem;
text-align: left;
vertical-align: middle;
> article.mark {
margin-left: 0rem;
}
&:first-child {
border-left: ${BORDER_SIDE};
}
&:last-child {
border-right: ${BORDER_SIDE};
}
label {
display: block !important;
white-space: nowrap;
}
div.empty {
opacity: var(--opacity-light);
padding: 0.25rem;
}
.ui--Spinner {
margin: 0 auto;
.text {
margin-bottom: 0;
}
}
&.actions {
padding-left: 0.35rem;
width: 1%;
> div {
display: flex;
align-items: center;
flex-wrap: nowrap;
justify-content: flex-end;
& > * + * {
margin-left: 0.35rem;
}
.ui--Button {
white-space: nowrap;
}
}
&:not(:last-child) {
padding-right: 0;
}
}
&.address {
max-width: 0;
min-width: 15rem;
overflow-x: hidden;
}
&.badge {
padding: 0.5rem;
}
&.balance {
min-width: 20rem;
padding: 0.5rem 0 0.75rem;
}
&.button {
padding: 0.25rem 0.35rem 0.5rem;
text-align: right;
white-space: nowrap;
> * {
vertical-align: middle;
}
.ui--Toggle {
display: inline-block;
white-space: nowrap;
label {
display: inline-block !important;
}
}
}
&.chart {
padding: 0;
}
&.expand {
&:not(.left) {
text-align: right;
}
.ui--Expander + .ui--Expander {
margin-top: 0.375rem;
}
}
&.hash {
// we actually want to use 10ch here, however in the
// block expand page gives different sizes to the hashes
min-width: 7.5rem;
white-space: nowrap;
> .shortHash {
max-width: var(--width-shorthash);
min-width: 3em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: var(--width-shorthash);
}
}
&.links {
padding: 0.5rem 0.75rem;
text-align: center;
width: 0;
}
&.no-pad-left {
padding-left: 0.125rem;
}
&.no-pad-right {
padding-right: 0.125rem;
}
&.no-pad-top {
padding-top: 0.125rem;
}
&.no-pad {
padding: 0;
}
&.number {
font-variant-numeric: tabular-nums;
text-align: right;
}
&.relative {
position: relative;
.absolute {
position: absolute;
right: 0.5rem;
// this seems aligned with expander (when zoomed in)
top: 0.72rem;
white-space: nowrap;
}
}
&.overflow {
max-width: 0;
overflow: hidden;
text-overflow: ellipsis;
word-break: none;
}
&.start {
text-align: left;
}
&.together {
white-space: nowrap;
}
&.top {
vertical-align: top;
}
&.columar {
vertical-align: top;
.ui--Columar .ui--Column {
margin: 1rem 0 0.75rem 0;
padding: 0;
* + h5 {
margin-top: 1rem;
}
.ui--Chart-Line {
padding: 0 0.5rem;
}
}
}
&.middle {
text-align: center;
}
&.mini {
padding: 0 !important;
width: fit-content;
white-space: normal;
> div {
margin-right: 0.75rem;
max-width: 3.8rem;
min-width: 3.8rem;
}
}
&.upper {
text-transform: uppercase;
}
.ui--Button-Group .ui--Button:not(.isToplevel) {
margin: 0;
}
}
tr {
&:not(.isExpanded) {
td {
border-top: ${BORDER_TOP};
&:first-child {
border-top-left-radius: ${BORDER_RADIUS};
border-bottom-left-radius: ${BORDER_RADIUS};
}
&:last-child {
border-top-right-radius: ${BORDER_RADIUS};
border-bottom-right-radius: ${BORDER_RADIUS};
}
}
}
&.isExpanded {
&.isFirst {
td {
border-top: ${BORDER_TOP};
&:first-child {
border-top-left-radius: ${BORDER_RADIUS};
}
&:last-child {
border-top-right-radius: ${BORDER_RADIUS};
}
}
}
&.isLast {
td {
&:first-child {
border-bottom-left-radius: ${BORDER_RADIUS};
}
&:last-child {
border-bottom-right-radius: ${BORDER_RADIUS};
}
}
}
}
&.packedBottom {
td {
padding-bottom: 0;
}
}
&.packedTop {
td {
padding-top: 0;
}
}
&.packedAll {
td {
padding-bottom: 0;
padding-top: 0;
}
}
&.transparent {
background: transparent;
}
&.isCollapsed {
display: none;
}
.ui--Button-Group {
margin: 0;
}
.ui--Button:not(.isIcon):not(:hover) {
background: transparent !important;
box-shadow: none !important;
}
.ui.toggle.checkbox input:checked ~ .box:before,
.ui.toggle.checkbox input:checked ~ label:before {
background-color: #eee !important;
}
}
}
thead {
tr {
&:first-child {
th {
border-top: ${BORDER_TOP};
&:first-child {
border-top-left-radius: ${BORDER_RADIUS};
}
&:last-child {
border-top-right-radius: ${BORDER_RADIUS};
}
}
}
&:last-child {
th {
padding-top: 1rem;
&:first-child {
border-bottom-left-radius: ${BORDER_RADIUS};
}
&:last-child {
border-bottom-right-radius: ${BORDER_RADIUS};
}
}
}
th {
&:first-child {
border-left: ${BORDER_SIDE};
}
&:last-child {
border-right: ${BORDER_SIDE};
}
}
}
}
`;
const Table = React.memo(TableBase) as unknown as typeof TableBase & {
Column: typeof Column,
Row: typeof Row
};
Table.Column = Column;
Table.Row = Row;
export default Table;