@extrachill/components 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +65 -0
- package/dist/DataTable.d.ts +19 -0
- package/dist/DataTable.d.ts.map +1 -0
- package/dist/DataTable.js +27 -0
- package/dist/Modal.d.ts +10 -0
- package/dist/Modal.d.ts.map +1 -0
- package/dist/Modal.js +19 -0
- package/dist/Pagination.d.ts +8 -0
- package/dist/Pagination.d.ts.map +1 -0
- package/dist/Pagination.js +7 -0
- package/dist/SearchBox.d.ts +8 -0
- package/dist/SearchBox.d.ts.map +1 -0
- package/dist/SearchBox.js +23 -0
- package/dist/Tabs.d.ts +14 -0
- package/dist/Tabs.d.ts.map +1 -0
- package/dist/Tabs.js +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/package.json +62 -0
- package/src/DataTable.tsx +109 -0
- package/src/Modal.tsx +52 -0
- package/src/Pagination.tsx +41 -0
- package/src/SearchBox.tsx +56 -0
- package/src/Tabs.tsx +51 -0
- package/src/index.tsx +11 -0
- package/styles/components.scss +160 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0] - 2026-03-25
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Convert all components from JSX to TypeScript with exported prop interfaces
|
|
7
|
+
- Remove @wordpress/components and @wordpress/element peer dependencies — use plain React
|
|
8
|
+
- Add proper npm package config (exports, tsconfig, homeboy.json)
|
|
9
|
+
- Add DataTable, Pagination, SearchBox, Modal, Tabs components
|
|
10
|
+
|
|
11
|
+
## [0.1.0] - 2026-03-02
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Initial release with DataTable, Pagination, SearchBox, Modal components
|
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @extrachill/components
|
|
2
|
+
|
|
3
|
+
Shared React components for the Extra Chill Platform ecosystem.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides reusable UI components used across multiple Extra Chill WordPress plugins, ensuring consistent design and reducing code duplication.
|
|
8
|
+
|
|
9
|
+
## Components
|
|
10
|
+
|
|
11
|
+
- **DataTable** - Sortable data table with configurable columns
|
|
12
|
+
- **Pagination** - Page navigation with configurable items per page
|
|
13
|
+
- **SearchBox** - Debounced search input
|
|
14
|
+
- **Modal** - Accessible modal dialog
|
|
15
|
+
- **Tabs** - Controlled tab navigation for React apps and blocks
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
From a plugin within the Extra Chill Platform:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@extrachill/components": "file:../../extrachill-components"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```jsx
|
|
32
|
+
import { DataTable, Pagination, SearchBox, Modal, Tabs } from '@extrachill/components';
|
|
33
|
+
import '@extrachill/components/styles/components.scss';
|
|
34
|
+
|
|
35
|
+
function MyComponent() {
|
|
36
|
+
return (
|
|
37
|
+
<DataTable
|
|
38
|
+
columns={[
|
|
39
|
+
{ key: 'name', label: 'Name', sortable: true },
|
|
40
|
+
{ key: 'email', label: 'Email' }
|
|
41
|
+
]}
|
|
42
|
+
data={users}
|
|
43
|
+
onSort={handleSort}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Peer Dependencies
|
|
50
|
+
|
|
51
|
+
This package requires the following peer dependencies (provided by `@wordpress/scripts`):
|
|
52
|
+
|
|
53
|
+
- `@wordpress/components` ^28.0.0
|
|
54
|
+
- `@wordpress/element` ^6.0.0
|
|
55
|
+
- `react` ^18.0.0
|
|
56
|
+
|
|
57
|
+
## Used By
|
|
58
|
+
|
|
59
|
+
- `extrachill-admin-tools` - Network administration tools
|
|
60
|
+
- `extrachill-analytics` - Analytics dashboard
|
|
61
|
+
- `extrachill-studio` - Team collaboration workspace
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
GPL-2.0+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface DataTableColumn<T = Record<string, unknown>> {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
width?: string;
|
|
6
|
+
render?: (value: unknown, row: T) => ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export interface DataTableProps<T = Record<string, unknown>> {
|
|
9
|
+
columns: DataTableColumn<T>[];
|
|
10
|
+
data: T[];
|
|
11
|
+
isLoading?: boolean;
|
|
12
|
+
selectable?: boolean;
|
|
13
|
+
selectedIds?: Array<string | number>;
|
|
14
|
+
onSelectChange?: (ids: Array<string | number>) => void;
|
|
15
|
+
emptyMessage?: string;
|
|
16
|
+
rowKey?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function DataTable<T extends Record<string, unknown>>({ columns, data, isLoading, selectable, selectedIds, onSelectChange, emptyMessage, rowKey, }: DataTableProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=DataTable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DataTable.d.ts","sourceRoot":"","sources":["../src/DataTable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC3D,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC;CAC/C;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC1D,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9B,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IACrC,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC5D,OAAO,EACP,IAAI,EACJ,SAAiB,EACjB,UAAkB,EAClB,WAAgB,EAChB,cAAyB,EACzB,YAA+B,EAC/B,MAAa,GACb,EAAE,cAAc,CAAC,CAAC,CAAC,2CA+EnB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function DataTable({ columns, data, isLoading = false, selectable = false, selectedIds = [], onSelectChange = () => { }, emptyMessage = 'No data found.', rowKey = 'id', }) {
|
|
3
|
+
const allSelected = data.length > 0 && selectedIds.length === data.length;
|
|
4
|
+
const handleSelectAll = (e) => {
|
|
5
|
+
if (e.target.checked) {
|
|
6
|
+
onSelectChange(data.map((row) => row[rowKey]));
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
onSelectChange([]);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const handleSelectRow = (id, checked) => {
|
|
13
|
+
if (checked) {
|
|
14
|
+
onSelectChange([...selectedIds, id]);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
onSelectChange(selectedIds.filter((sid) => sid !== id));
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
if (isLoading) {
|
|
21
|
+
return (_jsx("div", { className: "ec-data-table__loading", children: _jsx("span", { children: "Loading..." }) }));
|
|
22
|
+
}
|
|
23
|
+
if (data.length === 0) {
|
|
24
|
+
return (_jsx("div", { className: "ec-data-table__empty", children: _jsx("p", { children: emptyMessage }) }));
|
|
25
|
+
}
|
|
26
|
+
return (_jsxs("table", { className: "ec-data-table wp-list-table widefat fixed striped", children: [_jsx("thead", { children: _jsxs("tr", { children: [selectable && (_jsx("th", { className: "ec-data-table__check-column", children: _jsx("input", { type: "checkbox", checked: allSelected, onChange: handleSelectAll }) })), columns.map((col) => (_jsx("th", { style: col.width ? { width: col.width } : undefined, children: col.label }, col.key)))] }) }), _jsx("tbody", { children: data.map((row) => (_jsxs("tr", { children: [selectable && (_jsx("td", { className: "ec-data-table__check-column", children: _jsx("input", { type: "checkbox", checked: selectedIds.includes(row[rowKey]), onChange: (e) => handleSelectRow(row[rowKey], e.target.checked) }) })), columns.map((col) => (_jsx("td", { children: col.render ? col.render(row[col.key], row) : row[col.key] }, col.key)))] }, row[rowKey]))) })] }));
|
|
27
|
+
}
|
package/dist/Modal.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface ModalProps {
|
|
3
|
+
title: string;
|
|
4
|
+
isOpen: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function Modal({ title, isOpen, onClose, children, className, }: ModalProps): import("react/jsx-runtime").JSX.Element | null;
|
|
10
|
+
//# sourceMappingURL=Modal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../src/Modal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE/D,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,KAAK,CAAC,EACrB,KAAK,EACL,MAAM,EACN,OAAO,EACP,QAAQ,EACR,SAAc,GACd,EAAE,UAAU,kDAmCZ"}
|
package/dist/Modal.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useCallback } from 'react';
|
|
3
|
+
export function Modal({ title, isOpen, onClose, children, className = '', }) {
|
|
4
|
+
const handleKeyDown = useCallback((e) => {
|
|
5
|
+
if (e.key === 'Escape') {
|
|
6
|
+
onClose();
|
|
7
|
+
}
|
|
8
|
+
}, [onClose]);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (isOpen) {
|
|
11
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
12
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
13
|
+
}
|
|
14
|
+
}, [isOpen, handleKeyDown]);
|
|
15
|
+
if (!isOpen) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return (_jsxs("div", { className: `ec-modal ${className}`, role: "dialog", "aria-label": title, children: [_jsx("div", { className: "ec-modal__backdrop", onClick: onClose, "aria-hidden": "true" }), _jsxs("div", { className: "ec-modal__content", children: [_jsxs("div", { className: "ec-modal__header", children: [_jsx("h2", { children: title }), _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close", children: "\u00D7" })] }), _jsx("div", { className: "ec-modal__body", children: children })] })] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface PaginationProps {
|
|
2
|
+
currentPage: number;
|
|
3
|
+
totalPages: number;
|
|
4
|
+
totalItems: number;
|
|
5
|
+
onPageChange: (page: number) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function Pagination({ currentPage, totalPages, totalItems, onPageChange, }: PaginationProps): import("react/jsx-runtime").JSX.Element | null;
|
|
8
|
+
//# sourceMappingURL=Pagination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Pagination.d.ts","sourceRoot":"","sources":["../src/Pagination.tsx"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAED,wBAAgB,UAAU,CAAC,EAC1B,WAAW,EACX,UAAU,EACV,UAAU,EACV,YAAY,GACZ,EAAE,eAAe,kDA4BjB"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export function Pagination({ currentPage, totalPages, totalItems, onPageChange, }) {
|
|
3
|
+
if (totalPages <= 1) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
return (_jsxs("div", { className: "ec-pagination", children: [_jsxs("span", { className: "ec-pagination__info", children: ["Page ", currentPage, " of ", totalPages, " (", totalItems, " items)"] }), _jsxs("div", { className: "ec-pagination__buttons", children: [_jsx("button", { type: "button", disabled: currentPage <= 1, onClick: () => onPageChange(currentPage - 1), children: "Previous" }), _jsx("button", { type: "button", disabled: currentPage >= totalPages, onClick: () => onPageChange(currentPage + 1), children: "Next" })] })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SearchBoxProps {
|
|
2
|
+
value?: string;
|
|
3
|
+
onSearch: (value: string) => void;
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
onClear?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function SearchBox({ value, onSearch, placeholder, onClear, }: SearchBoxProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=SearchBox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SearchBox.d.ts","sourceRoot":"","sources":["../src/SearchBox.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,wBAAgB,SAAS,CAAC,EACzB,KAAU,EACV,QAAQ,EACR,WAAyB,EACzB,OAAO,GACP,EAAE,cAAc,2CAyChB"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
export function SearchBox({ value = '', onSearch, placeholder = 'Search...', onClear, }) {
|
|
4
|
+
const [inputValue, setInputValue] = useState(value);
|
|
5
|
+
const handleSearch = () => {
|
|
6
|
+
onSearch(inputValue);
|
|
7
|
+
};
|
|
8
|
+
const handleClear = () => {
|
|
9
|
+
setInputValue('');
|
|
10
|
+
if (onClear) {
|
|
11
|
+
onClear();
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
onSearch('');
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const handleKeyDown = (e) => {
|
|
18
|
+
if (e.key === 'Enter') {
|
|
19
|
+
handleSearch();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
return (_jsxs("div", { className: "ec-search-box", children: [_jsx("input", { type: "text", value: inputValue, onChange: (e) => setInputValue(e.target.value), placeholder: placeholder, onKeyDown: handleKeyDown }), _jsx("button", { type: "button", onClick: handleSearch, children: "Search" }), inputValue && (_jsx("button", { type: "button", onClick: handleClear, children: "Clear" }))] }));
|
|
23
|
+
}
|
package/dist/Tabs.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TabItem {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
badge?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface TabsProps {
|
|
7
|
+
tabs: TabItem[];
|
|
8
|
+
active: string;
|
|
9
|
+
onChange: (id: string) => void;
|
|
10
|
+
classPrefix?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function Tabs({ tabs, active, onChange, classPrefix, className, }: TabsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
14
|
+
//# sourceMappingURL=Tabs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../src/Tabs.tsx"],"names":[],"mappings":"AAAA,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,IAAI,CAAC,EACpB,IAAS,EACT,MAAM,EACN,QAAQ,EACR,WAAuB,EACvB,SAAc,GACd,EAAE,SAAS,kDA8BX"}
|
package/dist/Tabs.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function Tabs({ tabs = [], active, onChange, classPrefix = 'ec-tabs', className = '', }) {
|
|
3
|
+
if (tabs.length === 0) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
const rootClass = [`${classPrefix}__tabs`, className].filter(Boolean).join(' ');
|
|
7
|
+
return (_jsx("div", { className: rootClass, role: "tablist", "aria-orientation": "horizontal", children: tabs.map((tab) => {
|
|
8
|
+
const isActive = active === tab.id;
|
|
9
|
+
return (_jsxs("button", { type: "button", role: "tab", "aria-selected": isActive, className: `${classPrefix}__tab${isActive ? ' is-active' : ''}`, onClick: () => onChange(tab.id), children: [tab.label, tab.badge != null && tab.badge > 0 && (_jsx("span", { className: `${classPrefix}__tab-badge`, children: tab.badge }))] }, tab.id));
|
|
10
|
+
}) }));
|
|
11
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @extrachill/components
|
|
3
|
+
*
|
|
4
|
+
* Shared React components for the Extra Chill Platform.
|
|
5
|
+
*/
|
|
6
|
+
export { DataTable, type DataTableProps, type DataTableColumn } from './DataTable.tsx';
|
|
7
|
+
export { Pagination, type PaginationProps } from './Pagination.tsx';
|
|
8
|
+
export { SearchBox, type SearchBoxProps } from './SearchBox.tsx';
|
|
9
|
+
export { Modal, type ModalProps } from './Modal.tsx';
|
|
10
|
+
export { Tabs, type TabsProps, type TabItem } from './Tabs.tsx';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @extrachill/components
|
|
3
|
+
*
|
|
4
|
+
* Shared React components for the Extra Chill Platform.
|
|
5
|
+
*/
|
|
6
|
+
export { DataTable } from "./DataTable.js";
|
|
7
|
+
export { Pagination } from "./Pagination.js";
|
|
8
|
+
export { SearchBox } from "./SearchBox.js";
|
|
9
|
+
export { Modal } from "./Modal.js";
|
|
10
|
+
export { Tabs } from "./Tabs.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@extrachill/components",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Shared React components for the Extra Chill Platform.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./package.json": "./package.json",
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./styles/components.scss": "./styles/components.scss",
|
|
15
|
+
"./styles/*": "./styles/*",
|
|
16
|
+
"./src": {
|
|
17
|
+
"types": "./src/index.tsx",
|
|
18
|
+
"default": "./src/index.tsx"
|
|
19
|
+
},
|
|
20
|
+
"./src/*": {
|
|
21
|
+
"types": "./src/*.tsx",
|
|
22
|
+
"default": "./src/*.tsx"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"dist",
|
|
28
|
+
"styles",
|
|
29
|
+
"README.md",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "rm -rf dist *.tsbuildinfo && tsc",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"prepublishOnly": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"extrachill",
|
|
39
|
+
"react",
|
|
40
|
+
"components",
|
|
41
|
+
"wordpress",
|
|
42
|
+
"design-system"
|
|
43
|
+
],
|
|
44
|
+
"author": "Chris Huber <hello@chubes.net>",
|
|
45
|
+
"license": "GPL-2.0+",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/Extra-Chill/extrachill-components.git"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/Extra-Chill/extrachill-components",
|
|
51
|
+
"bugs": "https://github.com/Extra-Chill/extrachill-components/issues",
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"react": ">=18.0.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/react": "^18.0.0",
|
|
60
|
+
"typescript": "^5.7.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface DataTableColumn<T = Record<string, unknown>> {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
width?: string;
|
|
7
|
+
render?: (value: unknown, row: T) => ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DataTableProps<T = Record<string, unknown>> {
|
|
11
|
+
columns: DataTableColumn<T>[];
|
|
12
|
+
data: T[];
|
|
13
|
+
isLoading?: boolean;
|
|
14
|
+
selectable?: boolean;
|
|
15
|
+
selectedIds?: Array<string | number>;
|
|
16
|
+
onSelectChange?: (ids: Array<string | number>) => void;
|
|
17
|
+
emptyMessage?: string;
|
|
18
|
+
rowKey?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function DataTable<T extends Record<string, unknown>>({
|
|
22
|
+
columns,
|
|
23
|
+
data,
|
|
24
|
+
isLoading = false,
|
|
25
|
+
selectable = false,
|
|
26
|
+
selectedIds = [],
|
|
27
|
+
onSelectChange = () => {},
|
|
28
|
+
emptyMessage = 'No data found.',
|
|
29
|
+
rowKey = 'id',
|
|
30
|
+
}: DataTableProps<T>) {
|
|
31
|
+
const allSelected = data.length > 0 && selectedIds.length === data.length;
|
|
32
|
+
|
|
33
|
+
const handleSelectAll = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
34
|
+
if (e.target.checked) {
|
|
35
|
+
onSelectChange(data.map((row) => row[rowKey] as string | number));
|
|
36
|
+
} else {
|
|
37
|
+
onSelectChange([]);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleSelectRow = (id: string | number, checked: boolean) => {
|
|
42
|
+
if (checked) {
|
|
43
|
+
onSelectChange([...selectedIds, id]);
|
|
44
|
+
} else {
|
|
45
|
+
onSelectChange(selectedIds.filter((sid) => sid !== id));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (isLoading) {
|
|
50
|
+
return (
|
|
51
|
+
<div className="ec-data-table__loading">
|
|
52
|
+
<span>Loading...</span>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (data.length === 0) {
|
|
58
|
+
return (
|
|
59
|
+
<div className="ec-data-table__empty">
|
|
60
|
+
<p>{emptyMessage}</p>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<table className="ec-data-table wp-list-table widefat fixed striped">
|
|
67
|
+
<thead>
|
|
68
|
+
<tr>
|
|
69
|
+
{selectable && (
|
|
70
|
+
<th className="ec-data-table__check-column">
|
|
71
|
+
<input
|
|
72
|
+
type="checkbox"
|
|
73
|
+
checked={allSelected}
|
|
74
|
+
onChange={handleSelectAll}
|
|
75
|
+
/>
|
|
76
|
+
</th>
|
|
77
|
+
)}
|
|
78
|
+
{columns.map((col) => (
|
|
79
|
+
<th key={col.key} style={col.width ? { width: col.width } : undefined}>
|
|
80
|
+
{col.label}
|
|
81
|
+
</th>
|
|
82
|
+
))}
|
|
83
|
+
</tr>
|
|
84
|
+
</thead>
|
|
85
|
+
<tbody>
|
|
86
|
+
{data.map((row) => (
|
|
87
|
+
<tr key={row[rowKey] as string | number}>
|
|
88
|
+
{selectable && (
|
|
89
|
+
<td className="ec-data-table__check-column">
|
|
90
|
+
<input
|
|
91
|
+
type="checkbox"
|
|
92
|
+
checked={selectedIds.includes(row[rowKey] as string | number)}
|
|
93
|
+
onChange={(e) =>
|
|
94
|
+
handleSelectRow(row[rowKey] as string | number, e.target.checked)
|
|
95
|
+
}
|
|
96
|
+
/>
|
|
97
|
+
</td>
|
|
98
|
+
)}
|
|
99
|
+
{columns.map((col) => (
|
|
100
|
+
<td key={col.key}>
|
|
101
|
+
{col.render ? col.render(row[col.key], row) : (row[col.key] as ReactNode)}
|
|
102
|
+
</td>
|
|
103
|
+
))}
|
|
104
|
+
</tr>
|
|
105
|
+
))}
|
|
106
|
+
</tbody>
|
|
107
|
+
</table>
|
|
108
|
+
);
|
|
109
|
+
}
|
package/src/Modal.tsx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useEffect, useCallback, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ModalProps {
|
|
4
|
+
title: string;
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Modal({
|
|
12
|
+
title,
|
|
13
|
+
isOpen,
|
|
14
|
+
onClose,
|
|
15
|
+
children,
|
|
16
|
+
className = '',
|
|
17
|
+
}: ModalProps) {
|
|
18
|
+
const handleKeyDown = useCallback(
|
|
19
|
+
(e: KeyboardEvent) => {
|
|
20
|
+
if (e.key === 'Escape') {
|
|
21
|
+
onClose();
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
[onClose]
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (isOpen) {
|
|
29
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
30
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
31
|
+
}
|
|
32
|
+
}, [isOpen, handleKeyDown]);
|
|
33
|
+
|
|
34
|
+
if (!isOpen) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={`ec-modal ${className}`} role="dialog" aria-label={title}>
|
|
40
|
+
<div className="ec-modal__backdrop" onClick={onClose} aria-hidden="true" />
|
|
41
|
+
<div className="ec-modal__content">
|
|
42
|
+
<div className="ec-modal__header">
|
|
43
|
+
<h2>{title}</h2>
|
|
44
|
+
<button type="button" onClick={onClose} aria-label="Close">
|
|
45
|
+
×
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="ec-modal__body">{children}</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface PaginationProps {
|
|
2
|
+
currentPage: number;
|
|
3
|
+
totalPages: number;
|
|
4
|
+
totalItems: number;
|
|
5
|
+
onPageChange: (page: number) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Pagination({
|
|
9
|
+
currentPage,
|
|
10
|
+
totalPages,
|
|
11
|
+
totalItems,
|
|
12
|
+
onPageChange,
|
|
13
|
+
}: PaginationProps) {
|
|
14
|
+
if (totalPages <= 1) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="ec-pagination">
|
|
20
|
+
<span className="ec-pagination__info">
|
|
21
|
+
Page {currentPage} of {totalPages} ({totalItems} items)
|
|
22
|
+
</span>
|
|
23
|
+
<div className="ec-pagination__buttons">
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
disabled={currentPage <= 1}
|
|
27
|
+
onClick={() => onPageChange(currentPage - 1)}
|
|
28
|
+
>
|
|
29
|
+
Previous
|
|
30
|
+
</button>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
disabled={currentPage >= totalPages}
|
|
34
|
+
onClick={() => onPageChange(currentPage + 1)}
|
|
35
|
+
>
|
|
36
|
+
Next
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useState, type KeyboardEvent } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SearchBoxProps {
|
|
4
|
+
value?: string;
|
|
5
|
+
onSearch: (value: string) => void;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
onClear?: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function SearchBox({
|
|
11
|
+
value = '',
|
|
12
|
+
onSearch,
|
|
13
|
+
placeholder = 'Search...',
|
|
14
|
+
onClear,
|
|
15
|
+
}: SearchBoxProps) {
|
|
16
|
+
const [inputValue, setInputValue] = useState(value);
|
|
17
|
+
|
|
18
|
+
const handleSearch = () => {
|
|
19
|
+
onSearch(inputValue);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const handleClear = () => {
|
|
23
|
+
setInputValue('');
|
|
24
|
+
if (onClear) {
|
|
25
|
+
onClear();
|
|
26
|
+
} else {
|
|
27
|
+
onSearch('');
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
32
|
+
if (e.key === 'Enter') {
|
|
33
|
+
handleSearch();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="ec-search-box">
|
|
39
|
+
<input
|
|
40
|
+
type="text"
|
|
41
|
+
value={inputValue}
|
|
42
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
43
|
+
placeholder={placeholder}
|
|
44
|
+
onKeyDown={handleKeyDown}
|
|
45
|
+
/>
|
|
46
|
+
<button type="button" onClick={handleSearch}>
|
|
47
|
+
Search
|
|
48
|
+
</button>
|
|
49
|
+
{inputValue && (
|
|
50
|
+
<button type="button" onClick={handleClear}>
|
|
51
|
+
Clear
|
|
52
|
+
</button>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
package/src/Tabs.tsx
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface TabItem {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
badge?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface TabsProps {
|
|
8
|
+
tabs: TabItem[];
|
|
9
|
+
active: string;
|
|
10
|
+
onChange: (id: string) => void;
|
|
11
|
+
classPrefix?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Tabs({
|
|
16
|
+
tabs = [],
|
|
17
|
+
active,
|
|
18
|
+
onChange,
|
|
19
|
+
classPrefix = 'ec-tabs',
|
|
20
|
+
className = '',
|
|
21
|
+
}: TabsProps) {
|
|
22
|
+
if (tabs.length === 0) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const rootClass = [`${classPrefix}__tabs`, className].filter(Boolean).join(' ');
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className={rootClass} role="tablist" aria-orientation="horizontal">
|
|
30
|
+
{tabs.map((tab) => {
|
|
31
|
+
const isActive = active === tab.id;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<button
|
|
35
|
+
key={tab.id}
|
|
36
|
+
type="button"
|
|
37
|
+
role="tab"
|
|
38
|
+
aria-selected={isActive}
|
|
39
|
+
className={`${classPrefix}__tab${isActive ? ' is-active' : ''}`}
|
|
40
|
+
onClick={() => onChange(tab.id)}
|
|
41
|
+
>
|
|
42
|
+
{tab.label}
|
|
43
|
+
{tab.badge != null && tab.badge > 0 && (
|
|
44
|
+
<span className={`${classPrefix}__tab-badge`}>{tab.badge}</span>
|
|
45
|
+
)}
|
|
46
|
+
</button>
|
|
47
|
+
);
|
|
48
|
+
})}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @extrachill/components
|
|
3
|
+
*
|
|
4
|
+
* Shared React components for the Extra Chill Platform.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { DataTable, type DataTableProps, type DataTableColumn } from './DataTable.tsx';
|
|
8
|
+
export { Pagination, type PaginationProps } from './Pagination.tsx';
|
|
9
|
+
export { SearchBox, type SearchBoxProps } from './SearchBox.tsx';
|
|
10
|
+
export { Modal, type ModalProps } from './Modal.tsx';
|
|
11
|
+
export { Tabs, type TabsProps, type TabItem } from './Tabs.tsx';
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Component Styles
|
|
3
|
+
*
|
|
4
|
+
* Styles for extrachill-components shared across plugins.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Data Table
|
|
8
|
+
.ec-data-table {
|
|
9
|
+
&__loading,
|
|
10
|
+
&__empty {
|
|
11
|
+
padding: 40px;
|
|
12
|
+
text-align: center;
|
|
13
|
+
background: #f9f9f9;
|
|
14
|
+
border: 1px solid #ddd;
|
|
15
|
+
border-radius: 4px;
|
|
16
|
+
|
|
17
|
+
.components-spinner {
|
|
18
|
+
margin-right: 10px;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&__check-column {
|
|
23
|
+
width: 40px;
|
|
24
|
+
padding: 8px !important;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Override WP table styles
|
|
28
|
+
&.wp-list-table {
|
|
29
|
+
margin-top: 15px;
|
|
30
|
+
|
|
31
|
+
th,
|
|
32
|
+
td {
|
|
33
|
+
vertical-align: middle;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Pagination
|
|
39
|
+
.ec-pagination {
|
|
40
|
+
display: flex;
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
align-items: center;
|
|
43
|
+
margin-top: 15px;
|
|
44
|
+
padding-top: 15px;
|
|
45
|
+
border-top: 1px solid #eee;
|
|
46
|
+
|
|
47
|
+
&__info {
|
|
48
|
+
color: #646970;
|
|
49
|
+
font-size: 13px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
&__buttons {
|
|
53
|
+
display: flex;
|
|
54
|
+
gap: 8px;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Search Box
|
|
59
|
+
.ec-search-box {
|
|
60
|
+
display: flex;
|
|
61
|
+
gap: 8px;
|
|
62
|
+
align-items: flex-end;
|
|
63
|
+
margin-bottom: 15px;
|
|
64
|
+
|
|
65
|
+
.components-text-control {
|
|
66
|
+
min-width: 300px;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Modal overrides
|
|
71
|
+
.ec-modal {
|
|
72
|
+
.components-modal__content {
|
|
73
|
+
min-width: 500px;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Tabs — default styles for classPrefix="ec-tabs"
|
|
78
|
+
// Consumers with custom classPrefix (e.g. "ec-am") duplicate
|
|
79
|
+
// these rules under their own namespace in their block SCSS.
|
|
80
|
+
.ec-tabs__tabs {
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
gap: var(--spacing-sm, 0.5rem);
|
|
83
|
+
flex-wrap: wrap;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ec-tabs__tab {
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: var(--spacing-xs, 0.25rem);
|
|
90
|
+
border: 1px solid var(--border-color, #ddd);
|
|
91
|
+
background: var(--background-color, #fff);
|
|
92
|
+
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
|
93
|
+
border-radius: var(--border-radius-sm, 4px);
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
color: var(--text-color, #111);
|
|
96
|
+
font: inherit;
|
|
97
|
+
font-size: var(--font-size-base, 1rem);
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
|
|
100
|
+
|
|
101
|
+
&:hover {
|
|
102
|
+
background: var(--card-background, #f1f5f9);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&.is-active {
|
|
106
|
+
border-color: var(--header-background, #1a1a1a);
|
|
107
|
+
background: var(--header-background, #1a1a1a);
|
|
108
|
+
color: var(--header-text-color, #fff);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.ec-tabs__tab-badge {
|
|
113
|
+
display: inline-flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
min-width: 20px;
|
|
117
|
+
height: 20px;
|
|
118
|
+
padding: 0 6px;
|
|
119
|
+
border-radius: 10px;
|
|
120
|
+
background: var(--accent, #53940b);
|
|
121
|
+
color: var(--background-color, #fff);
|
|
122
|
+
font-size: var(--font-size-xs, 0.625rem);
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
|
|
125
|
+
.ec-tabs__tab.is-active & {
|
|
126
|
+
background: var(--background-color, #fff);
|
|
127
|
+
color: var(--header-background, #1a1a1a);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Badges (commonly used with tables)
|
|
132
|
+
.ec-badge {
|
|
133
|
+
display: inline-block;
|
|
134
|
+
padding: 3px 8px;
|
|
135
|
+
border-radius: 3px;
|
|
136
|
+
font-size: 12px;
|
|
137
|
+
font-weight: 500;
|
|
138
|
+
|
|
139
|
+
&--yes,
|
|
140
|
+
&--success {
|
|
141
|
+
background: #d4edda;
|
|
142
|
+
color: #155724;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
&--no,
|
|
146
|
+
&--error {
|
|
147
|
+
background: #f8d7da;
|
|
148
|
+
color: #721c24;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
&--info {
|
|
152
|
+
background: #cce5ff;
|
|
153
|
+
color: #004085;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
&--warning {
|
|
157
|
+
background: #fff3cd;
|
|
158
|
+
color: #856404;
|
|
159
|
+
}
|
|
160
|
+
}
|