Compare commits

...

99 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
unkn0wn 18702da0b6 feat: update icon exports and add Search icon with multiple sizes 2026-05-15 18:51:05 +02:00
unkn0wn 678ce7f36f feat: add Share icon with multiple sizes (16, 24, 32) and update index export 2026-05-15 18:44:25 +02:00
unkn0wn a67d5eae12 feat: enforce required size prop and remove orientation prop from profile icon 2026-05-15 18:42:55 +02:00
unkn0wn 5323483d65 feat: add Arrow2 and Profile icons with multiple sizes and orientations
- Introduced new SVG icons for Arrow2 in sizes 16, 24, and 32 with orientations: up, down, left, and right.
- Added Profile icon in sizes 16, 24, and 32.
- Created a new index file for icons to facilitate easier imports.
- Updated the main page to showcase the new icons in a grid layout.
- Added global styles for the design system, including typography and spacing tokens.
- Updated layout styles for better visual consistency.
2026-05-15 18:38:11 +02:00
unkn0wn 35b9ca30cd feat: add typography tokens and mixins for consistent styling 2026-05-15 17:44:18 +02:00
unkn0wn a2684476da feat: add base spacing, radius, and border width tokens 2026-05-15 17:27:02 +02:00
unkn0wn cd2e88f20a feat: add semantic color tokens for light and dark themes 2026-05-15 17:24:56 +02:00
unkn0wn 2347af8d0a feat: update package.json and add generate-barrel script
- Set project name to "nova-design-system" and updated version to 0.1.0
- Added description for the project
- Defined exports for the main entry and styles
- Included a new script "barrel" to generate index.ts for components
- Added "sass" as a devDependency

chore: update package-lock.json

- Added "sass" as a devDependency with version ^1.99.0
- Updated dependencies including @parcel/watcher and others

refactor: remove unused assets and Welcome component

- Deleted astro.svg and background.svg assets
- Removed Welcome.astro component as it was no longer needed
2026-05-15 17:24:14 +02:00
71 changed files with 5328 additions and 2010 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.1 Personal and Internal Use
Use of NDS for personal projects and internal projects is permitted.
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.1 General Use
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.
### 2.2 Commercial Products and Services
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.
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.
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
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.
Re-licensing modified versions under a different license is prohibited.
## 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.
External reuse, extraction, redistribution, or standalone use of these assets is prohibited.
### 4.1 Third-Party Icons
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
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) is a lightweight, responsive, and secure UI library built with **Svelte**.
It is developed primarily for internal and personal projects and is available publicly for reference, testing, and contributions.
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.
---
## Table of Contents
1. [Overview](#overview)
2. [Project Status](#project-status)
3. [Core Principles](#core-principles)
4. [Available Components](#available-components)
5. [Installation](#installation)
6. [Usage](#usage)
7. [Theming and Variables](#theming-and-variables)
8. [Contributing](#contributing)
9. [Development and Documentation](#development-and-documentation)
10. [License](#license)
1. [Overview](#user-content-overview)
2. [Project Status](#user-content-project-status)
3. [Core Principles](#user-content-core-principles)
4. [Available Components](#user-content-available-components)
5. [Design Tokens](#user-content-design-tokens)
6. [Installation](#user-content-installation)
7. [Usage](#user-content-usage)
8. [Development](#user-content-development)
9. [License](#user-content-license)
---
## 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
**Current Phase: ALPHA**
The library is still under active development. Component APIs, styling structure, and internal architecture may change before the Beta release.
> **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.
## 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.
- **Native Theming:** Styling is based on centralized design tokens and supports Light and Dark themes.
- **Type Safety:** The library is 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 styling.
- **Responsive Architecture:** Components are built mobile-first and work correctly across modern screen sizes.
- **Security-Centric:** Unsafe patterns (injection, untrusted rendering) are avoided by design.
- **Native Theming:** All styling relies on centralized design tokens and supports Light and Dark modes.
- **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
### Actions
- `Button`
- `Toggle`
These are the components currently exported from the package (`src/components/index.ts`):
### Navigation
- `Navbar`
- `Sidebar`
- `Tabs`
- `Breadcrumb`
- `Paginate`
- `Stepper`
| Component | Category |
| -------------------------------------------------- | ------------ |
| `Button` | Actions |
| `Toggle` | Actions |
| `NumericStepper` | Actions |
| `Select` / `SelectOption` | Actions |
| `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
- `Checkbox`
- `Radio`
- `Select`
- `SelectOption`
- `Search`
- `Input`
- `File Upload`
- `Date Picker`
- `Slider`
- `Numeric Stepper`
> 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.
### Data Display
- `Badge`
- `Avatar`
- `Card`
- `Table`
- `Row`
- `Cell`
- `List Item`
## Design Tokens
### Overlay
- `Modal`
- `Dropdown`
- `Tooltip`
All styling is controlled via CSS custom properties prefixed with `--nds-`, defined in `src/styles/tokens/`:
### Feedback
- `Notification` *(error / warning / info / success)*
- `Loading Bar`
| File | Contents |
| ------------------ | ------------------------------------------------------------------------- |
| `_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
- `Divider`
- `Link`
**Rules:**
- 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
Install the package with npm:
Requires **Node.js >= 22.12.0**.
```bash
npm install @unkn0wn-pkgs/nds
npm install @unkn0wndo3s/nova-design-system
```
No extra Vite de-optimization configuration is required.
## Usage
### Basic Component Example
```svelte
<script lang="ts">
import { Notification } from '@unkn0wn-pkgs/nds';
</script>
Import components directly from the package:
```astro
---
import { Button } from '@unkn0wndo3s/nova-design-system';
---
<Button>Click me</Button>
```
```astro
---
import { Notification } from '@unkn0wndo3s/nova-design-system';
---
<Notification type="warning">
This is a warning message.
</Notification>
```
### Another Example
```svelte
<script lang="ts">
import { Button } from '@unkn0wn-pkgs/nds';
</script>
```astro
---
import { Modal } from '@unkn0wndo3s/nova-design-system';
---
<Button>
Click me
</Button>
<Modal>
This is a modal dialog.
</Modal>
```
## Theming and Variables
NDS relies on a centralized Design Token system.
All styling is controlled through CSS custom properties prefixed with `--nds-`.
```astro
---
import { Breadcrumb, BreadcrumbItem } from '@unkn0wndo3s/nova-design-system';
---
### Rules
- Do not hardcode colors, spacing, or radii directly inside component styles when a design token exists.
- Do not override tokens locally in isolated component scopes unless the system explicitly supports it.
- Keep all global token changes inside the core styling system to preserve consistency.
<Breadcrumb>
<BreadcrumbItem href="/">Home</BreadcrumbItem>
<BreadcrumbItem href="/docs">Docs</BreadcrumbItem>
</Breadcrumb>
```
## Contributing
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**.
## Development
### Clone the repository
```bash
git clone https://github.com/unkn0wndo3s/Design-System.git
git clone https://git.novaprojects.dev/unkn0wn/nova-design-system.git
```
### Install dependencies
```bash
npm install
```
### Launch Storybook
### Start the dev server
```bash
npm run storybook
npm run dev
```
### Useful scripts
```bash
npm run check
npm run lint
npm run format
npm run build
npm run build-storybook
npm run build # Build the project
npm run preview # Preview the production build
```
### 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
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.
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.
+1530 -1651
View File
File diff suppressed because it is too large Load Diff
+38 -3
View File
@@ -1,10 +1,38 @@
{
"name": "",
"name": "@unkn0wndo3s/nova-design-system",
"type": "module",
"version": "0.0.1",
"version": "1.2.1",
"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": {
"node": ">=22.12.0"
},
"exports": {
".": "./src/index.ts",
"./styles": "./src/styles/index.scss"
},
"files": [
"src",
"LICENSE.md",
"README.md"
],
"scripts": {
"dev": "astro dev",
"build": "astro build",
@@ -12,6 +40,13 @@
"astro": "astro"
},
"dependencies": {
"astro": "^6.3.3"
"@lucide/astro": "^1.21.0"
},
"peerDependencies": {
"astro": "^7.0.0"
},
"devDependencies": {
"astro": "^7.0.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" />
<style>
path { fill: #000; }

Before

Width:  |  Height:  |  Size: 749 B

After

Width:  |  Height:  |  Size: 757 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

+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>
+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>
-210
View File
@@ -1,210 +0,0 @@
---
import astroLogo from '../assets/astro.svg';
import background from '../assets/background.svg';
---
<div id="container">
<img id="background" src={background.src} alt="" fetchpriority="high" />
<main>
<section id="hero">
<a href="https://astro.build"
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
>
<h1>
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
</h1>
<section id="links">
<a class="button" href="https://docs.astro.build">Read our docs</a>
<a href="https://astro.build/chat"
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
><path
fill="currentColor"
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
></path></svg
>
</a>
</section>
</section>
</main>
<a href="https://astro.build/blog/astro-6-beta/" id="news" class="box">
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
><path
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
fill="#111827"></path></svg
>
<h2>What's New in Astro 6.0?</h2>
<p>
Redesigned dev server, fonts, live collections, built-in CSP support, and more! Click to
explore Astro 6.0's new features.
</p>
</a>
</div>
<style>
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
filter: blur(100px);
}
#container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
height: 100%;
}
main {
height: 100%;
display: flex;
justify-content: center;
}
#hero {
display: flex;
align-items: start;
flex-direction: column;
justify-content: center;
padding: 16px;
}
h1 {
font-size: 22px;
margin-top: 0.25em;
}
#links {
display: flex;
gap: 16px;
}
#links a {
display: flex;
align-items: center;
padding: 10px 12px;
color: #111827;
text-decoration: none;
transition: color 0.2s;
}
#links a:hover {
color: rgb(78, 80, 86);
}
#links a svg {
height: 1em;
margin-left: 8px;
}
#links a.button {
color: white;
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
border-radius: 10px;
}
#links a.button:hover {
color: rgb(230, 230, 230);
box-shadow: none;
}
pre {
font-family:
ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
monospace;
font-weight: normal;
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}
h2 {
margin: 0 0 1em;
font-weight: normal;
color: #111827;
font-size: 20px;
}
p {
color: #4b5563;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.006em;
margin: 0;
}
code {
display: inline-block;
background:
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
border-radius: 8px;
border: 1px solid transparent;
padding: 6px 8px;
}
.box {
padding: 16px;
background: rgba(255, 255, 255, 1);
border-radius: 16px;
border: 1px solid white;
}
#news {
position: absolute;
bottom: 16px;
right: 16px;
max-width: 300px;
text-decoration: none;
transition: background 0.2s;
backdrop-filter: blur(50px);
}
#news:hover {
background: rgba(255, 255, 255, 0.55);
}
@media screen and (max-height: 368px) {
#news {
display: none;
}
}
@media screen and (max-width: 768px) {
#container {
display: flex;
flex-direction: column;
}
#hero {
display: block;
padding-top: 10%;
}
#links {
flex-wrap: wrap;
}
#links a.button {
padding: 14px 18px;
}
#news {
right: 16px;
left: 16px;
bottom: 2.5rem;
max-width: 100%;
}
h1 {
line-height: 1.5;
}
}
</style>
+29
View File
@@ -0,0 +1,29 @@
export { default as Notification } from "./Notifications/notification.astro";
export { default as Toggle } from "./Toggle/toggle.astro";
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>
+1
View File
@@ -0,0 +1 @@
export * from './components/index.ts';
+15 -21
View File
@@ -1,23 +1,17 @@
---
import '../styles/index.scss';
interface Props { title?: string; }
const { title = 'Nova Design System' } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<meta name="generator" content={Astro.generator} />
<title>Astro Basics</title>
</head>
<body>
<slot />
</body>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
}
</style>
+1055 -6
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
@use 'tokens/colors';
@use 'tokens/typography';
@use 'tokens/spacing';
// Global reset (opt-in)
*,
*::before,
*::after {
box-sizing: border-box;
}
+98
View File
@@ -0,0 +1,98 @@
@mixin _light {
// ── Brand ──
--nds-text: #050A0A;
--nds-neutral: #5E6E6E; // refined: warmer, AA on background
--nds-disabled: #C2D4D4;
--nds-background: #F4FBFB;
--nds-primary: #1FA6AD; // refined: AA contrast for text/borders
--nds-secondary: #71EAEA;
--nds-accent: #05D1D1;
// ── Semantic surfaces & borders ──
--nds-surface: #FFFFFF;
--nds-surface-2: #E9F4F4;
--nds-surface-hover: #EEF8F8;
--nds-border: #DCEAEA;
--nds-border-strong: #C2D4D4;
// ── On-color & soft tints ──
--nds-on-primary: #FFFFFF;
--nds-primary-soft: #DBF3F4;
--nds-ring: color-mix(in srgb, var(--nds-primary) 45%, transparent);
// ── Status ──
--nds-success-high: #3B6D11;
--nds-success-medium: #5C9120;
--nds-success-low: #EAF3DE;
--nds-error-high: #A32D2D;
--nds-error-medium: #E24B4A;
--nds-error-low: #FCEBEB;
--nds-warning-high: #854F0B;
--nds-warning-medium: #BA7517;
--nds-warning-low: #FAEEDA;
--nds-info-high: #185FA5;
--nds-info-medium: #378ADD;
--nds-info-low: #E6F1FB;
// ── Elevation ──
--nds-shadow-sm: 0 1px 2px rgba(5, 20, 20, 0.06);
--nds-shadow-md: 0 4px 14px rgba(5, 20, 20, 0.10);
--nds-shadow-lg: 0 16px 40px rgba(5, 20, 20, 0.16);
}
@mixin _dark {
// ── Brand ──
--nds-text: #ECF5F5;
--nds-neutral: #8AA0A0; // refined: lighter for AA on dark background
--nds-disabled: #324242;
--nds-background: #040B0B;
--nds-primary: #34C2C9; // refined: brighter on dark
--nds-secondary: #288181;
--nds-accent: #2EFAFA;
// ── Semantic surfaces & borders ──
--nds-surface: #0B1717;
--nds-surface-2: #102020;
--nds-surface-hover: #122525;
--nds-border: #1E3030;
--nds-border-strong: #2A4040;
// ── On-color & soft tints ──
--nds-on-primary: #03191A;
--nds-primary-soft: #0A2A2C;
--nds-ring: color-mix(in srgb, var(--nds-primary) 50%, transparent);
// ── Status ──
--nds-success-high: #B6DC84;
--nds-success-medium: #97C459;
--nds-success-low: #17260A;
--nds-error-high: #F09595;
--nds-error-medium: #E05A59;
--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;
// ── Elevation ──
--nds-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.40);
--nds-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.50);
--nds-shadow-lg: 0 18px 44px rgba(0, 0, 0, 0.62);
}
:root,
[data-theme="light"] {
@include _light;
}
[data-theme="dark"] {
@include _dark;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
@include _dark;
}
}
+31
View File
@@ -0,0 +1,31 @@
// ─── Base tokens (theme-independent) ──────────────────────────────────────────
:root {
--nds-none: 0;
// ── Spacing ───────────────────────────────────────────────────────────────
--nds-spacing-3xs: 2px;
--nds-spacing-2xs: 4px;
--nds-spacing-xs: 8px;
--nds-spacing-sm: 12px;
--nds-spacing-md: 16px;
--nds-spacing-lg: 24px;
--nds-spacing-xl: 32px;
--nds-spacing-2xl: 48px;
--nds-spacing-3xl: 64px;
// ── Radius ────────────────────────────────────────────────────────────────
--nds-radius-xs: 2px;
--nds-radius-sm: 4px;
--nds-radius-md: 8px;
--nds-radius-lg: 12px;
--nds-radius-xl: 16px;
--nds-radius-2xl: 24px;
--nds-radius-3xl: 32px;
--nds-radius-full: 9999px;
// ── Border widths ─────────────────────────────────────────────────────────
--nds-border-width-thin: 1px;
--nds-border-width-medium: 2px;
--nds-border-width-thick: 4px;
}
+93
View File
@@ -0,0 +1,93 @@
// ─── Nova typography ──────────────────────────────────────────────────────────
// Display / headings: Intel One Mono (brand). UI / body: Geist (legibility).
@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 {
--nds-font-mono: #{$font-mono};
--nds-font-sans: #{$font-sans};
}
// ── UI / body (Geist) ──
@mixin text-sm {
font-family: $font-sans;
font-size: 0.75rem; // 12px
font-weight: 400;
line-height: 1.45;
letter-spacing: 0.005em;
}
@mixin text-base {
font-family: $font-sans;
font-size: 0.875rem; // 14px
font-weight: 400;
line-height: 1.5;
}
@mixin text-label {
font-family: $font-sans;
font-size: 0.8125rem; // 13px
font-weight: 600;
line-height: 1.4;
letter-spacing: 0.01em;
}
@mixin text-lg {
font-family: $font-sans;
font-size: 1rem; // 16px
font-weight: 600;
line-height: 1.5;
}
// ── Display / headings (Intel One Mono) ──
@mixin text-xl {
font-family: $font-mono;
font-size: 1.125rem; // 18px
font-weight: 700;
line-height: 1.35;
letter-spacing: -0.01em;
}
@mixin text-2xl {
font-family: $font-mono;
font-size: 1.375rem; // 22px
font-weight: 700;
line-height: 1.3;
letter-spacing: -0.015em;
}
@mixin text-3xl {
font-family: $font-mono;
font-size: 1.875rem; // 30px
font-weight: 700;
line-height: 1.2;
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;
}