Compare commits
99 Commits
f87b267b7b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 94acacbd6c | |||
| 6811dda340 | |||
| bd0e5b0191 | |||
| 23a4175bc6 | |||
| 30a3b32d13 | |||
| 0405565fd1 | |||
| 352ec260fa | |||
| 53b3ee4301 | |||
| 88d56bbd38 | |||
| a40c1d1a7e | |||
| 237abcbd63 | |||
| 4777b1c68e | |||
| 4adc9b9eb6 | |||
| acc9bcb98b | |||
| 33131a8275 | |||
| 131d8376d4 | |||
| 71effe979a | |||
| 06a1695be4 | |||
| 734a593834 | |||
| a2ae6ce8a2 | |||
| 078b1d6e85 | |||
| 5bcf5b20a0 | |||
| e0ed0f557b | |||
| 276bb36586 | |||
| b697bb8491 | |||
| 5edab8d12d | |||
| c15a6ceff1 | |||
| f56077dba4 | |||
| 6595e8f4e0 | |||
| ca983468f0 | |||
| cd75e773f6 | |||
| 75aa782653 | |||
| f6594822c6 | |||
| 027cec577d | |||
| 6e077aaeed | |||
| 1d9001705f | |||
| 1878ec3a0e | |||
| b48581b089 | |||
| 3593305512 | |||
| 6967f3c7cd | |||
| dea517fbe5 | |||
| cc54ae46ff | |||
| b2f4cfe393 | |||
| 3e4d609d6c | |||
| 455acd9552 | |||
| 50e7618232 | |||
| c4eb4cc507 | |||
| 08a6e82760 | |||
| 2e36ac3b3e | |||
| 46020db913 | |||
| bacbde56ad | |||
| f8f2e23573 | |||
| b2a61008ba | |||
| 5d65cecf52 | |||
| c80d9b5259 | |||
| 3e6d89830b | |||
| f88dd862bf | |||
| 8c1e36aaa7 | |||
| 03a92db05e | |||
| 7c65e70c8f | |||
| b61720fd24 | |||
| 4d7c882324 | |||
| c508d6ae31 | |||
| 02ae2b143e | |||
| 40505bd05e | |||
| ef22eb5615 | |||
| 2dd911dbc5 | |||
| 27134ea118 | |||
| ee8be2e20b | |||
| b3ec60db5d | |||
| 4af10b10f7 | |||
| feebe17abf | |||
| 5600107e53 | |||
| c585ec3266 | |||
| dd48a02e3b | |||
| 3cdb0b709e | |||
| 72d5425090 | |||
| dd6cd34a61 | |||
| 17f37eec5a | |||
| 96b8192408 | |||
| 1aa34d2640 | |||
| 079bacf3a6 | |||
| 8b1016b1aa | |||
| c2e13d5c48 | |||
| eadf6481e8 | |||
| 7e803a24c4 | |||
| e8ed7e2a73 | |||
| b807a2a3c4 | |||
| 98db7328cb | |||
| 68641ef5fe | |||
| ed7bc3ab6b | |||
| 18702da0b6 | |||
| 678ce7f36f | |||
| a67d5eae12 | |||
| 5323483d65 | |||
| 35b9ca30cd | |||
| a2684476da | |||
| cd2e88f20a | |||
| 2347af8d0a |
@@ -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/
|
||||
@@ -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 }}
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
Generated
+1530
-1651
File diff suppressed because it is too large
Load Diff
+38
-3
@@ -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
@@ -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 +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 +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 |
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
@@ -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>
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
export * from './components/index.ts';
|
||||
+15
-21
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
@use 'tokens/colors';
|
||||
@use 'tokens/typography';
|
||||
@use 'tokens/spacing';
|
||||
|
||||
// Global reset (opt-in)
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user