` and documenting invalid shared-field exceptions in adjacent type comments.
+- Extended `UiInput` public props with `size` and `variant`, forwarded both to MUI `TextField`, and kept existing consumer behavior backward compatible.
+- Added `src/test/testing-library/UiCoreContract.test.tsx` to enforce package exports and the `UiInput` contract regression in CI.
+- Added Jest module mappings and test mocks for CSS/SVG assets, plus minimal React import/mock compatibility fixes required for the current unit-test transform pipeline.
+- Added a default fallback for `NEXT_PUBLIC_VILNACRM_GMAIL` so the existing footer email test remains deterministic when the environment variable is absent.
+- Verification evidence:
+- `make lint-tsc` passed.
+- `make lint-next` exited successfully with pre-existing warnings only.
+- `bunx jest --verbose --runInBand` passed with `19` test suites and `52` tests.
+- Residual warning-only issues remain outside Story 1.1 scope: React `act(...)` noise around `UiTooltipWrapper`, nested `` warnings in card/tooltip composition, uncontrolled-to-controlled warnings in `UiTextFieldForm`, and existing ESLint warnings reported by the make-based ESLint flow.
+
+### File List
+
+- jest.config.ts
+- specs/implementation-artifacts/1-1-core-contract-and-export-baseline.md
+- specs/implementation-artifacts/sprint-status.yaml
+- src/components/UiButton/index.tsx
+- src/components/UiButton/types.ts
+- src/components/UiCardItem/CardContent.tsx
+- src/components/UiCardList/CardGrid.tsx
+- src/components/UiCardList/CardSwiper.tsx
+- src/components/UiCardList/index.tsx
+- src/components/UiCheckbox/types.ts
+- src/components/UiFooter/DefaultFooter/DefaultFooter.tsx
+- src/components/UiFooter/UiFooter.tsx
+- src/components/UiFooter/VilnaCRMEmail/VilnaCRMGmail.tsx
+- src/components/UiImage/index.tsx
+- src/components/UiInput/index.tsx
+- src/components/UiInput/types.ts
+- src/components/UiLink/index.tsx
+- src/components/UiLink/types.ts
+- src/components/UiTextFieldForm/index.tsx
+- src/components/UiToolbar/index.tsx
+- src/test/mocks/styleMock.ts
+- src/test/mocks/svgMock.ts
+- src/test/testing-library/UiButton.test.tsx
+- src/test/testing-library/UiCardGrid.test.tsx
+- src/test/testing-library/UiCardItem.test.tsx
+- src/test/testing-library/UiCardList.test.tsx
+- src/test/testing-library/UiCoreContract.test.tsx
+- src/test/testing-library/UiFooterEmail.test.tsx
+- src/test/testing-library/UiImage.test.tsx
+- src/test/testing-library/UiTooltipWrapper.test.tsx
+
+## Change Log
+
+- 2026-03-09: Created Story 1.1 from PRD, architecture, epics, implementation plan, git history, and current repository inspection.
+- 2026-03-09: Implemented the core contract/export baseline, added contract regression coverage, and updated the Jest harness needed to execute the existing unit-test suite in this checkout.
+- 2026-03-09: Verified `lint:tsc`, `lint:next`, and the full Jest suite; story state advanced to `review` with warning-only residual risks documented in the Dev Agent Record.
diff --git a/specs/implementation-artifacts/sprint-status.yaml b/specs/implementation-artifacts/sprint-status.yaml
new file mode 100644
index 0000000..dcab6b6
--- /dev/null
+++ b/specs/implementation-artifacts/sprint-status.yaml
@@ -0,0 +1,77 @@
+# generated: 2026-03-09T18:52:20+02:00
+# project: ui-toolkit
+# project_key: NOKEY
+# tracking_system: file-system
+# story_location: /home/dima/Desktop/ui-toolkit/specs/implementation-artifacts
+#
+# STATUS DEFINITIONS:
+# ==================
+# Epic Status:
+# - backlog: Epic not yet started
+# - in-progress: Epic actively being worked on
+# - done: All stories in epic completed
+#
+# Epic Status Transitions:
+# - backlog -> in-progress: Automatically when first story is created (via create-story)
+# - in-progress -> done: Manually when all stories reach 'done' status
+#
+# Story Status:
+# - backlog: Story only exists in epic file
+# - ready-for-dev: Story file created in stories folder
+# - in-progress: Developer actively working on implementation
+# - review: Ready for code review (via Dev's code-review workflow)
+# - done: Story completed
+#
+# Retrospective Status:
+# - optional: Can be completed but not required
+# - done: Retrospective has been completed
+#
+# WORKFLOW NOTES:
+# ===============
+# - Epic transitions to 'in-progress' automatically when first story is created
+# - Stories can be worked in parallel if team capacity allows
+# - SM typically creates next story after previous one is 'done' to incorporate learnings
+# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended)
+
+generated: 2026-03-09T18:52:20+02:00
+project: ui-toolkit
+project_key: NOKEY
+tracking_system: file-system
+story_location: /home/dima/Desktop/ui-toolkit/specs/implementation-artifacts
+
+development_status:
+ epic-1: in-progress
+ 1-1-core-contract-and-export-baseline: review
+ 1-2-core-control-state-parity-completion: backlog
+ 1-3-accessibility-and-interaction-consistency-hardening: backlog
+ 1-4-epic-1-quality-gate-closure: backlog
+ epic-1-retrospective: optional
+ epic-2: backlog
+ 2-1-search-and-select-foundation: backlog
+ 2-2-multi-select-interaction-workflow: backlog
+ 2-3-calendar-multi-select-variant: backlog
+ 2-4-radio-group-input-workflow: backlog
+ 2-4a-file-upload-input-workflows: backlog
+ 2-5-pagination-workflow-component-delivery: backlog
+ 2-6-epic-2-quality-gate-closure: backlog
+ epic-2-retrospective: optional
+ epic-3: backlog
+ 3-1-item-row-and-list-data-presentation: backlog
+ 3-2-task-card-workflow: backlog
+ 3-3-profile-select-card-workflow: backlog
+ 3-4-integration-card-workflow: backlog
+ 3-5-board-a-micro-components-delivery: backlog
+ 3-6-epic-3-quality-gate-closure: backlog
+ epic-3-retrospective: optional
+ epic-4: backlog
+ 4-1-crm-skeleton-baseline-and-provenance-lock: backlog
+ 4-2-skeleton-primitive-variants: backlog
+ 4-3-composed-skeleton-layout-variants: backlog
+ 4-4-skeleton-parity-and-quality-gate-closure: backlog
+ epic-4-retrospective: optional
+ epic-5: backlog
+ 5-1-board-coverage-closure-and-traceability: backlog
+ 5-2-reuse-canonical-compliance-and-provenance-completion: backlog
+ 5-3-export-contract-and-entry-point-integrity: backlog
+ 5-4-internal-release-readiness-governance-report: backlog
+ epic-5-retrospective: optional
diff --git a/specs/planning-artifacts/architecture.md b/specs/planning-artifacts/architecture.md
index 69b95b6..b9823af 100644
--- a/specs/planning-artifacts/architecture.md
+++ b/specs/planning-artifacts/architecture.md
@@ -87,15 +87,18 @@ Web frontend component library within an existing React + TypeScript workspace.
### Starter Options Considered
1. Existing repository baseline (no re-bootstrap)
+
- Best fit for current project state and minimizes migration risk.
- Preserves current contracts, test suites, Storybook setup, and release cadence.
2. `create-tsdown` React starter (greenfield library-first)
+
- Good option for a brand-new component library.
- Modern library bundling defaults and templates.
- Not selected due to migration overhead for this already-established codebase.
3. Bun + Storybook + Bulletproof React structure adaptation
+
- Strong option for a library-first workflow with Bun package/runtime consistency.
- Not selected as a re-bootstrap path because this workstream extends an existing repository baseline.
@@ -138,6 +141,7 @@ Established lint/test/storybook command surface and team-familiar workflows.
### Decision Priority Analysis
**Critical Decisions (Block Implementation):**
+
- Data Architecture: no persistent data layer; UI-library-only runtime model.
- Component Interface Model: props + callbacks only for UI component interaction; no backend API-contract layer.
- Distribution Model: public npm registry as the official package distribution path.
@@ -146,10 +150,12 @@ Established lint/test/storybook command surface and team-familiar workflows.
- Contract Enforcement: mandatory per-component checklist (exports, state matrix, accessibility, Storybook/tests, provenance note).
**Important Decisions (Shape Architecture):**
+
- Reuse governance from PRD remains active: `crm` canonical behavior, `website` visual/variant gap-fill.
- Existing repository baseline retained (no re-bootstrap).
**Deferred Decisions (Post-MVP):**
+
- Any adapter/hook integration abstractions for app-specific backend coupling.
- Any internal metadata service beyond file-based governance artifacts.
@@ -209,12 +215,14 @@ Established lint/test/storybook command surface and team-familiar workflows.
### Decision Impact Analysis
**Implementation Sequence:**
+
1. Define and enforce quality checklist as definition-of-done.
2. Implement/upgrade components using reuse-first policy.
3. Validate state matrix, tests, stories, exports per component.
4. Package and publish through public npm registry with versioned release gates.
**Cross-Component Dependencies:**
+
- Contract checklist influences every component implementation and release eligibility.
- API communication model constrains prop design across all modules.
- Distribution model constrains versioning, changelog discipline, and CI release behavior.
@@ -230,17 +238,20 @@ Established lint/test/storybook command surface and team-familiar workflows.
### Naming Patterns
**Code Naming Conventions:**
+
- Component folders/files use kebab-case.
- Component exported symbol names remain `UiPascalCase`.
- Variables/functions use camelCase.
- Type/interface names use PascalCase.
**Examples:**
+
- Folder: `src/components/ui-button/`
- File: `src/components/ui-button/index.tsx`
- Export: `export default function UiButton(...)`
**Transition Rule (Required due to current repo state):**
+
- Existing `UiPascalCase` folders remain valid until migration tasks are explicitly planned.
- New components follow kebab-case from this point forward.
- Do not rename legacy folders opportunistically inside feature tasks.
@@ -248,6 +259,7 @@ Established lint/test/storybook command surface and team-familiar workflows.
### Structure Patterns
**Project Organization:**
+
- Follow Bulletproof React structure boundaries for new work:
- `src/app/providers` for Storybook/dev-harness provider composition only
- `src/features//components` for domain-driven components
@@ -258,6 +270,7 @@ Established lint/test/storybook command surface and team-familiar workflows.
- Public exports remain centralized in the package entry boundary (`src/components/index.ts` until migration is complete).
**File Structure Patterns (per new component):**
+
- `src/features//components//index.tsx`
- `src/features//components//types.ts`
- `src/features//components//.stories.tsx`
@@ -275,6 +288,7 @@ Established lint/test/storybook command surface and team-familiar workflows.
### Format Patterns
**Public Component Contract:**
+
- Shared base props for interactive components where relevant:
- `value`
- `onChange`
@@ -286,6 +300,7 @@ Established lint/test/storybook command surface and team-familiar workflows.
- Exceptions must be documented in component-level notes and reflected in stories/tests.
**Data Exchange Formats:**
+
- Props/events use TypeScript-typed shapes.
- Event callback payloads should favor native React event signatures unless a value-first API is explicitly chosen and documented.
- No backend transport payload formats are defined in this library architecture.
@@ -293,11 +308,13 @@ Established lint/test/storybook command surface and team-familiar workflows.
### Communication Patterns
**Component Communication:**
+
- Props + callbacks only.
- No global event bus pattern in library architecture.
- No hidden cross-component side effects through shared mutable state.
**State Management Pattern:**
+
- Components are async-stateless.
- Consumer applications own loading, retry, and async error flows.
- Library exposes visual/control props (`loading`, `error`, `disabled`) when needed.
@@ -305,17 +322,20 @@ Established lint/test/storybook command surface and team-familiar workflows.
### Process Patterns
**Error Handling Patterns:**
+
- Library components render deterministic error visuals from props.
- No internal domain error mapping logic.
- Accessibility and disabled behavior consistency are mandatory across components.
**Loading State Patterns:**
+
- Loading UI is visual-only and externally driven by consumers.
- Components must not start network calls or retry loops internally.
### Provenance & Governance Patterns
**Source Provenance Tracking:**
+
- Maintain a central registry:
- `specs/planning-artifacts/component-provenance.md`
- For each component, record:
@@ -324,6 +344,7 @@ Established lint/test/storybook command surface and team-familiar workflows.
- notes on behavior/visual alignment decisions
**Quality Enforcement Checklist (Mandatory per component):**
+
- Export present in `src/components/index.ts`
- Required state matrix covered
- Accessibility baseline checks included
@@ -334,23 +355,27 @@ Established lint/test/storybook command surface and team-familiar workflows.
### Enforcement Guidelines
**All AI Agents MUST:**
+
- Follow naming and structure patterns exactly for new components.
- Apply shared prop contract policy and document exceptions.
- Complete quality checklist before considering a component done.
- Update provenance registry for each delivered/enhanced component.
**Pattern Enforcement:**
+
- Enforce via PR checklist + CI gates (tests/type checks/export checks).
- Pattern violations are documented in the relevant implementation artifact and corrected before release tagging.
### Pattern Examples
**Good Examples:**
+
- New component created under kebab-case path with co-located story and centralized test.
- Component exposes standardized `disabled/error/variant/size/sx` and documents any exception.
- Provenance updated with `crm` canonical behavior and `website` visual gap-fill note.
**Anti-Patterns:**
+
- Introducing a global event bus for component interactions.
- Embedding async fetch/retry in library UI components.
- Shipping component changes without export update, checklist completion, or provenance entry.
@@ -466,6 +491,7 @@ ui-toolkit/
```
**Lockfile Policy (Bun v1.2+):**
+
- Required lockfile format is text-based `bun.lock` (legacy `bun.lockb` is not allowed on active branches).
- Migration command:
@@ -474,6 +500,7 @@ bun install --save-text-lockfile --frozen-lockfile --lockfile-only
```
**`src/app` Scope Constraint:**
+
- `src/app/providers` is limited to provider wrappers used by Storybook/dev harnesses (for example `ThemeProvider` composition).
- Do not add app-level `routes/` or `stores/` to this library architecture.
- `src/app` must not contain async orchestration, backend coupling, or product application logic.
@@ -481,40 +508,48 @@ bun install --save-text-lockfile --frozen-lockfile --lockfile-only
### Architectural Boundaries
**API Boundaries:**
+
- Public component API is exported only through `src/components/index.ts`.
- Internal component files are non-public implementation details.
- No backend API surface is owned by this repository.
**Component Boundaries:**
+
- Component communication is props + callbacks only.
- No global event-bus architecture.
- Async orchestration (fetch/retry/session) stays in consumer apps (`crm`, `website`).
**Service Boundaries:**
+
- No application service layer in toolkit scope.
- Integration logic belongs to consuming applications, not library components.
**Data Boundaries:**
+
- No persistent data layer (no DB schema/migrations/caching tier).
- Data contracts exist as TypeScript props and test/story fixtures only.
### Requirements to Structure Mapping
**FR-01 Board Coverage Completeness**
+
- Component implementation: `src/features/*`, `src/shared/ui/*`, and exported entry boundary
- Coverage governance artifacts: `specs/planning-artifacts/*`
- Validation surfaces: stories in component folders + tests in `tests/unit`
**FR-02 Reuse-First Compliance**
+
- Provenance registry: `specs/planning-artifacts/component-provenance.md`
- Source alignment notes per component entry (`crm`/`website`/`new`)
**FR-03 Canonical Behavior Alignment**
+
- Behavioral baseline encoded in component implementations and tests:
- `src/features/*`, `src/shared/ui/*`
- `tests/unit/*`
**FR-04 Existing Control State Parity**
+
- Existing controls:
- `src/components/UiButton/`
- `src/components/UiInput/`
@@ -527,20 +562,24 @@ bun install --save-text-lockfile --frozen-lockfile --lockfile-only
- `tests/unit/UiLink.test.tsx`
**FR-05 Missing Module Delivery**
+
- New modules under Bulletproof feature/shared paths with kebab-case naming
- Matching tests in `tests/unit/`
**FR-06 Skeleton Parity**
+
- Skeleton implementation:
- `src/features/skeleton/components/ui-skeleton/`
- `src/features/skeleton/components/ui-skeleton-composed/`
- Parity verification tests in `tests/unit/`
**FR-07 API Contract Consistency**
+
- Prop/type definitions in each component `types.ts` (UI component interfaces only)
- Public export discipline in `src/components/index.ts`
**FR-08 Quality Gates**
+
- Stories co-located in component folders
- Unit tests centralized under `tests/unit/`
- CI gates in `.github/workflows/` aligned to `website`/`crm` matrices
@@ -548,14 +587,17 @@ bun install --save-text-lockfile --frozen-lockfile --lockfile-only
### Integration Points
**Internal Communication:**
+
- Props down, callbacks up, typed event payloads.
- Shared UI patterns through MUI theme/config and common prop contract rules.
**External Integrations:**
+
- Distribution via public npm registry.
- Consumer projects (`crm`, `website`, others) and external adopters import published package surface.
**Data Flow:**
+
- Consumer app state drives component props.
- Components emit interaction callbacks to consumer handlers.
- No repository-owned external data fetch lifecycle.
@@ -587,31 +629,38 @@ bun install --save-text-lockfile --frozen-lockfile --lockfile-only
### File Organization Patterns
**Configuration Files:**
+
- Root-level build/lint/test config files remain authoritative.
**Source Organization:**
+
- Bulletproof React boundaries are primary (`src/features`, `src/shared`, `src/app`).
- `src/components` remains as package export boundary during migration.
- Legacy `UiPascalCase` remains until explicit migration.
- New components use kebab-case folders/files.
**Test Organization:**
+
- Unit tests centralized in root `tests/unit`.
- Integration, E2E, visual, and memory-leak checks are mapped under root `tests/*`.
**Asset Organization:**
+
- Static visual assets in `src/assets`.
- Component-local style/type/theme files stay near implementation.
### Development Workflow Integration
**Development Server Structure:**
+
- Storybook and local dev scripts operate against Bulletproof domains (`src/features`, `src/shared`) and shared config.
**Build Process Structure:**
+
- Bun-driven build scripts and TypeScript/Jest/Playwright pipelines validate exported library surface and the full CI matrix.
**Deployment Structure:**
+
- CI workflow publishes versioned package to public npm registry after full quality gates pass.
## Architecture Validation Results
@@ -652,14 +701,17 @@ Naming, structure, contract, communication, and process patterns are specified w
### Gap Analysis Results
**Critical Gaps:**
+
- None.
**Important Gaps:**
+
- `specs/planning-artifacts/component-provenance.md` is defined but not yet created.
- Compatibility matrix is now defined in this document and must be mirrored in release-gate automation (validation + documentation checks) before publish.
- CI publish gate is defined conceptually but requires concrete workflow-level checklist.
**Nice-to-Have Gaps:**
+
- Add a migration playbook for eventual legacy `UiPascalCase` → kebab-case folder convergence.
### Validation Issues Addressed
@@ -672,24 +724,28 @@ Naming, structure, contract, communication, and process patterns are specified w
### Architecture Completeness Checklist
**✅ Requirements Analysis**
+
- [x] Project context thoroughly analyzed
- [x] Scale and complexity assessed
- [x] Technical constraints identified
- [x] Cross-cutting concerns mapped
**✅ Architectural Decisions**
+
- [x] Critical decisions documented
- [x] Technology stack baseline specified
- [x] Integration patterns defined
- [x] Security scope boundaries defined
**✅ Implementation Patterns**
+
- [x] Naming conventions established
- [x] Structure patterns defined
- [x] Communication patterns specified
- [x] Process patterns documented
**✅ Project Structure**
+
- [x] Complete directory structure defined
- [x] Component boundaries established
- [x] Integration points mapped
@@ -701,21 +757,25 @@ Naming, structure, contract, communication, and process patterns are specified w
**Confidence Level:** High
**Key Strengths:**
+
- Strong scope discipline for a shared internal UI library.
- Clear cross-repo reuse governance (`crm` canonical behavior).
- Practical enforcement model (tests, stories, exports, provenance).
**Areas for Future Enhancement:**
+
- Planned legacy folder naming migration path.
### Implementation Handoff
**AI Agent Guidelines:**
+
- Follow architectural decisions and patterns exactly.
- Treat checklist completion as definition-of-done.
- Keep component behavior deterministic and async-stateless.
**First Implementation Priority:**
Create governance artifact + bootstrap enforcement:
+
1. Create `specs/planning-artifacts/component-provenance.md`
2. Implement export/checklist scaffolding for first new component slice.
diff --git a/specs/planning-artifacts/epics.md b/specs/planning-artifacts/epics.md
index b17869d..89a0e97 100644
--- a/specs/planning-artifacts/epics.md
+++ b/specs/planning-artifacts/epics.md
@@ -86,22 +86,27 @@ Each delivery story must record source, reuse rationale, and reference IDs in PR
## Epic List
### Epic 1: Core Controls and Contract Foundation
+
Deliver stable, production-ready foundational controls and consistent contracts so product teams can build interactive UI without custom rework.
**FRs covered:** FR4, FR7, FR8
### Epic 2: Selection, Search, and Input Workflows
+
Enable users to search, select, and submit values through reusable input-selection components that support company app workflows.
**FRs covered:** FR5, FR7, FR8
### Epic 3: Data Presentation and Cards
+
Enable users to understand and act on structured information via reusable item rows, lists, and card patterns.
**FRs covered:** FR5, FR7, FR8
### Epic 4: Skeleton Loading Experience Parity
+
Provide trusted loading experiences by delivering skeleton primitives and composed variants with exact CRM animation parity.
**FRs covered:** FR5, FR6, FR8
### Epic 5: Production Adoption Readiness
+
Make the toolkit safely adoptable across company projects by closing coverage, provenance, export, and release-gate governance.
**FRs covered:** FR1, FR2, FR3, FR8 (plus consolidated traceability references for FR4, FR5, FR6, and FR7 delivered in Epics 1-4)
diff --git a/specs/planning-artifacts/implementation-plan.md b/specs/planning-artifacts/implementation-plan.md
index 13ebef3..5186daa 100644
--- a/specs/planning-artifacts/implementation-plan.md
+++ b/specs/planning-artifacts/implementation-plan.md
@@ -47,6 +47,7 @@
### Task 1: Bootstrap Governance Artifacts
**Files:**
+
- Create: `specs/planning-artifacts/component-provenance.md`
- Create: `specs/planning-artifacts/board-coverage-checklist.md`
- Create: `specs/implementation-artifacts/release-readiness-report.md`
@@ -55,6 +56,7 @@
**Step 1: Create provenance registry skeleton**
Add table columns:
+
- `component`
- `board`
- `source (crm|website|new)`
@@ -65,6 +67,7 @@ Add table columns:
**Step 2: Create board coverage checklist**
Add every scope item from Boards A-D with status fields:
+
- `implemented`
- `story-id`
- `storybook`
@@ -75,6 +78,7 @@ Add every scope item from Boards A-D with status fields:
**Step 3: Create release-readiness report template**
Add sections for:
+
- Epic closure status
- FR/NFR evidence links
- Blocking issues
@@ -83,6 +87,7 @@ Add sections for:
**Step 4: Create story DoD template**
Capture:
+
- changed files
- tests run
- stories added/updated
@@ -102,6 +107,7 @@ git commit -m "chore: bootstrap governance artifacts for ui-toolkit completion"
### Task 2: Inventory Existing Components and Map Reuse Sources
**Files:**
+
- Modify: `specs/planning-artifacts/component-provenance.md`
- Modify: `specs/planning-artifacts/board-coverage-checklist.md`
@@ -123,6 +129,7 @@ Run equivalent component listings in both repositories and map candidates to req
**Step 3: Decide source per component**
For each required component:
+
- pick `crm` for behavior baseline whenever available
- use `website` only for missing visual variants
- mark `new` only when neither source has viable implementation
@@ -142,6 +149,7 @@ git commit -m "docs: map board scope to crm/website/new provenance sources"
### Task 3: Epic 1 Story 1.1 - Core Contract and Export Baseline
**Files:**
+
- Modify: `src/components/UiButton/**`
- Modify: `src/components/UiInput/**`
- Modify: `src/components/UiCheckbox/**`
@@ -179,6 +187,7 @@ git commit -m "feat: align core control contracts and export baseline"
### Task 4: Epic 1 Stories 1.2 and 1.3 - State Parity + Accessibility Consistency
**Files:**
+
- Modify: `src/components/UiButton/**`
- Modify: `src/components/UiInput/**`
- Modify: `src/components/UiCheckbox/**`
@@ -191,6 +200,7 @@ git commit -m "feat: align core control contracts and export baseline"
**Step 1: Implement missing state parity**
Cover required states:
+
- button: rest, hover, active, disabled
- input: rest, hover, active, disabled, error
- checkbox: rest/checked/disabled combinations
@@ -228,6 +238,7 @@ git commit -m "feat: complete core state parity and accessibility consistency"
### Task 5: Epic 1 Story 1.4 - Quality Gate Closure
**Files:**
+
- Modify: `src/components/UiButton/*.stories.tsx`
- Modify: `src/components/UiInput/*.stories.tsx`
- Modify: `src/components/UiCheckbox/*.stories.tsx`
@@ -270,6 +281,7 @@ git commit -m "test: close epic 1 quality gates with story and test evidence"
**Canonical module locations for Tasks 6-11:** `src/features//components/`
**Files:**
+
- Create: `src/features/selection-input/components/ui-search-input/index.tsx`
- Create: `src/features/selection-input/components/ui-search-input/types.ts`
- Create: `src/features/selection-input/components/ui-search-input/UiSearchInput.stories.tsx`
@@ -322,6 +334,7 @@ git commit -m "feat: deliver epic 2 search/select foundation and multi-select"
### Task 7: Epic 2 Stories 2.3, 2.4, and 2.4A - Calendar Multi-Select + Radio/File Upload
**Files:**
+
- Create: `src/features/selection-input/components/ui-calendar-multi-select/index.tsx`
- Create: `src/features/selection-input/components/ui-calendar-multi-select/types.ts`
- Create: `src/features/selection-input/components/ui-calendar-multi-select/UiCalendarMultiSelect.stories.tsx`
@@ -374,6 +387,7 @@ git commit -m "feat: complete epic 2 calendar, radio, and file-upload workflows"
### Task 7.5: Epic 2 Story 2.5 - Pagination Delivery
**Files:**
+
- Create: `src/features/selection-input/components/ui-pagination/index.tsx`
- Create: `src/features/selection-input/components/ui-pagination/types.ts`
- Create: `src/features/selection-input/components/ui-pagination/UiPagination.stories.tsx`
@@ -409,6 +423,7 @@ git commit -m "feat: deliver epic 2 pagination module"
### Task 8: Epic 2 Story 2.6 - Quality Gate Closure
**Files:**
+
- Modify: `specs/planning-artifacts/board-coverage-checklist.md`
- Create: `specs/implementation-artifacts/epic-2-dod.md`
@@ -436,6 +451,7 @@ git commit -m "docs: close epic 2 quality gates and evidence tracking"
### Task 9: Epic 3 Stories 3.1 to 3.4 - Data Rows and Card Workflows
**Files:**
+
- Create: `src/features/data-cards/components/ui-item-row/**`
- Create: `src/features/data-cards/components/ui-items-list/**`
- Create: `src/features/data-cards/components/ui-task-card/**`
@@ -491,6 +507,7 @@ git commit -m "feat: deliver epic 3 data rows and card workflows"
### Task 10: Epic 3 Stories 3.5 and 3.6 - Micro-Components + Quality Closure
**Files:**
+
- Create: `src/features/micro-components/components/ui-filter-chip/**`
- Create: `src/features/micro-components/components/ui-pin-input/**`
- Create: `src/features/micro-components/components/ui-payment-option-card/**`
@@ -554,6 +571,7 @@ git commit -m "feat: complete epic 3 micro-components and quality closure"
### Task 11: Epic 4 Stories 4.1 to 4.3 - Skeleton Baseline, Primitive, and Composed Variants
**Files:**
+
- Create/Modify: `src/features/skeleton/components/ui-skeleton/**`
- Create/Modify: `src/features/skeleton/components/ui-skeleton-composed/**`
- Create: `tests/unit/ui-skeleton.test.tsx`
@@ -596,6 +614,7 @@ git commit -m "feat: deliver epic 4 skeleton baseline and variants with crm pari
### Task 12: Epic 4 Story 4.4 - Skeleton Quality Gate Closure
**Files:**
+
- Modify: `specs/planning-artifacts/board-coverage-checklist.md`
- Create: `specs/implementation-artifacts/epic-4-dod.md`
@@ -618,6 +637,7 @@ git commit -m "docs: close epic 4 skeleton parity and quality gates"
### Task 13: Epic 5 Stories 5.1 to 5.4 - Adoption Readiness and Governance Closure
**Files:**
+
- Modify: `specs/planning-artifacts/board-coverage-checklist.md`
- Modify: `specs/planning-artifacts/component-provenance.md`
- Modify: `src/components/index.ts` (if missing exports remain)
@@ -639,6 +659,7 @@ No required component missing from entrypoint exports.
**Step 4: Complete governance report**
Summarize:
+
- coverage closure
- provenance compliance
- export integrity
@@ -659,6 +680,7 @@ git commit -m "docs: finalize epic 5 adoption readiness governance"
### Task 14: Final Verification and Release Gate
**Files:**
+
- Modify: `specs/implementation-artifacts/release-readiness-report.md`
- Modify: `package.json`
- Modify: `CHANGELOG.md` (and/or `.changeset/**`, `bun.lock`) during version management
@@ -695,6 +717,7 @@ git commit -m "chore: bump ui-toolkit version to "
**Step 2: Confirm release exit criteria**
Checklist must be true:
+
- board coverage fully closed
- stories exist for new/enhanced components
- unit tests pass
diff --git a/src/assets/fonts/Golos/GolosText-Black.ttf b/src/assets/fonts/Golos/GolosText-Black.ttf
new file mode 100755
index 0000000..0f626e9
Binary files /dev/null and b/src/assets/fonts/Golos/GolosText-Black.ttf differ
diff --git a/src/assets/fonts/Golos/GolosText-Bold.ttf b/src/assets/fonts/Golos/GolosText-Bold.ttf
new file mode 100755
index 0000000..2cdc644
Binary files /dev/null and b/src/assets/fonts/Golos/GolosText-Bold.ttf differ
diff --git a/src/assets/fonts/Golos/GolosText-ExtraBold.ttf b/src/assets/fonts/Golos/GolosText-ExtraBold.ttf
new file mode 100755
index 0000000..10442ce
Binary files /dev/null and b/src/assets/fonts/Golos/GolosText-ExtraBold.ttf differ
diff --git a/src/assets/fonts/Golos/GolosText-Medium.ttf b/src/assets/fonts/Golos/GolosText-Medium.ttf
new file mode 100755
index 0000000..3afd20f
Binary files /dev/null and b/src/assets/fonts/Golos/GolosText-Medium.ttf differ
diff --git a/src/assets/fonts/Golos/GolosText-Regular.ttf b/src/assets/fonts/Golos/GolosText-Regular.ttf
new file mode 100755
index 0000000..3ad2c62
Binary files /dev/null and b/src/assets/fonts/Golos/GolosText-Regular.ttf differ
diff --git a/src/assets/fonts/Golos/GolosText-SemiBold.ttf b/src/assets/fonts/Golos/GolosText-SemiBold.ttf
new file mode 100755
index 0000000..5e1ccad
Binary files /dev/null and b/src/assets/fonts/Golos/GolosText-SemiBold.ttf differ
diff --git a/src/assets/fonts/Inter/Inter-Bold.ttf b/src/assets/fonts/Inter/Inter-Bold.ttf
new file mode 100755
index 0000000..fe23eeb
Binary files /dev/null and b/src/assets/fonts/Inter/Inter-Bold.ttf differ
diff --git a/src/assets/fonts/Inter/Inter-Medium.ttf b/src/assets/fonts/Inter/Inter-Medium.ttf
new file mode 100755
index 0000000..a01f377
Binary files /dev/null and b/src/assets/fonts/Inter/Inter-Medium.ttf differ
diff --git a/src/assets/fonts/Inter/Inter-Regular.ttf b/src/assets/fonts/Inter/Inter-Regular.ttf
new file mode 100755
index 0000000..5e4851f
Binary files /dev/null and b/src/assets/fonts/Inter/Inter-Regular.ttf differ
diff --git a/src/assets/svg/Features/code.svg b/src/assets/svg/Features/code.svg
new file mode 100755
index 0000000..ecedc50
--- /dev/null
+++ b/src/assets/svg/Features/code.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/svg/Features/integrations.svg b/src/assets/svg/Features/integrations.svg
new file mode 100755
index 0000000..e35a1b9
--- /dev/null
+++ b/src/assets/svg/Features/integrations.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/svg/Features/migration.svg b/src/assets/svg/Features/migration.svg
new file mode 100755
index 0000000..37b1e3d
--- /dev/null
+++ b/src/assets/svg/Features/migration.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/svg/Features/services.svg b/src/assets/svg/Features/services.svg
new file mode 100755
index 0000000..19bb185
--- /dev/null
+++ b/src/assets/svg/Features/services.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/svg/Features/settings.svg b/src/assets/svg/Features/settings.svg
new file mode 100755
index 0000000..112fd84
--- /dev/null
+++ b/src/assets/svg/Features/settings.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/svg/Features/templates.svg b/src/assets/svg/Features/templates.svg
new file mode 100755
index 0000000..3925a16
--- /dev/null
+++ b/src/assets/svg/Features/templates.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/svg/Gemstones/diamond.svg b/src/assets/svg/Gemstones/diamond.svg
new file mode 100755
index 0000000..70f155e
--- /dev/null
+++ b/src/assets/svg/Gemstones/diamond.svg
@@ -0,0 +1,17 @@
+
diff --git a/src/assets/svg/Gemstones/ruby.svg b/src/assets/svg/Gemstones/ruby.svg
new file mode 100755
index 0000000..a8b56f8
--- /dev/null
+++ b/src/assets/svg/Gemstones/ruby.svg
@@ -0,0 +1,17 @@
+
diff --git a/src/assets/svg/Gemstones/smallDiamond.svg b/src/assets/svg/Gemstones/smallDiamond.svg
new file mode 100755
index 0000000..f866743
--- /dev/null
+++ b/src/assets/svg/Gemstones/smallDiamond.svg
@@ -0,0 +1,19 @@
+
diff --git a/src/assets/svg/Gemstones/smallRuby.svg b/src/assets/svg/Gemstones/smallRuby.svg
new file mode 100755
index 0000000..64669a3
--- /dev/null
+++ b/src/assets/svg/Gemstones/smallRuby.svg
@@ -0,0 +1,19 @@
+
diff --git a/src/assets/svg/Logo.svg b/src/assets/svg/Logo.svg
new file mode 100755
index 0000000..245233c
--- /dev/null
+++ b/src/assets/svg/Logo.svg
@@ -0,0 +1,49 @@
+
diff --git a/src/assets/svg/TooltipIcons/Drupal.svg b/src/assets/svg/TooltipIcons/Drupal.svg
new file mode 100755
index 0000000..2b3e513
--- /dev/null
+++ b/src/assets/svg/TooltipIcons/Drupal.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/svg/TooltipIcons/Joomla.svg b/src/assets/svg/TooltipIcons/Joomla.svg
new file mode 100755
index 0000000..f24254b
--- /dev/null
+++ b/src/assets/svg/TooltipIcons/Joomla.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/svg/TooltipIcons/Magento.svg b/src/assets/svg/TooltipIcons/Magento.svg
new file mode 100755
index 0000000..674f541
--- /dev/null
+++ b/src/assets/svg/TooltipIcons/Magento.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/svg/TooltipIcons/Shopify.svg b/src/assets/svg/TooltipIcons/Shopify.svg
new file mode 100755
index 0000000..1e8bc0b
--- /dev/null
+++ b/src/assets/svg/TooltipIcons/Shopify.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/svg/TooltipIcons/Wix.svg b/src/assets/svg/TooltipIcons/Wix.svg
new file mode 100755
index 0000000..4a07215
--- /dev/null
+++ b/src/assets/svg/TooltipIcons/Wix.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/svg/TooltipIcons/WooCommerce.svg b/src/assets/svg/TooltipIcons/WooCommerce.svg
new file mode 100755
index 0000000..98c54d7
--- /dev/null
+++ b/src/assets/svg/TooltipIcons/WooCommerce.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/svg/TooltipIcons/WordPress.svg b/src/assets/svg/TooltipIcons/WordPress.svg
new file mode 100755
index 0000000..7b70945
--- /dev/null
+++ b/src/assets/svg/TooltipIcons/WordPress.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/svg/TooltipIcons/Zapier.svg b/src/assets/svg/TooltipIcons/Zapier.svg
new file mode 100755
index 0000000..d8e34cc
--- /dev/null
+++ b/src/assets/svg/TooltipIcons/Zapier.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/svg/check.svg b/src/assets/svg/check.svg
new file mode 100755
index 0000000..3e0b938
--- /dev/null
+++ b/src/assets/svg/check.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/svg/social-icons/facebook.svg b/src/assets/svg/social-icons/facebook.svg
new file mode 100755
index 0000000..2c4d8fc
--- /dev/null
+++ b/src/assets/svg/social-icons/facebook.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/svg/social-icons/github.svg b/src/assets/svg/social-icons/github.svg
new file mode 100755
index 0000000..9412ece
--- /dev/null
+++ b/src/assets/svg/social-icons/github.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/svg/social-icons/instagram.svg b/src/assets/svg/social-icons/instagram.svg
new file mode 100755
index 0000000..1432564
--- /dev/null
+++ b/src/assets/svg/social-icons/instagram.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/svg/social-icons/linked-in.svg b/src/assets/svg/social-icons/linked-in.svg
new file mode 100755
index 0000000..ef0b7f1
--- /dev/null
+++ b/src/assets/svg/social-icons/linked-in.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/AppTheme/index.ts b/src/components/AppTheme/index.ts
new file mode 100755
index 0000000..ce3246c
--- /dev/null
+++ b/src/components/AppTheme/index.ts
@@ -0,0 +1,31 @@
+import { Theme, createTheme } from '@mui/material';
+
+import breakpointsTheme from '../UiBreakpoints';
+import colorTheme from '../UiColorTheme';
+
+const theme: Theme = createTheme({
+ breakpoints: breakpointsTheme.breakpoints,
+ palette: colorTheme.palette,
+ components: {
+ MuiContainer: {
+ styleOverrides: {
+ root: {
+ '@media (min-width: 23.438rem)': {
+ padding: '0 2rem',
+ },
+ '@media (max-width: 26.563rem)': {
+ padding: '0 0.9375rem',
+ },
+ '@media (min-width: 64rem)': {
+ width: '100%',
+ maxWidth: '78.375rem',
+ paddingLeft: '2rem',
+ paddingRight: '2rem',
+ },
+ },
+ },
+ },
+ },
+});
+
+export default theme;
diff --git a/src/components/Types.d.ts b/src/components/Types.d.ts
new file mode 100755
index 0000000..a363a54
--- /dev/null
+++ b/src/components/Types.d.ts
@@ -0,0 +1,88 @@
+export declare module '@mui/material/styles' {
+ interface TypographyVariants {
+ medium16: React.CSSProperties;
+ medium15: ReactDOM.CSSProperties;
+ medium14: React.CSSProperties;
+ regular16: React.CSSProperties;
+ bodyText18: React.CSSProperties;
+ bodyText16: React.CSSProperties;
+ bold22: React.CSSProperties;
+ demi18: React.CSSProperties;
+ button: React.CSSProperties;
+ mobileText: React.CSSProperties;
+ }
+ interface TypographyVariantsOptions {
+ medium16?: React.CSSProperties;
+ medium15?: React.CSSProperties;
+ medium14?: React.CSSProperties;
+ regular16?: React.CSSProperties;
+ bodyText18?: React.CSSProperties;
+ bodyText16?: React.CSSProperties;
+ bold22?: React.CSSProperties;
+ demi18?: React.CSSProperties;
+ button?: React.CSSProperties;
+ mobileText?: React.CSSProperties;
+ }
+}
+export declare module '@mui/material/Typography' {
+ interface TypographyPropsVariantOverrides {
+ medium16: true;
+ medium15: true;
+ medium14: true;
+ regular16: true;
+ bodyText18: true;
+ bodyText16: true;
+ bold22: true;
+ demi18: true;
+ button: true;
+ mobileText: true;
+ }
+}
+
+declare module '@mui/material/styles' {
+ interface Palette {
+ darkPrimary: Palette['primary'];
+ darkSecondary: Palette['secondary'];
+ white: Palette['white'];
+ brandGray: Palette['brandGray'];
+ grey200: Palette['grey200'];
+ grey250: Palette['grey250'];
+ grey300: Palette['grey300'];
+ grey400: Palette['grey400'];
+ grey500: Palette['grey500'];
+ backgroundGrey100: Palette['backgroundGrey100'];
+ backgroundGrey200: Palette['backgroundGrey200'];
+ backgroundGrey300: Palette['backgroundGrey300'];
+ containedButtonHover: Palette['containedButtonHover'];
+ containedButtonActive: Palette['containedButtonActive'];
+ notchDeskBefore: Palette['notchDeskBefore'];
+ notchDeskAfter: Palette['notchDeskAfter'];
+ notchMobileBefore: Palette['notchMobileBefore'];
+ notchMobileAfter: Palette['notchMobileAfter'];
+ textLinkHover?: Palette['textLinkHover'];
+ textLinkActive?: Palette['textLinkActive'];
+ }
+
+ interface PaletteOptions {
+ darkPrimary?: PaletteOptions['primary'];
+ darkSecondary?: PaletteOptions['secondary'];
+ white?: PaletteOptions['white'];
+ brandGray?: PaletteOptions['brandGray'];
+ grey200?: PaletteOptions['grey200'];
+ grey250?: PaletteOptions['grey250'];
+ grey300?: PaletteOptions['grey300'];
+ grey400?: PaletteOptions['grey400'];
+ grey500?: PaletteOptions['grey500'];
+ backgroundGrey100?: PaletteOptions['backgroundGrey100'];
+ backgroundGrey200?: PaletteOptions['backgroundGrey200'];
+ backgroundGrey300?: PaletteOptions['backgroundGrey300'];
+ containedButtonHover?: PaletteOptions['containedButtonHover'];
+ containedButtonActive?: PaletteOptions['containedButtonActive'];
+ notchDeskBefore?: PaletteOptions['notchDeskBefore'];
+ notchDeskAfter?: PaletteOptions['notchDeskAfter'];
+ notchMobileBefore?: PaletteOptions['notchMobileBefore'];
+ notchMobileAfter?: PaletteOptions['notchMobileAfter'];
+ textLinkHover?: PaletteOptions['textLinkHover'];
+ textLinkActive?: PaletteOptions['textLinkActive'];
+ }
+}
diff --git a/src/components/UiBreakpoints/index.ts b/src/components/UiBreakpoints/index.ts
new file mode 100755
index 0000000..2180efd
--- /dev/null
+++ b/src/components/UiBreakpoints/index.ts
@@ -0,0 +1,15 @@
+import { Theme, createTheme } from '@mui/material';
+
+const breakpointsTheme: Theme = createTheme({
+ breakpoints: {
+ values: {
+ xs: 375,
+ sm: 640,
+ md: 768,
+ lg: 1024,
+ xl: 1440,
+ },
+ },
+});
+
+export default breakpointsTheme;
diff --git a/src/components/UiButton/Button.stories.tsx b/src/components/UiButton/Button.stories.tsx
new file mode 100755
index 0000000..cc023d0
--- /dev/null
+++ b/src/components/UiButton/Button.stories.tsx
@@ -0,0 +1,68 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { t } from 'i18next';
+
+import UiButton from './index';
+
+const meta: Meta = {
+ title: 'UiComponents/UiButton',
+ component: UiButton,
+ tags: ['autodocs'],
+ argTypes: {
+ variant: {
+ type: 'string',
+ description: 'Variant of the button',
+ options: ['contained', 'outlined'],
+ control: { type: 'radio' },
+ },
+ size: {
+ type: 'string',
+ description: 'Size of the button',
+ options: ['small', 'medium'],
+ control: { type: 'radio' },
+ },
+ children: {
+ type: 'string',
+ name: 'label',
+ description: 'Text of the button',
+ },
+ type: {
+ type: 'string',
+ description: 'Type of the button',
+ options: ['button', 'submit'],
+ control: { type: 'radio' },
+ },
+ disabled: {
+ type: 'boolean',
+ description: 'Whether the button is disabled',
+ control: { type: 'boolean' },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Contained: Story = {
+ args: {
+ children: t('header.actions.try_it_out'),
+ variant: 'contained',
+ size: 'small',
+ },
+};
+export const Outlined: Story = {
+ args: {
+ children: t('header.actions.log_in'),
+ variant: 'outlined',
+ size: 'small',
+ },
+};
+
+export const SocialButton: Story = {
+ args: {
+ children: t('Social Button'),
+ variant: 'outlined',
+ size: 'medium',
+ name: 'socialButton',
+ },
+};
diff --git a/src/components/UiButton/index.tsx b/src/components/UiButton/index.tsx
new file mode 100755
index 0000000..a2332c7
--- /dev/null
+++ b/src/components/UiButton/index.tsx
@@ -0,0 +1,36 @@
+import { Button, ThemeProvider } from '@mui/material';
+import React from 'react';
+
+import { theme } from './theme';
+import { UiButtonProps } from './types';
+
+function UiButton({
+ variant,
+ size,
+ disabled,
+ fullWidth,
+ onClick,
+ type,
+ children,
+ sx,
+ name,
+}: UiButtonProps): React.ReactElement {
+ return (
+
+
+
+ );
+}
+
+export default UiButton;
diff --git a/src/components/UiButton/theme.ts b/src/components/UiButton/theme.ts
new file mode 100755
index 0000000..6c2532a
--- /dev/null
+++ b/src/components/UiButton/theme.ts
@@ -0,0 +1,132 @@
+import { Interpolation, Theme, createTheme } from '@mui/material';
+
+import breakpointsTheme from '../UiBreakpoints';
+import colorTheme from '../UiColorTheme';
+
+export const containedStyles: Interpolation<{ theme: Theme }> = {
+ textTransform: 'none',
+ textDecoration: 'none',
+ fontSize: '0.938rem',
+ fontFamily: 'Golos Text',
+ fontWeight: '500',
+ lineHeight: '1.125',
+ letterSpacing: '0',
+ backgroundColor: colorTheme.palette.primary.main,
+ borderRadius: '3.563rem',
+ '&:hover': {
+ backgroundColor: colorTheme.palette.containedButtonHover.main,
+ },
+ '&:active': {
+ backgroundColor: colorTheme.palette.containedButtonActive.main,
+ },
+ '&:disabled': {
+ backgroundColor: colorTheme.palette.brandGray.main,
+ color: colorTheme.palette.white.main,
+ },
+};
+
+export const outlinedStyles: Interpolation<{ theme: Theme }> = {
+ textTransform: 'none',
+ textDecoration: 'none',
+ fontSize: '0.938rem',
+ fontFamily: 'Golos Text',
+ fontWeight: '500',
+ lineHeight: '1.125',
+ letterSpacing: '0',
+ color: colorTheme.palette.darkSecondary.main,
+ backgroundColor: colorTheme.palette.white.main,
+ border: `1px solid ${colorTheme.palette.grey300.main}`,
+ borderRadius: '3.563rem',
+ '&:hover': {
+ backgroundColor: colorTheme.palette.grey500.main,
+ border: '1px solid rgba(0,0,0,0)',
+ },
+ '&:active': {
+ border: `1px solid ${colorTheme.palette.grey500.main}`,
+ },
+ '&:disabled': {
+ backgroundColor: colorTheme.palette.brandGray.main,
+ color: colorTheme.palette.white.main,
+ border: 'none',
+ },
+};
+
+export const theme: Theme = createTheme({
+ components: {
+ MuiButton: {
+ variants: [
+ {
+ props: { variant: 'contained', size: 'small' },
+ style: { ...containedStyles, padding: '1rem 1.5rem' },
+ },
+ {
+ props: { variant: 'contained', size: 'medium' },
+ style: {
+ ...containedStyles,
+ alignSelf: 'center',
+ fontWeight: '600',
+ fontSize: '1.125rem',
+ padding: '1.25rem 2rem',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ fontSize: '0.9375rem',
+ fontWeight: '400',
+ lineHeight: '1.125rem',
+ padding: '1rem 1.438rem',
+ },
+ },
+ },
+ {
+ props: { variant: 'outlined', size: 'small' },
+ style: { ...outlinedStyles, padding: '1rem 1.438rem' },
+ },
+ {
+ props: { variant: 'outlined', size: 'medium' },
+ style: {
+ ...outlinedStyles,
+ fontWeight: '600',
+ fontSize: '1.125rem',
+ padding: '1.25rem 2rem',
+ },
+ },
+ {
+ props: {
+ name: 'socialButton',
+ variant: 'outlined',
+ size: 'medium',
+ },
+ style: {
+ fontFamily: 'Golos Text',
+ textTransform: 'none',
+ borderRadius: '0.75rem',
+ padding: '1.125rem',
+ gap: '0.563rem',
+ border: `1px solid ${colorTheme.palette.brandGray.main}`,
+ background: colorTheme.palette.white.main,
+ color: colorTheme.palette.grey200.main,
+ '&:hover': {
+ background: colorTheme.palette.white.main,
+ boxShadow: '0px 4px 7px 0px rgba(116, 134, 151, 0.17)',
+ border: `1px solid ${colorTheme.palette.brandGray.main}`,
+ },
+ '&:active': {
+ background: colorTheme.palette.white.main,
+ boxShadow: '0px 4px 7px 0px rgba(71, 85, 99, 0.21)',
+ border: `1px solid ${colorTheme.palette.grey300.main}`,
+ },
+ '&:disabled': {
+ background: colorTheme.palette.brandGray.main,
+ boxShadoiw: 'none',
+ border: 'none',
+ img: {
+ opacity: '0.2',
+ },
+ div: {
+ color: colorTheme.palette.white.main,
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+});
diff --git a/src/components/UiButton/types.ts b/src/components/UiButton/types.ts
new file mode 100755
index 0000000..f01ae6c
--- /dev/null
+++ b/src/components/UiButton/types.ts
@@ -0,0 +1,18 @@
+import { SxProps, Theme } from '@mui/material';
+
+/**
+ * Shared contract support:
+ * - supported: disabled, size, variant, sx
+ * - exceptions: value, onChange, error
+ */
+export interface UiButtonProps {
+ variant?: 'outlined' | 'contained';
+ size?: 'small' | 'medium' | 'large';
+ disabled?: boolean;
+ fullWidth?: boolean;
+ onClick?: () => void;
+ type?: 'button' | 'submit' | 'reset';
+ children?: React.ReactNode | string;
+ sx?: SxProps;
+ name?: string;
+}
diff --git a/src/components/UiCardItem/CardContent.tsx b/src/components/UiCardItem/CardContent.tsx
new file mode 100755
index 0000000..1a35c45
--- /dev/null
+++ b/src/components/UiCardItem/CardContent.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { Trans } from 'react-i18next';
+
+import UiTooltip from '../UiTooltip';
+import UiTypography from '../UiTypography';
+
+import ServicesHoverCard from './ServicesHoverCard';
+import styles from './styles';
+import { CardContentProps } from './types';
+
+function CardContent({ item, isSmallCard }: CardContentProps): React.ReactElement {
+ return (
+ <>
+
+
+
+
+ {isSmallCard ? (
+
+ Integrate
+ }
+ >
+ services
+
+
+ ) : (
+
+ )}
+
+ >
+ );
+}
+export default CardContent;
diff --git a/src/components/UiCardItem/CardItem.stories.tsx b/src/components/UiCardItem/CardItem.stories.tsx
new file mode 100755
index 0000000..7fdc059
--- /dev/null
+++ b/src/components/UiCardItem/CardItem.stories.tsx
@@ -0,0 +1,28 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import UiCardList from '../UiCardList';
+import type { CardList } from '../UiCardList/types';
+
+import { LARGE_CARD_ITEM, SMALL_CARD_ITEM } from './constants';
+
+const meta: Meta = {
+ title: 'UiComponents/UiCardItem',
+ component: UiCardList,
+ tags: ['autodocs'],
+};
+
+export default meta;
+type Story = StoryObj & {
+ args: CardList;
+};
+
+export const CardItemLarge: Story = {
+ args: {
+ cardList: [LARGE_CARD_ITEM],
+ },
+};
+export const CardItemSmall: Story = {
+ args: {
+ cardList: [SMALL_CARD_ITEM],
+ },
+};
diff --git a/src/components/UiCardItem/ServicesHoverCard/ImageItem/ImageItem.tsx b/src/components/UiCardItem/ServicesHoverCard/ImageItem/ImageItem.tsx
new file mode 100755
index 0000000..ea896cc
--- /dev/null
+++ b/src/components/UiCardItem/ServicesHoverCard/ImageItem/ImageItem.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+import { ImageList } from '../../types';
+
+function ImageItem({ item }: { item: ImageList }): React.ReactElement {
+ return
;
+}
+
+export default ImageItem;
diff --git a/src/components/UiCardItem/ServicesHoverCard/ImageItem/index.ts b/src/components/UiCardItem/ServicesHoverCard/ImageItem/index.ts
new file mode 100755
index 0000000..886258c
--- /dev/null
+++ b/src/components/UiCardItem/ServicesHoverCard/ImageItem/index.ts
@@ -0,0 +1,3 @@
+import ImageItem from './ImageItem';
+
+export default ImageItem;
diff --git a/src/components/UiCardItem/ServicesHoverCard/ServicesHoverCard.tsx b/src/components/UiCardItem/ServicesHoverCard/ServicesHoverCard.tsx
new file mode 100755
index 0000000..8b15725
--- /dev/null
+++ b/src/components/UiCardItem/ServicesHoverCard/ServicesHoverCard.tsx
@@ -0,0 +1,32 @@
+import { Box } from '@mui/material';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { UiTypography } from '@/components/';
+
+import { imageList } from '../constants';
+
+import ImageItem from './ImageItem';
+import styles from './styles';
+
+function ServicesHoverCard(): React.ReactElement {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t('unlimited_possibilities.service_text.title')}
+
+
+ {t('unlimited_possibilities.service_text.text')}
+
+
+ {imageList.map(item => (
+
+ ))}
+
+
+ );
+}
+
+export default ServicesHoverCard;
diff --git a/src/components/UiCardItem/ServicesHoverCard/index.ts b/src/components/UiCardItem/ServicesHoverCard/index.ts
new file mode 100755
index 0000000..48f0bf3
--- /dev/null
+++ b/src/components/UiCardItem/ServicesHoverCard/index.ts
@@ -0,0 +1,3 @@
+import ServicesHoverCard from './ServicesHoverCard';
+
+export default ServicesHoverCard;
diff --git a/src/components/UiCardItem/ServicesHoverCard/styles.ts b/src/components/UiCardItem/ServicesHoverCard/styles.ts
new file mode 100755
index 0000000..78f645b
--- /dev/null
+++ b/src/components/UiCardItem/ServicesHoverCard/styles.ts
@@ -0,0 +1,17 @@
+import breakpointsTheme from '@/components/UiBreakpoints';
+
+export default {
+ text: {
+ pt: '0.25rem',
+ pb: '1.375rem',
+ },
+
+ listWrapper: {
+ display: 'grid',
+ gridTemplateColumns: 'repeat(4, 1fr)',
+ gap: '1.875rem',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ gap: '1rem',
+ },
+ },
+};
diff --git a/src/components/UiCardItem/constants.ts b/src/components/UiCardItem/constants.ts
new file mode 100755
index 0000000..d604414
--- /dev/null
+++ b/src/components/UiCardItem/constants.ts
@@ -0,0 +1,43 @@
+import WhyUsTemplatesIcon from '@/assets/svg/Features/templates.svg';
+import Ruby from '@/assets/svg/Gemstones/ruby.svg';
+import Drupal from '@/assets/svg/TooltipIcons/Drupal.svg';
+import Joomla from '@/assets/svg/TooltipIcons/Joomla.svg';
+import Magento from '@/assets/svg/TooltipIcons/Magento.svg';
+import Shopify from '@/assets/svg/TooltipIcons/Shopify.svg';
+import Wix from '@/assets/svg/TooltipIcons/Wix.svg';
+import WooCommerce from '@/assets/svg/TooltipIcons/WooCommerce.svg';
+import WordPress from '@/assets/svg/TooltipIcons/WordPress.svg';
+import Zapier from '@/assets/svg/TooltipIcons/Zapier.svg';
+
+import { CardItem, ImageList } from './types';
+
+export const SMALL_CARD_TEXT: string = 'smallCard';
+
+export const SMALL_CARD_ITEM: CardItem = {
+ type: 'smallCard',
+ id: 'item_1',
+ imageSrc: Ruby,
+ text: 'unlimited_possibilities.cards_texts.text_for_cases',
+ title: 'unlimited_possibilities.cards_headings.heading_public_api',
+ alt: 'unlimited_possibilities.card_image_titles.title_for_first',
+};
+
+export const LARGE_CARD_ITEM: CardItem = {
+ type: 'largeCard',
+ id: 'card-item-3',
+ imageSrc: WhyUsTemplatesIcon,
+ title: 'why_us.headers.header_ready_templates',
+ text: 'why_us.texts.text_you_have_store',
+ alt: 'why_us.alt_image.alt_ready_templates',
+};
+
+export const imageList: ImageList[] = [
+ { image: Wix, alt: 'Wix' },
+ { image: WordPress, alt: 'WordPress' },
+ { image: Zapier, alt: 'Zapier' },
+ { image: Shopify, alt: 'Shopify' },
+ { image: Magento, alt: 'Magento' },
+ { image: Joomla, alt: 'Joomla' },
+ { image: Drupal, alt: 'Drupal' },
+ { image: WooCommerce, alt: 'WooCommerce' },
+];
diff --git a/src/components/UiCardItem/index.tsx b/src/components/UiCardItem/index.tsx
new file mode 100755
index 0000000..8a45a2f
--- /dev/null
+++ b/src/components/UiCardItem/index.tsx
@@ -0,0 +1,31 @@
+import { Stack } from '@mui/material';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import UiImage from '../UiImage';
+
+import CardContent from './CardContent';
+import { SMALL_CARD_TEXT } from './constants';
+import styles from './styles';
+import { UiCardItemProps } from './types';
+
+function UiCardItem({ item }: UiCardItemProps): React.ReactElement {
+ const { t } = useTranslation();
+
+ const isSmallCard: boolean = item.type === SMALL_CARD_TEXT;
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+export default UiCardItem;
diff --git a/src/components/UiCardItem/styles.ts b/src/components/UiCardItem/styles.ts
new file mode 100755
index 0000000..7f6e8f4
--- /dev/null
+++ b/src/components/UiCardItem/styles.ts
@@ -0,0 +1,115 @@
+import breakpointsTheme from '../UiBreakpoints';
+import colorTheme from '../UiColorTheme';
+
+export default {
+ smallWrapper: {
+ padding: '2.5rem 2rem 2.5rem 1.563rem',
+ borderRadius: '0.75rem',
+ border: `1px solid ${colorTheme.palette.grey500.main}`,
+ maxHeight: '20.75rem',
+ alignItems: 'start',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.xl - 1}px)`]: {
+ padding: '2.125rem 1.875rem 2.125rem 1.563rem',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: '2.813rem',
+ maxHeight: '11.375rem',
+ },
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ flexDirection: 'column',
+ padding: '1rem 1.125rem 0rem 1rem',
+ gap: '1rem',
+ alignItems: 'start',
+ minHeight: '15.125rem',
+ },
+ },
+
+ smallTitle: {
+ pt: '2rem',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.lg}px)`]: {
+ pt: '0',
+ },
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ fontSize: '1.125rem',
+ fontWeight: '600',
+ },
+ },
+
+ smallText: {
+ mt: '0.625rem',
+ zIndex: 2,
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.lg}px)`]: {
+ a: {
+ textDecoration: 'none',
+ fontWeight: '400',
+ color: colorTheme.palette.darkPrimary.main,
+ },
+ },
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ fontSize: '0.9375rem',
+ fontWeight: '400',
+ lineHeight: '1.563rem',
+ mt: '0.75rem',
+ },
+ },
+
+ smallImage: {
+ width: '5rem',
+ height: '5rem',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ width: '3.125rem',
+ height: '3.125rem',
+ },
+ },
+
+ hoveredCard: {
+ cursor: 'pointer',
+ color: colorTheme.palette.primary.main,
+ textDecoration: 'underline',
+ fontWeight: '700',
+ },
+
+ largeWrapper: {
+ p: '1.5rem',
+ borderRadius: '0.75rem',
+ border: `1px solid ${colorTheme.palette.grey500.main}`,
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ padding: '1rem 1.125rem 0 1rem',
+ borderRadius: '0.75rem',
+ border: `1px solid ${colorTheme.palette.grey500.main}`,
+ minHeight: '16.438rem',
+ },
+ ':hover': {
+ boxShadow: '0px 8px 27px 0px rgba(49, 59, 67, 0.14)',
+ border: `1px solid ${colorTheme.palette.grey400.main}`,
+ },
+ },
+
+ largeTitle: {
+ pt: '1rem',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.lg}px)`]: {
+ fontSize: '1.375rem',
+ },
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ pt: '1rem',
+ fontSize: '1.125rem',
+ },
+ },
+
+ largeText: {
+ mt: '0.75rem',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ fontSize: '0.9375rem',
+ lineHeight: '1.563rem',
+ },
+ },
+
+ largeImage: {
+ width: '4.375rem',
+ height: '4.375rem',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ width: '3.125rem',
+ height: '3.125rem',
+ },
+ },
+};
diff --git a/src/components/UiCardItem/types.ts b/src/components/UiCardItem/types.ts
new file mode 100755
index 0000000..776ce76
--- /dev/null
+++ b/src/components/UiCardItem/types.ts
@@ -0,0 +1,22 @@
+export type CardItem = {
+ type: string;
+ id: string;
+ imageSrc: string;
+ title: string;
+ text: string;
+ alt: string;
+};
+
+export interface UiCardItemProps {
+ item: CardItem;
+}
+
+export interface CardContentProps {
+ item: CardItem;
+ isSmallCard: boolean;
+}
+
+export interface ImageList {
+ image: string;
+ alt: string;
+}
diff --git a/src/components/UiCardList/CardGrid.tsx b/src/components/UiCardList/CardGrid.tsx
new file mode 100755
index 0000000..383f9a0
--- /dev/null
+++ b/src/components/UiCardList/CardGrid.tsx
@@ -0,0 +1,21 @@
+import { Grid } from '@mui/material';
+import React, { CSSProperties } from 'react';
+
+import UiCardItem from '../UiCardItem';
+
+import styles from './styles';
+import { CardList } from './types';
+
+function CardGrid({ cardList }: CardList): React.ReactElement {
+ const grid: CSSProperties =
+ cardList[0].type === 'smallCard' ? styles.smallGrid : styles.largeGrid;
+
+ return (
+
+ {cardList.map(item => (
+
+ ))}
+
+ );
+}
+export default CardGrid;
diff --git a/src/components/UiCardList/CardList.stories.tsx b/src/components/UiCardList/CardList.stories.tsx
new file mode 100755
index 0000000..c67062e
--- /dev/null
+++ b/src/components/UiCardList/CardList.stories.tsx
@@ -0,0 +1,33 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { LARGE_CARDLIST_ARRAY, SMALL_CARDLIST_ARRAY } from './constants';
+
+import UiCardList from './index';
+
+const meta: Meta = {
+ title: 'UiComponents/UiCardList',
+ component: UiCardList,
+ tags: ['autodocs'],
+ argTypes: {
+ cardList: {
+ control: 'object',
+ description: 'List of card items',
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const CardListLarge: Story = {
+ args: {
+ cardList: LARGE_CARDLIST_ARRAY,
+ },
+};
+
+export const CardListSmall: Story = {
+ args: {
+ cardList: SMALL_CARDLIST_ARRAY,
+ },
+};
diff --git a/src/components/UiCardList/CardSwiper.tsx b/src/components/UiCardList/CardSwiper.tsx
new file mode 100755
index 0000000..9a08d3d
--- /dev/null
+++ b/src/components/UiCardList/CardSwiper.tsx
@@ -0,0 +1,76 @@
+import { Grid } from '@mui/material';
+import React, { CSSProperties, useEffect, useRef } from 'react';
+import { Pagination } from 'swiper/modules';
+import { Swiper, SwiperSlide } from 'swiper/react';
+
+import UiCardItem from '../UiCardItem';
+
+import styles from './styles';
+import 'swiper/css';
+import 'swiper/css/pagination';
+import { CardList } from './types';
+
+function CardSwiper({ cardList }: CardList): React.ReactElement {
+ const swiperRef: React.RefObject = useRef(null);
+
+ useEffect(() => {
+ const target: HTMLElement | null = document.querySelector('body');
+
+ function isToolTip(node: Element): boolean {
+ return node.role === 'tooltip' && node.classList.contains('base-Popper-root');
+ }
+
+ const config: MutationObserverInit = {
+ childList: true,
+ };
+
+ const observer: MutationObserver = new MutationObserver((mutationsList: MutationRecord[]) => {
+ mutationsList.forEach((mutation: MutationRecord): void => {
+ if (mutation.type === 'childList') {
+ mutation.addedNodes.forEach((node: Node): void => {
+ if (node instanceof Element && isToolTip(node)) {
+ swiperRef.current!.style.pointerEvents = 'none';
+ }
+ });
+ mutation.removedNodes.forEach((node: Node): void => {
+ if (node instanceof Element && isToolTip(node)) {
+ swiperRef.current!.style.pointerEvents = 'auto';
+ }
+ });
+ }
+ });
+ });
+
+ if (target) {
+ observer.observe(target, config);
+ }
+
+ return (): void => observer.disconnect();
+ }, []);
+
+ const gridMobile: CSSProperties =
+ cardList[0].type === 'smallCard' ? styles.gridSmallMobile : styles.gridLargeMobile;
+
+ return (
+ }>
+
+ {cardList.map(item => (
+
+
+
+ ))}
+
+
+ );
+}
+
+export default CardSwiper;
diff --git a/src/components/UiCardList/constants.ts b/src/components/UiCardList/constants.ts
new file mode 100755
index 0000000..65b6034
--- /dev/null
+++ b/src/components/UiCardList/constants.ts
@@ -0,0 +1,99 @@
+import WhyUsCodeIcon from '@/assets/svg/Features/code.svg';
+import WhyUsIntegrationsIcon from '@/assets/svg/Features/integrations.svg';
+import WhyUsMigrationIcon from '@/assets/svg/Features/migration.svg';
+import WhyUsServicesIcon from '@/assets/svg/Features/services.svg';
+import WhyUsSettingsIcon from '@/assets/svg/Features/settings.svg';
+import WhyUsTemplatesIcon from '@/assets/svg/Features/templates.svg';
+import Diamond from '@/assets/svg/Gemstones/diamond.svg';
+import Ruby from '@/assets/svg/Gemstones/ruby.svg';
+import SmallDiamond from '@/assets/svg/Gemstones/smallDiamond.svg';
+import SmallRuby from '@/assets/svg/Gemstones/smallRuby.svg';
+
+import { CardItem } from './types';
+
+export const LARGE_CARDLIST_ARRAY: CardItem[] = [
+ {
+ type: 'largeCard',
+ id: 'card-item-1',
+ imageSrc: WhyUsCodeIcon,
+ title: 'why_us.headers.header_open_source',
+ text: 'why_us.texts.text_open_source',
+ alt: 'why_us.alt_image.alt_open_source',
+ },
+ {
+ type: 'largeCard',
+ id: 'card-item-2',
+ imageSrc: WhyUsSettingsIcon,
+ title: 'why_us.headers.header_ease_of_setup',
+ text: 'why_us.texts.text_configure_system',
+ alt: 'why_us.alt_image.alt_ease_of_setup',
+ },
+
+ {
+ type: 'largeCard',
+ id: 'card-item-3',
+ imageSrc: WhyUsTemplatesIcon,
+ title: 'why_us.headers.header_ready_templates',
+ text: 'why_us.texts.text_you_have_store',
+ alt: 'why_us.alt_image.alt_ready_templates',
+ },
+ {
+ type: 'largeCard',
+ id: 'card-item-4',
+ imageSrc: WhyUsServicesIcon,
+ title: 'why_us.headers.header_ideal_for_services',
+ text: 'why_us.texts.text_we_know_specific_needs',
+ alt: 'why_us.alt_image.alt_ideal_for_services',
+ },
+ {
+ type: 'largeCard',
+ id: 'card-item-5',
+ imageSrc: WhyUsIntegrationsIcon,
+ title: 'why_us.headers.header_all_required_integrations',
+ text: 'why_us.texts.text_connect_your_cms',
+ alt: 'why_us.alt_image.alt_all_required_integrations',
+ },
+ {
+ type: 'largeCard',
+ id: 'card-item-6',
+ imageSrc: WhyUsMigrationIcon,
+ title: 'why_us.headers.header_bonus',
+ text: 'why_us.texts.text_switch_to_vilna',
+ alt: 'why_us.alt_image.alt_bonus',
+ },
+];
+
+export const SMALL_CARDLIST_ARRAY: CardItem[] = [
+ {
+ type: 'smallCard',
+ id: 'item_1',
+ imageSrc: Ruby,
+ text: 'unlimited_possibilities.cards_texts.text_for_cases',
+ title: 'unlimited_possibilities.cards_headings.heading_public_api',
+ alt: 'unlimited_possibilities.card_image_titles.title_for_first',
+ },
+ {
+ type: 'smallCard',
+ id: 'item_2',
+ imageSrc: SmallDiamond,
+ text: 'unlimited_possibilities.cards_texts.text_integrate',
+ title: 'unlimited_possibilities.cards_headings.heading_ready_plugins',
+ alt: 'unlimited_possibilities.card_image_titles.title_for_second',
+ },
+ {
+ type: 'smallCard',
+ id: 'item_3',
+ imageSrc: SmallRuby,
+ text: 'unlimited_possibilities.cards_texts.text_get_data',
+ title: 'unlimited_possibilities.cards_headings.heading_system',
+ alt: 'unlimited_possibilities.card_image_titles.title_for_third',
+ },
+ {
+ type: 'smallCard',
+ id: 'item_4',
+ imageSrc: Diamond,
+ text: 'unlimited_possibilities.cards_texts.text_for_custom',
+ title: 'unlimited_possibilities.cards_headings.heading_libraries',
+ alt: 'unlimited_possibilities.card_image_titles.title_for_fourth',
+ },
+];
diff --git a/src/components/UiCardList/index.tsx b/src/components/UiCardList/index.tsx
new file mode 100755
index 0000000..2f6c42d
--- /dev/null
+++ b/src/components/UiCardList/index.tsx
@@ -0,0 +1,22 @@
+import { Box } from '@mui/material';
+import React from 'react';
+
+import CardGrid from './CardGrid';
+import CardSwiper from './CardSwiper';
+import styles from './styles';
+import { CardList } from './types';
+
+function UiCardList({ cardList }: CardList): React.ReactElement {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+}
+
+export default UiCardList;
diff --git a/src/components/UiCardList/styles.ts b/src/components/UiCardList/styles.ts
new file mode 100755
index 0000000..cfe3bae
--- /dev/null
+++ b/src/components/UiCardList/styles.ts
@@ -0,0 +1,84 @@
+import breakpointsTheme from '../UiBreakpoints';
+
+export default {
+ smallGrid: {
+ display: 'grid',
+ marginTop: '2rem',
+ gap: '0.75rem',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ display: 'none',
+ },
+ [`@media (min-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ gridTemplateColumns: 'repeat(2, 1fr)',
+ },
+ [`@media (min-width: ${breakpointsTheme.breakpoints.values.xl}px)`]: {
+ gridTemplateColumns: 'repeat(4, 289px)',
+ },
+ },
+
+ gridSmallMobile: {
+ display: 'none',
+ '& .swiper .swiper-pagination .swiper-pagination-bullet': {
+ marginRight: '1.25rem',
+ },
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ minHeight: '18.5rem',
+ display: 'grid',
+ marginTop: '1.5rem',
+ gap: '0.75rem',
+ },
+ },
+
+ largeGrid: {
+ display: 'grid',
+ marginTop: '2.5rem',
+ gap: '0.813rem',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.lg}px)`]: {
+ gap: '0.75rem',
+ marginTop: '2rem',
+ },
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ display: 'none',
+ },
+ [`@media (min-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ gridTemplateRows: 'repeat(2, 1fr)',
+ gridTemplateColumns: 'repeat(2, 1fr)',
+ },
+ [`@media (min-width: ${breakpointsTheme.breakpoints.values.lg}px)`]: {
+ gridTemplateRows: 'repeat(2, minmax(23.75rem, auto))',
+ gridTemplateColumns: 'repeat(3, minmax(15.625rem, 24.3125rem))',
+ },
+ [`@media (min-width: ${breakpointsTheme.breakpoints.values.xl}px)`]: {
+ gridTemplateRows: 'repeat(2, minmax(21.375rem, auto))',
+ },
+ },
+
+ gridLargeMobile: {
+ '& .swiper .swiper-pagination .swiper-pagination-bullet': {
+ marginRight: '1.25rem',
+ },
+ '& .swiper .swiper-pagination': {
+ marginLeft: '0.5rem',
+ },
+ display: 'none',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ display: 'grid',
+ marginTop: '1.5rem',
+ minHeight: '19.313rem',
+ },
+ },
+
+ gridContainerLargeScreen: {
+ display: 'none',
+ [`@media (min-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ display: 'block',
+ },
+ },
+
+ swiperContainerSmallScreen: {
+ display: 'none',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ display: 'block',
+ },
+ },
+};
diff --git a/src/components/UiCardList/types.ts b/src/components/UiCardList/types.ts
new file mode 100755
index 0000000..243b3a4
--- /dev/null
+++ b/src/components/UiCardList/types.ts
@@ -0,0 +1,12 @@
+export type CardItem = {
+ type: string;
+ id: string;
+ imageSrc: string;
+ title: string;
+ text: string;
+ alt: string;
+};
+
+export interface CardList {
+ cardList: CardItem[];
+}
diff --git a/src/components/UiCheckbox/Checkbox.stories.tsx b/src/components/UiCheckbox/Checkbox.stories.tsx
new file mode 100755
index 0000000..8a6d750
--- /dev/null
+++ b/src/components/UiCheckbox/Checkbox.stories.tsx
@@ -0,0 +1,41 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { t } from 'i18next';
+
+import UiCheckbox from './index';
+
+const meta: Meta = {
+ title: 'UiComponents/UiCheckbox',
+ component: UiCheckbox,
+ tags: ['autodocs'],
+ argTypes: {
+ disabled: {
+ type: 'boolean',
+ description: 'Whether the checkbox is disabled',
+ control: { type: 'boolean' },
+ },
+ label: {
+ type: 'string',
+ description: 'Label for the checkbox',
+ },
+ onChange: {
+ type: 'function',
+ description: 'Callback function when the checkbox is changed',
+ },
+ error: {
+ type: 'boolean',
+ description: 'Whether the checkbox is in error state',
+ control: { type: 'boolean' },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Checkbox: Story = {
+ args: {
+ error: false,
+ label: t('Checkbox label text'),
+ },
+};
diff --git a/src/components/UiCheckbox/index.tsx b/src/components/UiCheckbox/index.tsx
new file mode 100755
index 0000000..af53575
--- /dev/null
+++ b/src/components/UiCheckbox/index.tsx
@@ -0,0 +1,25 @@
+import { Box, FormControlLabel } from '@mui/material';
+import React from 'react';
+
+import styles from './styles';
+import { UiCheckboxProps } from './types';
+
+function UiCheckbox({ label, sx, onChange, error, disabled }: UiCheckboxProps): React.ReactElement {
+ return (
+
+
+
+ }
+ label={label}
+ />
+ );
+}
+
+export default UiCheckbox;
diff --git a/src/components/UiCheckbox/styles.ts b/src/components/UiCheckbox/styles.ts
new file mode 100755
index 0000000..af7402a
--- /dev/null
+++ b/src/components/UiCheckbox/styles.ts
@@ -0,0 +1,61 @@
+import Check from '@/assets/svg/check.svg';
+
+import colorTheme from '../UiColorTheme';
+
+const checkIconUrl: string = typeof Check === 'string' ? Check : Check.src;
+
+export default {
+ checkboxWrapper: {
+ display: 'grid',
+ marginRight: '0.813rem',
+ padding: '0',
+ input: {
+ WebkitAppearance: 'none',
+ appearance: 'none',
+ width: '1.5rem',
+ height: '1.5rem',
+ borderRadius: '0.5rem',
+ border: `1px solid ${colorTheme.palette.grey400.main}`,
+ background: colorTheme.palette.white.main,
+ '&:hover': {
+ cursor: 'pointer',
+ border: `1px solid ${colorTheme.palette.primary.main}`,
+ },
+ '&:checked': {
+ backgroundColor: colorTheme.palette.primary.main,
+ border: 'none',
+ backgroundImage: `url(${checkIconUrl})`,
+ backgroundPosition: 'center center',
+ backgroundRepeat: 'no-repeat',
+ },
+ '&:disabled': {
+ cursor: 'default',
+ backgroundColor: colorTheme.palette.grey500.main,
+ border: 'none',
+ },
+ },
+ },
+
+ checkboxWrapperError: {
+ display: 'grid',
+ marginRight: '0.813rem',
+ padding: '0',
+ input: {
+ cursor: 'pointer',
+ WebkitAppearance: 'none',
+ appearance: 'none',
+ width: '1.5rem',
+ height: '1.5rem',
+ borderRadius: '0.5rem',
+ border: `1px solid ${colorTheme.palette.error.main}`,
+ background: colorTheme.palette.white.main,
+ '&:checked': {
+ backgroundColor: colorTheme.palette.primary.main,
+ border: 'none',
+ backgroundImage: `url(${checkIconUrl})`,
+ backgroundPosition: 'center center',
+ backgroundRepeat: 'no-repeat',
+ },
+ },
+ },
+};
diff --git a/src/components/UiCheckbox/types.ts b/src/components/UiCheckbox/types.ts
new file mode 100755
index 0000000..284fa97
--- /dev/null
+++ b/src/components/UiCheckbox/types.ts
@@ -0,0 +1,14 @@
+import { SxProps, Theme } from '@mui/material';
+
+/**
+ * Shared contract support:
+ * - supported: onChange, disabled, error, sx
+ * - exceptions: value, size, variant
+ */
+export interface UiCheckboxProps {
+ onChange: (event: React.ChangeEvent) => void;
+ label: string | React.ReactNode;
+ disabled?: boolean;
+ sx?: SxProps;
+ error?: boolean;
+}
diff --git a/src/components/UiColorTheme/index.ts b/src/components/UiColorTheme/index.ts
new file mode 100755
index 0000000..99c3f93
--- /dev/null
+++ b/src/components/UiColorTheme/index.ts
@@ -0,0 +1,77 @@
+import { Theme, createTheme } from '@mui/material';
+
+const colorTheme: Theme = createTheme({
+ palette: {
+ primary: {
+ main: '#1EAEFF',
+ },
+ secondary: {
+ main: '#FFC01E',
+ },
+ error: {
+ main: '#DC3939',
+ },
+ white: {
+ main: '#FFF',
+ },
+ darkPrimary: {
+ main: '#1A1C1E',
+ },
+ darkSecondary: {
+ main: '#1B2327',
+ },
+ brandGray: {
+ main: '#E1E7EA',
+ },
+ grey200: {
+ main: '#404142',
+ },
+ grey250: {
+ main: '#57595B',
+ },
+ grey300: {
+ main: '#969B9D',
+ },
+ grey400: {
+ main: '#D0D4D8',
+ },
+ grey500: {
+ main: '#EAECEE',
+ },
+ backgroundGrey100: {
+ main: '#FBFBFB',
+ },
+ backgroundGrey200: {
+ main: '#f4f5f6',
+ },
+ backgroundGrey300: {
+ main: '#F5F6F7',
+ },
+ containedButtonHover: {
+ main: '#00A3FF',
+ },
+ containedButtonActive: {
+ main: '#0399ED',
+ },
+ notchDeskBefore: {
+ main: '#080805',
+ },
+ notchDeskAfter: {
+ main: '#0e314c',
+ },
+ notchMobileBefore: {
+ main: '#0c0b0e',
+ },
+ notchMobileAfter: {
+ main: '#0f0b25',
+ },
+ textLinkHover: {
+ main: '#297FFF',
+ },
+ textLinkActive: {
+ main: '#0399ED',
+ },
+ },
+});
+
+export default colorTheme;
diff --git a/src/components/UiFooter/DefaultFooter/DefaultFooter.tsx b/src/components/UiFooter/DefaultFooter/DefaultFooter.tsx
new file mode 100755
index 0000000..f9ba5a9
--- /dev/null
+++ b/src/components/UiFooter/DefaultFooter/DefaultFooter.tsx
@@ -0,0 +1,58 @@
+import { Box, Stack } from '@mui/material';
+import React, { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import Logo from '@/assets/svg/Logo.svg';
+import UiTypography from '@/components/UiTypography';
+
+import PrivacyPolicy from '../PrivacyPolicy';
+import SocialMediaItem from '../SocialMediaItem/SocialMediaItem';
+import { SocialMedia } from '../types';
+import VilnaCRMEmail from '../VilnaCRMEmail';
+
+import styles from './styles';
+
+function DefaultFooter({ socialLinks }: { socialLinks: SocialMedia[] }): React.ReactElement {
+ const { t } = useTranslation();
+ const logoUrl: string = typeof Logo === 'string' ? Logo : Logo.src;
+
+ const currentDate: Date = useMemo(() => new Date(), []);
+ const currentYear: number = useMemo(() => currentDate.getFullYear(), [currentDate]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('footer.copyright')}, {currentYear}
+
+
+
+
+ {socialLinks.map(item => (
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+export default DefaultFooter;
diff --git a/src/components/UiFooter/DefaultFooter/index.ts b/src/components/UiFooter/DefaultFooter/index.ts
new file mode 100755
index 0000000..ba87401
--- /dev/null
+++ b/src/components/UiFooter/DefaultFooter/index.ts
@@ -0,0 +1,3 @@
+import DefaultFooter from './DefaultFooter';
+
+export default DefaultFooter;
diff --git a/src/components/UiFooter/DefaultFooter/styles.ts b/src/components/UiFooter/DefaultFooter/styles.ts
new file mode 100755
index 0000000..d73b077
--- /dev/null
+++ b/src/components/UiFooter/DefaultFooter/styles.ts
@@ -0,0 +1,67 @@
+import breakpointsTheme from '../../UiBreakpoints';
+import colorTheme from '../../UiColorTheme';
+
+export default {
+ footerWrapper: {
+ borderTop: '1px solid #e1e7ea',
+ background: colorTheme.palette.white.main,
+ boxShadow: '0px -5px 46px 0px rgba(198, 209, 220, 0.25)',
+ },
+
+ topWrapper: {
+ width: '100%',
+ maxWidth: '1222px',
+ margin: '0 auto',
+ },
+
+ topContent: {
+ paddingLeft: '1rem',
+ paddingRight: '1rem',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.lg}px)`]: {
+ paddingLeft: '2rem',
+ paddingRight: '1.5rem',
+ },
+ },
+
+ copyrightAndLinksWrapper: {
+ width: '100%',
+ maxWidth: '1222px',
+ margin: '0 auto',
+ },
+
+ bottomWrapper: {
+ borderRadius: '1rem 1rem 0px 0px',
+ background: colorTheme.palette.backgroundGrey200.main,
+ },
+
+ copyrightAndLinks: {
+ paddingLeft: '1.3rem',
+ paddingRight: '1rem',
+ height: '3.688rem',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingBottom: '0.3rem',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.lg}px)`]: {
+ paddingRight: '2rem',
+ paddingLeft: '2rem',
+ pb: '0.2rem',
+ },
+ },
+
+ copyright: {
+ color: colorTheme.palette.grey200.main,
+ fontFamily: 'Golos Text',
+ },
+
+ listWrapper: {
+ gap: '0.5rem',
+ justifyContent: 'center',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ gap: '0.25rem',
+ },
+ '@media (max-width: 350px)': {
+ gap: '0',
+ },
+ },
+};
diff --git a/src/components/UiFooter/Footer.stories.tsx b/src/components/UiFooter/Footer.stories.tsx
new file mode 100755
index 0000000..5b73f3a
--- /dev/null
+++ b/src/components/UiFooter/Footer.stories.tsx
@@ -0,0 +1,15 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import UiFooter from './UiFooter';
+
+const meta: Meta = {
+ title: 'UiComponents/UiFooter',
+ component: UiFooter,
+ tags: ['autodocs'],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Footer: Story = {};
diff --git a/src/components/UiFooter/Mobile/Mobile.tsx b/src/components/UiFooter/Mobile/Mobile.tsx
new file mode 100755
index 0000000..0e4b186
--- /dev/null
+++ b/src/components/UiFooter/Mobile/Mobile.tsx
@@ -0,0 +1,39 @@
+import { Box, Container, Stack } from '@mui/material';
+import React, { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import Logo from '@/assets/svg/Logo.svg';
+import UiTypography from '@/components/UiTypography';
+
+import PrivacyPolicy from '../PrivacyPolicy';
+import SocialMediaItem from '../SocialMediaItem/SocialMediaItem';
+import { SocialMedia } from '../types';
+import VilnaCRMEmail from '../VilnaCRMEmail';
+
+import styles from './styles';
+
+function Mobile({ socialLinks }: { socialLinks: SocialMedia[] }): React.ReactElement {
+ const { t } = useTranslation();
+ const logoUrl: string = typeof Logo === 'string' ? Logo : Logo.src;
+ const currentDate: Date = useMemo(() => new Date(), []);
+ const currentYear: number = useMemo(() => currentDate.getFullYear(), [currentDate]);
+ return (
+
+
+
+
+ {socialLinks.map(item => (
+
+ ))}
+
+
+
+
+
+ {t('footer.copyright')}, {currentYear}
+
+
+ );
+}
+
+export default Mobile;
diff --git a/src/components/UiFooter/Mobile/index.ts b/src/components/UiFooter/Mobile/index.ts
new file mode 100755
index 0000000..bdd8768
--- /dev/null
+++ b/src/components/UiFooter/Mobile/index.ts
@@ -0,0 +1,3 @@
+import Mobile from './Mobile';
+
+export default Mobile;
diff --git a/src/components/UiFooter/Mobile/styles.ts b/src/components/UiFooter/Mobile/styles.ts
new file mode 100755
index 0000000..78ed6ab
--- /dev/null
+++ b/src/components/UiFooter/Mobile/styles.ts
@@ -0,0 +1,41 @@
+import breakpointsTheme from '@/components/UiBreakpoints';
+import colorTheme from '@/components/UiColorTheme';
+
+export default {
+ wrapper: {
+ marginBottom: '0.75rem',
+ borderTop: `1px solid ${colorTheme.palette.brandGray.main}`,
+ background: colorTheme.palette.white.main,
+ boxShadow:
+ ' 0px -5px 46px 0px rgba(198, 209, 220, 0.25), 0px -5px 46px 0px rgba(198, 209, 220, 0.25)',
+ },
+ content: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginTop: '1.125rem',
+ paddingBottom: '0.75rem',
+ '@media (max-width: 350px)': {
+ gap: '0.5rem',
+ },
+ },
+
+ copyright: {
+ fontFamily: 'Golos Text',
+ paddingBottom: '1.25rem',
+ color: colorTheme.palette.grey200.main,
+ textAlign: 'center',
+ width: '100%',
+ mt: '1rem',
+ },
+
+ listWrapper: {
+ gap: '0.5rem',
+ justifyContent: 'center',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ gap: '0.25rem',
+ },
+ '@media (max-width: 350px)': {
+ gap: '0',
+ },
+ },
+};
diff --git a/src/components/UiFooter/PrivacyPolicy/PrivacyPolicy.tsx b/src/components/UiFooter/PrivacyPolicy/PrivacyPolicy.tsx
new file mode 100755
index 0000000..a326147
--- /dev/null
+++ b/src/components/UiFooter/PrivacyPolicy/PrivacyPolicy.tsx
@@ -0,0 +1,36 @@
+import { Link, Stack } from '@mui/material';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { UiTypography } from '@/components/';
+
+import styles from './styles';
+
+function PrivacyPolicy(): React.ReactElement {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ {t('footer.privacy')}
+
+
+
+
+ {t('footer.usage_policy')}
+
+
+
+ );
+}
+
+export default PrivacyPolicy;
diff --git a/src/components/UiFooter/PrivacyPolicy/index.ts b/src/components/UiFooter/PrivacyPolicy/index.ts
new file mode 100755
index 0000000..aad8fd4
--- /dev/null
+++ b/src/components/UiFooter/PrivacyPolicy/index.ts
@@ -0,0 +1,3 @@
+import PrivacyPolicy from './PrivacyPolicy';
+
+export default PrivacyPolicy;
diff --git a/src/components/UiFooter/PrivacyPolicy/styles.ts b/src/components/UiFooter/PrivacyPolicy/styles.ts
new file mode 100755
index 0000000..cd7fe66
--- /dev/null
+++ b/src/components/UiFooter/PrivacyPolicy/styles.ts
@@ -0,0 +1,50 @@
+import breakpointsTheme from '@/components/UiBreakpoints';
+import colorTheme from '@/components/UiColorTheme';
+
+export default {
+ wrapper: {
+ gap: '0.5rem',
+ flexDirection: 'row',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ flexDirection: 'column',
+ gap: '0.25rem',
+ pt: '0.25rem',
+ },
+ },
+
+ textColor: {
+ color: colorTheme.palette.grey300.main,
+ },
+
+ privacy: {
+ color: 'inherit',
+ textDecoration: 'none',
+ padding: '0.5rem 1rem',
+ borderRadius: '0.5rem',
+ background: colorTheme.palette.backgroundGrey200.main,
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ textAlign: 'center',
+ width: '100%',
+ padding: '1.063rem 0 1.125rem',
+ },
+ '&:hover': {
+ background: colorTheme.palette.grey500.main,
+ },
+ },
+
+ usage_policy: {
+ color: 'inherit',
+ textDecoration: 'none',
+ padding: '0.5rem 1rem',
+ borderRadius: '0.5rem',
+ background: colorTheme.palette.backgroundGrey200.main,
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ textAlign: 'center',
+ width: '100%',
+ padding: '1.063rem 0 1.125rem',
+ },
+ '&:hover': {
+ background: colorTheme.palette.grey500.main,
+ },
+ },
+};
diff --git a/src/components/UiFooter/SocialMediaItem/SocialMediaItem.tsx b/src/components/UiFooter/SocialMediaItem/SocialMediaItem.tsx
new file mode 100755
index 0000000..3dc12ff
--- /dev/null
+++ b/src/components/UiFooter/SocialMediaItem/SocialMediaItem.tsx
@@ -0,0 +1,21 @@
+import { Box, Link } from '@mui/material';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { SocialMedia } from '../types';
+
+import styles from './styles';
+
+function SocialMediaItem({ item }: { item: SocialMedia }): React.ReactElement {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default SocialMediaItem;
diff --git a/src/components/UiFooter/SocialMediaItem/styles.ts b/src/components/UiFooter/SocialMediaItem/styles.ts
new file mode 100755
index 0000000..ad66a90
--- /dev/null
+++ b/src/components/UiFooter/SocialMediaItem/styles.ts
@@ -0,0 +1,6 @@
+export default {
+ navLink: {
+ margin: '0.625rem',
+ height: '1.25rem',
+ },
+};
diff --git a/src/components/UiFooter/UiFooter.tsx b/src/components/UiFooter/UiFooter.tsx
new file mode 100755
index 0000000..0276c06
--- /dev/null
+++ b/src/components/UiFooter/UiFooter.tsx
@@ -0,0 +1,22 @@
+import { Box } from '@mui/material';
+import React from 'react';
+
+import socialLinks from './constants';
+import DefaultFooter from './DefaultFooter';
+import Mobile from './Mobile';
+import styles from './styles';
+
+function UiFooter(): React.ReactElement {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export default UiFooter;
diff --git a/src/components/UiFooter/VilnaCRMEmail/VilnaCRMGmail.tsx b/src/components/UiFooter/VilnaCRMEmail/VilnaCRMGmail.tsx
new file mode 100755
index 0000000..4b8aaa9
--- /dev/null
+++ b/src/components/UiFooter/VilnaCRMEmail/VilnaCRMGmail.tsx
@@ -0,0 +1,24 @@
+import { Box, Link } from '@mui/material';
+import React from 'react';
+
+import { UiTypography } from '@/components/';
+
+import styles from './styles';
+
+const defaultEmailAddress: string = 'info@vilnacrm.com';
+
+function VilnaCRMEmail(): React.ReactElement {
+ const email: string = process.env.REACT_APP_VILNACRM_GMAIL ?? defaultEmailAddress;
+
+ return (
+
+
+
+ {email}
+
+
+
+ );
+}
+
+export default VilnaCRMEmail;
diff --git a/src/components/UiFooter/VilnaCRMEmail/index.ts b/src/components/UiFooter/VilnaCRMEmail/index.ts
new file mode 100755
index 0000000..4af4b7f
--- /dev/null
+++ b/src/components/UiFooter/VilnaCRMEmail/index.ts
@@ -0,0 +1,3 @@
+import VilnaCRMEmail from './VilnaCRMGmail';
+
+export default VilnaCRMEmail;
diff --git a/src/components/UiFooter/VilnaCRMEmail/styles.ts b/src/components/UiFooter/VilnaCRMEmail/styles.ts
new file mode 100755
index 0000000..fa707e9
--- /dev/null
+++ b/src/components/UiFooter/VilnaCRMEmail/styles.ts
@@ -0,0 +1,33 @@
+import breakpointsTheme from '../../UiBreakpoints';
+import colorTheme from '../../UiColorTheme';
+
+export default {
+ emailText: {
+ color: colorTheme.palette.darkSecondary.main,
+ textAlign: 'center',
+ width: '100%',
+ textDecoration: 'none',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ fontSize: '1.125rem',
+ fontStyle: 'normal',
+ fontWeight: '600',
+ lineHeight: 'normal',
+ },
+ },
+
+ emailLink: {
+ color: 'inherit',
+ textDecoration: 'none',
+ fontFamily: 'Golos Text',
+ },
+
+ emailWrapper: {
+ padding: '0.5rem 1rem',
+ borderRadius: '0.5rem',
+ background: colorTheme.palette.white.main,
+ border: `1px solid ${colorTheme.palette.grey400.main}`,
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ padding: '0.875rem 0 0.9375rem',
+ },
+ },
+};
diff --git a/src/components/UiFooter/constants.ts b/src/components/UiFooter/constants.ts
new file mode 100755
index 0000000..b6f34bc
--- /dev/null
+++ b/src/components/UiFooter/constants.ts
@@ -0,0 +1,39 @@
+import FacebookFooterIcon from '@/assets/svg/social-icons/facebook.svg';
+import GitHubFooterIcon from '@/assets/svg/social-icons/github.svg';
+import InstagramFooterIcon from '@/assets/svg/social-icons/instagram.svg';
+import LinkedinFooterIcon from '@/assets/svg/social-icons/linked-in.svg';
+
+import { SocialMedia } from './types';
+
+const socialLinks: SocialMedia[] = [
+ {
+ id: 'Instagram-link',
+ icon: InstagramFooterIcon,
+ alt: 'footer.alt_images.instagram',
+ linkHref: 'https://www.instagram.com/',
+ ariaLabel: 'footer.aria_labels.instagram',
+ },
+ {
+ id: 'GitHub-link',
+ icon: GitHubFooterIcon,
+ alt: 'footer.alt_images.github',
+ linkHref: ' https://github.com/VilnaCRM-Org',
+ ariaLabel: 'footer.aria_labels.github',
+ },
+ {
+ id: 'Facebook-link',
+ icon: FacebookFooterIcon,
+ alt: 'footer.alt_images.facebook',
+ linkHref: 'https://www.facebook.com/',
+ ariaLabel: 'footer.aria_labels.facebook',
+ },
+ {
+ id: 'Linkedin-link',
+ icon: LinkedinFooterIcon,
+ alt: 'footer.alt_images.linkedin',
+ linkHref: 'https://www.linkedin.com/',
+ ariaLabel: 'footer.aria_labels.linkedin',
+ },
+];
+
+export default socialLinks;
diff --git a/src/components/UiFooter/index.ts b/src/components/UiFooter/index.ts
new file mode 100755
index 0000000..3561472
--- /dev/null
+++ b/src/components/UiFooter/index.ts
@@ -0,0 +1,3 @@
+import UiFooter from './UiFooter';
+
+export default UiFooter;
diff --git a/src/components/UiFooter/styles.ts b/src/components/UiFooter/styles.ts
new file mode 100755
index 0000000..3f405c4
--- /dev/null
+++ b/src/components/UiFooter/styles.ts
@@ -0,0 +1,17 @@
+import breakpointsTheme from '../UiBreakpoints';
+
+export default {
+ default: {
+ display: 'block',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ display: 'none',
+ },
+ },
+
+ adaptive: {
+ display: 'none',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.md}px)`]: {
+ display: 'block',
+ },
+ },
+};
diff --git a/src/components/UiFooter/types.ts b/src/components/UiFooter/types.ts
new file mode 100644
index 0000000..dcc6cbc
--- /dev/null
+++ b/src/components/UiFooter/types.ts
@@ -0,0 +1,7 @@
+export interface SocialMedia {
+ id: string;
+ icon: string;
+ alt: string;
+ linkHref: string;
+ ariaLabel: string;
+}
diff --git a/src/components/UiImage/Image.stories.tsx b/src/components/UiImage/Image.stories.tsx
new file mode 100755
index 0000000..b7ead9e
--- /dev/null
+++ b/src/components/UiImage/Image.stories.tsx
@@ -0,0 +1,41 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { t } from 'i18next';
+
+import testImage from '../../assets/svg/TooltipIcons/Joomla.svg';
+
+import UiImage from './index';
+
+const meta: Meta = {
+ title: 'UiComponents/UiImage',
+ component: UiImage,
+ tags: ['autodocs'],
+ argTypes: {
+ src: {
+ control: 'text',
+ description: 'Image source URL',
+ },
+ alt: {
+ control: 'text',
+ description: 'Alternative text for the image',
+ },
+ sx: {
+ control: 'object',
+ description: 'Style object for the image',
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Image: Story = {
+ args: {
+ src: testImage,
+ alt: t('Story example image'),
+ sx: {
+ width: '200px',
+ height: '200px',
+ },
+ },
+};
diff --git a/src/components/UiImage/index.tsx b/src/components/UiImage/index.tsx
new file mode 100755
index 0000000..b0fcc98
--- /dev/null
+++ b/src/components/UiImage/index.tsx
@@ -0,0 +1,17 @@
+import { Box } from '@mui/material';
+import React from 'react';
+
+import styles from './styles';
+import { UiImageProps } from './types';
+
+function UiImage({ sx, alt, src }: UiImageProps): React.ReactElement {
+ const imageUrl: string = typeof src === 'string' ? src : src.src;
+
+ return (
+
+
+
+ );
+}
+
+export default UiImage;
diff --git a/src/components/UiImage/styles.ts b/src/components/UiImage/styles.ts
new file mode 100755
index 0000000..006f3bf
--- /dev/null
+++ b/src/components/UiImage/styles.ts
@@ -0,0 +1,8 @@
+export default {
+ wrapper: {
+ img: {
+ width: '100%',
+ height: '100%',
+ },
+ },
+};
diff --git a/src/components/UiImage/types.ts b/src/components/UiImage/types.ts
new file mode 100755
index 0000000..6745643
--- /dev/null
+++ b/src/components/UiImage/types.ts
@@ -0,0 +1,5 @@
+export interface UiImageProps {
+ sx?: React.CSSProperties;
+ src: { src: string } | string;
+ alt: string;
+}
diff --git a/src/components/UiInput/Input.stories.tsx b/src/components/UiInput/Input.stories.tsx
new file mode 100755
index 0000000..e3d6eea
--- /dev/null
+++ b/src/components/UiInput/Input.stories.tsx
@@ -0,0 +1,49 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { t } from 'i18next';
+
+import UiInput from './index';
+
+const meta: Meta = {
+ title: 'UiComponents/UiInput',
+ component: UiInput,
+ tags: ['autodocs'],
+ argTypes: {
+ placeholder: {
+ type: 'string',
+ description: 'Placeholder text for the input',
+ control: { type: 'text' },
+ },
+ value: {
+ type: 'string',
+ description: 'Value of the input element',
+ control: { type: 'text' },
+ },
+ disabled: {
+ type: 'boolean',
+ description: 'Whether the input is disabled',
+ control: { type: 'boolean' },
+ },
+ type: {
+ type: 'string',
+ description: 'Type of the input element (e.g., text, password)',
+ options: ['text', 'password', 'email', 'number'],
+ control: { type: 'radio' },
+ },
+ error: {
+ type: 'boolean',
+ description: 'Whether the input is in error state',
+ control: { type: 'boolean' },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Input: Story = {
+ args: {
+ placeholder: t('Input'),
+ error: false,
+ },
+};
diff --git a/src/components/UiInput/index.tsx b/src/components/UiInput/index.tsx
new file mode 100755
index 0000000..7e904e6
--- /dev/null
+++ b/src/components/UiInput/index.tsx
@@ -0,0 +1,53 @@
+import { TextField, ThemeProvider } from '@mui/material';
+import React from 'react';
+
+import theme from './theme';
+import { UiInputProps } from './types';
+
+const DISPLAY_NAME: string = 'UiInput';
+
+const UiInput: React.ForwardRefExoticComponent<
+ UiInputProps & React.RefAttributes
+> = React.forwardRef(
+ (
+ {
+ sx,
+ placeholder,
+ error,
+ size,
+ variant,
+ onBlur,
+ type,
+ fullWidth,
+ value,
+ onChange,
+ disabled,
+ onInput,
+ id,
+ },
+ ref
+ ) => (
+
+
+
+ )
+);
+
+UiInput.displayName = DISPLAY_NAME;
+
+export default UiInput;
diff --git a/src/components/UiInput/theme.ts b/src/components/UiInput/theme.ts
new file mode 100755
index 0000000..9d655b1
--- /dev/null
+++ b/src/components/UiInput/theme.ts
@@ -0,0 +1,75 @@
+import { Theme, createTheme } from '@mui/material';
+
+import breakpointsTheme from '@/components/UiBreakpoints';
+import colorTheme from '@/components/UiColorTheme';
+
+const theme: Theme = createTheme({
+ components: {
+ MuiOutlinedInput: {
+ styleOverrides: {
+ root: {
+ borderRadius: '0.5rem',
+ '&:hover .MuiOutlinedInput-notchedOutline': {
+ borderColor: colorTheme.palette.grey300.main,
+ },
+ '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
+ border: `1px solid ${colorTheme.palette.grey300.main}`,
+ },
+ '&.Mui-disabled': {
+ backgroundColor: colorTheme.palette.brandGray.main,
+ color: colorTheme.palette.grey300.main,
+ },
+ },
+ notchedOutline: {
+ border: `1px solid ${colorTheme.palette.grey400.main}`,
+ borderRadius: '0.5rem',
+ '&:hover': {
+ borderColor: colorTheme.palette.grey300.main,
+ },
+ },
+ },
+ },
+
+ MuiTextField: {
+ styleOverrides: {
+ root: {
+ input: {
+ padding: '0 1.75rem',
+ height: '4rem',
+ borderRadius: '0.5rem',
+ background: colorTheme.palette.white.main,
+ '&::placeholder': {
+ color: colorTheme.palette.grey300.main,
+ fontFamily: 'Inter',
+ fontSize: '1rem',
+ fontStyle: 'normal',
+ fontWeight: '400',
+ lineHeight: '1.125rem',
+ },
+ [`@media (max-width: 1130px)`]: {
+ height: '4.938rem',
+ '&::placeholder': {
+ fontSize: '1.125rem',
+ },
+ },
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ padding: '0 1.25rem',
+ height: '3rem',
+ '&::placeholder': {
+ fontSize: '0.875rem',
+ fontWeight: '500',
+ lineHeight: '1.125rem',
+ },
+ },
+ '&.Mui-disabled': {
+ backgroundColor: colorTheme.palette.brandGray.main,
+ color: colorTheme.palette.grey300.main,
+ },
+ },
+ },
+ },
+ },
+ },
+});
+
+export default theme;
diff --git a/src/components/UiInput/types.ts b/src/components/UiInput/types.ts
new file mode 100755
index 0000000..451a042
--- /dev/null
+++ b/src/components/UiInput/types.ts
@@ -0,0 +1,23 @@
+import { SxProps, TextFieldProps, Theme } from '@mui/material';
+
+/**
+ * Shared contract support:
+ * - supported: value, onChange, disabled, error, size, variant, sx
+ * - exceptions: none for Story 1.1 baseline
+ */
+export interface UiInputProps {
+ sx?: SxProps;
+ placeholder?: string;
+ value?: string;
+ onChange?: (event: React.ChangeEvent) => void;
+ ref?: React.ForwardedRef;
+ error?: boolean;
+ size?: TextFieldProps['size'];
+ variant?: TextFieldProps['variant'];
+ onBlur?: (event: React.FocusEvent) => void;
+ type?: string;
+ fullWidth?: boolean;
+ disabled?: boolean;
+ onInput?: (event: React.ChangeEvent) => void;
+ id?: string;
+}
diff --git a/src/components/UiLink/Link.stories.tsx b/src/components/UiLink/Link.stories.tsx
new file mode 100755
index 0000000..f5762fd
--- /dev/null
+++ b/src/components/UiLink/Link.stories.tsx
@@ -0,0 +1,31 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { t } from 'i18next';
+
+import UiLink from './index';
+
+const meta: Meta = {
+ title: 'UiComponents/UiLink',
+ component: UiLink,
+ tags: ['autodocs'],
+ argTypes: {
+ children: {
+ type: 'string',
+ description: 'Text for the link',
+ },
+ href: {
+ type: 'string',
+ description: 'Link URL',
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Link: Story = {
+ args: {
+ children: t('Link'),
+ href: '/',
+ },
+};
diff --git a/src/components/UiLink/index.tsx b/src/components/UiLink/index.tsx
new file mode 100755
index 0000000..9db82fc
--- /dev/null
+++ b/src/components/UiLink/index.tsx
@@ -0,0 +1,17 @@
+import { Link, ThemeProvider } from '@mui/material';
+import React from 'react';
+
+import theme from './theme';
+import { UiLinkProps } from './types';
+
+function UiLink({ children, href, target, sx }: UiLinkProps): React.ReactElement {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export default UiLink;
diff --git a/src/components/UiLink/theme.ts b/src/components/UiLink/theme.ts
new file mode 100755
index 0000000..181117a
--- /dev/null
+++ b/src/components/UiLink/theme.ts
@@ -0,0 +1,36 @@
+import { Theme, createTheme } from '@mui/material';
+
+import breakpointsTheme from '../UiBreakpoints';
+import colorTheme from '../UiColorTheme';
+
+const theme: Theme = createTheme({
+ components: {
+ MuiLink: {
+ styleOverrides: {
+ root: {
+ color: colorTheme.palette.primary.main,
+ fontFamily: 'Inter',
+ fontSize: '0.875rem',
+ fontStyle: 'normal',
+ fontWeight: '700',
+ lineHeight: '1.125rem',
+ textDecoration: 'underline',
+ [`@media (max-width: 1130px)`]: {
+ fontSize: '1rem',
+ },
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ fontSize: '0.875rem',
+ },
+ '&:hover': {
+ color: colorTheme.palette.textLinkHover.main,
+ },
+ '&:active': {
+ color: colorTheme.palette.textLinkActive.main,
+ },
+ },
+ },
+ },
+ },
+});
+
+export default theme;
diff --git a/src/components/UiLink/types.ts b/src/components/UiLink/types.ts
new file mode 100755
index 0000000..449fcf3
--- /dev/null
+++ b/src/components/UiLink/types.ts
@@ -0,0 +1,13 @@
+import { SxProps, Theme } from '@mui/material';
+
+/**
+ * Shared contract support:
+ * - supported: sx
+ * - exceptions: value, onChange, disabled, error, size, variant
+ */
+export interface UiLinkProps {
+ children: React.ReactNode;
+ href: string;
+ target?: string;
+ sx?: SxProps;
+}
diff --git a/src/components/UiTextFieldForm/TextFieldForm.stories.tsx b/src/components/UiTextFieldForm/TextFieldForm.stories.tsx
new file mode 100755
index 0000000..f3d0ab6
--- /dev/null
+++ b/src/components/UiTextFieldForm/TextFieldForm.stories.tsx
@@ -0,0 +1,95 @@
+import { Stack } from '@mui/material';
+import { Meta, StoryObj } from '@storybook/react';
+import { t } from 'i18next';
+import { useForm } from 'react-hook-form';
+
+import UiButton from '../UiButton';
+
+import { CustomTextField } from './types';
+
+import UiTextFieldForm from './index';
+
+const meta: Meta = {
+ title: 'UiComponents/UiTextFieldForm',
+ component: UiTextFieldForm,
+ tags: ['autodocs'],
+ argTypes: {
+ type: {
+ control: { type: 'radio' },
+ options: ['text', 'email', 'password'],
+ defaultValue: 'text',
+ },
+ rules: {
+ control: { type: 'object' },
+ defaultValue: {
+ required: 'This field is required',
+ },
+ },
+ placeholder: {
+ control: { type: 'text' },
+ defaultValue: 'Enter text...',
+ },
+ fullWidth: {
+ control: { type: 'boolean' },
+ defaultValue: false,
+ },
+ },
+};
+
+export default meta;
+
+type TextFieldFormStoryArgs = Omit, 'control' | 'name'>;
+type Story = StoryObj & {
+ args: TextFieldFormStoryArgs;
+};
+
+function TextFieldFormStory(args: TextFieldFormStoryArgs): React.ReactElement {
+ const { handleSubmit, control } = useForm<{ FullName: string }>({
+ mode: 'onTouched',
+ });
+
+ const { rules, placeholder, type, fullWidth } = args;
+
+ return (
+
+ );
+}
+
+export const TextFieldForm: Story = {
+ render: args => (
+
+ ),
+ args: {
+ rules: {
+ required: t('This field is required'),
+ validate: (value: string) => {
+ if (value.length < 3) {
+ return t('Name must be at least 3 characters');
+ }
+ return true;
+ },
+ },
+ type: 'text',
+ placeholder: t('Enter text...'),
+ fullWidth: false,
+ },
+};
diff --git a/src/components/UiTextFieldForm/index.tsx b/src/components/UiTextFieldForm/index.tsx
new file mode 100755
index 0000000..0b0ce57
--- /dev/null
+++ b/src/components/UiTextFieldForm/index.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { Controller, FieldValues } from 'react-hook-form';
+
+import UiInput from '../UiInput';
+import UiTypography from '../UiTypography';
+
+import styles from './styles';
+import { CustomTextField } from './types';
+
+function UiTextFieldForm({
+ control,
+ rules,
+ placeholder,
+ type,
+ name,
+ fullWidth,
+}: CustomTextField): React.ReactElement {
+ return (
+ (
+ <>
+ field.onChange(e)}
+ onBlur={field.onBlur}
+ value={field.value}
+ error={!!error}
+ fullWidth={fullWidth}
+ />
+ {error && (
+
+ {error.message}
+
+ )}
+ >
+ )}
+ />
+ );
+}
+
+export default UiTextFieldForm;
diff --git a/src/components/UiTextFieldForm/styles.ts b/src/components/UiTextFieldForm/styles.ts
new file mode 100755
index 0000000..53dd184
--- /dev/null
+++ b/src/components/UiTextFieldForm/styles.ts
@@ -0,0 +1,14 @@
+import breakpointsTheme from '../UiBreakpoints';
+import colorTheme from '../UiColorTheme';
+
+export default {
+ errorText: {
+ top: '100%',
+ position: 'absolute',
+ paddingBottom: '10px',
+ color: colorTheme.palette.error.main,
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ fontSize: '0.75rem',
+ },
+ },
+};
diff --git a/src/components/UiTextFieldForm/types.ts b/src/components/UiTextFieldForm/types.ts
new file mode 100755
index 0000000..e5697bb
--- /dev/null
+++ b/src/components/UiTextFieldForm/types.ts
@@ -0,0 +1,11 @@
+import { TextFieldProps } from '@mui/material';
+import { Control, FieldValues, Path } from 'react-hook-form';
+
+export interface CustomTextField extends TextFieldProps<'standard'> {
+ control: Control;
+ rules: FieldValues;
+ name: Path;
+ placeholder: string;
+ type?: string;
+ fullWidth?: boolean;
+}
diff --git a/src/components/UiToolbar/Toolbar.stories.tsx b/src/components/UiToolbar/Toolbar.stories.tsx
new file mode 100755
index 0000000..b3ef993
--- /dev/null
+++ b/src/components/UiToolbar/Toolbar.stories.tsx
@@ -0,0 +1,43 @@
+import { Box } from '@mui/material';
+import type { Meta, StoryObj } from '@storybook/react';
+import { t } from 'i18next';
+
+import UiToolbar from './index';
+
+const meta: Meta = {
+ title: 'UiComponents/UiToolbar',
+ component: UiToolbar,
+ tags: ['autodocs'],
+ argTypes: {
+ children: {
+ control: 'object',
+ },
+ },
+};
+export default meta;
+
+function ToolbarComponent(): React.ReactElement {
+ return (
+
+ {t('Click here')}
+
+ );
+}
+
+type Story = StoryObj;
+
+export const Toolbar: Story = {
+ args: {
+ children: ,
+ },
+};
diff --git a/src/components/UiToolbar/index.tsx b/src/components/UiToolbar/index.tsx
new file mode 100755
index 0000000..a994f29
--- /dev/null
+++ b/src/components/UiToolbar/index.tsx
@@ -0,0 +1,14 @@
+import { Toolbar, ThemeProvider } from '@mui/material';
+import React from 'react';
+
+import theme from './theme';
+
+function UiToolbar({ children }: { children: React.ReactNode }): React.ReactElement {
+ return (
+
+ {children}
+
+ );
+}
+
+export default UiToolbar;
diff --git a/src/components/UiToolbar/theme.ts b/src/components/UiToolbar/theme.ts
new file mode 100755
index 0000000..f5d667f
--- /dev/null
+++ b/src/components/UiToolbar/theme.ts
@@ -0,0 +1,26 @@
+import { createTheme, Theme } from '@mui/material';
+
+const theme: Theme = createTheme({
+ components: {
+ MuiToolbar: {
+ styleOverrides: {
+ root: {
+ padding: 0,
+ margin: 0,
+ justifyContent: 'space-between',
+ '@media (min-width: 425px)': {
+ padding: '0 2rem',
+ width: '100%',
+ margin: '0 auto',
+ maxWidth: '78.375rem',
+ },
+ '@media (max-width: 425px)': {
+ padding: '0 0.9375rem',
+ },
+ },
+ },
+ },
+ },
+});
+
+export default theme;
diff --git a/src/components/UiTooltip/Tooltip.stories.tsx b/src/components/UiTooltip/Tooltip.stories.tsx
new file mode 100755
index 0000000..e149386
--- /dev/null
+++ b/src/components/UiTooltip/Tooltip.stories.tsx
@@ -0,0 +1,45 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { t } from 'i18next';
+
+import UiTooltip from '.';
+
+const meta: Meta = {
+ title: 'UiComponents/UITooltip',
+ component: UiTooltip,
+ tags: ['autodocs'],
+ argTypes: {
+ children: {
+ type: 'string',
+ name: 'children',
+ description: 'Text of the button',
+ },
+ placement: {
+ type: 'string',
+ description: 'Placement of the tooltip',
+ options: ['top', 'bottom', 'left', 'right'],
+ control: { type: 'radio' },
+ },
+ arrow: {
+ type: 'boolean',
+ description: 'Whether the tooltip has an arrow',
+ control: { type: 'boolean' },
+ },
+ title: {
+ type: 'string',
+ description: 'Content of the tooltip',
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Tooltip: Story = {
+ args: {
+ children: t('Hello World!'),
+ placement: 'bottom',
+ arrow: true,
+ title: 'UiTooltip',
+ },
+};
diff --git a/src/components/UiTooltip/TooltipWrapper.tsx b/src/components/UiTooltip/TooltipWrapper.tsx
new file mode 100755
index 0000000..f57f743
--- /dev/null
+++ b/src/components/UiTooltip/TooltipWrapper.tsx
@@ -0,0 +1,33 @@
+import { ClickAwayListener, Tooltip, Typography, useMediaQuery } from '@mui/material';
+import React from 'react';
+
+import { UiTooltipProps } from './types';
+
+export default function WrapperUiTooltip({
+ title,
+ placement,
+ arrow,
+ sx,
+ children,
+}: UiTooltipProps): React.ReactElement {
+ const [open, setOpen] = React.useState(false);
+ const isWideScreenMaxWidth: boolean = useMediaQuery('(max-width: 640px)');
+ const isWideScreenMinWidth: boolean = useMediaQuery('(min-width: 640px)');
+
+ React.useEffect(() => {
+ setOpen(false);
+ }, [isWideScreenMaxWidth, isWideScreenMinWidth]);
+
+ const closeTooltip: () => void = () => setOpen(false);
+ const toggleTooltip: () => void = () => setOpen(!open);
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/components/UiTooltip/index.tsx b/src/components/UiTooltip/index.tsx
new file mode 100755
index 0000000..34cc279
--- /dev/null
+++ b/src/components/UiTooltip/index.tsx
@@ -0,0 +1,18 @@
+import { ThemeProvider } from '@mui/material';
+import React from 'react';
+
+import theme from './theme';
+import WrapperUiTooltip from './TooltipWrapper';
+import { UiTooltipProps } from './types';
+
+function UiTooltip({ title, placement, arrow, sx, children }: UiTooltipProps): React.ReactElement {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export default UiTooltip;
diff --git a/src/components/UiTooltip/theme.ts b/src/components/UiTooltip/theme.ts
new file mode 100755
index 0000000..323b530
--- /dev/null
+++ b/src/components/UiTooltip/theme.ts
@@ -0,0 +1,30 @@
+import { Theme, createTheme } from '@mui/material';
+
+import breakpointsTheme from '../UiBreakpoints';
+import colorTheme from '../UiColorTheme';
+
+const theme: Theme = createTheme({
+ components: {
+ MuiTooltip: {
+ styleOverrides: {
+ tooltip: {
+ color: colorTheme.palette.darkPrimary.main,
+ backgroundColor: colorTheme.palette.white.main,
+ borderRadius: '0.5rem',
+ border: `1px solid ${colorTheme.palette.grey400.main}`,
+ maxWidth: '20.625rem',
+ padding: '1.12rem 1.5rem',
+ [`@media (max-width: ${breakpointsTheme.breakpoints.values.sm}px)`]: {
+ maxWidth: '16rem',
+ padding: '0.5rem 0.75rem',
+ },
+ },
+ arrow: {
+ color: colorTheme.palette.grey400.main,
+ },
+ },
+ },
+ },
+});
+
+export default theme;
diff --git a/src/components/UiTooltip/types.ts b/src/components/UiTooltip/types.ts
new file mode 100755
index 0000000..7b2c1e4
--- /dev/null
+++ b/src/components/UiTooltip/types.ts
@@ -0,0 +1,7 @@
+export interface UiTooltipProps {
+ children: React.ReactNode;
+ title: string | React.ReactNode;
+ placement?: 'top' | 'bottom' | 'left' | 'right';
+ arrow?: boolean;
+ sx?: React.CSSProperties;
+}
diff --git a/src/components/UiTypography/Typography.stories.tsx b/src/components/UiTypography/Typography.stories.tsx
new file mode 100755
index 0000000..1ab672a
--- /dev/null
+++ b/src/components/UiTypography/Typography.stories.tsx
@@ -0,0 +1,50 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { t } from 'i18next';
+
+import UiTypography from './index';
+
+const meta: Meta = {
+ title: 'UiComponents/UiTypography',
+ component: UiTypography,
+ tags: ['autodocs'],
+ argTypes: {
+ children: {
+ type: 'string',
+ description: 'Text for the typography',
+ },
+ variant: {
+ type: 'string',
+ description: 'Variant of the typography',
+ options: [
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'medium16',
+ 'medium15',
+ 'medium14',
+ 'regular16',
+ 'bodyText18',
+ 'bodyText16',
+ 'bold22',
+ 'demi18',
+ 'button',
+ 'mobileText',
+ ],
+ control: { type: 'select' },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Typography: Story = {
+ args: {
+ children: t('Typography'),
+ variant: 'h5',
+ },
+};
diff --git a/src/components/UiTypography/index.tsx b/src/components/UiTypography/index.tsx
new file mode 100755
index 0000000..53df3d7
--- /dev/null
+++ b/src/components/UiTypography/index.tsx
@@ -0,0 +1,24 @@
+import { ThemeProvider, Typography } from '@mui/material';
+import React from 'react';
+
+import theme from './theme';
+import { UiTypographyProps } from './types';
+
+function UiTypography({
+ sx,
+ children,
+ component,
+ variant,
+ id,
+ role,
+}: UiTypographyProps): React.ReactElement {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export default UiTypography;
diff --git a/src/components/UiTypography/theme.ts b/src/components/UiTypography/theme.ts
new file mode 100755
index 0000000..588b32a
--- /dev/null
+++ b/src/components/UiTypography/theme.ts
@@ -0,0 +1,133 @@
+import { Theme, createTheme } from '@mui/material';
+import { CSSProperties } from '@mui/material/styles/createMixins';
+
+import colorTheme from '../UiColorTheme';
+
+const hStyles: CSSProperties = {
+ color: colorTheme.palette.darkPrimary.main,
+ fontWeight: '700',
+ lineHeight: 'normal',
+ fontFamily: 'Golos Text',
+ letterSpacing: '',
+};
+
+const theme: Theme = createTheme({
+ components: {
+ MuiTypography: {
+ defaultProps: {
+ variantMapping: {
+ medium16: 'p',
+ medium15: 'p',
+ medium14: 'p',
+ regular16: 'p',
+ bodyText18: 'p',
+ bodyText16: 'p',
+ bold22: 'p',
+ demi18: 'p',
+ button: 'p',
+ mobileText: 'p',
+ },
+ },
+ },
+ },
+ typography: {
+ h1: {
+ ...hStyles,
+ fontSize: '3.5rem',
+ },
+ h2: {
+ ...hStyles,
+ fontSize: '2.875rem',
+ },
+ h3: {
+ ...hStyles,
+ fontSize: '2.25rem',
+ fontWeight: '600',
+ },
+ h4: {
+ ...hStyles,
+ color: '#484848',
+ fontSize: '1.875rem',
+ fontWeight: '600',
+ },
+ h5: {
+ ...hStyles,
+ fontSize: '1.75rem',
+ },
+ h6: {
+ ...hStyles,
+ fontSize: '1.375rem',
+ },
+ medium16: {
+ fontFamily: 'Inter',
+ fontWeight: '500',
+ fontSize: '1rem',
+ lineHeight: '1.125rem',
+ color: colorTheme.palette.grey300.main,
+ },
+ medium15: {
+ fontWeight: '500',
+ fontSize: '0.9375rem',
+ lineHeight: '1.125rem',
+ color: colorTheme.palette.grey250.main,
+ },
+ medium14: {
+ fontWeight: '500',
+ fontSize: '0.875rem',
+ lineHeight: '1.125rem',
+ color: colorTheme.palette.grey200.main,
+ fontFamily: 'Inter',
+ },
+ regular16: {
+ fontWeight: '500',
+ fontSize: '1rem',
+ lineHeight: '1.125rem',
+ color: colorTheme.palette.grey300.main,
+ fontFamily: 'Golos Text',
+ },
+ bodyText18: {
+ fontWeight: '400',
+ fontSize: '1.125rem',
+ lineHeight: '1.875rem',
+ color: colorTheme.palette.darkPrimary.main,
+ fontFamily: 'Golos Text',
+ },
+ bodyText16: {
+ fontWeight: '400',
+ fontSize: '1rem',
+ lineHeight: '1.625rem',
+ color: colorTheme.palette.darkPrimary.main,
+ fontFamily: 'Golos Text',
+ },
+ bold22: {
+ fontWeight: '700',
+ fontSize: '1.375rem',
+ lineHeight: 'normal',
+ color: colorTheme.palette.grey250.main,
+ fontFamily: 'Golos Text',
+ },
+ demi18: {
+ fontWeight: '600',
+ fontSize: '1.125rem',
+ lineHeight: 'normal',
+ color: colorTheme.palette.darkPrimary.main,
+ fontFamily: 'Golos Text',
+ },
+ button: {
+ fontWeight: '600',
+ fontSize: '1.125rem',
+ lineHeight: 'normal',
+ color: colorTheme.palette.white.main,
+ fontFamily: 'Golos Text',
+ },
+ mobileText: {
+ fontWeight: '400',
+ fontSize: '0.9375rem',
+ lineHeight: '1.563rem',
+ color: colorTheme.palette.darkPrimary.main,
+ fontFamily: 'Golos Text',
+ },
+ },
+});
+
+export default theme;
diff --git a/src/components/UiTypography/types.ts b/src/components/UiTypography/types.ts
new file mode 100755
index 0000000..9697692
--- /dev/null
+++ b/src/components/UiTypography/types.ts
@@ -0,0 +1,26 @@
+import { SxProps, Theme } from '@mui/material';
+
+export interface UiTypographyProps {
+ sx?: SxProps;
+ variant?:
+ | 'h1'
+ | 'h2'
+ | 'h3'
+ | 'h4'
+ | 'h5'
+ | 'h6'
+ | 'medium16'
+ | 'medium15'
+ | 'medium14'
+ | 'regular16'
+ | 'bodyText18'
+ | 'bodyText16'
+ | 'bold22'
+ | 'demi18'
+ | 'button'
+ | 'mobileText';
+ children: React.ReactNode;
+ component?: 'section' | 'p' | 'div' | 'span' | 'a' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
+ id?: string;
+ role?: React.AriaRole;
+}
diff --git a/src/components/fonts.css b/src/components/fonts.css
new file mode 100644
index 0000000..2d32ba8
--- /dev/null
+++ b/src/components/fonts.css
@@ -0,0 +1,62 @@
+@font-face {
+ font-family: 'Golos Text';
+ src: url('../assets/fonts/Golos/GolosText-Regular.ttf') format('truetype');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Golos Text';
+ src: url('../assets/fonts/Golos/GolosText-Medium.ttf') format('truetype');
+ font-weight: 500;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Golos Text';
+ src: url('../assets/fonts/Golos/GolosText-SemiBold.ttf') format('truetype');
+ font-weight: 600;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Golos Text';
+ src: url('../assets/fonts/Golos/GolosText-Bold.ttf') format('truetype');
+ font-weight: 700;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Golos Text';
+ src: url('../assets/fonts/Golos/GolosText-ExtraBold.ttf') format('truetype');
+ font-weight: 800;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Golos Text';
+ src: url('../assets/fonts/Golos/GolosText-Black.ttf') format('truetype');
+ font-weight: 900;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Inter';
+ src: url('../assets/fonts/Inter/Inter-Regular.ttf') format('truetype');
+ font-weight: 400;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Inter';
+ src: url('../assets/fonts/Inter/Inter-Medium.ttf') format('truetype');
+ font-weight: 500;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Inter';
+ src: url('../assets/fonts/Inter/Inter-Bold.ttf') format('truetype');
+ font-weight: 700;
+ font-style: normal;
+}
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100755
index 0000000..8c44a42
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,13 @@
+import './fonts.css';
+
+export { default as UiButton } from './UiButton';
+export { default as UiCheckbox } from './UiCheckbox';
+export { default as UiInput } from './UiInput';
+export { default as UiLink } from './UiLink';
+export { default as UiTypography } from './UiTypography';
+export { default as UiImage } from './UiImage';
+export { default as UiToolbar } from './UiToolbar';
+export { default as UiColorTheme } from './UiColorTheme';
+export { default as UiBreakpoints } from './UiBreakpoints';
+export { default as UiTextFieldForm } from './UiTextFieldForm';
+export { default as UiTooltip } from './UiTooltip';
diff --git a/src/hooks/.gitignore b/src/hooks/.gitignore
new file mode 100755
index 0000000..e69de29
diff --git a/src/index.ts b/src/index.ts
new file mode 100755
index 0000000..cb0ff5c
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1 @@
+export {};
diff --git a/src/lib/.gitignore b/src/lib/.gitignore
new file mode 100755
index 0000000..e69de29
diff --git a/src/providers/.gitignore b/src/providers/.gitignore
new file mode 100755
index 0000000..e69de29
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
new file mode 100755
index 0000000..a666151
--- /dev/null
+++ b/src/react-app-env.d.ts
@@ -0,0 +1,4 @@
+declare module '*.png';
+declare module '*.svg';
+declare module '*.jpeg';
+declare module '*.jpg';
diff --git a/src/routes/.gitignore b/src/routes/.gitignore
new file mode 100755
index 0000000..e69de29
diff --git a/src/stores/.gitignore b/src/stores/.gitignore
new file mode 100755
index 0000000..e69de29
diff --git a/src/test/e2e/button.spec.ts b/src/test/e2e/button.spec.ts
new file mode 100644
index 0000000..7761451
--- /dev/null
+++ b/src/test/e2e/button.spec.ts
@@ -0,0 +1,27 @@
+import { test, expect, Locator, Page } from '@playwright/test';
+
+import { containedButtonText, outlinedButtonText, socialButtonText } from './constants';
+
+async function checkButtonVisibility(
+ page: Page,
+ storyPath: string,
+ buttonText: string
+): Promise {
+ await page.goto(`http://localhost:6006/?path=/story/${storyPath}`);
+ const button: Locator = page.getByText(buttonText);
+ await expect(button).toBeVisible();
+}
+
+test.describe('Buttons tests', () => {
+ test('Check if the contained button is rendered', async ({ page }) => {
+ await checkButtonVisibility(page, 'uicomponents-uibutton--contained', containedButtonText);
+ });
+
+ test('Check if the outlined button is rendered', async ({ page }) => {
+ await checkButtonVisibility(page, 'uicomponents-uibutton--outlined', outlinedButtonText);
+ });
+
+ test('Check if the social button is rendered', async ({ page }) => {
+ await checkButtonVisibility(page, 'uicomponents-uibutton--social-button', socialButtonText);
+ });
+});
diff --git a/src/test/e2e/constants.ts b/src/test/e2e/constants.ts
new file mode 100644
index 0000000..f634788
--- /dev/null
+++ b/src/test/e2e/constants.ts
@@ -0,0 +1,4 @@
+export const containedButtonText: string = 'Try it out';
+export const outlinedButtonText: string = 'Log in';
+export const socialButtonText: string = 'Social Button';
+export const storybookUrl: string | undefined = process.env.STORYBOOK_URL;
diff --git a/src/test/memory-leak/runMemlabTests.js b/src/test/memory-leak/runMemlabTests.js
new file mode 100644
index 0000000..6881fdd
--- /dev/null
+++ b/src/test/memory-leak/runMemlabTests.js
@@ -0,0 +1,40 @@
+const fs = require('node:fs');
+
+const { run, analyze } = require('@memlab/api');
+const { StringAnalysis } = require('@memlab/heap-analysis');
+
+const memoryLeakDir = './src/test/memory-leak';
+const testsDir = './tests';
+
+const workDir = './src/test/memory-leak/results';
+const consoleMode = 'VERBOSE';
+
+async function runScenario(testFilePath) {
+ const scenario = require(testFilePath);
+
+ const { runResult } = await run({
+ scenario,
+ consoleMode,
+ workDir,
+ });
+
+ const analyzer = new StringAnalysis();
+ await analyze(runResult, analyzer);
+
+ runResult.cleanup();
+}
+
+async function runMemlabTests() {
+ const testFilePaths = fs
+ .readdirSync(`${memoryLeakDir}/${testsDir}`)
+ .map(test => `${testsDir}/${test}`);
+
+ await testFilePaths.reduce(
+ (previousRun, testFilePath) => previousRun.then(() => runScenario(testFilePath)),
+ Promise.resolve()
+ );
+}
+
+runMemlabTests().catch(error => {
+ throw error;
+});
diff --git a/src/test/memory-leak/tests/fillForm.js b/src/test/memory-leak/tests/fillForm.js
new file mode 100644
index 0000000..d13acbd
--- /dev/null
+++ b/src/test/memory-leak/tests/fillForm.js
@@ -0,0 +1,44 @@
+const { faker } = require('@faker-js/faker');
+
+const ScenarioBuilder = require('../utils/ScenarioBuilder');
+
+const scenarioBuilder = new ScenarioBuilder();
+
+const fullNameInputSelector = 'input[id=":R6j59al2m:"]';
+const emailInputSelector = 'input[id=":R6l59al2m:"]';
+const passwordInputSelector = 'input[id=":R6n59al2m:"]';
+const privacyCheckboxSelector = 'input[type="checkbox"]';
+
+const fakeFullName = faker.person.fullName();
+const fakeEmail = faker.internet.email();
+const fakePassword = faker.internet.password();
+
+const clickSettings = { clickCount: 3 };
+
+const backspace = 'Backspace';
+
+async function action(page) {
+ await page.type(fullNameInputSelector, fakeFullName);
+ await page.type(emailInputSelector, fakeEmail);
+ await page.type(passwordInputSelector, fakePassword);
+ await page.click(privacyCheckboxSelector);
+}
+
+async function back(page) {
+ const fullNameInput = await page.$(fullNameInputSelector);
+ const emailInput = await page.$(emailInputSelector);
+ const passwordInput = await page.$(passwordInputSelector);
+
+ await fullNameInput.click(clickSettings);
+ await page.keyboard.press(backspace);
+
+ await emailInput.click(clickSettings);
+ await page.keyboard.press(backspace);
+
+ await passwordInput.click(clickSettings);
+ await page.keyboard.press(backspace);
+
+ await page.click(privacyCheckboxSelector);
+}
+
+module.exports = scenarioBuilder.createScenario({ action, back });
diff --git a/src/test/memory-leak/tests/horizontalSlider.js b/src/test/memory-leak/tests/horizontalSlider.js
new file mode 100644
index 0000000..a80f9eb
--- /dev/null
+++ b/src/test/memory-leak/tests/horizontalSlider.js
@@ -0,0 +1,24 @@
+const ScenarioBuilder = require('../utils/ScenarioBuilder');
+const swipeSlider = require('../utils/swipeSlider');
+
+const scenarioBuilder = new ScenarioBuilder();
+
+const mobileViewport = { width: 400, height: 812 };
+
+const sliderSelector = '.swiper-wrapper';
+
+const iterations = 6;
+
+async function setup(page) {
+ await page.setViewport(mobileViewport);
+}
+
+async function action(page) {
+ await swipeSlider(page, sliderSelector, iterations, 'left');
+}
+
+async function back(page) {
+ await swipeSlider(page, sliderSelector, iterations, 'right');
+}
+
+module.exports = scenarioBuilder.createScenario({ setup, action, back });
diff --git a/src/test/memory-leak/tests/navbarNavigation.js b/src/test/memory-leak/tests/navbarNavigation.js
new file mode 100644
index 0000000..81e80dc
--- /dev/null
+++ b/src/test/memory-leak/tests/navbarNavigation.js
@@ -0,0 +1,38 @@
+const ScenarioBuilder = require('../utils/ScenarioBuilder');
+
+const scenarioBuilder = new ScenarioBuilder();
+
+const advantagesLinkSelector = 'a[href="#Advantages"]';
+const forWhoSectionLinkSelector = 'a[href="#forWhoSection"]';
+const integrationLinkSelector = 'a[href="#Integration"]';
+const contactsLinkSelector = 'a[href="#Contacts"]';
+
+const coordinateX = 0;
+const coordinateY = 0;
+
+async function action(page) {
+ await page.click(advantagesLinkSelector);
+ await page.waitForTimeout(1500);
+
+ await page.click(forWhoSectionLinkSelector);
+ await page.waitForTimeout(1500);
+
+ await page.click(integrationLinkSelector);
+ await page.waitForTimeout(1500);
+
+ await page.click(contactsLinkSelector);
+ await page.waitForTimeout(2000);
+}
+
+async function back(page) {
+ await page.evaluate(
+ (x, y) => {
+ window.scrollTo(x, y);
+ },
+ coordinateX,
+ coordinateY
+ );
+ await page.waitForTimeout(2000);
+}
+
+module.exports = scenarioBuilder.createScenario({ action, back });
diff --git a/src/test/memory-leak/tests/servicesTooltip.js b/src/test/memory-leak/tests/servicesTooltip.js
new file mode 100644
index 0000000..f268d36
--- /dev/null
+++ b/src/test/memory-leak/tests/servicesTooltip.js
@@ -0,0 +1,23 @@
+const ScenarioBuilder = require('../utils/ScenarioBuilder');
+
+const scenarioBuilder = new ScenarioBuilder();
+
+const servicesButtonSelector = 'span.css-1rp615p-MuiTypography-root';
+const tooltipSelector = '.MuiTooltip-popper';
+
+const coordinateX = 100;
+const coordinateY = 100;
+
+async function action(page) {
+ await page.click(servicesButtonSelector);
+
+ await page.waitForSelector(tooltipSelector, { visible: true });
+}
+
+async function back(page) {
+ await page.mouse.click(coordinateX, coordinateY);
+
+ await page.waitForSelector(tooltipSelector, { hidden: true });
+}
+
+module.exports = scenarioBuilder.createScenario({ action, back });
diff --git a/src/test/memory-leak/tests/toggleMobileMenu.js b/src/test/memory-leak/tests/toggleMobileMenu.js
new file mode 100755
index 0000000..5762f36
--- /dev/null
+++ b/src/test/memory-leak/tests/toggleMobileMenu.js
@@ -0,0 +1,26 @@
+const ScenarioBuilder = require('../utils/ScenarioBuilder');
+
+const scenarioBuilder = new ScenarioBuilder();
+
+const mobileViewport = { width: 400, height: 812 };
+
+const menuIconSelector = 'img[alt="Bars Icon"]';
+const closeIconSelector = 'img[alt="Exit Icon"]';
+
+async function setup(page) {
+ await page.setViewport(mobileViewport);
+}
+
+async function action(page) {
+ await page.click(menuIconSelector);
+
+ await page.waitForSelector(closeIconSelector, { visible: true });
+}
+
+async function back(page) {
+ await page.click(closeIconSelector);
+
+ await page.waitForSelector(closeIconSelector, { hidden: true });
+}
+
+module.exports = scenarioBuilder.createScenario({ setup, action, back });
diff --git a/src/test/memory-leak/tests/tryItNowButton.js b/src/test/memory-leak/tests/tryItNowButton.js
new file mode 100644
index 0000000..91b9dd9
--- /dev/null
+++ b/src/test/memory-leak/tests/tryItNowButton.js
@@ -0,0 +1,28 @@
+const ScenarioBuilder = require('../utils/ScenarioBuilder');
+
+const scenarioBuilder = new ScenarioBuilder();
+
+const signUpButtonSelector = 'a[href="#signUp"]';
+
+const coordinateX = 0;
+const coordinateY = 0;
+
+async function action(page) {
+ await page.click(signUpButtonSelector);
+
+ await page.waitForTimeout(2000);
+}
+
+async function back(page) {
+ await page.evaluate(
+ (x, y) => {
+ window.scrollTo(x, y);
+ },
+ coordinateX,
+ coordinateY
+ );
+
+ await page.waitForTimeout(2000);
+}
+
+module.exports = scenarioBuilder.createScenario({ action, back });
diff --git a/src/test/memory-leak/utils/ScenarioBuilder.js b/src/test/memory-leak/utils/ScenarioBuilder.js
new file mode 100644
index 0000000..893bfd1
--- /dev/null
+++ b/src/test/memory-leak/utils/ScenarioBuilder.js
@@ -0,0 +1,13 @@
+class ScenarioBuilder {
+ constructor() {
+ this.url = () => process.env.MEMLAB_WEBSITE_URL;
+ }
+
+ createScenario(scenarioOptions) {
+ const scenario = { url: this.url, ...scenarioOptions };
+
+ return scenario;
+ }
+}
+
+module.exports = ScenarioBuilder;
diff --git a/src/test/memory-leak/utils/swipeSlider.js b/src/test/memory-leak/utils/swipeSlider.js
new file mode 100644
index 0000000..babd737
--- /dev/null
+++ b/src/test/memory-leak/utils/swipeSlider.js
@@ -0,0 +1,38 @@
+async function swipeSlider(page, selector, iterationsNumber, direction = 'left') {
+ const slider = await page.$(selector);
+ const boundingBox = await slider.boundingBox();
+
+ const coordinates = calculateCoordinates(boundingBox, direction);
+
+ await horizontalDragAndDrop(page, coordinates, iterationsNumber);
+}
+
+function calculateCoordinates(boundingBox, direction) {
+ const startX = direction === 'left' ? boundingBox.x + boundingBox.width - 10 : boundingBox.x + 10;
+ const endX = direction === 'left' ? boundingBox.x + 10 : boundingBox.x + boundingBox.width - 10;
+ const startY = boundingBox.y + boundingBox.height / 2;
+ const endY = boundingBox.y + boundingBox.height / 2;
+
+ return { startX, endX, startY, endY };
+}
+
+async function horizontalDragAndDrop(page, coordinates, iterationsNumber) {
+ async function runIteration(iteration) {
+ if (iteration >= iterationsNumber) {
+ return;
+ }
+
+ await page.mouse.move(coordinates.startX, coordinates.startY);
+ await page.mouse.down();
+
+ await page.mouse.move(coordinates.endX, coordinates.endY, { steps: 20 });
+ await page.mouse.up();
+
+ await page.waitForTimeout(500);
+ await runIteration(iteration + 1);
+ }
+
+ await runIteration(0);
+}
+
+module.exports = swipeSlider;
diff --git a/src/test/mocks/styleMock.ts b/src/test/mocks/styleMock.ts
new file mode 100644
index 0000000..9741db8
--- /dev/null
+++ b/src/test/mocks/styleMock.ts
@@ -0,0 +1,3 @@
+const styleMock: Record = {};
+
+export default styleMock;
diff --git a/src/test/mocks/svgMock.ts b/src/test/mocks/svgMock.ts
new file mode 100644
index 0000000..c97d128
--- /dev/null
+++ b/src/test/mocks/svgMock.ts
@@ -0,0 +1,5 @@
+const svgMock: { src: string } = {
+ src: 'svg-mock',
+};
+
+export default svgMock;
diff --git a/src/test/testing-library/ServicesHoverCard.test.tsx b/src/test/testing-library/ServicesHoverCard.test.tsx
new file mode 100644
index 0000000..b2c6ee5
--- /dev/null
+++ b/src/test/testing-library/ServicesHoverCard.test.tsx
@@ -0,0 +1,24 @@
+import '@testing-library/jest-dom';
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import ServicesHoverCard from '../../components/UiCardItem/ServicesHoverCard';
+
+const hoverCardtitle: string = 'Services';
+const hoverCardtext: string = 'Integrate in a few clicks';
+
+describe('ServicesHoverCard component', () => {
+ it('renders title and text correctly', () => {
+ const { getByText } = render();
+
+ expect(getByText(hoverCardtitle)).toBeInTheDocument();
+ expect(getByText(hoverCardtext)).toBeInTheDocument();
+ });
+
+ it('renders images correctly', () => {
+ const { getAllByAltText } = render();
+
+ const images: HTMLElement[] = getAllByAltText(/.+/);
+ expect(images.length).toBe(8);
+ });
+});
diff --git a/src/test/testing-library/SwipeSlider.test.tsx b/src/test/testing-library/SwipeSlider.test.tsx
new file mode 100644
index 0000000..e80c79e
--- /dev/null
+++ b/src/test/testing-library/SwipeSlider.test.tsx
@@ -0,0 +1,85 @@
+import swipeSlider from '../memory-leak/utils/swipeSlider';
+
+type BoundingBox = {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+};
+
+type SliderHandle = {
+ boundingBox: () => Promise;
+};
+
+type PageHandle = {
+ $: (selector: string) => Promise;
+ mouse: {
+ move: (x: number, y: number, options?: { steps: number }) => Promise;
+ down: () => Promise;
+ up: () => Promise;
+ };
+ waitForTimeout: (timeout: number) => Promise;
+};
+
+describe('swipeSlider', () => {
+ it('performs the configured number of drag cycles', async () => {
+ const boundingBox: BoundingBox = { x: 10, y: 20, width: 100, height: 40 };
+ const slider: {
+ boundingBox: jest.MockedFunction;
+ } = {
+ boundingBox: jest.fn().mockResolvedValue(boundingBox) as jest.MockedFunction<
+ SliderHandle['boundingBox']
+ >,
+ };
+
+ const events: string[] = [];
+ const page: {
+ $: jest.MockedFunction;
+ mouse: {
+ move: jest.MockedFunction;
+ down: jest.MockedFunction;
+ up: jest.MockedFunction;
+ };
+ waitForTimeout: jest.MockedFunction;
+ } = {
+ $: jest.fn().mockResolvedValue(slider) as jest.MockedFunction,
+ mouse: {
+ move: jest.fn(async (...args: Parameters) => {
+ expect(args.length).toBeGreaterThanOrEqual(2);
+ events.push('move');
+ }) as jest.MockedFunction,
+ down: jest.fn(async () => {
+ events.push('down');
+ }) as jest.MockedFunction,
+ up: jest.fn(async () => {
+ events.push('up');
+ }) as jest.MockedFunction,
+ },
+ waitForTimeout: jest.fn(async (...args: Parameters) => {
+ expect(args).toHaveLength(1);
+ events.push('wait');
+ }) as jest.MockedFunction,
+ };
+
+ await swipeSlider(page, '.swiper-wrapper', 2, 'left');
+
+ expect(page.$).toHaveBeenCalledWith('.swiper-wrapper');
+ expect(slider.boundingBox).toHaveBeenCalledTimes(1);
+ expect(page.mouse.move).toHaveBeenCalledTimes(4);
+ expect(page.mouse.down).toHaveBeenCalledTimes(2);
+ expect(page.mouse.up).toHaveBeenCalledTimes(2);
+ expect(page.waitForTimeout).toHaveBeenCalledTimes(2);
+ expect(events).toEqual([
+ 'move',
+ 'down',
+ 'move',
+ 'up',
+ 'wait',
+ 'move',
+ 'down',
+ 'move',
+ 'up',
+ 'wait',
+ ]);
+ });
+});
diff --git a/src/test/testing-library/UiButton.test.tsx b/src/test/testing-library/UiButton.test.tsx
new file mode 100644
index 0000000..cae47c8
--- /dev/null
+++ b/src/test/testing-library/UiButton.test.tsx
@@ -0,0 +1,58 @@
+import { render, fireEvent } from '@testing-library/react';
+import React from 'react';
+
+import { UiButton } from '../../components';
+
+import { testText } from './constants';
+
+describe('UiButton', () => {
+ it('renders the button with the correct props', () => {
+ const onClick: () => void = jest.fn();
+ const { getByRole } = render(
+
+ {testText}
+
+ );
+
+ const button: HTMLElement = getByRole('button', { name: testText });
+ expect(button).toBeEnabled();
+ expect(button).toBeInTheDocument();
+ });
+
+ it('calls the onClick handler when the button is clicked', () => {
+ const onClick: () => void = jest.fn();
+ const { getByRole } = render({testText});
+
+ const button: HTMLElement = getByRole('button', { name: testText });
+ fireEvent.click(button);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ });
+
+ describe('UiButton component', () => {
+ it('renders with given text', () => {
+ const { getByText } = render(Click me);
+ expect(getByText('Click me')).toBeInTheDocument();
+ });
+
+ it('calls onClick handler when clicked', () => {
+ const onClickMock: () => void = jest.fn();
+ const { getByText } = render(Click me);
+ fireEvent.click(getByText('Click me'));
+ expect(onClickMock).toHaveBeenCalled();
+ });
+
+ it('disables button when disabled prop is true', () => {
+ const { getByText } = render(Disabled Button);
+ expect(getByText('Disabled Button')).toBeDisabled();
+ });
+ });
+});
diff --git a/src/test/testing-library/UiCardGrid.test.tsx b/src/test/testing-library/UiCardGrid.test.tsx
new file mode 100644
index 0000000..57a446b
--- /dev/null
+++ b/src/test/testing-library/UiCardGrid.test.tsx
@@ -0,0 +1,46 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import CardGrid from '../../components/UiCardList/CardGrid';
+
+import { cardList, largeCardList, smallCardList } from './constants';
+
+jest.mock('../../components/UiCardItem', () => {
+ const mockReact: typeof import('react') = jest.requireActual('react');
+
+ return {
+ __esModule: true,
+ default: jest.fn(() =>
+ mockReact.createElement('div', {
+ 'data-testid': 'mock-ui-card-item',
+ })
+ ),
+ };
+});
+
+describe('CardGrid component', () => {
+ it('renders with correct props', () => {
+ const { getByTestId } = render(React.createElement(CardGrid, { cardList }));
+
+ const cardGrid: HTMLElement = getByTestId('mock-ui-card-item');
+ expect(cardGrid).toBeInTheDocument();
+ });
+
+ it('renders with smallGrid style when cardList[0].type is smallCard', () => {
+ const { container } = render(React.createElement(CardGrid, { cardList: smallCardList }));
+
+ const gridElement: ChildNode | null = container.firstChild;
+ const computedStyles: CSSStyleDeclaration = window.getComputedStyle(gridElement as Element);
+
+ expect(computedStyles).toHaveProperty('gridTemplateColumns');
+ });
+
+ it('renders with largeGrid style when cardList[0].type is largeGrid', () => {
+ const { container } = render(React.createElement(CardGrid, { cardList: largeCardList }));
+
+ const gridElement: ChildNode | null = container.firstChild;
+ const computedStyles: CSSStyleDeclaration = window.getComputedStyle(gridElement as Element);
+
+ expect(computedStyles).toHaveProperty('gridTemplateColumns');
+ });
+});
diff --git a/src/test/testing-library/UiCardItem.test.tsx b/src/test/testing-library/UiCardItem.test.tsx
new file mode 100644
index 0000000..85c15c3
--- /dev/null
+++ b/src/test/testing-library/UiCardItem.test.tsx
@@ -0,0 +1,72 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import UiCardItem from '../../components/UiCardItem';
+import CardContent from '../../components/UiCardItem/CardContent';
+
+import { cardItem, largeCard, smallCard } from './constants';
+
+const cardTitleRole: string = 'heading';
+
+describe('UiCardItem Component', () => {
+ describe('CardContent', () => {
+ const integrateText: string = 'Integrate';
+ const servicesText: string = 'services';
+
+ it('renders correctly with large card', () => {
+ const { getByText, getByRole } = render();
+
+ const titleElement: HTMLElement = getByRole(cardTitleRole);
+ const textElement: HTMLElement = getByText(cardItem.text);
+
+ expect(titleElement).toBeInTheDocument();
+ expect(titleElement).toHaveTextContent(cardItem.title);
+ expect(textElement).toBeInTheDocument();
+ });
+
+ it('renders correctly with small card', () => {
+ const { getByText, getByRole } = render();
+
+ const titleElement: HTMLElement = getByRole(cardTitleRole);
+ const integrateElement: HTMLElement = getByText(integrateText);
+ const servicesElement: HTMLElement = getByText(servicesText);
+
+ expect(titleElement).toBeInTheDocument();
+ expect(titleElement).toHaveTextContent(cardItem.title);
+ expect(integrateElement).toBeInTheDocument();
+ expect(servicesElement).toBeInTheDocument();
+ });
+ });
+});
+describe('UiCardItem', () => {
+ const stackElementClass: string = '.MuiStack-root';
+
+ it('renders UiCardItem with small card style', () => {
+ const { container, getByText, queryByText } = render();
+
+ const element: HTMLElement | null = container.querySelector(stackElementClass);
+
+ expect(element).toBeInTheDocument();
+ expect(getByText('services')).toBeInTheDocument();
+ expect(queryByText(smallCard.text)).not.toBeInTheDocument();
+ });
+
+ it('renders UiCardItem with large card style', () => {
+ const { container, getByText, queryByText } = render();
+
+ const element: HTMLElement | null = container.querySelector(stackElementClass);
+
+ expect(element).toBeInTheDocument();
+ expect(getByText(largeCard.text)).toBeInTheDocument();
+ expect(queryByText('services')).not.toBeInTheDocument();
+ });
+
+ it('renders correct UiImage', () => {
+ const { getByRole } = render();
+
+ const cardImage: HTMLElement = getByRole('img');
+
+ expect(cardImage).toBeInTheDocument();
+ expect(cardImage).toHaveAttribute('alt', cardItem.alt);
+ });
+});
diff --git a/src/test/testing-library/UiCardList.test.tsx b/src/test/testing-library/UiCardList.test.tsx
new file mode 100644
index 0000000..524a401
--- /dev/null
+++ b/src/test/testing-library/UiCardList.test.tsx
@@ -0,0 +1,25 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import UiCardList from '../../components/UiCardList';
+
+import { cardList } from './constants';
+
+jest.mock('../../components/UiCardList/CardSwiper', () => {
+ const mockReact: typeof import('react') = jest.requireActual('react');
+
+ return jest.fn(() =>
+ mockReact.createElement('div', {
+ 'data-testid': 'card-swiper',
+ })
+ );
+});
+
+describe('UiCardList component', () => {
+ it('renders CardSwiper with correct props', () => {
+ const { getByTestId } = render(React.createElement(UiCardList, { cardList }));
+
+ const cardSwiper: HTMLElement = getByTestId('card-swiper');
+ expect(cardSwiper).toBeInTheDocument();
+ });
+});
diff --git a/src/test/testing-library/UiCheckBox.test.tsx b/src/test/testing-library/UiCheckBox.test.tsx
new file mode 100644
index 0000000..0499075
--- /dev/null
+++ b/src/test/testing-library/UiCheckBox.test.tsx
@@ -0,0 +1,41 @@
+import { render, fireEvent } from '@testing-library/react';
+import React from 'react';
+
+import { UiCheckbox } from '../../components';
+
+import { testText } from './constants';
+
+const mockOnChange: () => void = jest.fn();
+
+const borderStyle: string = 'border: 1px solid #DC3939';
+
+describe('UiCheckbox', () => {
+ it('renders the checkbox with the provided label', () => {
+ const { getByLabelText } = render();
+ const checkboxLabel: HTMLElement = getByLabelText(testText);
+ expect(checkboxLabel).toBeInTheDocument();
+ });
+
+ it('calls the onChange function when the checkbox is clicked', () => {
+ const { getByRole } = render();
+ const checkboxInput: HTMLElement = getByRole('checkbox');
+ fireEvent.click(checkboxInput);
+ expect(mockOnChange).toHaveBeenCalled();
+ });
+
+ it('disables the checkbox when the disabled prop is true', () => {
+ const { getByRole } = render();
+ const checkboxInput: HTMLElement = getByRole('checkbox');
+ expect(checkboxInput).toBeDisabled();
+ });
+
+ it('renders the checkbox with the provided error', () => {
+ const { getByLabelText, getByRole } = render(
+
+ );
+ const checkboxLabel: HTMLElement = getByLabelText(testText);
+ const checkboxInput: HTMLElement = getByRole('checkbox');
+ expect(checkboxLabel).toBeInTheDocument();
+ expect(checkboxInput).toHaveStyle(borderStyle);
+ });
+});
diff --git a/src/test/testing-library/UiCoreContract.test.tsx b/src/test/testing-library/UiCoreContract.test.tsx
new file mode 100644
index 0000000..2c7f206
--- /dev/null
+++ b/src/test/testing-library/UiCoreContract.test.tsx
@@ -0,0 +1,64 @@
+import type { Theme, SxProps } from '@mui/material';
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import { UiButton, UiCheckbox, UiInput, UiLink } from '../../components';
+import type { UiButtonProps } from '../../components/UiButton/types';
+import type { UiCheckboxProps } from '../../components/UiCheckbox/types';
+import type { UiInputProps } from '../../components/UiInput/types';
+import type { UiLinkProps } from '../../components/UiLink/types';
+
+const sharedSxFn: (theme: Theme) => { color: string } = (theme: Theme): { color: string } => ({
+ color: theme.palette.primary.main,
+});
+
+const buttonSxContract: UiButtonProps['sx'] = sharedSxFn;
+const checkboxSxContract: UiCheckboxProps['sx'] = sharedSxFn;
+const inputSxContract: UiInputProps['sx'] = sharedSxFn;
+const linkSxContract: UiLinkProps['sx'] = sharedSxFn;
+
+const inputSharedContractProps: Pick = {
+ size: 'small',
+ variant: 'filled',
+};
+
+type AssertAssignable> = T;
+
+const assertMuiSxContract: >(value: T) => AssertAssignable = <
+ T extends SxProps,
+>(
+ value: T
+): AssertAssignable => value;
+
+const assertedButtonSx: NonNullable =
+ assertMuiSxContract>(buttonSxContract);
+const assertedCheckboxSx: NonNullable =
+ assertMuiSxContract>(checkboxSxContract);
+const assertedInputSx: NonNullable =
+ assertMuiSxContract>(inputSxContract);
+const assertedLinkSx: NonNullable =
+ assertMuiSxContract>(linkSxContract);
+
+describe('Ui core contract', () => {
+ it('exports the four core controls from the package entrypoint', () => {
+ expect(UiButton).toBeDefined();
+ expect(UiCheckbox).toBeDefined();
+ expect(UiInput).toBeDefined();
+ expect(UiLink).toBeDefined();
+ expect(assertedButtonSx).toBeDefined();
+ expect(assertedCheckboxSx).toBeDefined();
+ expect(assertedInputSx).toBeDefined();
+ expect(assertedLinkSx).toBeDefined();
+ expect(inputSharedContractProps).toEqual({
+ size: 'small',
+ variant: 'filled',
+ });
+ });
+
+ it('forwards size and variant to UiInput', () => {
+ const { container } = render();
+
+ expect(container.querySelector('.MuiFilledInput-root')).toBeInTheDocument();
+ expect(container.querySelector('.MuiInputBase-sizeSmall')).toBeInTheDocument();
+ });
+});
diff --git a/src/test/testing-library/UiDefaultFooter.test.tsx b/src/test/testing-library/UiDefaultFooter.test.tsx
new file mode 100644
index 0000000..9ed7972
--- /dev/null
+++ b/src/test/testing-library/UiDefaultFooter.test.tsx
@@ -0,0 +1,24 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import DefaultFooter from '../../components/UiFooter/DefaultFooter';
+
+import { mockedSocialLinks } from './constants';
+
+const mockedDate: number = new Date().getFullYear();
+const defaultFooterClass: string = '.MuiStack-root';
+const logoAlt: string = 'Vilna logo';
+const copyright: RegExp = /Copyright/;
+
+describe('DefaultFooter', () => {
+ it('should render the component correctly', () => {
+ const { container, getByAltText, getByText } = render(
+
+ );
+
+ expect(container.querySelector(defaultFooterClass)).toBeInTheDocument();
+ expect(getByAltText(logoAlt)).toBeInTheDocument();
+ expect(getByText(copyright)).toBeInTheDocument();
+ expect(getByText(mockedDate.toString())).toBeInTheDocument();
+ });
+});
diff --git a/src/test/testing-library/UiFooter.test.tsx b/src/test/testing-library/UiFooter.test.tsx
new file mode 100644
index 0000000..6987441
--- /dev/null
+++ b/src/test/testing-library/UiFooter.test.tsx
@@ -0,0 +1,29 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import UiFooter from '../../components/UiFooter';
+
+const stackElementClass: string = '.MuiStack-root';
+const containerElementClass: string = '.MuiContainer-root';
+
+describe('UiFooter Component', () => {
+ it('renders DefaultFooter component with provided social links', () => {
+ const { container } = render();
+
+ const footerElement: HTMLElement | null = container.querySelector('footer');
+ const defaultFooterWrapper: HTMLElement | null = container.querySelector(stackElementClass);
+
+ expect(footerElement).toBeInTheDocument();
+ expect(defaultFooterWrapper).toBeInTheDocument();
+ });
+
+ it('renders Mobile component with provided social links', () => {
+ const { container } = render();
+
+ const footerElement: HTMLElement | null = container.querySelector('footer');
+ const mobileWrapper: HTMLElement | null = container.querySelector(containerElementClass);
+
+ expect(footerElement).toBeInTheDocument();
+ expect(mobileWrapper).toBeInTheDocument();
+ });
+});
diff --git a/src/test/testing-library/UiFooterEmail.test.tsx b/src/test/testing-library/UiFooterEmail.test.tsx
new file mode 100644
index 0000000..e1fafbf
--- /dev/null
+++ b/src/test/testing-library/UiFooterEmail.test.tsx
@@ -0,0 +1,40 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import VilnaCRMEmail from '../../components/UiFooter/VilnaCRMEmail';
+
+import { mockEmail } from './constants';
+
+describe('VilnaCRMEmail component', () => {
+ const originalEmail: string | undefined = process.env.REACT_APP_VILNACRM_GMAIL;
+
+ afterEach(() => {
+ if (originalEmail === undefined) {
+ delete process.env.REACT_APP_VILNACRM_GMAIL;
+ return;
+ }
+
+ process.env.REACT_APP_VILNACRM_GMAIL = originalEmail;
+ });
+
+ it('renders email address correctly', () => {
+ delete process.env.REACT_APP_VILNACRM_GMAIL;
+
+ const { getByText } = render();
+
+ const emailLink: HTMLElement = getByText(mockEmail);
+ expect(emailLink).toBeInTheDocument();
+ });
+
+ it('uses the configured email for both text and mailto href', () => {
+ const configuredEmail: string = 'support@example.com';
+ process.env.REACT_APP_VILNACRM_GMAIL = configuredEmail;
+
+ const { getByRole } = render();
+
+ expect(getByRole('link', { name: configuredEmail })).toHaveAttribute(
+ 'href',
+ `mailto:${configuredEmail}`
+ );
+ });
+});
diff --git a/src/test/testing-library/UiImage.test.tsx b/src/test/testing-library/UiImage.test.tsx
new file mode 100644
index 0000000..81c8095
--- /dev/null
+++ b/src/test/testing-library/UiImage.test.tsx
@@ -0,0 +1,17 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import { UiImage } from '../../components';
+
+import { testImg, testText } from './constants';
+
+describe('UiImage', () => {
+ it('renders the image with the correct props', () => {
+ const { getByAltText } = render(
+
+ );
+
+ const image: HTMLElement = getByAltText(testText);
+ expect(image).toBeInTheDocument();
+ });
+});
diff --git a/src/test/testing-library/UiInput.test.tsx b/src/test/testing-library/UiInput.test.tsx
new file mode 100644
index 0000000..12f9502
--- /dev/null
+++ b/src/test/testing-library/UiInput.test.tsx
@@ -0,0 +1,58 @@
+import { render, fireEvent } from '@testing-library/react';
+import React from 'react';
+
+import { UiInput } from '../../components';
+
+import { testText, testEmail, testPlaceholder } from './constants';
+
+const testType: string = 'email';
+
+describe('UiInput', () => {
+ it('renders the input with the provided props', () => {
+ const { getByPlaceholderText } = render(
+
+ );
+ const inputElement: HTMLElement = getByPlaceholderText(testPlaceholder);
+ expect(inputElement).toBeInTheDocument();
+ expect(inputElement).toHaveAttribute('type', testType);
+ expect(inputElement).toHaveValue(testEmail);
+ });
+
+ it('calls the onChange function when the input value changes', () => {
+ const mockOnChange: () => void = jest.fn();
+ const { getByRole } = render();
+ const inputElement: HTMLElement = getByRole('textbox');
+ fireEvent.change(inputElement, { target: { value: testText } });
+ expect(mockOnChange).toHaveBeenCalled();
+ });
+
+ it('calls the onBlur function when the input loses focus', () => {
+ const mockOnBlur: () => void = jest.fn();
+ const { getByRole } = render();
+ const inputElement: HTMLElement = getByRole('textbox');
+ fireEvent.blur(inputElement);
+ expect(mockOnBlur).toHaveBeenCalled();
+ });
+
+ it('applies the correct styles based on the error prop', () => {
+ const { rerender, getByRole } = render();
+ let inputElement: HTMLElement = getByRole('textbox');
+ expect(inputElement).toBeInTheDocument();
+ expect(inputElement).toHaveAttribute('aria-invalid', 'false');
+
+ rerender();
+ inputElement = getByRole('textbox');
+ expect(inputElement).toBeInTheDocument();
+ expect(inputElement).toHaveAttribute('aria-invalid', 'true');
+ });
+
+ it('disables the input when the disabled prop is true', () => {
+ const { getByRole } = render();
+ const inputElement: HTMLElement = getByRole('textbox');
+ expect(inputElement).toBeDisabled();
+ });
+
+ it('should be a non-empty string', () => {
+ expect(UiInput.displayName).toBe('UiInput');
+ });
+});
diff --git a/src/test/testing-library/UiLink.test.tsx b/src/test/testing-library/UiLink.test.tsx
new file mode 100644
index 0000000..8bc0488
--- /dev/null
+++ b/src/test/testing-library/UiLink.test.tsx
@@ -0,0 +1,22 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import { UiLink } from '../../components';
+
+import { testText, testUrl } from './constants';
+
+describe('UiLink', () => {
+ it('renders the Link with the provided children and href', () => {
+ const testHref: string = testUrl;
+ const { getByText } = render({testText});
+ const linkElement: HTMLElement = getByText(testText);
+ expect(linkElement).toBeInTheDocument();
+ expect(linkElement).toHaveAttribute('href', testHref);
+ });
+
+ it('applies the theme provided to the Link', () => {
+ const { getByText } = render({testText});
+ const linkElement: HTMLElement = getByText(testText);
+ expect(linkElement).toBeInTheDocument();
+ });
+});
diff --git a/src/test/testing-library/UiMobile.test.tsx b/src/test/testing-library/UiMobile.test.tsx
new file mode 100644
index 0000000..65b9aef
--- /dev/null
+++ b/src/test/testing-library/UiMobile.test.tsx
@@ -0,0 +1,24 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import Mobile from '../../components/UiFooter/Mobile';
+
+import { mockedSocialLinks } from './constants';
+
+const mockedDate: number = new Date().getFullYear();
+const defaultFooterClass: string = '.MuiContainer-root';
+const logoAlt: string = 'Vilna logo';
+const copyright: RegExp = /Copyright/;
+
+describe('DefaultFooter', () => {
+ it('should render the component correctly', () => {
+ const { container, getByAltText, getByText } = render(
+
+ );
+
+ expect(container.querySelector(defaultFooterClass)).toBeInTheDocument();
+ expect(getByAltText(logoAlt)).toBeInTheDocument();
+ expect(getByText(copyright)).toBeInTheDocument();
+ expect(getByText(mockedDate.toString())).toBeInTheDocument();
+ });
+});
diff --git a/src/test/testing-library/UiTextFieldForm.test.tsx b/src/test/testing-library/UiTextFieldForm.test.tsx
new file mode 100644
index 0000000..08f76a5
--- /dev/null
+++ b/src/test/testing-library/UiTextFieldForm.test.tsx
@@ -0,0 +1,49 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import React from 'react';
+import { useForm } from 'react-hook-form';
+
+import { UiTextFieldForm } from '../../components';
+
+import { testPlaceholder, testText } from './constants';
+
+describe('UiTextFieldForm', () => {
+ function TestWrapper(): React.ReactElement {
+ const { control, handleSubmit } = useForm();
+ const onSubmit: () => void = jest.fn();
+
+ return (
+
+ );
+ }
+
+ it('renders the UiInput component with the correct props', () => {
+ render();
+
+ const uiInput: HTMLElement = screen.getByRole('textbox');
+
+ expect(uiInput).toHaveAttribute('type', 'text');
+ expect(uiInput).toHaveAttribute('placeholder', testPlaceholder);
+ expect(uiInput).toHaveValue('');
+ expect(uiInput).not.toHaveAttribute('error');
+ });
+
+ it('updates the form field value on input change', () => {
+ render();
+
+ const uiInput: HTMLElement = screen.getByRole('textbox');
+
+ fireEvent.change(uiInput, { target: { value: testText } });
+
+ expect(uiInput).toHaveValue(testText);
+ });
+});
diff --git a/src/test/testing-library/UiToolbar.test.tsx b/src/test/testing-library/UiToolbar.test.tsx
new file mode 100644
index 0000000..2b6111c
--- /dev/null
+++ b/src/test/testing-library/UiToolbar.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import { UiToolbar } from '../../components';
+
+import { testText } from './constants';
+
+describe('UiToolbar', () => {
+ it('renders the Toolbar with the children', () => {
+ const { getByText } = render({testText});
+ const toolbarElement: HTMLElement = getByText(testText);
+ expect(toolbarElement).toBeInTheDocument();
+ });
+});
diff --git a/src/test/testing-library/UiTooltip.test.tsx b/src/test/testing-library/UiTooltip.test.tsx
new file mode 100644
index 0000000..156610c
--- /dev/null
+++ b/src/test/testing-library/UiTooltip.test.tsx
@@ -0,0 +1,29 @@
+import { ThemeProvider } from '@mui/material';
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import { UiTooltip } from '../../components';
+import theme from '../../components/UiTooltip/theme';
+
+import { testText } from './constants';
+
+const title: string = testText;
+const placement: 'top' | 'bottom' | 'left' | 'right' = 'top';
+const sx: object = { color: 'red' };
+const children: React.ReactNode = {testText}
;
+
+describe('UiTooltip', () => {
+ it('renders the tooltip with the correct props', () => {
+ const { getByText } = render(
+
+
+ {children}
+
+
+ );
+
+ const trigger: HTMLElement = getByText(testText);
+
+ expect(trigger).toBeInTheDocument();
+ });
+});
diff --git a/src/test/testing-library/UiTooltipWrapper.test.tsx b/src/test/testing-library/UiTooltipWrapper.test.tsx
new file mode 100644
index 0000000..3ae9112
--- /dev/null
+++ b/src/test/testing-library/UiTooltipWrapper.test.tsx
@@ -0,0 +1,64 @@
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+
+import WrapperUiTooltip from '../../components/UiTooltip/TooltipWrapper';
+
+const triggerText: string = 'Trigger';
+const tooltipContent: string = 'Tooltip Text';
+const tooltipRole: string = 'tooltip';
+
+jest.mock('@mui/material', () => ({
+ ...jest.requireActual('@mui/material'),
+ useMediaQuery: jest.fn(),
+}));
+
+describe('WrapperUiTooltip', () => {
+ const setup: () => void = () => {
+ render({triggerText});
+ };
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the tooltip trigger', () => {
+ setup();
+ const trigger: HTMLElement = screen.getByText(triggerText);
+ expect(trigger).toBeInTheDocument();
+ });
+
+ it('opens the tooltip on click', () => {
+ setup();
+ const trigger: HTMLElement = screen.getByText(triggerText);
+ fireEvent.click(trigger);
+ const tooltip: HTMLElement = screen.getByText(tooltipContent);
+ expect(tooltip).toBeInTheDocument();
+ });
+
+ it('closes the tooltip on click away', () => {
+ setup();
+ const trigger: HTMLElement = screen.getByText(triggerText);
+ fireEvent.click(trigger);
+ const tooltip: HTMLElement = screen.getByText(tooltipContent);
+ expect(tooltip).toBeInTheDocument();
+ });
+
+ it('open and clone tooltip', async () => {
+ const user: { click: (element: Element) => Promise } = userEvent.setup();
+ setup();
+
+ const trigger: HTMLElement = screen.getByText(triggerText);
+ await user.click(trigger);
+
+ let tooltip: HTMLElement | null = screen.getByRole(tooltipRole);
+ expect(tooltip).toBeInTheDocument();
+
+ await user.click(document.body);
+
+ await waitFor(() => {
+ tooltip = screen.queryByRole(tooltipRole);
+ expect(tooltip).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/test/testing-library/UiTypography.test.tsx b/src/test/testing-library/UiTypography.test.tsx
new file mode 100644
index 0000000..c8ef083
--- /dev/null
+++ b/src/test/testing-library/UiTypography.test.tsx
@@ -0,0 +1,40 @@
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import { UiTypography } from '../../components';
+
+import { testText } from './constants';
+
+describe('UiTypography', () => {
+ it('should render the Typography component with the correct props', () => {
+ const { getByText } = render(
+
+ {testText}
+
+ );
+
+ const typography: HTMLElement = getByText(testText);
+ expect(typography).toBeInTheDocument();
+ });
+
+ it('should render the Typography component with the default props', () => {
+ const { getByText } = render({testText});
+
+ const typography: HTMLElement = getByText(testText);
+ expect(typography.tagName).toBe('P');
+ });
+
+ it('renders with default component "p" when component prop is not provided', () => {
+ const { container } = render(Test Text);
+ const element: HTMLElement | null = container.querySelector('p');
+ expect(element).toBeInTheDocument();
+ expect(element).toHaveTextContent('Test Text');
+ });
+
+ it('renders with specified component when component prop is provided', () => {
+ const { container } = render(Test Text);
+ const element: HTMLElement | null = container.querySelector('h1');
+ expect(element).toBeInTheDocument();
+ expect(element).toHaveTextContent('Test Text');
+ });
+});
diff --git a/src/test/testing-library/constants.ts b/src/test/testing-library/constants.ts
new file mode 100644
index 0000000..3b53089
--- /dev/null
+++ b/src/test/testing-library/constants.ts
@@ -0,0 +1,83 @@
+import { faker } from '@faker-js/faker';
+
+import { CardItem } from '@/components/UiCardList/types';
+import { SocialMedia } from '@/components/UiFooter/types';
+
+export const testId: string = faker.string.uuid();
+export const testTitle: string = faker.lorem.word(6);
+export const testText: string = faker.lorem.word(6);
+export const testImg: string = faker.image.avatar();
+export const testInitials: string = faker.person.fullName();
+export const testEmail: string = faker.internet.email();
+export const testPassword: string = faker.internet.password();
+export const testPlaceholder: string = faker.lorem.word(8);
+export const testUrl: string = faker.internet.url();
+export const mockEmail: string = 'info@vilnacrm.com';
+
+export const typeOfCard: string = 'smallCard';
+
+export const cardItem: CardItem = {
+ id: testId,
+ title: testTitle,
+ text: testText,
+ type: typeOfCard,
+ alt: testText,
+ imageSrc: testImg,
+};
+export const smallCard: CardItem = {
+ id: testId,
+ title: testTitle,
+ text: testText,
+ type: 'smallCard',
+ alt: testText,
+ imageSrc: testImg,
+};
+export const largeCard: CardItem = {
+ id: testId,
+ title: testTitle,
+ text: testText,
+ type: 'largeCard',
+ alt: testText,
+ imageSrc: testImg,
+};
+
+export const cardList: CardItem[] = [
+ {
+ id: testId,
+ title: testTitle,
+ text: testText,
+ type: typeOfCard,
+ alt: testText,
+ imageSrc: testImg,
+ },
+];
+export const smallCardList: CardItem[] = [
+ {
+ id: testId,
+ title: testTitle,
+ text: testText,
+ type: 'smallCard',
+ alt: testText,
+ imageSrc: testImg,
+ },
+];
+export const largeCardList: CardItem[] = [
+ {
+ id: testId,
+ title: testTitle,
+ text: testText,
+ type: 'largeCard',
+ alt: testText,
+ imageSrc: testImg,
+ },
+];
+
+export const mockedSocialLinks: SocialMedia[] = [
+ {
+ id: testId,
+ icon: testImg,
+ alt: testText,
+ linkHref: 'https://www.instagram.com/',
+ ariaLabel: testTitle,
+ },
+];
diff --git a/src/types/.gitignore b/src/types/.gitignore
new file mode 100755
index 0000000..e69de29
diff --git a/src/utils/.gitignore b/src/utils/.gitignore
new file mode 100755
index 0000000..e69de29
diff --git a/stryker.config.mjs b/stryker.config.mjs
new file mode 100644
index 0000000..33e1194
--- /dev/null
+++ b/stryker.config.mjs
@@ -0,0 +1,16 @@
+/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
+const config = {
+ packageManager: 'npm',
+ reporters: ['html', 'clear-text', 'progress'],
+ testRunner: 'jest',
+ coverageAnalysis: 'perTest',
+ plugins: ['@stryker-mutator/jest-runner'],
+ tsconfigFile: 'tsconfig.json',
+ concurrency: 2,
+ timeoutMS: 20000,
+ timeoutFactor: 4,
+ mutate: ['./src/components/**/index.tsx'],
+ thresholds: { high: 90, break: 80 },
+};
+
+export default config;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100755
index 0000000..fd6b301
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,35 @@
+{
+ "compilerOptions": {
+ "target": "ES6",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true
+ },
+ "include": [
+ "./src",
+ "./pages",
+ "./scripts",
+ "next-env.d.ts",
+ "/*.ts",
+ "/.tsx",
+ "playwrite",
+ "src/components/UiFooter/.stories.tsx",
+ "memoryLeak"
+ ],
+ "exclude": ["node_modules"],
+ "extends": "./tsconfig.paths.json"
+}
diff --git a/tsconfig.paths.json b/tsconfig.paths.json
new file mode 100755
index 0000000..b8d6842
--- /dev/null
+++ b/tsconfig.paths.json
@@ -0,0 +1,7 @@
+{
+ "compilerOptions": {
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}