Compare commits

..

91 Commits

Author SHA1 Message Date
unkn0wn 94acacbd6c fix: update current version to 1.2.1 in project status section of README.md
Deploy Documentation / check-and-deploy (push) Successful in 11s
2026-06-24 09:23:50 +02:00
unkn0wn 6811dda340 fix: update version to 1.2.1 in package.json and related components
Deploy Documentation / check-and-deploy (push) Successful in 11s
Publish to npm / check-and-publish (push) Successful in 19s
2026-06-24 09:21:40 +02:00
unkn0wn bd0e5b0191 fix: refactor modal close button to use icon directly and remove unnecessary button element 2026-06-24 09:20:43 +02:00
unkn0wn 23a4175bc6 fix: remove paths filter from deployment trigger and update layout title and badge version
Deploy Documentation / check-and-deploy (push) Successful in 10s
2026-06-24 09:15:47 +02:00
unkn0wn 30a3b32d13 fix: remove version check condition from deployment step 2026-06-24 09:01:01 +02:00
unkn0wn 0405565fd1 fix: update deployment step to delete existing files before copying new documentation 2026-06-24 09:00:23 +02:00
unkn0wn 352ec260fa fix: remove conditional checks for version changes in deploy workflow 2026-06-24 08:58:45 +02:00
unkn0wn 53b3ee4301 fix: update deploy workflow to install Node.js requirements before checkout 2026-06-24 08:57:03 +02:00
unkn0wn 88d56bbd38 fix: remove version check step from deploy workflow 2026-06-24 08:55:19 +02:00
unkn0wn a40c1d1a7e fix: update git clone URL in deploy workflow 2026-06-24 08:54:42 +02:00
unkn0wn 237abcbd63 fix: replace actions/checkout with custom checkout logic in deploy workflow 2026-06-24 08:53:17 +02:00
unkn0wn 4777b1c68e fix: update deploy workflow to remove unnecessary comments and adjust container settings 2026-06-24 08:51:49 +02:00
unkn0wn 4adc9b9eb6 fix: update deploy workflow to use container volumes and clean up comments 2026-06-24 08:46:21 +02:00
unkn0wn acc9bcb98b fix: clean up old documentation files before deployment 2026-06-24 08:38:40 +02:00
unkn0wn 33131a8275 feat: add workflow_dispatch trigger to deploy documentation workflow 2026-06-24 08:32:01 +02:00
unkn0wn 131d8376d4 feat: add GitHub Actions workflow for documentation deployment 2026-06-24 08:24:04 +02:00
unkn0wn 71effe979a chore: update version to 1.2.0 and fix license field in package.json
Publish to npm / check-and-publish (push) Successful in 19s
2026-06-24 07:59:21 +02:00
unkn0wn 06a1695be4 fix: add missing newline at end of package.json
Publish to npm / check-and-publish (push) Successful in 20s
2026-06-24 07:56:06 +02:00
unkn0wn 734a593834 chore: update package.json with repository, bugs, homepage, and keywords information 2026-06-24 07:54:03 +02:00
unkn0wn a2ae6ce8a2 chore: bump version to 1.1.1 and update files list in package.json 2026-06-24 07:51:44 +02:00
unkn0wn 078b1d6e85 refactor: update attribution and README to reflect removal of custom icon components and source from Lucide 2026-06-24 07:45:35 +02:00
unkn0wn 5bcf5b20a0 chore: bump version to 1.01.0 in package.json
Publish to npm / check-and-publish (push) Successful in 20s
2026-06-24 07:14:02 +02:00
unkn0wn e0ed0f557b fix: align export statements formatting in index.ts 2026-06-24 07:13:37 +02:00
unkn0wn 276bb36586 Remove source comment from _spacing.scss 2026-06-24 07:06:20 +02:00
unkn0wn b697bb8491 Remove unused SVG icons and related components for sorting and uploading; clean up index.astro by eliminating icon imports and references. 2026-06-24 06:57:58 +02:00
unkn0wn 5edab8d12d chore: bump version to 1.00.3 in package.json
Publish to npm / check-and-publish (push) Successful in 22s
2026-06-24 06:47:25 +02:00
unkn0wn c15a6ceff1 refactor: replace icon imports with Lucide icons across components 2026-06-24 06:46:29 +02:00
unkn0wn f56077dba4 fix: add legacy-peer-deps flag to npm ci command in publish workflow 2026-06-24 06:19:17 +02:00
unkn0wn 6595e8f4e0 chore: rename LICENSE TO LICENSE.md 2026-06-24 06:16:02 +02:00
unkn0wn ca983468f0 Refactor code structure for improved readability and maintainability
Publish to npm / check-and-publish (push) Failing after 13s
2026-06-23 12:59:53 +02:00
unkn0wn cd75e773f6 chore: removing galerie page 2026-06-23 12:58:46 +02:00
unkn0wn 75aa782653 feat: updating package to version 1.00.1
Publish to npm / check-and-publish (push) Successful in 22s
2026-06-23 12:39:43 +02:00
unkn0wn f6594822c6 style: remove unnecessary width override from textarea in textField component 2026-06-23 12:39:31 +02:00
unkn0wn 027cec577d feat: updating package to version 1.00.0
Publish to npm / check-and-publish (push) Successful in 22s
2026-06-23 12:36:10 +02:00
unkn0wn 6e077aaeed style: update Navbar and Select component styles for improved consistency and usability 2026-06-23 12:35:23 +02:00
unkn0wn 1d9001705f feat: adding Sidebar and SidebarItem 2026-06-23 12:35:12 +02:00
unkn0wn 1878ec3a0e feat: add Card and Navbar components with styles
- Implemented Card component with cover, title, subtitle, content, and footer slots.
- Added styles for Card component in _card.scss.
- Created Navbar component with brand and links, including styles in _navbar.scss.
- Updated index.astro to include new Card and Navbar components.
2026-06-23 11:40:04 +02:00
unkn0wn b48581b089 feat: implement Modal component with overlay and dialog structure 2026-06-23 11:25:39 +02:00
unkn0wn 3593305512 feat: add Tooltip component and update styles in Button and index page 2026-06-23 11:08:24 +02:00
unkn0wn 6967f3c7cd refactor: reorganize component imports and enhance index page layout
- Grouped component imports into categories for better organization.
- Updated the layout of the index page to include a gallery structure.
- Added new sections for Button, Badge, Selection Controls, TextField, Select, Numeric Stepper, Avatar, Loading Bar, List Item, Link, Tabs, Breadcrumb, Pagination, Notification, Tooltip, Card, Navbar, Sidebar, Modal, and Icons.
- Improved styling for better visual hierarchy and spacing.
- Implemented a theme toggle feature with persistent state using localStorage.
2026-06-23 10:51:40 +02:00
unkn0wn dea517fbe5 feat: adding Badge, Breadcrumb, Checkbox et Radio components 2026-06-23 10:12:04 +02:00
unkn0wn cc54ae46ff style: refactor Button and numericStepper components for improved accessibility and styling 2026-06-23 10:07:39 +02:00
unkn0wn b2f4cfe393 fix: update version to 0.06.1 in package.json
Publish to npm / check-and-publish (push) Successful in 20s
2026-06-23 08:02:52 +02:00
unkn0wn 3e4d609d6c style: refactor avatar component styles 2026-06-23 08:02:31 +02:00
unkn0wn 455acd9552 style: refine typo tokens 2026-06-23 08:00:54 +02:00
unkn0wn 50e7618232 style: refine color tokens for light and dark themes 2026-06-23 07:58:42 +02:00
unkn0wn c4eb4cc507 style: update textField styles 2026-06-23 07:55:38 +02:00
unkn0wn 08a6e82760 fix: remove redundant closing div in index.astro 2026-06-23 07:26:25 +02:00
unkn0wn 2e36ac3b3e chore: update astro dependency to version 7.0.0 2026-06-23 07:26:15 +02:00
unkn0wn 46020db913 feat: add Attribution file listing third-party icons used in NDS 2026-06-23 07:10:40 +02:00
unkn0wn bacbde56ad chore: rename LICENSE.md to LICENSE 2026-06-23 07:04:48 +02:00
unkn0wn f8f2e23573 feat: update LICENSE.md to clarify usage rights and restrictions 2026-06-23 07:04:14 +02:00
unkn0wn b2a61008ba feat: updating package to version 0.06.0
Publish to npm / check-and-publish (push) Successful in 23s
2026-06-18 13:52:07 +02:00
unkn0wn 5d65cecf52 feat: add Pagination and PaginationNumber components with styles and integration into index page 2026-06-18 13:51:31 +02:00
unkn0wn c80d9b5259 feat: add TextField component with styles and integration into index page 2026-06-18 08:52:51 +02:00
unkn0wn 3e6d89830b feat: update README for version 0.05.0 and move Select components imports 2026-06-16 14:18:17 +02:00
unkn0wn f88dd862bf feat: updating package to version 0.05.0
Publish to npm / check-and-publish (push) Successful in 22s
2026-06-16 14:11:10 +02:00
unkn0wn 8c1e36aaa7 feat: add Select and SelectOption components with styles and integration into index page 2026-06-16 14:10:49 +02:00
unkn0wn 03a92db05e feat: updating package to version 0.04.0
Publish to npm / check-and-publish (push) Successful in 23s
2026-06-16 08:32:33 +02:00
unkn0wn 7c65e70c8f feat: add Avatar component and integrate into index page 2026-06-16 08:30:29 +02:00
unkn0wn b61720fd24 feat: update README with current version and component list enhancements 2026-06-09 14:47:58 +02:00
unkn0wn 4d7c882324 feat: updating package to version 0.03.0
Publish to npm / check-and-publish (push) Successful in 21s
2026-06-09 14:32:48 +02:00
unkn0wn c508d6ae31 feat: add NumericStepper component and integrate into index page 2026-06-09 14:30:40 +02:00
unkn0wn 02ae2b143e feat: add icon support to Button component 2026-06-09 14:08:53 +02:00
unkn0wn 40505bd05e Merge branch 'main' of https://git.novaprojects.dev/unkn0wn/nova-design-system 2026-06-09 13:15:04 +02:00
unkn0wn ef22eb5615 feat: add LoadingBar component with known and unknown states 2026-06-09 13:15:01 +02:00
unkn0wn 2dd911dbc5 feat: update package to version 0.02.0
Publish to npm / check-and-publish (push) Successful in 1m42s
2026-06-04 12:32:57 +00:00
unkn0wn 27134ea118 feat: adding auto publish pipeline 2026-06-04 12:31:51 +00:00
unkn0wn ee8be2e20b fix: updating readme to give accurate informations 2026-06-04 12:13:30 +00:00
unkn0wn b3ec60db5d feat: add ListItem component with title and subtitle 2026-06-04 14:07:25 +02:00
unkn0wn 4af10b10f7 fix: update package name to include scope and clean up scripts 2026-06-04 13:44:47 +02:00
LOUIS POTEVIN feebe17abf feat: add Link component with hover and active states 2026-06-04 12:49:20 +02:00
LOUIS POTEVIN 5600107e53 feat: add Button component with primary and secondary styles, including disabled state 2026-06-04 12:09:34 +02:00
LOUIS POTEVIN c585ec3266 feat: add Tabs component with Tab, TabItem, and TabContent for improved navigation 2026-06-04 11:36:32 +02:00
LOUIS POTEVIN dd48a02e3b fix: removing obsolete component listing file 2026-06-04 11:36:18 +02:00
LOUIS POTEVIN 3cdb0b709e fix: adding generic font family 2026-06-04 10:05:09 +02:00
LOUIS POTEVIN 72d5425090 fix: Update sort icons to use currentColor for theming support 2026-05-27 15:18:52 +02:00
LOUIS POTEVIN dd6cd34a61 feat: update Toggle component to use 'data-checked' prop and adding theme toggle functionality, default to dark mode 2026-05-27 15:09:52 +02:00
LOUIS POTEVIN 17f37eec5a fix: update SVG icons to use 'none' for fill attribute 2026-05-27 15:07:15 +02:00
LOUIS POTEVIN 96b8192408 fix: removing comment 2026-05-27 14:45:38 +02:00
LOUIS POTEVIN 1aa34d2640 feat: add Toggle component 2026-05-27 14:45:19 +02:00
LOUIS POTEVIN 079bacf3a6 feat: refactor notification styles and structure 2026-05-27 13:23:51 +02:00
LOUIS POTEVIN 8b1016b1aa feat: add multiple notification types to the Notifications section 2026-05-27 13:23:26 +02:00
LOUIS POTEVIN c2e13d5c48 feat: rename color variables from correct to success 2026-05-27 13:21:35 +02:00
LOUIS POTEVIN eadf6481e8 Update SVG icons to use currentColor for fill and stroke attributes
- Changed fill attribute from "none" to "currentColor" in sort and stats SVGs.
- Updated stroke color to "currentColor" in upload SVGs.
- Ensured all SVGs maintain their original design while allowing for dynamic color changes based on parent elements.
2026-05-27 13:21:17 +02:00
LOUIS POTEVIN 7e803a24c4 feat: update dependancies versions and setting up new color themes and new fonts 2026-05-27 12:04:06 +02:00
unkn0wn e8ed7e2a73 feat: add Notification component with styling and integration in the main page 2026-05-15 21:26:16 +02:00
unkn0wn b807a2a3c4 feat: update Sort icon to remove the use of currentColor 2026-05-15 21:01:57 +02:00
unkn0wn 98db7328cb feat: add icon set with size variants
Added icons:
- Arrow2Icon
- BinIcon
- BurgerIcon
- CalendarIcon
- CheckIcon
- CloseIcon
- CodeIcon
- CubeIcon
- DownloadIcon
- FilterIcon
- HelpIcon
- HomeIcon
- LinkIcon
- MinusIcon
- MoreIcon
- OverviewIcon
- PlusIcon
- ProfileIcon
- SearchIcon
- SettingsIcon
- ShareIcon
- ShieldIcon
- SortIcon
- StatsIcon
- UploadIcon

Available sizes:
- 16px
- 24px
- 32px

SortIcon variants:
- default
- ascend
- descend

Notes:
- Added a dedicated icon grid preview in the design system page
- Verified consistency across all supported sizes
2026-05-15 20:59:28 +02:00
unkn0wn 68641ef5fe feat: add Upload icon with multiple sizes (16, 24, 32) and update index export 2026-05-15 19:30:42 +02:00
unkn0wn ed7bc3ab6b feat: add Close icon with multiple sizes (16, 24, 32) and update index export 2026-05-15 19:07:08 +02:00
93 changed files with 4802 additions and 2366 deletions
+37
View File
@@ -0,0 +1,37 @@
name: Deploy Documentation
on:
push:
branches:
- main
workflow_dispatch:
jobs:
check-and-deploy:
runs-on: host
steps:
- name: Install Node.js requirements for Actions
run: |
if ! command -v node &> /dev/null; then
apk add --no-cache nodejs npm
fi
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Build documentation
run: npm run build
- name: Deploy to Apache volume
run: |
find /var/www/design-system -mindepth 1 -delete
cp -r dist/. /var/www/design-system/
chmod -R 755 /var/www/design-system/
+52
View File
@@ -0,0 +1,52 @@
name: Publish to npm
on:
push:
branches:
- main
paths:
- 'package.json' # se déclenche uniquement si package.json change
jobs:
check-and-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2 # pour pouvoir comparer avec le commit précédent
- name: Check if version changed
id: version_check
run: |
PREV=$(git show HEAD~1:package.json | node -p "require('/dev/stdin').version" 2>/dev/null || echo "none")
CURR=$(node -p "require('./package.json').version")
echo "prev=$PREV"
echo "curr=$CURR"
if [ "$PREV" != "$CURR" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
- name: Setup Node
if: steps.version_check.outputs.changed == 'true'
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Update npm
if: steps.version_check.outputs.changed == 'true'
run: npm install -g npm@latest
- name: Install dependencies
if: steps.version_check.outputs.changed == 'true'
run: npm ci --legacy-peer-deps
- name: Publish
if: steps.version_check.outputs.changed == 'true'
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+18
View File
@@ -0,0 +1,18 @@
# Attribution
This file lists all third-party assets used within Nova Design System (NDS) that are not proprietary to the project, as referenced in `LICENSE.md` (section 4.1).
## Icons
As of version `1.00.3`, NDS no longer ships a custom set of per-icon SVG wrapper components. All icons rendered inside NDS components (e.g. `Select`, `Avatar`, `Notification`, `numericStepper`, `Modal`) are sourced directly from the **[Lucide](https://lucide.dev/)** icon set via the **[`@lucide/astro`](https://www.npmjs.com/package/@lucide/astro)** package (license: **ISC** — no attribution required, commercial use allowed).
This means there is no longer a per-component mapping to maintain here: any icon visible in NDS is a Lucide icon, used directly through `@lucide/astro`. The previous standalone `Icons` component family (`Arrow2Icon`, `BinIcon`, `BurgerIcon`, `SortIcon`, `UploadIcon`, etc.) has been removed from the package.
## Custom Assets
Any icon or asset **not covered above** is created specifically for NDS and falls under section 4.2 of `LICENSE.md` instead. At the time of writing, there are no remaining custom (non-Lucide) icons in the project.
## Notes
- The Lucide icon set is released under the ISC license, which permits commercial use without requiring attribution. This file is maintained anyway for transparency and traceability.
- This file must be kept up to date whenever a new third-party icon set or asset is added to NDS.
+28 -12
View File
@@ -8,27 +8,43 @@ This license applies to both the public and private versions of the repository.
## 2. Usage Rights and Restrictions ## 2. Usage Rights and Restrictions
### 2.1 Personal and Internal Use ### 2.1 General Use
Use of NDS for personal projects and internal projects is permitted. NDS may be used freely and without limitation in personal, internal, and commercial projects — including projects developed for paying clients, such as the design or development of a website, application, or service.
You may study the project and draw inspiration from it, but direct copying of the core logic, styles, or components is prohibited unless explicitly authorized.
### 2.2 Commercial Products and Services You may study the project and draw inspiration from it, but direct copying of the core logic, styles, or components outside of using the library as intended is prohibited unless explicitly authorized.
Any use of NDS in a commercial product or service requires explicit written authorization from the original creator.
Only the original creator may grant that authorization.
Contributors do not have the authority to grant usage rights to third parties.
Selling a website, application, service, or product that uses NDS is prohibited unless a specific commercial agreement has been granted by the creator. ### 2.2 Billing and Resale Restriction
You may not charge or invoice anyone specifically **for the use of NDS itself**. This includes, but is not limited to:
- selling NDS as a standalone product or component library,
- sublicensing or redistributing NDS to third parties,
- adding a distinct line item, fee, or surcharge tied to "using NDS" on a client invoice,
- building a product or service (e.g. a SaaS) whose core purpose or monetized value is to give paying users access to NDS's components or their usage.
This restriction does **not** prevent you from billing normally for a broader deliverable (e.g. a website, application, or platform) simply because NDS was used as part of the underlying toolkit. In that case, NDS is a tool used to produce the deliverable, not the product being sold.
**Example — SaaS:** if you build a SaaS and NDS is merely used internally to build its interface (the same way any UI library would be), this is authorized. If, however, the SaaS's purpose or value proposition is to give users access to NDS's components or their usage (e.g. a website-builder that resells NDS components to its own customers), this is prohibited.
**Exception:** if a client explicitly requests that NDS specifically be used in their project, you may bill for that work as part of the agreed engagement, since the choice to use NDS was made knowingly by the client.
Only the original creator may authorize the sale, sublicensing, or commercial redistribution of NDS as a standalone product. Contributors do not have the authority to grant such rights to third parties.
## 3. Modifications and Contributions ## 3. Modifications and Contributions
All modifications to the library must go through the official GitHub workflow, including Issues and Pull Requests. All modifications to the library must go through the official Gitea workflow on the project's self-hosted instance, including Issues and Pull Requests.
Any authorized modification or derivative work of NDS must remain under this same license. Any authorized modification or derivative work of NDS must remain under this same license.
Re-licensing modified versions under a different license is prohibited. Re-licensing modified versions under a different license is prohibited.
## 4. Assets and Iconography ## 4. Assets and Iconography
Icons and visual assets introduced into NDS remain part of the project.
Unless explicitly authorized, these assets may only be used through the NDS library itself. ### 4.1 Third-Party Icons
External reuse, extraction, redistribution, or standalone use of these assets is prohibited. Some icons used within NDS are not proprietary to NDS and originate from public or third-party sources (e.g. Iconify). These icons remain governed by their own original license and attribution requirements, independently of this license.
A full list of these third-party icons, including their name, source (e.g. Iconify), and a link to their origin, is maintained in the `ATTRIBUTION.md` file at the root of the repository. Any icon not listed in `ATTRIBUTION.md` is presumed to fall under section 4.2.
### 4.2 Icons Created for NDS
Icons specifically created for NDS may be used outside of the NDS project, subject to the following conditions:
- they may not be sold or resold, whether individually, in a set, or as part of another icon library or product,
- their origin must be explicitly credited wherever they are used (e.g. "Icon from Nova Design System (NDS)" with a link to the project), in a clear and visible manner.
## 5. Disclaimer of Warranty and Liability ## 5. Disclaimer of Warranty and Liability
This library is provided on an **"as is"** basis, without warranty of any kind. This library is provided on an **"as is"** basis, without warranty of any kind.
+147 -104
View File
@@ -1,165 +1,208 @@
# Nova Design System (NDS) # Nova Design System (NDS)
Nova Design System (NDS) is a lightweight, responsive, and secure UI library built with **Svelte**. Nova Design System (NDS) is a lightweight, responsive UI component library built with **Astro**. It is developed primarily for internal and personal projects, and is publicly available for reference and contributions.
It is developed primarily for internal and personal projects and is available publicly for reference, testing, and contributions.
--- ---
## Table of Contents ## Table of Contents
1. [Overview](#overview)
2. [Project Status](#project-status) 1. [Overview](#user-content-overview)
3. [Core Principles](#core-principles) 2. [Project Status](#user-content-project-status)
4. [Available Components](#available-components) 3. [Core Principles](#user-content-core-principles)
5. [Installation](#installation) 4. [Available Components](#user-content-available-components)
6. [Usage](#usage) 5. [Design Tokens](#user-content-design-tokens)
7. [Theming and Variables](#theming-and-variables) 6. [Installation](#user-content-installation)
8. [Contributing](#contributing) 7. [Usage](#user-content-usage)
9. [Development and Documentation](#development-and-documentation) 8. [Development](#user-content-development)
10. [License](#license) 9. [License](#user-content-license)
--- ---
## Overview ## Overview
NDS aims to provide a robust set of reusable UI components that are easy to integrate while maintaining consistency, responsiveness, and a strong focus on safety and maintainability.
NDS provides a focused set of reusable Astro components with centralized design tokens, light/dark theming, and a strong focus on consistency and maintainability. The component documentation and showcase are also built with Astro.
## Project Status ## Project Status
**Current Phase: ALPHA** > **ALPHA** — The library is under active development. Current version: `1.2.1`. Component APIs, token naming, and internal architecture may change before a stable release.
The library is still under active development. Component APIs, styling structure, and internal architecture may change before the Beta release.
## Core Principles ## Core Principles
- **Responsive Architecture:** Components are built with a mobile-first mindset and are expected to behave correctly across modern screen sizes.
- **Security-Centric:** Unsafe patterns are avoided, especially anything that could introduce injection or untrusted rendering issues. - **Responsive Architecture:** Components are built mobile-first and work correctly across modern screen sizes.
- **Native Theming:** Styling is based on centralized design tokens and supports Light and Dark themes. - **Security-Centric:** Unsafe patterns (injection, untrusted rendering) are avoided by design.
- **Type Safety:** The library is written in TypeScript for stronger reliability and a better developer experience. - **Native Theming:** All styling relies on centralized design tokens and supports Light and Dark modes.
- **Consistency First:** Shared tokens, naming conventions, and reusable patterns take priority over one-off styling. - **Type Safety:** Written in TypeScript for stronger reliability and a better developer experience.
- **Consistency First:** Shared tokens, naming conventions, and reusable patterns take priority over one-off styles.
## Available Components ## Available Components
### Actions These are the components currently exported from the package (`src/components/index.ts`):
- `Button`
- `Toggle`
### Navigation | Component | Category |
- `Navbar` | -------------------------------------------------- | ------------ |
- `Sidebar` | `Button` | Actions |
- `Tabs` | `Toggle` | Actions |
- `Breadcrumb` | `NumericStepper` | Actions |
- `Paginate` | `Select` / `SelectOption` | Actions |
- `Stepper` | `TextField` | Forms |
| `Checkbox` | Forms |
| `Radio` | Forms |
| `Tab` / `TabItem` / `TabContent` | Navigation |
| `Link` | Navigation |
| `Breadcrumb` / `BreadcrumbItem` | Navigation |
| `Pagination` / `PaginationNumber` | Navigation |
| `Navbar` | Navigation |
| `Sidebar` / `SidebarItem` | Navigation |
| `Avatar` | Data Display |
| `Badge` | Data Display |
| `Card` | Data Display |
| `ListItem` / `ListItemTitle` / `ListItemSubtitle` | Data Display |
| `Notification` | Feedback |
| `LoadingBar` | Feedback |
| `Tooltip` | Feedback |
| `Modal` | Overlay |
### Form > Icons are no longer shipped as standalone NDS components. As of `1.00.3`, all icons used internally (e.g. in `Select`, `Avatar`, `Notification`, `numericStepper`) come directly from the **[`@lucide/astro`](https://www.npmjs.com/package/@lucide/astro)** package. See `ATTRIBUTION.md` for details.
- `Checkbox`
- `Radio`
- `Select`
- `SelectOption`
- `Search`
- `Input`
- `File Upload`
- `Date Picker`
- `Slider`
- `Numeric Stepper`
### Data Display ## Design Tokens
- `Badge`
- `Avatar`
- `Card`
- `Table`
- `Row`
- `Cell`
- `List Item`
### Overlay All styling is controlled via CSS custom properties prefixed with `--nds-`, defined in `src/styles/tokens/`:
- `Modal`
- `Dropdown`
- `Tooltip`
### Feedback | File | Contents |
- `Notification` *(error / warning / info / success)* | ------------------ | ------------------------------------------------------------------------- |
- `Loading Bar` | `_colors.scss` | Color palette and semantic colors (`success`, `warning`, `error`, `info`) |
| `_spacing.scss` | Base spacing scale, border radius, and border width |
| `_typography.scss` | Font families, sizes, and weights |
### Layout **Rules:**
- `Divider`
- `Link` - Never hardcode colors, spacing, or radii in component styles when a token exists.
- Never override tokens locally in component scope unless explicitly supported.
- All global token changes must go through the core styling system.
## Installation ## Installation
Install the package with npm:
Requires **Node.js >= 22.12.0**.
```bash ```bash
npm install @unkn0wn-pkgs/nds npm install @unkn0wndo3s/nova-design-system
``` ```
No extra Vite de-optimization configuration is required. No extra Vite de-optimization configuration is required.
## Usage ## Usage
### Basic Component Example
```svelte Import components directly from the package:
<script lang="ts">
import { Notification } from '@unkn0wn-pkgs/nds'; ```astro
</script> ---
import { Button } from '@unkn0wndo3s/nova-design-system';
---
<Button>Click me</Button>
```
```astro
---
import { Notification } from '@unkn0wndo3s/nova-design-system';
---
<Notification type="warning"> <Notification type="warning">
This is a warning message. This is a warning message.
</Notification> </Notification>
``` ```
### Another Example ```astro
```svelte ---
<script lang="ts"> import { Modal } from '@unkn0wndo3s/nova-design-system';
import { Button } from '@unkn0wn-pkgs/nds'; ---
</script>
<Button> <Modal>
Click me This is a modal dialog.
</Button> </Modal>
``` ```
## Theming and Variables ```astro
NDS relies on a centralized Design Token system. ---
All styling is controlled through CSS custom properties prefixed with `--nds-`. import { Breadcrumb, BreadcrumbItem } from '@unkn0wndo3s/nova-design-system';
---
### Rules <Breadcrumb>
- Do not hardcode colors, spacing, or radii directly inside component styles when a design token exists. <BreadcrumbItem href="/">Home</BreadcrumbItem>
- Do not override tokens locally in isolated component scopes unless the system explicitly supports it. <BreadcrumbItem href="/docs">Docs</BreadcrumbItem>
- Keep all global token changes inside the core styling system to preserve consistency. </Breadcrumb>
```
## Contributing ## Development
Contributions are welcome.
If you want to report a bug, suggest a feature, or submit a Pull Request, follow the repository contribution rules and templates.
Please read the contribution guide before opening a PR.
### Icon Contribution Policy
By contributing a custom icon to NDS, you agree that the icon may be used inside the design system according to the repository license and contribution policy.
## Development and Documentation
Documentation is managed with **Storybook**.
### Clone the repository ### Clone the repository
```bash ```bash
git clone https://github.com/unkn0wndo3s/Design-System.git git clone https://git.novaprojects.dev/unkn0wn/nova-design-system.git
``` ```
### Install dependencies ### Install dependencies
```bash ```bash
npm install npm install
``` ```
### Launch Storybook ### Start the dev server
```bash ```bash
npm run storybook npm run dev
``` ```
### Useful scripts ### Useful scripts
```bash ```bash
npm run check npm run build # Build the project
npm run lint npm run preview # Preview the production build
npm run format
npm run build
npm run build-storybook
``` ```
### Project Structure
```text
src/
├── components/ # Astro components
│ ├── Avatar/
│ ├── Badge/
│ ├── Breadcrumb/
│ ├── Button/
│ ├── Card/
│ ├── Checkbox/
│ ├── Link/
│ ├── ListItem/
│ ├── LoadingBar/
│ ├── Modal/
│ ├── Navbar/
│ ├── Notifications/
│ ├── numericStepper/
│ ├── pagination/
│ ├── Radio/
│ ├── Select/
│ ├── Sidebar/
│ ├── Tabs/
│ ├── textField/
│ ├── Toggle/
│ ├── Tooltip/
│ └── index.ts
├── layouts/ # Astro layouts
├── pages/ # Documentation pages
├── styles/
│ ├── tokens/
│ │ ├── _colors.scss
│ │ ├── _spacing.scss
│ │ └── _typography.scss
│ └── index.scss
└── index.ts
```
## Contributing
Contributions are welcome. To report a bug, suggest a feature, or open a Pull Request, follow the repository contribution rules and templates.
NDS no longer maintains a custom set of icon components — icons are sourced from the **[Lucide](https://lucide.dev/)** icon set via `@lucide/astro`. If a needed icon isn't in Lucide, open an issue to discuss it before adding any new icon dependency.
## License ## License
This project is **not distributed under a standard open-source license**.
Usage, redistribution, commercial use, and derivative work rules are defined in `LICENSE.md`. This project is **not distributed under a standard open-source license**. Usage, redistribution, commercial use, and derivative work rules are defined in `LICENSE.md`. Read that file carefully before using the library in a product or service.
Read that file carefully before using the library in a product or service.
+1131 -1657
View File
File diff suppressed because it is too large Load Diff
+30 -10
View File
@@ -1,8 +1,26 @@
{ {
"name": "nova-design-system", "name": "@unkn0wndo3s/nova-design-system",
"type": "module", "type": "module",
"version": "0.1.0", "version": "1.2.1",
"description": "Nova Design System — Astro component library", "description": "Nova Design System — Astro component library",
"license": "LICENSE.md",
"author": "Unkn0wn",
"main": "./src/index.ts",
"repository": {
"type": "git",
"url": "https://git.novaprojects.dev/unkn0wn/nova-design-system.git"
},
"bugs": {
"url": "https://git.novaprojects.dev/unkn0wn/nova-design-system/issues"
},
"homepage": "https://git.novaprojects.dev/unkn0wn/nova-design-system#readme",
"keywords": [
"astro",
"astro-components",
"design-system",
"ui-library",
"nova"
],
"engines": { "engines": {
"node": ">=22.12.0" "node": ">=22.12.0"
}, },
@@ -11,22 +29,24 @@
"./styles": "./src/styles/index.scss" "./styles": "./src/styles/index.scss"
}, },
"files": [ "files": [
"src" "src",
"LICENSE.md",
"README.md"
], ],
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro", "astro": "astro"
"barrel": "node scripts/generate-barrel.mjs"
},
"peerDependencies": {
"astro": ">=4.0.0"
}, },
"dependencies": { "dependencies": {
"astro": "^6.3.3" "@lucide/astro": "^1.21.0"
},
"peerDependencies": {
"astro": "^7.0.0"
}, },
"devDependencies": { "devDependencies": {
"astro": "^7.0.0",
"sass": "^1.99.0" "sass": "^1.99.0"
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128"> <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" /> <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style> <style>
path { fill: #000; } path { fill: #000; }

Before

Width:  |  Height:  |  Size: 749 B

After

Width:  |  Height:  |  Size: 757 B

-42
View File
@@ -1,42 +0,0 @@
/**
* Scans src/components/ and regenerates src/components/index.ts.
* Each component folder must have an index.ts that exports the component.
* Run: npm run barrel
*/
import { readdir, writeFile, stat } from 'fs/promises';
import { join, resolve } from 'path';
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const componentsDir = resolve(__dirname, '../src/components');
const outputFile = join(componentsDir, 'index.ts');
const entries = await readdir(componentsDir);
const componentFolders = (
await Promise.all(
entries.map(async (name) => {
const fullPath = join(componentsDir, name);
const info = await stat(fullPath);
return info.isDirectory() ? name : null;
})
)
).filter(Boolean);
if (componentFolders.length === 0) {
console.log('No component folders found.');
process.exit(0);
}
const lines = [
'// Auto-generated by scripts/generate-barrel.mjs — do not edit manually.',
'',
...componentFolders.map((name) => `export * from './${name}/index.ts';`),
'',
];
await writeFile(outputFile, lines.join('\n'), 'utf-8');
console.log(`Barrel generated with ${componentFolders.length} component(s):`);
componentFolders.forEach((name) => console.log(` - ${name}`));
+32
View File
@@ -0,0 +1,32 @@
@use "../../styles/tokens/typography" as *;
.avatar {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--nds-radius-full);
overflow: hidden;
width: 40px;
height: 40px;
flex-shrink: 0;
background-color: var(--nds-primary-soft);
box-shadow: inset 0 0 0 1px var(--nds-border);
&__content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
@include text-label;
color: var(--nds-primary);
object-fit: cover;
}
&__fallback {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: var(--nds-primary);
}
}
+31
View File
@@ -0,0 +1,31 @@
---
import { User } from '@lucide/astro';
export interface Props {
type: 'photo' | 'initials';
name: string;
image?: string;
}
const { type, name, image } = Astro.props;
---
<div class={`avatar`}>
{type === 'photo' && image ? (
<div class="avatar__content avatar__content--photo">
<img
src={image}
alt={name}
class="avatar__content"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';"
/>
<span class="avatar__fallback" style="display:none;">
<User size={24}/>
</span>
</div>
) : (
<span class="avatar__content">{name.split(' ').map(word => word[0]).join('').toUpperCase()}</span>
)}
</div>
<style lang="scss">
@use './_avatar.scss';
</style>
+58
View File
@@ -0,0 +1,58 @@
@use "../../styles/tokens/typography" as *;
.badge {
display: inline-flex;
align-items: center;
gap: var(--nds-spacing-2xs);
padding: 3px var(--nds-spacing-xs);
border-radius: var(--nds-radius-full);
@include text-sm;
font-weight: 600;
line-height: 1.2;
width: fit-content;
white-space: nowrap;
&__primary {
--c: var(--nds-primary);
--on: var(--nds-on-primary);
--soft: var(--nds-primary-soft);
}
&__neutral {
--c: var(--nds-neutral);
--on: var(--nds-surface);
--soft: color-mix(in srgb, var(--nds-neutral) 16%, transparent);
}
&__success {
--c: var(--nds-success-medium);
--on: #fff;
--soft: var(--nds-success-low);
}
&__warning {
--c: var(--nds-warning-medium);
--on: #fff;
--soft: var(--nds-warning-low);
}
&__error {
--c: var(--nds-error-medium);
--on: #fff;
--soft: var(--nds-error-low);
}
&__info {
--c: var(--nds-info-medium);
--on: #fff;
--soft: var(--nds-info-low);
}
&--soft {
background-color: var(--soft);
color: var(--c);
}
&--solid {
background-color: var(--c);
color: var(--on);
}
&__dot {
width: 6px;
height: 6px;
border-radius: var(--nds-radius-full);
background-color: currentColor;
}
}
+15
View File
@@ -0,0 +1,15 @@
---
export interface Props {
type?: "primary" | "neutral" | "success" | "warning" | "error" | "info";
variant?: "soft" | "solid";
}
const { type = "primary", variant = "soft" } = Astro.props;
---
<span class={`badge badge__${type} badge--${variant}`}>
<slot />
</span>
<style lang="scss">
@use "./_badge.scss";
</style>
@@ -0,0 +1,27 @@
@use "../../styles/tokens/typography" as *;
.breadcrumb {
display: flex;
align-items: center;
gap: var(--nds-spacing-xs);
@include text-base;
&__item {
text-decoration: none;
color: var(--nds-neutral);
transition: color 120ms ease;
&:hover {
color: var(--nds-text);
}
&:not(:first-child)::before {
content: "/";
color: var(--nds-disabled);
margin-right: var(--nds-spacing-xs);
}
&--current {
color: var(--nds-text);
font-weight: 600;
pointer-events: none;
}
}
}
@@ -0,0 +1,11 @@
---
export interface Props {}
---
<nav class="breadcrumb" aria-label="Breadcrumb">
<slot />
</nav>
<style lang="scss">
@use "./_breadcrumb.scss";
</style>
@@ -0,0 +1,19 @@
---
export interface Props {
href?: string;
current?: boolean;
}
const { href = "#", current = false } = Astro.props;
---
<a
href={href}
class={`breadcrumb__item ${current ? "breadcrumb__item--current" : ""}`}
aria-current={current ? "page" : undefined}
>
<slot />
</a>
<style lang="scss">
@use "./_breadcrumb.scss";
</style>
+97
View File
@@ -0,0 +1,97 @@
@use "../../styles/tokens/typography" as *;
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--nds-spacing-xs);
width: fit-content;
border: var(--nds-border-width-thin) solid transparent;
border-radius: var(--nds-radius-md);
@include text-label;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
user-select: none;
transition:
background-color 140ms ease,
border-color 140ms ease,
color 140ms ease,
box-shadow 140ms ease,
transform 80ms ease;
&:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--nds-ring);
}
&:active:not(.button--disabled) {
transform: translateY(0.5px);
}
&--sm {
height: 30px;
padding: 0 var(--nds-spacing-sm);
}
&--md {
height: 38px;
padding: 0 var(--nds-spacing-md);
}
&--lg {
height: 46px;
padding: 0 var(--nds-spacing-lg);
font-size: 0.9375rem;
}
&--icon {
padding: 0;
aspect-ratio: 1;
}
&--icon.button--sm {
width: 30px;
}
&--icon.button--md {
width: 38px;
}
&--icon.button--lg {
width: 46px;
}
&__primary {
background-color: var(--nds-primary);
color: var(--nds-on-primary);
border-color: var(--nds-primary);
&:hover:not(.button--disabled) {
background-color: color-mix(in srgb, var(--nds-primary) 88%, #000);
border-color: color-mix(in srgb, var(--nds-primary) 88%, #000);
}
}
&__secondary {
background-color: transparent;
color: var(--nds-primary);
border-color: var(--nds-border-strong);
&:hover:not(.button--disabled) {
border-color: var(--nds-primary);
background-color: var(--nds-primary-soft);
}
}
&__ghost {
background-color: transparent;
color: var(--nds-text);
border-color: transparent;
&:hover:not(.button--disabled) {
background-color: var(--nds-surface-hover);
}
}
&__danger {
background-color: var(--nds-error-medium);
color: #fff;
border-color: var(--nds-error-medium);
&:hover:not(.button--disabled) {
background-color: color-mix(in srgb, var(--nds-error-medium) 88%, #000);
border-color: color-mix(in srgb, var(--nds-error-medium) 88%, #000);
}
}
&--disabled {
cursor: not-allowed;
background-color: var(--nds-disabled);
color: var(--nds-neutral);
border-color: transparent;
box-shadow: none;
}
}
+47
View File
@@ -0,0 +1,47 @@
---
export interface Props {
type?: "primary" | "secondary" | "ghost" | "danger";
size?: "sm" | "md" | "lg";
disabled?: boolean;
icon?: boolean;
href?: string;
htmlType?: "button" | "submit" | "reset";
}
const {
type = "primary",
size = "md",
disabled = false,
icon = false,
href,
htmlType = "button",
...rest
} = Astro.props;
const classes = [
"button",
`button__${type}`,
`button--${size}`,
icon ? "button--icon" : "",
disabled ? "button--disabled" : "",
]
.filter(Boolean)
.join(" ");
const Tag = href ? "a" : "button";
---
<Tag
class={classes}
href={href}
type={href ? undefined : htmlType}
aria-disabled={disabled ? "true" : undefined}
{...rest}
>
<slot name="icon-left" />
<slot />
<slot name="icon-right" />
</Tag>
<style lang="scss">
@use "./_button.scss";
</style>
+53
View File
@@ -0,0 +1,53 @@
@use "../../styles/tokens/typography" as *;
.card {
display: flex;
flex-direction: column;
background-color: var(--nds-surface);
border: var(--nds-border-width-thin) solid var(--nds-border);
border-radius: var(--nds-radius-lg);
overflow: hidden;
box-shadow: var(--nds-shadow-sm);
width: fit-content;
transition:
box-shadow 160ms ease,
border-color 160ms ease;
&:hover {
box-shadow: var(--nds-shadow-md);
}
&__cover {
height: 150px;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
&__body {
display: flex;
flex-direction: column;
gap: var(--nds-spacing-xs);
padding: var(--nds-spacing-md);
}
&__title {
@include text-xl;
color: var(--nds-text);
margin: 0;
}
&__subtitle {
@include text-base;
color: var(--nds-neutral);
margin: 0;
}
&__content {
@include text-base;
color: var(--nds-text);
&:empty {
display: none;
}
}
&__footer {
margin-top: var(--nds-spacing-2xs);
display: flex;
gap: var(--nds-spacing-xs);
}
}
+33
View File
@@ -0,0 +1,33 @@
---
export interface Props {
title?: string;
subtitle?: string;
cover?: string;
}
const { title, subtitle, cover } = Astro.props;
const hasFooter = Astro.slots.has("footer");
---
<div class="card">
{
cover && (
<div class="card__cover" style={`background-image: url(${cover})`} />
)
}
<div class="card__body">
{title && <p class="card__title">{title}</p>}
{subtitle && <p class="card__subtitle">{subtitle}</p>}
<div class="card__content"><slot /></div>
{
hasFooter && (
<div class="card__footer">
<slot name="footer" />
</div>
)
}
</div>
</div>
<style lang="scss">
@use "./_card.scss";
</style>
+51
View File
@@ -0,0 +1,51 @@
@use "../../styles/tokens/typography" as *;
.checkbox {
display: inline-flex;
align-items: center;
gap: var(--nds-spacing-xs);
cursor: pointer;
@include text-base;
color: var(--nds-text);
&__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
&__box {
width: 18px;
height: 18px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border: var(--nds-border-width-medium) solid var(--nds-border-strong);
border-radius: var(--nds-radius-sm);
background-color: var(--nds-surface);
color: var(--nds-on-primary);
transition: all 130ms ease;
svg {
width: 12px;
height: 12px;
opacity: 0;
transform: scale(0.6);
transition: all 130ms ease;
}
}
&__input:checked + &__box {
background-color: var(--nds-primary);
border-color: var(--nds-primary);
svg {
opacity: 1;
transform: scale(1);
}
}
&__input:focus-visible + &__box {
box-shadow: 0 0 0 3px var(--nds-ring);
}
&--disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
+37
View File
@@ -0,0 +1,37 @@
---
export interface Props {
id: string;
name?: string;
checked?: boolean;
disabled?: boolean;
}
const { id, name, checked = false, disabled = false } = Astro.props;
---
<label class={`checkbox ${disabled ? "checkbox--disabled" : ""}`} for={id}>
<input
type="checkbox"
id={id}
name={name}
checked={checked}
disabled={disabled}
class="checkbox__input"
/>
<span class="checkbox__box" aria-hidden="true">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 13l4 4L19 7"></path>
</svg>
</span>
<span class="checkbox__label"><slot /></span>
</label>
<style lang="scss">
@use "./_checkbox.scss";
</style>
-45
View File
@@ -1,45 +0,0 @@
---
import arrow16down from './svgs/arrow2-16-down.svg?raw';
import arrow16left from './svgs/arrow2-16-left.svg?raw';
import arrow16right from './svgs/arrow2-16-right.svg?raw';
import arrow16up from './svgs/arrow2-16-up.svg?raw';
import arrow24down from './svgs/arrow2-24-down.svg?raw';
import arrow24left from './svgs/arrow2-24-left.svg?raw';
import arrow24right from './svgs/arrow2-24-right.svg?raw';
import arrow24up from './svgs/arrow2-24-up.svg?raw';
import arrow32down from './svgs/arrow2-32-down.svg?raw';
import arrow32left from './svgs/arrow2-32-left.svg?raw';
import arrow32right from './svgs/arrow2-32-right.svg?raw';
import arrow32up from './svgs/arrow2-32-up.svg?raw';
export interface Props {
size: 16 | 24 | 32;
orientation: 'up' | 'down' | 'left' | 'right';
label?: string;
class?: string;
}
const { size = 24, orientation = 'down', label, class: className } = Astro.props;
let raw: string;
if (size === 16 && orientation === 'down') raw = arrow16down;
else if (size === 16 && orientation === 'left') raw = arrow16left;
else if (size === 16 && orientation === 'right') raw = arrow16right;
else if (size === 16 && orientation === 'up') raw = arrow16up;
else if (size === 24 && orientation === 'down') raw = arrow24down;
else if (size === 24 && orientation === 'left') raw = arrow24left;
else if (size === 24 && orientation === 'right') raw = arrow24right;
else if (size === 24 && orientation === 'up') raw = arrow24up;
else if (size === 32 && orientation === 'down') raw = arrow32down;
else if (size === 32 && orientation === 'left') raw = arrow32left;
else if (size === 32 && orientation === 'right') raw = arrow32right;
else if (size === 32 && orientation === 'up') raw = arrow32up;
else raw = ''; // Fallback to a blank string if no match is found, though this should not happen due to the type constraints on size and orientation.
const classes = ['nds-icon-arrow2', className].filter(Boolean).join(' ');
const svg = raw
.replace(/fill="#[A-Fa-f0-9]{3,8}"/g, 'fill="currentColor"')
.replace('<svg ', `<svg class="${classes}" aria-hidden="${label ? 'false' : 'true'}" ${label ? `aria-label="${label}" role="img"` : ''} `);
---
<Fragment set:html={svg} />
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.44167 9.69166C8.32448 9.8087 8.16563 9.87444 8 9.87444C7.83438 9.87444 7.67553 9.8087 7.55834 9.69166L5.20084 7.33499C5.08365 7.21775 5.01783 7.05875 5.01787 6.89297C5.01791 6.7272 5.0838 6.56823 5.20105 6.45104C5.31829 6.33384 5.47729 6.26803 5.64307 6.26807C5.80884 6.26811 5.96781 6.334 6.085 6.45124L8 8.36624L9.915 6.45124C10.0328 6.33734 10.1907 6.27427 10.3545 6.27561C10.5184 6.27696 10.6752 6.34262 10.7911 6.45844C10.9071 6.57427 10.9729 6.731 10.9744 6.89487C10.9759 7.05874 10.913 7.21665 10.7992 7.33458L8.44209 9.69208L8.44167 9.69166Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 723 B

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.30833 7.55834C6.19129 7.67553 6.12555 7.83439 6.12555 8.00001C6.12555 8.16564 6.19129 8.32449 6.30833 8.44168L8.665 10.7992C8.72305 10.8572 8.79197 10.9032 8.86781 10.9346C8.94366 10.966 9.02494 10.9822 9.10702 10.9821C9.1891 10.9821 9.27038 10.9659 9.34621 10.9345C9.42204 10.9031 9.49093 10.857 9.54896 10.799C9.60699 10.7409 9.65301 10.672 9.6844 10.5962C9.7158 10.5203 9.73195 10.439 9.73193 10.3569C9.73191 10.2749 9.71572 10.1936 9.68429 10.1178C9.65286 10.0419 9.6068 9.97304 9.54875 9.91501L7.63417 8.00001L9.54917 6.08501C9.60889 6.02738 9.65653 5.95844 9.68932 5.8822C9.72211 5.80597 9.73939 5.72396 9.74015 5.64098C9.74091 5.55799 9.72514 5.47568 9.69375 5.39886C9.66236 5.32204 9.61599 5.25223 9.55733 5.19352C9.49868 5.13481 9.42892 5.08837 9.35212 5.05691C9.27532 5.02545 9.19303 5.00959 9.11005 5.01027C9.02706 5.01096 8.94504 5.02816 8.86877 5.06088C8.7925 5.0936 8.72352 5.14118 8.66583 5.20084L6.30833 7.55834Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.69167 7.55834C9.80871 7.67553 9.87445 7.83439 9.87445 8.00001C9.87445 8.16564 9.80871 8.32449 9.69167 8.44168L7.335 10.7992C7.27695 10.8572 7.20803 10.9032 7.13219 10.9346C7.05634 10.966 6.97506 10.9822 6.89298 10.9821C6.8109 10.9821 6.72962 10.9659 6.65379 10.9345C6.57796 10.9031 6.50907 10.857 6.45104 10.799C6.39301 10.7409 6.34699 10.672 6.3156 10.5962C6.2842 10.5203 6.26805 10.439 6.26807 10.3569C6.26809 10.2749 6.28428 10.1936 6.31571 10.1178C6.34714 10.0419 6.3932 9.97304 6.45125 9.91501L8.36583 8.00001L6.45083 6.08501C6.39111 6.02738 6.34347 5.95844 6.31068 5.8822C6.27789 5.80597 6.26061 5.72396 6.25985 5.64098C6.25909 5.55799 6.27486 5.47568 6.30625 5.39886C6.33764 5.32204 6.38401 5.25223 6.44267 5.19352C6.50132 5.13481 6.57108 5.08837 6.64788 5.05691C6.72468 5.02545 6.80697 5.00959 6.88995 5.01027C6.97294 5.01096 7.05496 5.02816 7.13123 5.06088C7.2075 5.0936 7.27648 5.14118 7.33417 5.20084L9.69167 7.55834Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.55833 6.30833C7.67552 6.19129 7.83437 6.12555 8 6.12555C8.16562 6.12555 8.32447 6.19129 8.44166 6.30833L10.7992 8.665C10.8572 8.72305 10.9032 8.79197 10.9346 8.86781C10.966 8.94366 10.9822 9.02494 10.9821 9.10702C10.9821 9.1891 10.9659 9.27038 10.9345 9.34621C10.9031 9.42204 10.857 9.49093 10.799 9.54896C10.7409 9.60699 10.672 9.65301 10.5961 9.6844C10.5203 9.7158 10.439 9.73195 10.3569 9.73193C10.2748 9.73191 10.1936 9.71572 10.1177 9.68429C10.0419 9.65286 9.97302 9.6068 9.915 9.54875L8 7.63417L6.085 9.54917C6.02737 9.60889 5.95843 9.65653 5.88219 9.68932C5.80595 9.72211 5.72395 9.73939 5.64096 9.74015C5.55798 9.74091 5.47567 9.72514 5.39884 9.69375C5.32202 9.66236 5.25222 9.61599 5.19351 9.55733C5.1348 9.49868 5.08835 9.42892 5.05689 9.35212C5.02543 9.27532 5.00958 9.19303 5.01026 9.11005C5.01094 9.02706 5.02814 8.94504 5.06086 8.86877C5.09358 8.7925 5.14116 8.72352 5.20083 8.66583L7.55833 6.30833Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.795 15.045C12.5841 15.2557 12.2981 15.374 12 15.374C11.7019 15.374 11.4159 15.2557 11.205 15.045L6.9615 10.803C6.75056 10.592 6.63209 10.3058 6.63216 10.0074C6.63223 9.70897 6.75083 9.42283 6.96188 9.21188C7.17292 9.00094 7.45912 8.88247 7.75752 8.88254C8.05591 8.88261 8.34205 9.00121 8.553 9.21226L12 12.6593L15.447 9.21226C15.6591 9.00723 15.9432 8.8937 16.2382 8.89612C16.5332 8.89855 16.8154 9.01673 17.0241 9.22521C17.2327 9.4337 17.3512 9.71581 17.3539 10.0108C17.3566 10.3058 17.2433 10.59 17.0385 10.8023L12.7958 15.0458L12.795 15.045Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 719 B

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.95499 11.205C8.74431 11.4159 8.62598 11.7019 8.62598 12C8.62598 12.2981 8.74431 12.5841 8.95499 12.795L13.197 17.0385C13.3015 17.143 13.4255 17.2258 13.5621 17.2823C13.6986 17.3388 13.8449 17.3679 13.9926 17.3678C14.1404 17.3678 14.2867 17.3387 14.4232 17.2821C14.5597 17.2255 14.6837 17.1426 14.7881 17.0381C14.8926 16.9336 14.9754 16.8096 15.0319 16.6731C15.0884 16.5365 15.1175 16.3902 15.1175 16.2425C15.1174 16.0947 15.0883 15.9484 15.0317 15.812C14.9751 15.6755 14.8922 15.5515 14.7877 15.447L11.3415 12L14.7885 8.553C14.896 8.44927 14.9817 8.32518 15.0408 8.18795C15.0998 8.05072 15.1309 7.90312 15.1323 7.75374C15.1336 7.60437 15.1052 7.45621 15.0487 7.31793C14.9922 7.17964 14.9088 7.054 14.8032 6.94832C14.6976 6.84264 14.572 6.75905 14.4338 6.70242C14.2956 6.64578 14.1474 6.61725 13.9981 6.61848C13.8487 6.6197 13.7011 6.65067 13.5638 6.70956C13.4265 6.76846 13.3023 6.8541 13.1985 6.9615L8.95499 11.205Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.045 11.205C15.2557 11.4159 15.374 11.7019 15.374 12C15.374 12.2981 15.2557 12.5841 15.045 12.795L10.803 17.0385C10.6985 17.143 10.5745 17.2258 10.4379 17.2823C10.3014 17.3388 10.1551 17.3679 10.0074 17.3678C9.85962 17.3678 9.71333 17.3387 9.57684 17.2821C9.44035 17.2255 9.31634 17.1426 9.21189 17.0381C9.10744 16.9336 9.02459 16.8096 8.96808 16.6731C8.91158 16.5365 8.88251 16.3902 8.88254 16.2425C8.88258 16.0947 8.91171 15.9484 8.96829 15.812C9.02486 15.6755 9.10776 15.5515 9.21226 15.447L12.6585 12L9.21151 8.553C9.10402 8.44927 9.01825 8.32518 8.95923 8.18795C8.90021 8.05072 8.8691 7.90312 8.86774 7.75374C8.86637 7.60437 8.89476 7.45621 8.95126 7.31793C9.00776 7.17964 9.09124 7.054 9.19682 6.94832C9.3024 6.84264 9.42797 6.75905 9.5662 6.70242C9.70443 6.64578 9.85255 6.61725 10.0019 6.61848C10.1513 6.6197 10.2989 6.65067 10.4362 6.70956C10.5735 6.76846 10.6977 6.8541 10.8015 6.9615L15.045 11.205Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.205 8.95499C11.4159 8.74431 11.7019 8.62598 12 8.62598C12.2981 8.62598 12.5841 8.74431 12.795 8.95499L17.0385 13.197C17.143 13.3015 17.2258 13.4255 17.2823 13.5621C17.3388 13.6986 17.3679 13.8449 17.3678 13.9926C17.3678 14.1404 17.3387 14.2867 17.2821 14.4232C17.2255 14.5597 17.1426 14.6837 17.0381 14.7881C16.9336 14.8926 16.8096 14.9754 16.6731 15.0319C16.5365 15.0884 16.3902 15.1175 16.2425 15.1175C16.0947 15.1174 15.9484 15.0883 15.812 15.0317C15.6755 14.9751 15.5515 14.8922 15.447 14.7877L12 11.3415L8.553 14.7885C8.44927 14.896 8.32518 14.9817 8.18795 15.0408C8.05072 15.0998 7.90312 15.1309 7.75374 15.1323C7.60437 15.1336 7.45621 15.1052 7.31793 15.0487C7.17964 14.9922 7.054 14.9088 6.94832 14.8032C6.84264 14.6976 6.75905 14.572 6.70242 14.4338C6.64578 14.2956 6.61725 14.1474 6.61848 13.9981C6.6197 13.8487 6.65067 13.7011 6.70956 13.5638C6.76846 13.4265 6.8541 13.3023 6.9615 13.1985L11.205 8.95499Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.1483 20.3984C16.8436 20.7027 16.4306 20.8736 16 20.8736C15.5694 20.8736 15.1564 20.7027 14.8517 20.3984L8.72217 14.271C8.41747 13.9662 8.24634 13.5528 8.24644 13.1218C8.24655 12.6908 8.41786 12.2774 8.72271 11.9727C9.02755 11.668 9.44095 11.4969 9.87197 11.497C10.303 11.4971 10.7163 11.6684 11.021 11.9733L16 16.9523L20.979 11.9733C21.2853 11.6771 21.6957 11.5131 22.1218 11.5166C22.5479 11.5201 22.9555 11.6908 23.257 11.992C23.5584 12.2931 23.7295 12.7006 23.7334 13.1267C23.7373 13.5528 23.5737 13.9633 23.2778 14.2699L17.1494 20.3994L17.1483 20.3984Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 730 B

@@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.6016 14.8517C11.2973 15.1563 11.1264 15.5694 11.1264 16C11.1264 16.4306 11.2973 16.8436 11.6016 17.1483L17.729 23.2778C17.8799 23.4287 18.0591 23.5484 18.2563 23.63C18.4535 23.7116 18.6648 23.7536 18.8782 23.7535C19.0917 23.7535 19.303 23.7114 19.5001 23.6297C19.6973 23.548 19.8764 23.4282 20.0273 23.2773C20.1781 23.1263 20.2978 22.9472 20.3794 22.75C20.4611 22.5528 20.503 22.3414 20.503 22.128C20.5029 21.9146 20.4609 21.7033 20.3791 21.5061C20.2974 21.309 20.1777 21.1299 20.0267 20.979L15.0488 16L20.0278 11.021C20.1831 10.8712 20.307 10.6919 20.3922 10.4937C20.4775 10.2955 20.5224 10.0823 20.5244 9.86651C20.5264 9.65074 20.4853 9.43674 20.4037 9.237C20.3221 9.03725 20.2015 8.85576 20.049 8.70312C19.8965 8.55047 19.7152 8.42972 19.5155 8.34792C19.3158 8.26612 19.1019 8.22491 18.8861 8.22668C18.6703 8.22845 18.4571 8.27318 18.2588 8.35825C18.0605 8.44332 17.8811 8.56703 17.7311 8.72216L11.6016 14.8517Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.3984 14.8517C20.7027 15.1563 20.8736 15.5694 20.8736 16C20.8736 16.4306 20.7027 16.8436 20.3984 17.1483L14.271 23.2778C14.1201 23.4287 13.9409 23.5484 13.7437 23.63C13.5465 23.7116 13.3352 23.7536 13.1218 23.7535C12.9084 23.7535 12.697 23.7114 12.4999 23.6297C12.3027 23.548 12.1236 23.4282 11.9727 23.2773C11.8219 23.1263 11.7022 22.9472 11.6206 22.75C11.5389 22.5528 11.497 22.3414 11.497 22.128C11.4971 21.9146 11.5391 21.7033 11.6209 21.5061C11.7026 21.309 11.8223 21.1299 11.9733 20.979L16.9512 16L11.9722 11.021C11.8169 10.8712 11.693 10.6919 11.6078 10.4937C11.5225 10.2955 11.4776 10.0823 11.4756 9.86651C11.4736 9.65074 11.5147 9.43674 11.5963 9.237C11.6779 9.03725 11.7985 8.85576 11.951 8.70312C12.1035 8.55047 12.2848 8.42972 12.4845 8.34792C12.6842 8.26612 12.8981 8.22491 13.1139 8.22668C13.3297 8.22845 13.5429 8.27318 13.7412 8.35825C13.9395 8.44332 14.1189 8.56703 14.2689 8.72216L20.3984 14.8517Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.8517 11.6017C15.1563 11.2974 15.5694 11.1264 16 11.1264C16.4306 11.1264 16.8436 11.2974 17.1483 11.6017L23.2778 17.729C23.4287 17.8799 23.5484 18.0591 23.63 18.2563C23.7116 18.4535 23.7536 18.6648 23.7535 18.8783C23.7535 19.0917 23.7114 19.303 23.6297 19.5001C23.548 19.6973 23.4282 19.8764 23.2773 20.0273C23.1263 20.1782 22.9472 20.2978 22.75 20.3795C22.5528 20.4611 22.3414 20.5031 22.128 20.503C21.9146 20.503 21.7033 20.4609 21.5061 20.3792C21.309 20.2974 21.1299 20.1777 20.979 20.0268L16 15.0488L11.021 20.0278C10.8712 20.1831 10.6919 20.307 10.4937 20.3922C10.2955 20.4775 10.0823 20.5224 9.86651 20.5244C9.65074 20.5264 9.43674 20.4854 9.237 20.4038C9.03725 20.3221 8.85576 20.2016 8.70312 20.0491C8.55047 19.8966 8.42972 19.7152 8.34792 19.5155C8.26612 19.3159 8.22491 19.1019 8.22668 18.8861C8.22845 18.6704 8.27318 18.4571 8.35825 18.2588C8.44332 18.0605 8.56703 17.8811 8.72216 17.7312L14.8517 11.6017Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-4
View File
@@ -1,4 +0,0 @@
export { default as Arrow2Icon } from './Arrow2/Arrow2.astro';
export { default as ProfileIcon } from './profile/profile.astro';
export { default as ShareIcon } from './share/share.astro';
export { default as SearchIcon } from './search/search.astro';
@@ -1,27 +0,0 @@
---
import profile16 from './svgs/profile-16.svg?raw';
import profile24 from './svgs/profile-24.svg?raw';
import profile32 from './svgs/profile-32.svg?raw';
export interface Props {
size: 16 | 24 | 32;
label?: string;
class?: string;
}
const { size = 24, label, class: className } = Astro.props;
let raw: string;
if (size === 16 ) raw = profile16;
else if (size === 24 ) raw = profile24;
else if (size === 32 ) raw = profile32;
else raw = ''; // Fallback to a blank string if no match is found, though this should not happen due to the type constraints on size.
const classes = ['nds-icon-profile', className].filter(Boolean).join(' ');
const svg = raw
.replace(/fill="#[A-Fa-f0-9]{3,8}"/g, 'fill="currentColor"')
.replace('<svg ', `<svg class="${classes}" aria-hidden="${label ? 'false' : 'true'}" ${label ? `aria-label="${label}" role="img"` : ''} `);
---
<Fragment set:html={svg} />
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.77778 5.22222C5.77778 4.63285 6.0119 4.06762 6.42865 3.65087C6.8454 3.23413 7.41063 3 8 3C8.58937 3 9.1546 3.23413 9.57135 3.65087C9.9881 4.06762 10.2222 4.63285 10.2222 5.22222C10.2222 5.81159 9.9881 6.37682 9.57135 6.79357C9.1546 7.21032 8.58937 7.44444 8 7.44444C7.41063 7.44444 6.8454 7.21032 6.42865 6.79357C6.0119 6.37682 5.77778 5.81159 5.77778 5.22222ZM5.77778 8.55556C5.04107 8.55556 4.33453 8.84821 3.81359 9.36915C3.29266 9.89008 3 10.5966 3 11.3333C3 11.7754 3.17559 12.1993 3.48816 12.5118C3.80072 12.8244 4.22464 13 4.66667 13H11.3333C11.7754 13 12.1993 12.8244 12.5118 12.5118C12.8244 12.1993 13 11.7754 13 11.3333C13 10.5966 12.7073 9.89008 12.1864 9.36915C11.6655 8.84821 10.9589 8.55556 10.2222 8.55556H5.77778Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 903 B

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 7.27778C8 6.27585 8.42143 5.31496 9.17157 4.60649C9.92172 3.89801 10.9391 3.5 12 3.5C13.0609 3.5 14.0783 3.89801 14.8284 4.60649C15.5786 5.31496 16 6.27585 16 7.27778C16 8.27971 15.5786 9.2406 14.8284 9.94907C14.0783 10.6575 13.0609 11.0556 12 11.0556C10.9391 11.0556 9.92172 10.6575 9.17157 9.94907C8.42143 9.2406 8 8.27971 8 7.27778ZM8 12.9444C6.67392 12.9444 5.40215 13.442 4.46447 14.3276C3.52678 15.2131 3 16.4143 3 17.6667C3 18.4181 3.31607 19.1388 3.87868 19.6701C4.44129 20.2015 5.20435 20.5 6 20.5H18C18.7956 20.5 19.5587 20.2015 20.1213 19.6701C20.6839 19.1388 21 18.4181 21 17.6667C21 16.4143 20.4732 15.2131 19.5355 14.3276C18.5979 13.442 17.3261 12.9444 16 12.9444H8Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 854 B

@@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.2222 8.77778C10.2222 7.24542 10.831 5.77582 11.9145 4.69227C12.998 3.60873 14.4676 3 16 3C17.5324 3 19.002 3.60873 20.0855 4.69227C21.1691 5.77582 21.7778 7.24542 21.7778 8.77778C21.7778 10.3101 21.1691 11.7797 20.0855 12.8633C19.002 13.9468 17.5324 14.5556 16 14.5556C14.4676 14.5556 12.998 13.9468 11.9145 12.8633C10.831 11.7797 10.2222 10.3101 10.2222 8.77778ZM10.2222 17.4444C8.30677 17.4444 6.46977 18.2054 5.11534 19.5598C3.76091 20.9142 3 22.7512 3 24.6667C3 25.8159 3.45655 26.9181 4.2692 27.7308C5.08186 28.5435 6.18406 29 7.33333 29H24.6667C25.8159 29 26.9181 28.5435 27.7308 27.7308C28.5435 26.9181 29 25.8159 29 24.6667C29 22.7512 28.2391 20.9142 26.8847 19.5598C25.5302 18.2054 23.6932 17.4444 21.7778 17.4444H10.2222Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 906 B

-27
View File
@@ -1,27 +0,0 @@
---
import search16 from './svgs/search-16.svg?raw';
import search24 from './svgs/search-24.svg?raw';
import search32 from './svgs/search-32.svg?raw';
export interface Props {
size: 16 | 24 | 32;
label?: string;
class?: string;
}
const { size = 24, label, class: className } = Astro.props;
let raw: string;
if (size === 16 ) raw = search16;
else if (size === 24 ) raw = search24;
else if (size === 32 ) raw = search32;
else raw = ''; // Fallback to a blank string if no match is found, though this should not happen due to the type constraints on size.
const classes = ['nds-icon-search', className].filter(Boolean).join(' ');
const svg = raw
.replace(/fill="#[A-Fa-f0-9]{3,8}"/g, 'fill="currentColor"')
.replace('<svg ', `<svg class="${classes}" aria-hidden="${label ? 'false' : 'true'}" ${label ? `aria-label="${label}" role="img"` : ''} `);
---
<Fragment set:html={svg} />
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.69844 10.3969C5.66477 10.3969 4.79004 10.0388 4.07425 9.32262C3.35846 8.60645 3.00038 7.73172 3 6.69843C2.99962 5.66515 3.35771 4.79042 4.07425 4.07425C4.7908 3.35808 5.66553 3 6.69844 3C7.73134 3 8.60626 3.35808 9.32319 4.07425C10.0401 4.79042 10.398 5.66515 10.3969 6.69843C10.3969 7.11569 10.3305 7.50925 10.1977 7.87909C10.065 8.24893 9.88478 8.5761 9.65718 8.8606L12.8435 12.0469C12.9478 12.1513 13 12.284 13 12.4452C13 12.6064 12.9478 12.7392 12.8435 12.8435C12.7392 12.9478 12.6064 13 12.4452 13C12.284 13 12.1513 12.9478 12.0469 12.8435L8.8606 9.65718C8.5761 9.88478 8.24893 10.065 7.87909 10.1977C7.50925 10.3305 7.11569 10.3969 6.69844 10.3969ZM6.69844 9.25889C7.40967 9.25889 8.01432 9.01005 8.51238 8.51237C9.01043 8.0147 9.25927 7.41005 9.25889 6.69843C9.25851 5.98682 9.00967 5.38236 8.51238 4.88506C8.01508 4.38777 7.41043 4.13874 6.69844 4.13798C5.98644 4.13722 5.38198 4.38625 4.88506 4.88506C4.38815 5.38388 4.13912 5.98834 4.13798 6.69843C4.13684 7.40853 4.38587 8.01318 4.88506 8.51237C5.38426 9.01157 5.98872 9.26041 6.69844 9.25889Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.65718 16.3144C7.79659 16.3144 6.22208 15.6698 4.93366 14.3807C3.64524 13.0916 3.00068 11.5171 3 9.65718C2.99932 7.79727 3.64387 6.22276 4.93366 4.93366C6.22344 3.64455 7.79795 3 9.65718 3C11.5164 3 13.0913 3.64455 14.3817 4.93366C15.6722 6.22276 16.3164 7.79727 16.3144 9.65718C16.3144 10.4083 16.1949 11.1166 15.9559 11.7824C15.7169 12.4481 15.3926 13.037 14.9829 13.5491L20.7183 19.2845C20.9061 19.4723 21 19.7112 21 20.0014C21 20.2916 20.9061 20.5306 20.7183 20.7183C20.5306 20.9061 20.2916 21 20.0014 21C19.7112 21 19.4723 20.9061 19.2845 20.7183L13.5491 14.9829C13.037 15.3926 12.4481 15.7169 11.7824 15.9559C11.1166 16.1949 10.4083 16.3144 9.65718 16.3144ZM9.65718 14.266C10.9374 14.266 12.0258 13.8181 12.9223 12.9223C13.8188 12.0265 14.2667 10.9381 14.266 9.65718C14.2653 8.37627 13.8174 7.28825 12.9223 6.39312C12.0271 5.49798 10.9388 5.04973 9.65718 5.04836C8.37559 5.047 7.28757 5.49525 6.39312 6.39312C5.49866 7.29098 5.05041 8.379 5.04836 9.65718C5.04632 10.9354 5.49457 12.0237 6.39312 12.9223C7.29167 13.8208 8.37969 14.2687 9.65718 14.266Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6159 22.2319C9.9284 22.2319 7.65411 21.3008 5.79306 19.4388C3.93201 17.5768 3.00099 15.3025 3 12.6159C2.99901 9.92939 3.93003 7.6551 5.79306 5.79306C7.65608 3.93102 9.93038 3 12.6159 3C15.3015 3 17.5763 3.93102 19.4403 5.79306C21.3043 7.6551 22.2348 9.92939 22.2319 12.6159C22.2319 13.7008 22.0593 14.724 21.7141 15.6856C21.3689 16.6472 20.9004 17.4979 20.3087 18.2376L28.5932 26.522C28.8644 26.7933 29 27.1385 29 27.5576C29 27.9768 28.8644 28.322 28.5932 28.5932C28.322 28.8644 27.9768 29 27.5576 29C27.1385 29 26.7933 28.8644 26.522 28.5932L18.2376 20.3087C17.4979 20.9004 16.6472 21.3689 15.6856 21.7141C14.724 22.0593 13.7008 22.2319 12.6159 22.2319ZM12.6159 19.2731C14.4651 19.2731 16.0372 18.6261 17.3322 17.3322C18.6271 16.0382 19.2741 14.4661 19.2731 12.6159C19.2721 10.7657 18.6251 9.19414 17.3322 7.90117C16.0392 6.60819 14.4671 5.96072 12.6159 5.95875C10.7647 5.95678 9.19315 6.60425 7.90117 7.90117C6.60918 9.19808 5.96171 10.7697 5.95875 12.6159C5.95579 14.4622 6.60326 16.0343 7.90117 17.3322C9.19907 18.6301 10.7707 19.2771 12.6159 19.2731Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-27
View File
@@ -1,27 +0,0 @@
---
import share16 from './svgs/share-16.svg?raw';
import share24 from './svgs/share-24.svg?raw';
import share32 from './svgs/share-32.svg?raw';
export interface Props {
size: 16 | 24 | 32;
label?: string;
class?: string;
}
const { size = 24, label, class: className } = Astro.props;
let raw: string;
if (size === 16 ) raw = share16;
else if (size === 24 ) raw = share24;
else if (size === 32 ) raw = share32;
else raw = ''; // Fallback to a blank string if no match is found, though this should not happen due to the type constraints on size.
const classes = ['nds-icon-share', className].filter(Boolean).join(' ');
const svg = raw
.replace(/fill="#[A-Fa-f0-9]{3,8}"/g, 'fill="currentColor"')
.replace('<svg ', `<svg class="${classes}" aria-hidden="${label ? 'false' : 'true'}" ${label ? `aria-label="${label}" role="img"` : ''} `);
---
<Fragment set:html={svg} />
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 13C10.5833 13 10.2292 12.8542 9.9375 12.5625C9.64583 12.2708 9.5 11.9167 9.5 11.5C9.5 11.45 9.5125 11.3333 9.5375 11.15L6.025 9.1C5.89167 9.225 5.7375 9.323 5.5625 9.394C5.3875 9.465 5.2 9.50033 5 9.5C4.58333 9.5 4.22917 9.35417 3.9375 9.0625C3.64583 8.77083 3.5 8.41667 3.5 8C3.5 7.58333 3.64583 7.22917 3.9375 6.9375C4.22917 6.64583 4.58333 6.5 5 6.5C5.2 6.5 5.3875 6.5355 5.5625 6.6065C5.7375 6.6775 5.89167 6.77533 6.025 6.9L9.5375 4.85C9.52083 4.79167 9.5105 4.7355 9.5065 4.6815C9.5025 4.6275 9.50033 4.567 9.5 4.5C9.5 4.08333 9.64583 3.72917 9.9375 3.4375C10.2292 3.14583 10.5833 3 11 3C11.4167 3 11.7708 3.14583 12.0625 3.4375C12.3542 3.72917 12.5 4.08333 12.5 4.5C12.5 4.91667 12.3542 5.27083 12.0625 5.5625C11.7708 5.85417 11.4167 6 11 6C10.8 6 10.6125 5.9645 10.4375 5.8935C10.2625 5.8225 10.1083 5.72467 9.975 5.6L6.4625 7.65C6.47917 7.70833 6.48967 7.76467 6.494 7.819C6.49833 7.87333 6.50033 7.93367 6.5 8C6.49967 8.06633 6.49767 8.12683 6.494 8.1815C6.49033 8.23617 6.47983 8.29233 6.4625 8.35L9.975 10.4C10.1083 10.275 10.2625 10.1772 10.4375 10.1065C10.6125 10.0358 10.8 10.0003 11 10C11.4167 10 11.7708 10.1458 12.0625 10.4375C12.3542 10.7292 12.5 11.0833 12.5 11.5C12.5 11.9167 12.3542 12.2708 12.0625 12.5625C11.7708 12.8542 11.4167 13 11 13Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.4 21C16.65 21 16.0125 20.7375 15.4875 20.2125C14.9625 19.6875 14.7 19.05 14.7 18.3C14.7 18.21 14.7225 18 14.7675 17.67L8.445 13.98C8.205 14.205 7.9275 14.3814 7.6125 14.5092C7.2975 14.637 6.96 14.7006 6.6 14.7C5.85 14.7 5.2125 14.4375 4.6875 13.9125C4.1625 13.3875 3.9 12.75 3.9 12C3.9 11.25 4.1625 10.6125 4.6875 10.0875C5.2125 9.5625 5.85 9.3 6.6 9.3C6.96 9.3 7.2975 9.3639 7.6125 9.4917C7.9275 9.6195 8.205 9.7956 8.445 10.02L14.7675 6.33C14.7375 6.225 14.7189 6.1239 14.7117 6.0267C14.7045 5.9295 14.7006 5.8206 14.7 5.7C14.7 4.95 14.9625 4.3125 15.4875 3.7875C16.0125 3.2625 16.65 3 17.4 3C18.15 3 18.7875 3.2625 19.3125 3.7875C19.8375 4.3125 20.1 4.95 20.1 5.7C20.1 6.45 19.8375 7.0875 19.3125 7.6125C18.7875 8.1375 18.15 8.4 17.4 8.4C17.04 8.4 16.7025 8.3361 16.3875 8.2083C16.0725 8.0805 15.795 7.9044 15.555 7.68L9.2325 11.37C9.2625 11.475 9.2814 11.5764 9.2892 11.6742C9.297 11.772 9.3006 11.8806 9.3 12C9.2994 12.1194 9.2958 12.2283 9.2892 12.3267C9.2826 12.4251 9.2637 12.5262 9.2325 12.63L15.555 16.32C15.795 16.095 16.0725 15.9189 16.3875 15.7917C16.7025 15.6645 17.04 15.6006 17.4 15.6C18.15 15.6 18.7875 15.8625 19.3125 16.3875C19.8375 16.9125 20.1 17.55 20.1 18.3C20.1 19.05 19.8375 19.6875 19.3125 20.2125C18.7875 20.7375 18.15 21 17.4 21Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.8 29C22.7167 29 21.7958 28.6208 21.0375 27.8625C20.2792 27.1042 19.9 26.1833 19.9 25.1C19.9 24.97 19.9325 24.6667 19.9975 24.19L10.865 18.86C10.5183 19.185 10.1175 19.4398 9.6625 19.6244C9.2075 19.809 8.72 19.9009 8.2 19.9C7.11667 19.9 6.19583 19.5208 5.4375 18.7625C4.67917 18.0042 4.3 17.0833 4.3 16C4.3 14.9167 4.67917 13.9958 5.4375 13.2375C6.19583 12.4792 7.11667 12.1 8.2 12.1C8.72 12.1 9.2075 12.1923 9.6625 12.3769C10.1175 12.5615 10.5183 12.8159 10.865 13.14L19.9975 7.81C19.9542 7.65833 19.9273 7.5123 19.9169 7.3719C19.9065 7.2315 19.9009 7.0742 19.9 6.9C19.9 5.81667 20.2792 4.89583 21.0375 4.1375C21.7958 3.37917 22.7167 3 23.8 3C24.8833 3 25.8042 3.37917 26.5625 4.1375C27.3208 4.89583 27.7 5.81667 27.7 6.9C27.7 7.98333 27.3208 8.90417 26.5625 9.6625C25.8042 10.4208 24.8833 10.8 23.8 10.8C23.28 10.8 22.7925 10.7077 22.3375 10.5231C21.8825 10.3385 21.4817 10.0841 21.135 9.76L12.0025 15.09C12.0458 15.2417 12.0731 15.3881 12.0844 15.5294C12.0957 15.6707 12.1009 15.8275 12.1 16C12.0991 16.1725 12.0939 16.3298 12.0844 16.4719C12.0749 16.614 12.0476 16.7601 12.0025 16.91L21.135 22.24C21.4817 21.915 21.8825 21.6606 22.3375 21.4769C22.7925 21.2932 23.28 21.2009 23.8 21.2C24.8833 21.2 25.8042 21.5792 26.5625 22.3375C27.3208 23.0958 27.7 24.0167 27.7 25.1C27.7 26.1833 27.3208 27.1042 26.5625 27.8625C25.8042 28.6208 24.8833 29 23.8 29Z" fill="#F4F2EF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+28
View File
@@ -0,0 +1,28 @@
@use "../../styles/tokens/typography" as *;
.link__container {
width: fit-content;
display: inline-flex;
align-items: center;
}
.link {
text-decoration: none;
color: var(--nds-primary);
@include text-base;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: var(--nds-spacing-3xs);
border-bottom: 1px solid transparent;
transition:
border-color 130ms ease,
color 130ms ease;
&:hover {
border-bottom-color: var(--nds-primary);
}
&:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--nds-ring);
border-radius: var(--nds-radius-xs);
}
}
+16
View File
@@ -0,0 +1,16 @@
---
export interface Props {
url: string;
blank?: boolean;
}
const { url, blank } = Astro.props;
---
<div class='link__container'>
<a href={url} class='link' { ...(blank && { target: '_blank', rel: 'noopener noreferrer' }) }>
<slot/>
</a>
</div>
<style lang="scss">
@use './link';
</style>
+31
View File
@@ -0,0 +1,31 @@
@use "../../styles/tokens/typography" as *;
.list-item {
display: flex;
align-items: center;
gap: var(--nds-spacing-sm);
padding: var(--nds-spacing-sm) var(--nds-spacing-md);
border-radius: var(--nds-radius-md);
transition: background-color 120ms ease;
&:hover {
background-color: var(--nds-surface-hover);
}
&__body {
display: flex;
flex-direction: column;
gap: var(--nds-spacing-3xs);
flex: 1;
min-width: 0;
}
&__title {
@include text-label;
color: var(--nds-text);
margin: 0;
}
&__subtitle {
@include text-sm;
color: var(--nds-neutral);
margin: 0;
}
}
+11
View File
@@ -0,0 +1,11 @@
---
export interface Props {}
---
<div class="list-item">
<slot />
</div>
<style lang="scss">
@use "./_listItem.scss";
</style>
@@ -0,0 +1,11 @@
---
export interface Props {}
---
<p class="list-item__subtitle">
<slot />
</p>
<style lang="scss">
@use "./_listItem.scss";
</style>
@@ -0,0 +1,11 @@
---
export interface Props {}
---
<p class="list-item__title">
<slot />
</p>
<style lang="scss">
@use "./_listItem.scss";
</style>
@@ -0,0 +1,34 @@
@use "../../styles/tokens/typography" as *;
.loading-bar {
position: relative;
width: 100%;
height: 6px;
background-color: var(--nds-surface-2);
overflow: hidden;
border-radius: var(--nds-radius-full);
&__progress {
height: 100%;
background-color: var(--nds-primary);
border-radius: var(--nds-radius-full);
transition: width 0.35s ease;
}
&__unknown {
position: absolute;
top: 0;
left: 0;
width: 40%;
height: 100%;
background-color: var(--nds-primary);
border-radius: var(--nds-radius-full);
animation: nds-lb 1.3s ease-in-out infinite;
}
}
@keyframes nds-lb {
0% {
left: -40%;
}
100% {
left: 100%;
}
}
@@ -0,0 +1,25 @@
---
export interface Props {
type: "known" | "unknown";
percentage?: number;
width?: string;
}
const { type, percentage, width } = Astro.props;
---
<div
class={`loading-bar loading-bar--${type}`}
style={width ? `width: ${width}` : undefined}
>
{
type === "known" && percentage !== undefined ? (
<div class="loading-bar__progress" style={`width: ${percentage}%`} />
) : (
<div class="loading-bar__unknown" />
)
}
</div>
<style lang="scss">
@use "./_loadingBar.scss";
</style>
+78
View File
@@ -0,0 +1,78 @@
@use "../../styles/tokens/typography" as *;
.modal {
&__overlay {
position: fixed;
inset: 0;
background-color: rgba(2, 12, 12, 0.55);
backdrop-filter: blur(3px);
display: none;
align-items: center;
justify-content: center;
z-index: 200;
padding: var(--nds-spacing-md);
}
&__dialog {
width: min(460px, 100%);
background-color: var(--nds-surface);
border: var(--nds-border-width-thin) solid var(--nds-border);
border-radius: var(--nds-radius-xl);
overflow: hidden;
box-shadow: var(--nds-shadow-lg);
animation: nds-modal-in 180ms cubic-bezier(0.2, 0.8, 0.3, 1);
}
&__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--nds-spacing-md) var(--nds-spacing-lg);
border-bottom: var(--nds-border-width-thin) solid var(--nds-border);
}
&__title {
@include text-xl;
color: var(--nds-text);
}
&__close {
background: transparent;
border: none;
color: var(--nds-neutral);
cursor: pointer;
display: flex;
padding: var(--nds-spacing-3xs);
border-radius: var(--nds-radius-sm);
transition:
color 120ms ease,
background-color 120ms ease;
&:hover {
color: var(--nds-text);
background-color: var(--nds-surface-hover);
}
}
&__body {
padding: var(--nds-spacing-lg);
@include text-base;
color: var(--nds-text);
}
&__footer {
display: flex;
justify-content: flex-end;
gap: var(--nds-spacing-xs);
padding: var(--nds-spacing-md) var(--nds-spacing-lg);
border-top: var(--nds-border-width-thin) solid var(--nds-border);
}
}
nds-modal[data-open="true"] .modal__overlay {
display: flex;
}
@keyframes nds-modal-in {
from {
opacity: 0;
transform: translateY(8px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
+82
View File
@@ -0,0 +1,82 @@
---
import { XIcon } from '@lucide/astro'
export interface Props {
id: string;
title?: string;
open?: boolean;
}
const { id, title, open = false } = Astro.props;
const hasFooter = Astro.slots.has("footer");
---
<nds-modal id={id} class="modal" data-open={open ? "true" : undefined}>
<div class="modal__overlay" data-modal-close>
<div
class="modal__dialog"
role="dialog"
aria-modal="true"
aria-label={title}
>
<div class="modal__head">
<span class="modal__title">{title}</span>
<XIcon data-modal-close class="modal__close" size={18} aria-label="Close"/>
</div>
<div class="modal__body"><slot /></div>
{
hasFooter && (
<div class="modal__footer">
<slot name="footer" />
</div>
)
}
</div>
</div>
</nds-modal>
<script>
class NdsModal extends HTMLElement {
connectedCallback() {
this.addEventListener("click", (e) => {
const target = e.target as HTMLElement;
if (
target.closest("[data-modal-close]") ===
target.closest(".modal__dialog")
) {
// clicked dialog interior, ignore
}
if (
target.matches("[data-modal-close]") ||
target.classList.contains("modal__overlay")
) {
this.close();
}
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && this.dataset.open === "true") this.close();
});
}
open() {
this.dataset.open = "true";
this.dispatchEvent(new CustomEvent("nds:open", { bubbles: true }));
}
close() {
delete this.dataset.open;
this.dispatchEvent(new CustomEvent("nds:close", { bubbles: true }));
}
}
customElements.define("nds-modal", NdsModal);
document.addEventListener("click", (e) => {
const trigger = (e.target as HTMLElement).closest<HTMLElement>(
"[data-modal-open]",
);
if (!trigger) return;
const modal = document.getElementById(trigger.dataset.modalOpen!) as any;
modal?.open?.();
});
</script>
<style lang="scss">
@use "./_modal.scss";
</style>
+58
View File
@@ -0,0 +1,58 @@
@use "../../styles/tokens/typography" as *;
.navbar {
display: flex;
align-items: center;
gap: var(--nds-spacing-lg);
height: 60px;
padding: 0 var(--nds-spacing-lg);
background-color: var(--nds-surface);
border-bottom: var(--nds-border-width-thin) solid var(--nds-border);
&__brand {
display: flex;
align-items: center;
gap: var(--nds-spacing-xs);
}
&__mark {
width: 24px;
height: 24px;
border-radius: var(--nds-radius-md);
background: linear-gradient(135deg, var(--nds-primary), var(--nds-accent));
}
&__title {
@include text-xl;
font-size: 1rem;
color: var(--nds-text);
}
&__links {
display: flex;
align-items: center;
gap: var(--nds-spacing-xs);
flex: 1;
:global(.navbar__link) {
padding: var(--nds-spacing-2xs) var(--nds-spacing-sm);
border-radius: var(--nds-radius-md);
@include text-label;
color: var(--nds-text);
text-decoration: none !important;
transition: all 120ms ease;
}
:global(.navbar__link:hover) {
background-color: var(--nds-surface-hover);
}
// On cible la variante active globale séparément
:global(.navbar__link--active) {
color: var(--nds-primary);
}
}
&__right {
display: flex;
align-items: center;
gap: var(--nds-spacing-sm);
}
}
+19
View File
@@ -0,0 +1,19 @@
---
export interface Props {
brand?: string;
}
const { brand = "Nova" } = Astro.props;
---
<header class="navbar">
<div class="navbar__brand">
<span class="navbar__mark"></span>
<span class="navbar__title">{brand}</span>
</div>
<nav class="navbar__links"><slot /></nav>
<div class="navbar__right"><slot name="right" /></div>
</header>
<style lang="scss">
@use "./_navbar.scss";
</style>
@@ -0,0 +1,50 @@
@use "../../styles/tokens/typography" as *;
.notification {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--nds-spacing-sm);
padding: var(--nds-spacing-sm) var(--nds-spacing-md);
border-radius: var(--nds-radius-lg);
background-color: var(--nds-surface);
box-shadow: var(--nds-shadow-md);
border-left: 3px solid var(--nds-info-medium);
@include text-base;
max-width: 400px;
&__info {
border-left-color: var(--nds-info-medium);
}
&__success {
border-left-color: var(--nds-success-medium);
}
&__warning {
border-left-color: var(--nds-warning-medium);
}
&__error {
border-left-color: var(--nds-error-medium);
}
&__info__content,
&__success__content,
&__warning__content,
&__error__content {
color: var(--nds-text);
flex: 1;
}
&__info__close,
&__success__close,
&__warning__close,
&__error__close {
background: transparent;
border: none;
cursor: pointer;
display: flex;
color: var(--nds-neutral);
padding: 0;
border-radius: var(--nds-radius-sm);
transition: color 120ms ease;
&:hover {
color: var(--nds-text);
}
}
}
@@ -0,0 +1,30 @@
---
import { XIcon } from '@lucide/astro'
export interface Props {
type: "info" | "success" | "warning" | "error";
}
const notificationUUID = `notification-${Math.random().toString(36).slice(2, 11)}`;
const { type } = Astro.props;
---
<div
class={`notification notification__${type}`}
role="alert"
id={notificationUUID}
>
<div class={`notification__${type}__content`}>
<slot />
</div>
<button
class={`notification__${type}__close`}
aria-label="Close notification"
onclick={`document.getElementById('${notificationUUID}')?.remove()`}
>
<XIcon size={16} />
</button>
</div>
<style lang="scss">
@use "./_notification.scss";
</style>
+50
View File
@@ -0,0 +1,50 @@
@use "../../styles/tokens/typography" as *;
.radio {
display: inline-flex;
align-items: center;
gap: var(--nds-spacing-xs);
cursor: pointer;
@include text-base;
color: var(--nds-text);
&__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
&__dot {
width: 18px;
height: 18px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border: var(--nds-border-width-medium) solid var(--nds-border-strong);
border-radius: var(--nds-radius-full);
background-color: var(--nds-surface);
transition: border-color 130ms ease;
&::after {
content: "";
width: 8px;
height: 8px;
border-radius: var(--nds-radius-full);
background-color: var(--nds-primary);
transform: scale(0);
transition: transform 130ms cubic-bezier(0.34, 1.4, 0.6, 1);
}
}
&__input:checked + &__dot {
border-color: var(--nds-primary);
&::after {
transform: scale(1);
}
}
&__input:focus-visible + &__dot {
box-shadow: 0 0 0 3px var(--nds-ring);
}
&--disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
+28
View File
@@ -0,0 +1,28 @@
---
export interface Props {
id: string;
name: string;
value?: string;
checked?: boolean;
disabled?: boolean;
}
const { id, name, value, checked = false, disabled = false } = Astro.props;
---
<label class={`radio ${disabled ? "radio--disabled" : ""}`} for={id}>
<input
type="radio"
id={id}
name={name}
value={value}
checked={checked}
disabled={disabled}
class="radio__input"
/>
<span class="radio__dot" aria-hidden="true"></span>
<span class="radio__label"><slot /></span>
</label>
<style lang="scss">
@use "./_radio.scss";
</style>
+90
View File
@@ -0,0 +1,90 @@
@use "../../styles/tokens/typography" as *;
.select {
position: relative;
display: inline-flex;
flex-direction: column;
min-width: 200px;
&__trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--nds-spacing-xs);
height: 38px;
padding: 0 var(--nds-spacing-sm);
background-color: var(--nds-surface);
border: var(--nds-border-width-thin) solid var(--nds-border-strong);
border-radius: var(--nds-radius-md);
cursor: pointer;
@include text-base;
color: var(--nds-text);
transition:
border-color 130ms ease,
box-shadow 130ms ease;
&:hover {
border-color: var(--nds-neutral);
}
&:focus-visible {
outline: none;
border-color: var(--nds-primary);
box-shadow: 0 0 0 3px var(--nds-ring);
}
svg {
color: var(--nds-neutral);
transition: transform 160ms ease;
flex-shrink: 0;
}
}
&__placeholder {
color: var(--nds-neutral);
}
&[data-open="true"] &__trigger {
border-color: var(--nds-primary);
}
&[data-open="true"] &__trigger svg {
transform: rotate(180deg);
}
&__menu {
position: absolute;
top: calc(100% + 6px);
left: 0;
right: 0;
z-index: 40;
background-color: var(--nds-surface);
border: var(--nds-border-width-thin) solid var(--nds-border);
border-radius: var(--nds-radius-md);
box-shadow: var(--nds-shadow-md);
padding: var(--nds-spacing-3xs);
max-height: 240px;
overflow-y: auto;
display: none;
}
&__arrow {
display: flex;
align-items: center;
justify-content: center;
&--hidden {
display: none;
}
}
&--disabled {
cursor: not-allowed;
background-color: var(--nds-surface);
color: var(--nds-neutral);
border-color: var(--nds-border-strong);
opacity: 0.55;
& .select__trigger {
cursor: not-allowed;
&:hover {
border-color: var(--nds-border-strong);
}
}
}
&[data-open="true"] &__menu {
display: block;
}
}
+28
View File
@@ -0,0 +1,28 @@
@use "../../styles/tokens/typography" as *;
.select-option {
display: flex;
align-items: center;
gap: var(--nds-spacing-xs);
padding: var(--nds-spacing-xs) var(--nds-spacing-sm);
cursor: pointer;
border-radius: var(--nds-radius-sm);
@include text-base;
color: var(--nds-text);
transition: background-color 110ms ease;
&:hover {
background-color: var(--nds-surface-hover);
}
&[aria-selected="true"] {
color: var(--nds-primary);
font-weight: 600;
}
&__disabled {
color: var(--nds-disabled);
cursor: not-allowed;
&:hover {
background: transparent;
}
}
}
+136
View File
@@ -0,0 +1,136 @@
---
import { ChevronLeft, ChevronDown } from '@lucide/astro'
export interface Props {
default?: string;
disabled?: boolean;
value?: string;
}
const { default: defaultValue, disabled = false, value } = Astro.props;
---
<nds-select
data-default={defaultValue}
data-value={value}
data-disabled={disabled ? 'true' : undefined}
class={`select ${disabled ? 'select--disabled' : ''}`}
>
<button
class="select__trigger"
type="button"
aria-haspopup="listbox"
aria-expanded="false"
disabled={disabled}
>
<span class="select__label"></span><span class="select__arrow select__arrow--closed">
<ChevronLeft size={16} />
</span>
<span class="select__arrow select__arrow--open select__arrow--hidden">
<ChevronDown size={16} />
</span>
</button>
<ul class="select__dropdown" role="listbox">
<slot />
</ul>
</nds-select>
<style lang="scss">
@use './select.scss';
</style>
<script>
class NdsSelect extends HTMLElement {
private _value: string | null = null;
connectedCallback() {
const trigger = this.querySelector<HTMLButtonElement>('.select__trigger')!;
requestAnimationFrame(() => {
const options = this.getOptions();
const initial = this.dataset.value ?? this.dataset.default ?? null;
const match = options.find(o => o.dataset.value === initial);
this.select(match ?? options[0] ?? null, { silent: true });
trigger.addEventListener('click', () => this.toggle());
document.addEventListener('click', (e) => {
if (!this.contains(e.target as Node)) this.close();
});
this.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.close();
});
const observer = new MutationObserver(() => {
const newVal = this.dataset.value ?? null;
if (newVal !== this._value) {
const match = this.getOptions().find(o => o.dataset.value === newVal);
this.select(match ?? this.getOptions()[0] ?? null, { silent: true });
}
});
observer.observe(this, { attributes: true, attributeFilter: ['data-value'] });
});
}
getOptions(): HTMLElement[] {
return Array.from(this.querySelectorAll<HTMLElement>('[data-nds-option]'));
}
select(option: HTMLElement | null, { silent = false } = {}) {
if (!option) return;
this.getOptions().forEach(o => {
o.setAttribute('aria-selected', 'false');
o.classList.remove('select-option__selected');
});
option.setAttribute('aria-selected', 'true');
option.classList.add('select-option__selected');
const value = option.dataset.value ?? option.textContent?.trim() ?? '';
const label = option.textContent?.trim() ?? '';
this._value = value;
const labelEl = this.querySelector<HTMLElement>('.select__label');
if (labelEl) labelEl.textContent = label;
this.close();
if (!silent) {
this.dispatchEvent(new CustomEvent('nds:change', {
detail: { value, label },
bubbles: true,
}));
}
}
toggle() {
this.isOpen() ? this.close() : this.open();
}
open() {
this.classList.add('select--open');
this.querySelector('.select__trigger')?.setAttribute('aria-expanded', 'true');
this.querySelector('.select__dropdown')?.removeAttribute('hidden');
this.querySelector('.select__arrow--closed')?.classList.add('select__arrow--hidden');
this.querySelector('.select__arrow--open')?.classList.remove('select__arrow--hidden');
}
close() {
this.classList.remove('select--open');
this.querySelector('.select__trigger')?.setAttribute('aria-expanded', 'false');
this.querySelector('.select__dropdown')?.setAttribute('hidden', '');
this.querySelector('.select__arrow--closed')?.classList.remove('select__arrow--hidden');
this.querySelector('.select__arrow--open')?.classList.add('select__arrow--hidden');
}
isOpen() {
return this.classList.contains('select--open');
}
}
customElements.define('nds-select', NdsSelect);
</script>
+32
View File
@@ -0,0 +1,32 @@
---
export interface Props {
value: string;
disabled?: boolean;
}
const { value, disabled = false } = Astro.props;
---
<li
data-nds-option
data-value={value}
role="option"
aria-selected="false"
aria-disabled={disabled ? 'true' : undefined}
class={`select-option ${disabled ? 'select-option__disabled' : ''}`}
>
<slot />
</li>
<script>
document.addEventListener('click', (e) => {
const option = (e.target as HTMLElement).closest<HTMLElement>('[data-nds-option]');
if (!option || option.getAttribute('aria-disabled') === 'true') return;
const select = option.closest<any>('nds-select');
select?.select(option);
});
</script>
<style lang="scss">
@use './selectOption.scss';
</style>
+67
View File
@@ -0,0 +1,67 @@
@use "../../styles/tokens/typography" as *;
.sidebar {
display: flex;
flex-direction: column;
width: 240px;
height: 100%;
box-sizing: border-box;
padding: var(--nds-spacing-sm);
background-color: var(--nds-surface);
border-right: var(--nds-border-width-thin) solid var(--nds-border);
&__head {
display: flex;
align-items: center;
gap: var(--nds-spacing-xs);
padding: var(--nds-spacing-xs) var(--nds-spacing-xs) var(--nds-spacing-md);
}
&__mark {
width: 24px;
height: 24px;
border-radius: var(--nds-radius-md);
background: linear-gradient(135deg, var(--nds-primary), var(--nds-accent));
}
&__title {
@include text-xl;
font-size: 1rem;
color: var(--nds-text);
}
&__nav {
display: flex;
flex-direction: column;
gap: var(--nds-spacing-3xs);
flex: 1;
}
&__item {
display: flex;
align-items: center;
gap: var(--nds-spacing-xs);
padding: var(--nds-spacing-xs) var(--nds-spacing-sm);
border-radius: var(--nds-radius-md);
text-decoration: none;
color: var(--nds-text);
@include text-label;
font-weight: 500;
transition:
background-color 120ms ease,
color 120ms ease;
&:hover {
background-color: var(--nds-surface-hover);
}
&--active {
background-color: var(--nds-primary-soft);
color: var(--nds-primary);
font-weight: 600;
}
}
&__item-icon {
display: flex;
flex-shrink: 0;
}
&__footer {
padding-top: var(--nds-spacing-sm);
}
}
+23
View File
@@ -0,0 +1,23 @@
---
export interface Props {
header?: string;
}
const { header = "Nova" } = Astro.props;
---
<aside class="sidebar">
<div class="sidebar__head">
<span class="sidebar__mark"></span>
<span class="sidebar__title">{header}</span>
</div>
<nav class="sidebar__nav">
<slot />
</nav>
<div class="sidebar__footer">
<slot name="footer" />
</div>
</aside>
<style lang="scss">
@use "./_sidebar.scss";
</style>
+23
View File
@@ -0,0 +1,23 @@
---
export interface Props {
href?: string;
active?: boolean;
}
const { href = "#", active = false } = Astro.props;
const hasIcon = Astro.slots.has("icon");
---
<a href={href} class={`sidebar__item ${active ? "sidebar__item--active" : ""}`}>
{
hasIcon && (
<span class="sidebar__item-icon">
<slot name="icon" />
</span>
)
}
<span class="sidebar__item-label"><slot /></span>
</a>
<style lang="scss">
@use "./_sidebar.scss";
</style>
+40
View File
@@ -0,0 +1,40 @@
@use "../../styles/tokens/typography" as *;
.tab {
display: flex;
align-items: stretch;
gap: var(--nds-spacing-2xs);
border-bottom: 1px solid var(--nds-border);
width: fit-content;
&__item {
display: inline-flex;
align-items: center;
gap: var(--nds-spacing-2xs);
padding: var(--nds-spacing-xs) var(--nds-spacing-sm);
cursor: pointer;
@include text-label;
color: var(--nds-neutral);
background: transparent;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition:
color 130ms ease,
border-color 130ms ease;
&:hover {
color: var(--nds-text);
}
&--active {
color: var(--nds-text);
border-bottom-color: var(--nds-primary);
}
}
}
.tab__content {
padding-top: var(--nds-spacing-md);
@include text-base;
color: var(--nds-text);
&[hidden] {
display: none;
}
}
+54
View File
@@ -0,0 +1,54 @@
---
export interface Props {
id: string;
defaultActive?: string;
}
const { id, defaultActive } = Astro.props;
---
<div class="tab" data-tab-group id={id} data-default-active={defaultActive}>
<slot />
</div>
<script>
document.querySelectorAll<HTMLElement>('[data-tab-group]').forEach((group) => {
const defaultActive = group.dataset.defaultActive;
function setActive(itemId: string) {
const groupId = group.id;
group.querySelectorAll<HTMLElement>('[data-tab-item]').forEach((item) => {
item.classList.toggle('tab__item--active', item.dataset.tabItem === itemId);
item.setAttribute('aria-selected', String(item.dataset.tabItem === itemId));
});
document.querySelectorAll<HTMLElement>(`[data-tab-content][data-tab-id="${groupId}"]`).forEach((content) => {
content.hidden = content.dataset.tabItemId !== itemId;
});
group.dispatchEvent(new CustomEvent('tab:change', {
detail: { itemId },
bubbles: true,
}));
}
const firstItem = group.querySelector<HTMLElement>('[data-tab-item]');
const initialId = defaultActive ?? firstItem?.dataset.tabItem ?? '';
if (initialId) setActive(initialId);
group.addEventListener('tab-item:click', (e: Event) => {
setActive((e as CustomEvent<{ itemId: string }>).detail.itemId);
});
group.addEventListener('tab:set', (e: Event) => {
setActive((e as CustomEvent<{ itemId: string }>).detail.itemId);
});
(group as any).setActiveTab = (itemId: string) => setActive(itemId);
});
</script>
<style lang="scss">
@use './tab';
</style>
+21
View File
@@ -0,0 +1,21 @@
---
export interface Props {
id: string;
itemId: string;
}
const { id, itemId } = Astro.props;
---
<div
data-tab-content
data-tab-id={id}
data-tab-item-id={itemId}
hidden
>
<slot />
</div>
<style lang="scss">
@use './tab';
</style>
+41
View File
@@ -0,0 +1,41 @@
---
export interface Props {
id: string;
}
const { id } = Astro.props;
---
<div
class="tab__item"
data-tab-item={id}
role="tab"
aria-selected="false"
tabindex="0"
>
<slot />
</div>
<script>
document.querySelectorAll<HTMLElement>('[data-tab-item]').forEach((item) => {
function notify() {
const group = item.closest('[data-tab-group]');
if (!group) return;
group.dispatchEvent(new CustomEvent('tab-item:click', {
detail: { itemId: item.dataset.tabItem },
}));
}
item.addEventListener('click', notify);
item.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
notify();
}
});
});
</script>
<style lang="scss">
@use './tab';
</style>
+13
View File
@@ -0,0 +1,13 @@
.toggle {
--w: 44px; --h: 24px; --pad: 3px;
position: relative; width: var(--w); height: var(--h);
background-color: var(--nds-disabled); border-radius: var(--nds-radius-full);
padding: var(--pad); display: flex; align-items: center; cursor: pointer;
transition: background-color 180ms ease;
&-switch { width: calc(var(--h) - var(--pad) * 2); height: calc(var(--h) - var(--pad) * 2);
background-color: #fff; border-radius: var(--nds-radius-full); box-shadow: var(--nds-shadow-sm);
transition: transform 180ms cubic-bezier(.34,1.4,.6,1); }
&-active { background-color: var(--nds-primary); }
&-active .toggle-switch { transform: translateX(calc(var(--w) - var(--h))); }
}
toggle-switch:focus-visible .toggle { box-shadow: 0 0 0 3px var(--nds-ring); outline: none; }
+60
View File
@@ -0,0 +1,60 @@
---
export interface Props {
'data-checked'?: boolean | string;
name?: string;
id: string;
}
const { 'data-checked': dataChecked = 'false', name = 'toggle', id } = Astro.props;
---
<toggle-switch data-checked={String(dataChecked)} data-name={name} id={id}>
<div class="toggle">
<div class="toggle-switch"></div>
</div>
</toggle-switch>
<script>
class ToggleSwitch extends HTMLElement {
private _checked: boolean = false;
connectedCallback() {
this._checked = this.dataset.checked === 'true';
this.updateUI();
this.querySelector('.toggle')?.addEventListener('click', () => {
this.toggle();
});
}
toggle() {
this._checked = !this._checked;
this.updateUI();
this.dispatchEvent(new CustomEvent('change', {
detail: { checked: this._checked, name: this.dataset.name },
bubbles: true,
composed: true,
}));
}
set checked(value: boolean) {
this._checked = value;
this.updateUI();
}
get checked() {
return this._checked;
}
private updateUI() {
this.dataset.checked = String(this._checked);
this.querySelector('.toggle')?.classList.toggle('toggle-active', this._checked);
}
}
customElements.define('toggle-switch', ToggleSwitch);
</script>
<style lang="scss">
@use './_toggle.scss';
</style>
+45
View File
@@ -0,0 +1,45 @@
@use "../../styles/tokens/typography" as *;
.tooltip {
position: relative;
display: inline-flex;
&__bubble {
position: absolute;
white-space: nowrap;
padding: var(--nds-spacing-2xs) var(--nds-spacing-xs);
background-color: var(--nds-text);
color: var(--nds-background);
border-radius: var(--nds-radius-sm);
@include text-sm;
font-weight: 500;
box-shadow: var(--nds-shadow-md);
z-index: 100;
opacity: 0;
pointer-events: none;
transition: opacity 130ms ease;
}
&:hover &__bubble,
&:focus-within &__bubble {
opacity: 1;
}
&--top &__bubble {
bottom: 100%;
left: 50%;
transform: translate(-50%, -6px);
}
&--bottom &__bubble {
top: 100%;
left: 50%;
transform: translate(-50%, 6px);
}
&--left &__bubble {
right: 100%;
top: 50%;
transform: translate(-6px, -50%);
}
&--right &__bubble {
left: 100%;
top: 50%;
transform: translate(6px, -50%);
}
}
+16
View File
@@ -0,0 +1,16 @@
---
export interface Props {
label: string;
position?: "top" | "bottom" | "left" | "right";
}
const { label, position = "top" } = Astro.props;
---
<span class={`tooltip tooltip--${position}`}>
<slot />
<span class="tooltip__bubble" role="tooltip">{label}</span>
</span>
<style lang="scss">
@use "./_tooltip.scss";
</style>
+29 -3
View File
@@ -1,3 +1,29 @@
// Auto-generated by scripts/generate-barrel.mjs — do not edit manually. export { default as Notification } from "./Notifications/notification.astro";
export { default as Toggle } from "./Toggle/toggle.astro";
export * from './Icons/index.ts'; export { default as Tab } from "./Tabs/tab.astro";
export { default as TabItem } from "./Tabs/tabItem.astro";
export { default as TabContent } from "./Tabs/tabContent.astro";
export { default as Button } from "./Button/button.astro";
export { default as Link } from "./Link/link.astro";
export { default as ListItem } from "./ListItem/listItem.astro";
export { default as ListItemTitle } from "./ListItem/listItemTitle.astro";
export { default as ListItemSubtitle } from "./ListItem/listItemSubtitle.astro";
export { default as LoadingBar } from "./LoadingBar/loadingBar.astro";
export { default as NumericStepper } from "./numericStepper/numericStepper.astro";
export { default as Avatar } from "./Avatar/avatar.astro";
export { default as Select } from "./Select/select.astro";
export { default as SelectOption } from "./Select/selectOption.astro";
export { default as TextField } from "./textField/textField.astro";
export { default as Pagination } from "./pagination/pagination.astro";
export { default as PaginationNumber } from "./pagination/paginationNumber.astro";
export { default as Badge } from "./Badge/badge.astro";
export { default as Sidebar } from "./Sidebar/sidebar.astro";
export { default as SidebarItem } from "./Sidebar/sidebarItem.astro";
export { default as Tooltip } from "./Tooltip/tooltip.astro";
export { default as Modal } from "./Modal/modal.astro";
export { default as Radio } from "./Radio/radio.astro";
export { default as Card } from "./Card/card.astro";
export { default as Breadcrumb } from "./Breadcrumb/breadcrumb.astro";
export { default as BreadcrumbItem } from "./Breadcrumb/breadcrumbItem.astro";
export { default as Checkbox } from "./Checkbox/checkbox.astro";
export { default as Navbar } from "./Navbar/navbar.astro";
@@ -0,0 +1,44 @@
@use "../../styles/tokens/typography" as *;
.numeric-stepper {
display: inline-flex;
align-items: center;
gap: 0;
border: var(--nds-border-width-thin) solid var(--nds-border-strong);
border-radius: var(--nds-radius-md);
background-color: var(--nds-surface);
overflow: hidden;
width: fit-content;
height: fit-content;
&__button {
width: 36px;
height: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
cursor: pointer;
color: var(--nds-text);
transition: background-color 120ms ease;
&:hover:not(:disabled) {
background-color: var(--nds-surface-hover);
}
&:disabled {
color: var(--nds-disabled);
cursor: not-allowed;
}
}
&__value {
width: 48px;
text-align: center;
@include text-label;
color: var(--nds-text);
padding: 0;
margin: 0;
border: none;
outline: none;
background: transparent;
}
}
@@ -0,0 +1,117 @@
---
import Button from '../Button/button.astro';
import { Minus, Plus } from '@lucide/astro'
export interface Props {
id: string;
min?: number;
max?: number;
value?: number;
step?: number;
}
const { id, min, max, value = 0, step = 1 } = Astro.props;
---
<div
class="numeric-stepper"
id={id}
data-numeric-stepper
data-min={min}
data-max={max}
data-value={value}
data-step={step}
>
<span class="numeric-stepper__control" data-action="increment">
<Button type="ghost" icon={true}>
<Minus size={24} />
</Button>
</span>
<p class="numeric-stepper__value">{value}</p>
<span class="numeric-stepper__control" data-action="decrement">
<Button type="ghost" icon={true}>
<Plus size={24} />
</Button>
</span>
</div>
<script>
type StepperAction = 'increment' | 'decrement';
interface StepperChangeDetail {
value: number;
}
interface StepperSetDetail {
value: number;
}
interface NumericStepperElement extends HTMLElement {
setValue: (value: number) => void;
getValue: () => number;
getStep: () => number;
}
declare global {
interface HTMLElementEventMap {
'stepper:change': CustomEvent<StepperChangeDetail>;
'stepper:set': CustomEvent<StepperSetDetail>;
}
}
document.querySelectorAll<NumericStepperElement>('[data-numeric-stepper]').forEach((stepper) => {
const { min: minAttr, max: maxAttr, step: stepAttr, value: valueAttr } = stepper.dataset;
const min = minAttr !== undefined ? Number(minAttr) : -Infinity;
const max = maxAttr !== undefined ? Number(maxAttr) : Infinity;
const step = stepAttr !== undefined ? Number(stepAttr) : 1;
let current = valueAttr !== undefined ? Number(valueAttr) : 0;
const display = stepper.querySelector<HTMLParagraphElement>('.numeric-stepper__value');
function render(): void {
if (display) display.textContent = String(current);
stepper.dataset.value = String(current);
}
function setValue(newValue: number): void {
const clamped = Math.min(max, Math.max(min, newValue));
if (clamped === current) return;
current = clamped;
render();
stepper.dispatchEvent(
new CustomEvent<StepperChangeDetail>('stepper:change', {
detail: { value: current },
bubbles: true,
}),
);
}
stepper.addEventListener('click', (event: MouseEvent) => {
const target = event.target as HTMLElement;
const control = target.closest<HTMLElement>('[data-action]');
if (!control || !stepper.contains(control)) return;
const action = control.dataset.action as StepperAction | undefined;
if (action === 'increment') setValue(current + step);
else if (action === 'decrement') setValue(current - step);
});
stepper.addEventListener('stepper:set', (event) => {
setValue(event.detail.value);
});
stepper.setValue = (value: number): void => setValue(value);
stepper.getValue = (): number => current;
stepper.getStep = (): number => step;
});
</script>
<style lang="scss">
@use "./numericStepper";
</style>
@@ -0,0 +1,7 @@
@use "../../styles/tokens/typography" as *;
.pagination {
display: flex;
align-items: center;
gap: var(--nds-spacing-2xs);
}
@@ -0,0 +1,30 @@
@use "../../styles/tokens/typography" as *;
.pagination-number {
min-width: 32px;
height: 32px;
padding: 0 var(--nds-spacing-2xs);
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--nds-radius-md);
cursor: pointer;
@include text-label;
color: var(--nds-text);
background: transparent;
border: 1px solid transparent;
transition: all 120ms ease;
user-select: none;
&:hover:not(.pagination-number__disabled):not(.pagination-number__selected) {
border-color: var(--nds-border-strong);
background-color: var(--nds-surface-hover);
}
&__selected {
background-color: var(--nds-primary);
color: var(--nds-on-primary);
}
&__disabled {
color: var(--nds-disabled);
cursor: not-allowed;
}
}
@@ -0,0 +1,36 @@
---
export interface Props {
defaultPage?: number;
}
const { defaultPage = 1 } = Astro.props;
---
<div class="pagination" data-current={defaultPage}>
<slot/>
</div>
<script>
const pagination = document.querySelector('.pagination');
const numbers = pagination?.querySelectorAll('.pagination-number');
numbers?.forEach((el) => {
if (
!el.classList.contains('pagination-number__disabled') &&
el.textContent?.trim() === pagination?.getAttribute('data-current')
) {
el.classList.add('pagination-number__selected');
}
el.addEventListener('click', () => {
if (el.classList.contains('pagination-number__disabled')) return;
numbers.forEach(n => n.classList.remove('pagination-number__selected'));
el.classList.add('pagination-number__selected');
pagination?.setAttribute('data-current', el.textContent?.trim() ?? '');
});
});
</script>
<style lang="scss">
@use './pagination';
</style>
@@ -0,0 +1,20 @@
---
export interface Props {
disabled?: boolean;
}
const { disabled } = Astro.props;
const classes = [
'pagination-number',
disabled ? 'pagination-number__disabled' : ''
].filter(Boolean).join(' ');
---
<div class={classes}>
<slot/>
</div>
<style lang="scss">
@use './paginationNumber';
</style>
+64
View File
@@ -0,0 +1,64 @@
@use "../../styles/tokens/typography" as *;
.textField {
display: flex;
flex-direction: column;
gap: var(--nds-spacing-2xs);
& label {
@include text-label;
color: var(--nds-text);
}
&__input,
&__textarea {
background-color: var(--nds-surface);
border: var(--nds-border-width-thin) solid var(--nds-border-strong);
border-radius: var(--nds-radius-md);
padding: 0 var(--nds-spacing-sm);
@include text-base;
color: var(--nds-text);
transition:
border-color 130ms ease,
box-shadow 130ms ease;
&::placeholder {
color: var(--nds-neutral);
}
&:hover {
border-color: var(--nds-neutral);
}
&:focus {
outline: none;
border-color: var(--nds-primary);
box-shadow: 0 0 0 3px var(--nds-ring);
}
&:disabled {
opacity: 0.55;
cursor: not-allowed;
}
}
&__input {
height: 38px;
}
&__textarea {
min-height: 84px;
padding: var(--nds-spacing-sm);
width: 100%;
line-height: 1.5;
}
&__charCount {
@include text-sm;
color: var(--nds-neutral);
text-align: right;
}
&__error {
@include text-sm;
color: var(--nds-error-medium);
}
&--invalid &__input,
&--invalid &__textarea {
border-color: var(--nds-error-medium);
&:focus {
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--nds-error-medium) 35%, transparent);
}
}
}
+65
View File
@@ -0,0 +1,65 @@
---
export interface Props {
id?: string;
label?: string;
type?: 'text' | 'email' | 'password' | 'textarea';
placeholder?: string;
min?: number;
max?: number;
value?: number;
step?: number;
}
const { id, label, type = 'text', placeholder, min, max, value, step } = Astro.props;
const charCounter = 0;
---
<div
class="textField"
id={id}
>
{label && <label for={id}>{label}</label>}
{type === 'textarea' ? (
<div>
<textarea
id={id}
placeholder={placeholder}
maxlength={max}
class="textField__textarea"
></textarea>
{max && (
<div class="textField__charCount">
<span id={`${id}-charCount`}>{charCounter}</span> / {max}
</div>
)}
</div>
) : (
<input
id={id}
type={type}
placeholder={placeholder}
min={min}
max={max}
value={value}
step={step}
class="textField__input"
/>
)}
</div>
<script>
document.querySelectorAll<HTMLElement>('.textField').forEach((field) => {
const textarea = field.querySelector<HTMLTextAreaElement>('.textField__textarea');
const counter = field.querySelector<HTMLElement>('[id$="-charCount"]');
if (!textarea || !counter) return;
textarea.addEventListener('input', () => {
counter.textContent = String(textarea.value.length);
});
});
</script>
<style lang="scss">
@use './textField';
</style>
+12 -24
View File
@@ -1,29 +1,17 @@
--- ---
import '../styles/index.scss'; import '../styles/index.scss';
interface Props { title?: string; }
const { title = 'Nova Design System' } = Astro.props;
--- ---
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en" data-theme="dark">
<head> <head>
<meta charset="UTF-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" /> <meta name="generator" content={Astro.generator} />
<meta name="generator" content={Astro.generator} /> <title>{title}</title>
<title>Nova Design System</title> </head>
</head> <body>
<body> <slot />
<slot /> </body>
</body>
</html> </html>
<style>
html,
body {
margin: 0;
width: 100%;
min-height: 100%;
font-family: var(--nds-font-family-base);
background-color: var(--nds-page-low);
color: var(--nds-default);
}
</style>
+1036 -86
View File
File diff suppressed because it is too large Load Diff
+60 -138
View File
@@ -1,165 +1,87 @@
// ─── Semantic color tokens — Light & Dark ────────────────────────────────────
// Source: Light.tokens.json / Dark.tokens.json (Figma export)
// Usage : --nds-<category>-<variant>
@mixin _light { @mixin _light {
// ── Base ────────────────────────────────────────────────────────────────── // ── Brand ──
--nds-default: #1C1C1A; --nds-text: #050A0A;
--nds-icon: #1C1C1A; --nds-neutral: #5E6E6E; // refined: warmer, AA on background
--nds-overlay: rgba(28, 28, 26, 0.60); --nds-disabled: #C2D4D4;
--nds-scrim: rgba(28, 28, 26, 0.35); --nds-background: #F4FBFB;
--nds-primary: #1FA6AD; // refined: AA contrast for text/borders
--nds-secondary: #71EAEA;
--nds-accent: #05D1D1;
// ── Brand ───────────────────────────────────────────────────────────────── // ── Semantic surfaces & borders ──
--nds-brand-primary: #534AB7; --nds-surface: #FFFFFF;
--nds-brand-high: #3C3489; --nds-surface-2: #E9F4F4;
--nds-brand-medium: #7F77DD; --nds-surface-hover: #EEF8F8;
--nds-brand-low: #EEEDFE; --nds-border: #DCEAEA;
--nds-border-strong: #C2D4D4;
// ── Neutral ─────────────────────────────────────────────────────────────── // ── On-color & soft tints ──
--nds-neutral-high: #2C2C2A; --nds-on-primary: #FFFFFF;
--nds-neutral-medium: #888780; --nds-primary-soft: #DBF3F4;
--nds-neutral-low: #F1EFE8; --nds-ring: color-mix(in srgb, var(--nds-primary) 45%, transparent);
// ── Correct ─────────────────────────────────────────────────────────────── // ── Status ──
--nds-correct-high: #3B6D11; --nds-success-high: #3B6D11;
--nds-correct-medium: #639922; --nds-success-medium: #5C9120;
--nds-correct-low: #EAF3DE; --nds-success-low: #EAF3DE;
// ── Error ─────────────────────────────────────────────────────────────────
--nds-error-high: #A32D2D; --nds-error-high: #A32D2D;
--nds-error-medium: #E24B4A; --nds-error-medium: #E24B4A;
--nds-error-low: #FCEBEB; --nds-error-low: #FCEBEB;
// ── Warning ───────────────────────────────────────────────────────────────
--nds-warning-high: #854F0B; --nds-warning-high: #854F0B;
--nds-warning-medium: #BA7517; --nds-warning-medium: #BA7517;
--nds-warning-low: #FAEEDA; --nds-warning-low: #FAEEDA;
// ── Info ──────────────────────────────────────────────────────────────────
--nds-info-high: #185FA5; --nds-info-high: #185FA5;
--nds-info-medium: #378ADD; --nds-info-medium: #378ADD;
--nds-info-low: #E6F1FB; --nds-info-low: #E6F1FB;
// ── Interaction ─────────────────────────────────────────────────────────── // ── Elevation ──
--nds-interaction-high: #26215C; --nds-shadow-sm: 0 1px 2px rgba(5, 20, 20, 0.06);
--nds-interaction-medium: #534AB7; --nds-shadow-md: 0 4px 14px rgba(5, 20, 20, 0.10);
--nds-interaction-low: #CECBF6; --nds-shadow-lg: 0 16px 40px rgba(5, 20, 20, 0.16);
// ── Disabled ──────────────────────────────────────────────────────────────
--nds-disabled-high: #B4B2A9;
--nds-disabled-medium: #D3D1C7;
--nds-disabled-low: #F1EFE8;
// ── Overlay (alpha) ───────────────────────────────────────────────────────
--nds-overlay-high: rgba(0, 0, 0, 0.72);
--nds-overlay-medium: rgba(0, 0, 0, 0.40);
--nds-overlay-low: rgba(0, 0, 0, 0.16);
// ── Focus ─────────────────────────────────────────────────────────────────
--nds-focus-ring: #534AB7;
// ── Text ──────────────────────────────────────────────────────────────────
--nds-text-muted: #888780;
--nds-text-placeholder: #B4B2A9;
--nds-text-link: #534AB7;
--nds-text-on-brand: #F4F2EF;
// ── Border colors ─────────────────────────────────────────────────────────
--nds-border-high: #B4B2A9;
--nds-border-medium: #D3D1C7;
--nds-border-low: #F1EFE8;
// ── Component backgrounds ─────────────────────────────────────────────────
--nds-component-low: #ECEAE3;
--nds-component-medium: #F8F7F4;
--nds-component-high: #FFFFFF;
// ── Page backgrounds ──────────────────────────────────────────────────────
--nds-page-low: #F4F2EF;
--nds-page-medium: #ECEAE3;
--nds-page-high: #E4E2DB;
} }
@mixin _dark { @mixin _dark {
// ── Base ────────────────────────────────────────────────────────────────── // ── Brand ──
--nds-default: #F4F2EF; --nds-text: #ECF5F5;
--nds-icon: #F4F2EF; --nds-neutral: #8AA0A0; // refined: lighter for AA on dark background
--nds-overlay: rgba(0, 0, 0, 0.75); --nds-disabled: #324242;
--nds-scrim: rgba(0, 0, 0, 0.50); --nds-background: #040B0B;
--nds-primary: #34C2C9; // refined: brighter on dark
--nds-secondary: #288181;
--nds-accent: #2EFAFA;
// ── Brand ───────────────────────────────────────────────────────────────── // ── Semantic surfaces & borders ──
--nds-brand-primary: #7F77DD; --nds-surface: #0B1717;
--nds-brand-high: #AFA9EC; --nds-surface-2: #102020;
--nds-brand-medium: #534AB7; --nds-surface-hover: #122525;
--nds-brand-low: #26215C; --nds-border: #1E3030;
--nds-border-strong: #2A4040;
// ── Neutral ─────────────────────────────────────────────────────────────── // ── On-color & soft tints ──
--nds-neutral-high: #F1EFE8; --nds-on-primary: #03191A;
--nds-neutral-medium: #888780; --nds-primary-soft: #0A2A2C;
--nds-neutral-low: #2C2C2A; --nds-ring: color-mix(in srgb, var(--nds-primary) 50%, transparent);
// ── Correct ─────────────────────────────────────────────────────────────── // ── Status ──
--nds-correct-high: #97C459; --nds-success-high: #B6DC84;
--nds-correct-medium: #97C459; --nds-success-medium: #97C459;
--nds-correct-low: #173404; --nds-success-low: #17260A;
// ── Error ─────────────────────────────────────────────────────────────────
--nds-error-high: #F09595; --nds-error-high: #F09595;
--nds-error-medium: #A32D2D; --nds-error-medium: #E05A59;
--nds-error-low: #501313; --nds-error-low: #2E1212;
--nds-warning-high: #EFB45A;
--nds-warning-medium: #E0A23A;
--nds-warning-low: #2A1D06;
--nds-info-high: #9CC6F2;
--nds-info-medium: #5BA0E6;
--nds-info-low: #0A2236;
// ── Warning ─────────────────────────────────────────────────────────────── // ── Elevation ──
--nds-warning-high: #EF9F27; --nds-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.40);
--nds-warning-medium: #EF9F27; --nds-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.50);
--nds-warning-low: #412402; --nds-shadow-lg: 0 18px 44px rgba(0, 0, 0, 0.62);
// ── Info ──────────────────────────────────────────────────────────────────
--nds-info-high: #85B7EB;
--nds-info-medium: #85B7EB;
--nds-info-low: #042C53;
// ── Interaction ───────────────────────────────────────────────────────────
--nds-interaction-high: #AFA9EC;
--nds-interaction-medium: #7F77DD;
--nds-interaction-low: #26215C;
// ── Disabled ──────────────────────────────────────────────────────────────
--nds-disabled-high: #5F5E5A;
--nds-disabled-medium: #444441;
--nds-disabled-low: #2C2C2A;
// ── Overlay (alpha) ───────────────────────────────────────────────────────
--nds-overlay-high: rgba(255, 255, 255, 0.80);
--nds-overlay-medium: rgba(255, 255, 255, 0.50);
--nds-overlay-low: rgba(255, 255, 255, 0.24);
// ── Focus ─────────────────────────────────────────────────────────────────
--nds-focus-ring: #7F77DD;
// ── Text ──────────────────────────────────────────────────────────────────
--nds-text-muted: #888780;
--nds-text-placeholder: #5F5E5A;
--nds-text-link: #7F77DD;
--nds-text-on-brand: #1C1C1A;
// ── Border colors ─────────────────────────────────────────────────────────
--nds-border-high: #5F5E5A;
--nds-border-medium: #444441;
--nds-border-low: #2C2C2A;
// ── Component backgrounds ─────────────────────────────────────────────────
--nds-component-low: #222220;
--nds-component-medium: #2A2A28;
--nds-component-high: #333330;
// ── Page backgrounds ──────────────────────────────────────────────────────
--nds-page-low: #1C1C1A;
--nds-page-medium: #242422;
--nds-page-high: #2C2C2A;
} }
// ─── Apply themes ─────────────────────────────────────────────────────────────
:root, :root,
[data-theme="light"] { [data-theme="light"] {
@include _light; @include _light;
-1
View File
@@ -1,5 +1,4 @@
// ─── Base tokens (theme-independent) ────────────────────────────────────────── // ─── Base tokens (theme-independent) ──────────────────────────────────────────
// Source: Light.tokens.json / Dark.tokens.json — Spacing, Radius, Border (same in both themes)
:root { :root {
--nds-none: 0; --nds-none: 0;
+76 -95
View File
@@ -1,112 +1,93 @@
// ─── Typography tokens ──────────────────────────────────────────────────────── // ─── Nova typography ──────────────────────────────────────────────────────────
// Font: Geist (Vercel) — https://vercel.com/font // Display / headings: Intel One Mono (brand). UI / body: Geist (legibility).
// Values: size in px, line-height in px, letter-spacing as em @import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Intel+One+Mono:wght@400;500;700&display=swap');
$font-mono: 'Intel One Mono', ui-monospace, monospace;
$font-sans: 'Geist', system-ui, -apple-system, 'Segoe UI', sans-serif;
:root { :root {
// ── Font families ───────────────────────────────────────────────────────── --nds-font-mono: #{$font-mono};
--nds-font-family-base: 'Geist', system-ui, -apple-system, sans-serif; --nds-font-sans: #{$font-sans};
--nds-font-family-mono: 'Geist Mono', 'JetBrains Mono', monospace;
// ── Font weights ──────────────────────────────────────────────────────────
--nds-font-weight-medium: 500;
--nds-font-weight-semibold: 600;
--nds-font-weight-bold: 700;
// ── Body ──────────────────────────────────────────────────────────────────
--nds-type-body-small-size: 12px;
--nds-type-body-small-weight: var(--nds-font-weight-medium);
--nds-type-body-small-line-height: 10px;
--nds-type-body-small-letter-spacing: 0.015em;
--nds-type-body-medium-size: 14px;
--nds-type-body-medium-weight: var(--nds-font-weight-medium);
--nds-type-body-medium-line-height: 10px;
--nds-type-body-medium-letter-spacing: 0.015em;
--nds-type-body-large-size: 16px;
--nds-type-body-large-weight: var(--nds-font-weight-medium);
--nds-type-body-large-line-height: 12px;
--nds-type-body-large-letter-spacing: 0.015em;
// ── Heading ───────────────────────────────────────────────────────────────
--nds-type-heading-small-size: 20px;
--nds-type-heading-small-weight: var(--nds-font-weight-semibold);
--nds-type-heading-small-line-height: 15px;
--nds-type-heading-small-letter-spacing: 0.014em;
--nds-type-heading-medium-size: 24px;
--nds-type-heading-medium-weight: var(--nds-font-weight-semibold);
--nds-type-heading-medium-line-height: 18px;
--nds-type-heading-medium-letter-spacing: 0.013em;
// ── Display ───────────────────────────────────────────────────────────────
--nds-type-display-small-size: 36px;
--nds-type-display-small-weight: var(--nds-font-weight-bold);
--nds-type-display-small-line-height: 30px;
--nds-type-display-small-letter-spacing: 0.012em;
--nds-type-display-medium-size: 48px;
--nds-type-display-medium-weight: var(--nds-font-weight-bold);
--nds-type-display-medium-line-height: 38px;
--nds-type-display-medium-letter-spacing: 0.011em;
} }
// ─── SASS mixins ────────────────────────────────────────────────────────────── // ── UI / body (Geist) ──
// Usage: @use 'path/to/tokens/typography' as typo; @mixin text-sm {
// @include typo.body-small; font-family: $font-sans;
font-size: 0.75rem; // 12px
@mixin body-small { font-weight: 400;
font-family: var(--nds-font-family-base); line-height: 1.45;
font-size: var(--nds-type-body-small-size); letter-spacing: 0.005em;
font-weight: var(--nds-type-body-small-weight);
line-height: var(--nds-type-body-small-line-height);
letter-spacing: var(--nds-type-body-small-letter-spacing);
} }
@mixin body-medium { @mixin text-base {
font-family: var(--nds-font-family-base); font-family: $font-sans;
font-size: var(--nds-type-body-medium-size); font-size: 0.875rem; // 14px
font-weight: var(--nds-type-body-medium-weight); font-weight: 400;
line-height: var(--nds-type-body-medium-line-height); line-height: 1.5;
letter-spacing: var(--nds-type-body-medium-letter-spacing);
} }
@mixin body-large { @mixin text-label {
font-family: var(--nds-font-family-base); font-family: $font-sans;
font-size: var(--nds-type-body-large-size); font-size: 0.8125rem; // 13px
font-weight: var(--nds-type-body-large-weight); font-weight: 600;
line-height: var(--nds-type-body-large-line-height); line-height: 1.4;
letter-spacing: var(--nds-type-body-large-letter-spacing); letter-spacing: 0.01em;
} }
@mixin heading-small { @mixin text-lg {
font-family: var(--nds-font-family-base); font-family: $font-sans;
font-size: var(--nds-type-heading-small-size); font-size: 1rem; // 16px
font-weight: var(--nds-type-heading-small-weight); font-weight: 600;
line-height: var(--nds-type-heading-small-line-height); line-height: 1.5;
letter-spacing: var(--nds-type-heading-small-letter-spacing);
} }
@mixin heading-medium { // ── Display / headings (Intel One Mono) ──
font-family: var(--nds-font-family-base); @mixin text-xl {
font-size: var(--nds-type-heading-medium-size); font-family: $font-mono;
font-weight: var(--nds-type-heading-medium-weight); font-size: 1.125rem; // 18px
line-height: var(--nds-type-heading-medium-line-height); font-weight: 700;
letter-spacing: var(--nds-type-heading-medium-letter-spacing); line-height: 1.35;
letter-spacing: -0.01em;
} }
@mixin display-small { @mixin text-2xl {
font-family: var(--nds-font-family-base); font-family: $font-mono;
font-size: var(--nds-type-display-small-size); font-size: 1.375rem; // 22px
font-weight: var(--nds-type-display-small-weight); font-weight: 700;
line-height: var(--nds-type-display-small-line-height); line-height: 1.3;
letter-spacing: var(--nds-type-display-small-letter-spacing); letter-spacing: -0.015em;
} }
@mixin display-medium { @mixin text-3xl {
font-family: var(--nds-font-family-base); font-family: $font-mono;
font-size: var(--nds-type-display-medium-size); font-size: 1.875rem; // 30px
font-weight: var(--nds-type-display-medium-weight); font-weight: 700;
line-height: var(--nds-type-display-medium-line-height); line-height: 1.2;
letter-spacing: var(--nds-type-display-medium-letter-spacing); letter-spacing: -0.02em;
}
@mixin text-4xl {
font-family: $font-mono;
font-size: 2.5rem; // 40px
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.025em;
}
@mixin text-5xl {
font-family: $font-mono;
font-size: 3.25rem; // 52px
font-weight: 700;
line-height: 1.05;
letter-spacing: -0.03em;
}
// ── Eyebrow / section label ──
@mixin text-eyebrow {
font-family: $font-sans;
font-size: 0.6875rem; // 11px
font-weight: 600;
line-height: 1.4;
letter-spacing: 0.08em;
text-transform: uppercase;
} }