This commit is contained in:
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
29
.eslintrc
Normal file
29
.eslintrc
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": ["@typescript-eslint", "no-only-tests", "eslint-comments"],
|
||||||
|
"ignorePatterns": ["node_modules", "dist", "dev", "tsup.config.ts", "vitest.config.ts"],
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./tsconfig.json",
|
||||||
|
"tsconfigRootDir": ".",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"prefer-const": "warn",
|
||||||
|
"no-console": "warn",
|
||||||
|
"no-debugger": "warn",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"caughtErrorsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unnecessary-type-assertion": "warn",
|
||||||
|
"@typescript-eslint/no-unnecessary-condition": "warn",
|
||||||
|
"@typescript-eslint/no-useless-empty-export": "warn",
|
||||||
|
"no-only-tests/no-only-tests": "warn",
|
||||||
|
"eslint-comments/no-unused-disable": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
31
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: "🐛 Bug report"
|
||||||
|
description: Create a report to help us improve
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for reporting an issue :pray:.
|
||||||
|
|
||||||
|
The more information you fill in, the better the community can help you.
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: Provide a clear and concise description of the challenge you are running into.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: link
|
||||||
|
attributes:
|
||||||
|
label: Minimal Reproduction Link
|
||||||
|
description: |
|
||||||
|
Please provide a link to a minimal reproduction of the bug you are running into.
|
||||||
|
It makes the process of verifying and fixing the bug much easier.
|
||||||
|
Note:
|
||||||
|
- Your bug will may get fixed much faster if we can run your code and it doesn't have dependencies other than the solid-js and solid-primitives.
|
||||||
|
- To create a shareable code example you can use [Stackblitz](https://stackblitz.com/) (https://solid.new). Please no localhost URLs.
|
||||||
|
- Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve.
|
||||||
|
placeholder: |
|
||||||
|
e.g. https://stackblitz.com/edit/...... OR Github Repo
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
27
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
27
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: "Feature Request"
|
||||||
|
description: For feature/enhancement requests. Please search for existing issues first.
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for bringing your ideas here :pray:.
|
||||||
|
|
||||||
|
The more information you fill in, the better the community can understand your idea.
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Describe The Problem To Be Solved
|
||||||
|
description: Provide a clear and concise description of the challenge you are running into.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: Suggest A Solution
|
||||||
|
description: |
|
||||||
|
A concise description of your preferred solution. Things to address include:
|
||||||
|
- Details of the technical implementation
|
||||||
|
- Tradeoffs made in design decisions
|
||||||
|
- Caveats and considerations for the future
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
46
.github/workflows/codeql.yml
vendored
Normal file
46
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * 0"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: javascript
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
||||||
|
with:
|
||||||
|
upload: false
|
||||||
|
output: sarif-results
|
||||||
|
|
||||||
|
# Only include files that are public
|
||||||
|
- name: filter-sarif
|
||||||
|
uses: advanced-security/filter-sarif@main
|
||||||
|
with:
|
||||||
|
patterns: |
|
||||||
|
/src/**/*.*
|
||||||
|
-**/*.test.*
|
||||||
|
input: sarif-results/javascript.sarif
|
||||||
|
output: sarif-results/javascript.sarif
|
||||||
|
|
||||||
|
- name: Upload SARIF
|
||||||
|
uses: github/codeql-action/upload-sarif@v1
|
||||||
|
with:
|
||||||
|
sarif_file: sarif-results/javascript.sarif
|
||||||
37
.github/workflows/format.yml
vendored
Normal file
37
.github/workflows/format.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Format
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
# Give the default GITHUB_TOKEN write permission to commit and push the
|
||||||
|
# added or changed files to the repository.
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: pnpm/action-setup@v2.2.4
|
||||||
|
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
|
||||||
|
# "git restore ." discards changes to package-lock.json
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pnpm install --no-frozen-lockfile --ignore-scripts
|
||||||
|
git restore .
|
||||||
|
|
||||||
|
- name: Format
|
||||||
|
run: pnpm run format
|
||||||
|
|
||||||
|
- name: Add, Commit and Push
|
||||||
|
uses: stefanzweifel/git-auto-commit-action@v4
|
||||||
|
with:
|
||||||
|
commit_message: 'Format'
|
||||||
39
.github/workflows/tests.yml
vendored
Normal file
39
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Build and Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v2.2.4
|
||||||
|
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pnpm run test
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm run lint
|
||||||
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
dist
|
||||||
|
.wrangler
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.vinxi
|
||||||
|
app.config.timestamp_*.js
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
# Temp
|
||||||
|
gitignore
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 100,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"useTabs": false,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"bracketSpacing": true
|
||||||
|
}
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 {{me}}
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
25
README.md
Normal file
25
README.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<p>
|
||||||
|
<img width="100%" src="https://assets.solidjs.com/banner?type=@sortsys/ui&background=tiles&project=%20" alt="@sortsys/ui">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# @sortsys/ui
|
||||||
|
|
||||||
|
Simple and beautiful UI library for sortsys
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
Install it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i @sortsys/ui
|
||||||
|
# or
|
||||||
|
yarn add @sortsys/ui
|
||||||
|
# or
|
||||||
|
pnpm add @sortsys/ui
|
||||||
|
```
|
||||||
|
|
||||||
|
Use it:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { SSShell, SSForm } from '@sortsys/ui'
|
||||||
|
```
|
||||||
17
dev/App.tsx
Normal file
17
dev/App.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Component } from 'solid-js';
|
||||||
|
import type { RouteSectionProps } from '@solidjs/router';
|
||||||
|
import { HashRouter } from '@solidjs/router';
|
||||||
|
import { SSModalsProvider } from 'src';
|
||||||
|
import { routes } from './routes';
|
||||||
|
|
||||||
|
const AppRoot: Component<RouteSectionProps> = (props) => (
|
||||||
|
<SSModalsProvider>{props.children}</SSModalsProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const App: Component = () => (
|
||||||
|
<HashRouter root={AppRoot}>
|
||||||
|
{routes}
|
||||||
|
</HashRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default App;
|
||||||
106
dev/demo/content.tsx
Normal file
106
dev/demo/content.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { JSXElement } from 'solid-js';
|
||||||
|
import { SSChip } from 'src';
|
||||||
|
|
||||||
|
export const IconSpark = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-sparkles"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2a2 2 0 0 1 -2 2a2 2 0 0 1 2 -2" /><path d="M12 4a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2a2 2 0 0 1 -2 2a2 2 0 0 1 2 -2" /><path d="M5 14a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2a2 2 0 0 1 -2 2a2 2 0 0 1 2 -2" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IconBolt = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-bolt"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M13 3v6h6l-8 12v-6h-6l8 -12" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IconShield = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-shield"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 3l8 4v5c0 5 -3 9 -8 9s-8 -4 -8 -9v-5l8 -4" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IconUser = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-user"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" /><path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IconChart = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chart-bar"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M3 12m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v7a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /><path d="M9 8m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v11a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /><path d="M15 4m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v15a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /><path d="M4 20l14 0" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IconSettings = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-settings"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" /><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IconBell = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-bell"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M10 5a2 2 0 0 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6" /><path d="M9 17v1a3 3 0 0 0 6 0v-1" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IconCheck = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-check"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M5 12l5 5l10 -10" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IconLink = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-link"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M9 15l6 -6" /><path d="M11 6l-2.5 2.5a3.535 3.535 0 0 0 5 5l2.5 -2.5" /><path d="M13 18l2.5 -2.5a3.535 3.535 0 0 0 -5 -5l-2.5 2.5" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IconDots = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dots-vertical"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M11 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M11 19a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M11 5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const demoRows = [
|
||||||
|
{
|
||||||
|
name: 'Lia Morgan',
|
||||||
|
role: 'Design Ops',
|
||||||
|
status: 'Active',
|
||||||
|
updated: 'Vor 2 Tagen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Jonas Beck',
|
||||||
|
role: 'Frontend',
|
||||||
|
status: 'Review',
|
||||||
|
updated: 'Heute',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Ava Ruiz',
|
||||||
|
role: 'Support',
|
||||||
|
status: 'Active',
|
||||||
|
updated: 'Vor 1 Stunde',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Marek Ali',
|
||||||
|
role: 'Platform',
|
||||||
|
status: 'On hold',
|
||||||
|
updated: 'Vor 5 Tagen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sina Weber',
|
||||||
|
role: 'Growth',
|
||||||
|
status: 'Active',
|
||||||
|
updated: 'Gestern',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const teamOptions = [
|
||||||
|
{ id: 'blue', label: 'Blue team' },
|
||||||
|
{ id: 'gold', label: 'Gold team' },
|
||||||
|
{ id: 'orchid', label: 'Orchid team' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const peopleOptions = [
|
||||||
|
{ id: 'lia', name: 'Lia Morgan', role: 'Design Ops' },
|
||||||
|
{ id: 'jonas', name: 'Jonas Beck', role: 'Frontend' },
|
||||||
|
{ id: 'ava', name: 'Ava Ruiz', role: 'Support' },
|
||||||
|
{ id: 'marek', name: 'Marek Ali', role: 'Platform' },
|
||||||
|
{ id: 'sina', name: 'Sina Weber', role: 'Growth' },
|
||||||
|
{ id: 'tom', name: 'Tom Richter', role: 'Security' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const stackSuggestions = [
|
||||||
|
'SolidStart',
|
||||||
|
'SolidJS',
|
||||||
|
'TypeScript',
|
||||||
|
'Vite',
|
||||||
|
'Tsup',
|
||||||
|
'Tailwind',
|
||||||
|
'Radix',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const renderStatus = (status: string): JSXElement => (
|
||||||
|
<SSChip color={status === 'Active' ? 'green' : status === 'Review' ? 'amber' : 'grey'}>
|
||||||
|
{status}
|
||||||
|
</SSChip>
|
||||||
|
);
|
||||||
18
dev/index.html
Normal file
18
dev/index.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<title>@sortsys/ui demo</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
<script src="./index.tsx" type="module"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
7
dev/index.tsx
Normal file
7
dev/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { render } from 'solid-js/web'
|
||||||
|
import 'src/styles/default.css'
|
||||||
|
import './styles.css'
|
||||||
|
|
||||||
|
import App from './App'
|
||||||
|
|
||||||
|
render(() => <App />, document.getElementById('root')!)
|
||||||
1
dev/logo.svg
Normal file
1
dev/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 166 155.3"><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" fill="#76b3e1"/><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="27.5" y1="3" x2="152" y2="63.5"><stop offset=".1" stop-color="#76b3e1"/><stop offset=".3" stop-color="#dcf2fd"/><stop offset="1" stop-color="#76b3e1"/></linearGradient><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" opacity=".3" fill="url(#a)"/><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" fill="#518ac8"/><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="95.8" y1="32.6" x2="74" y2="105.2"><stop offset="0" stop-color="#76b3e1"/><stop offset=".5" stop-color="#4377bb"/><stop offset="1" stop-color="#1f3b77"/></linearGradient><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" opacity=".3" fill="url(#b)"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="18.4" y1="64.2" x2="144.3" y2="149.8"><stop offset="0" stop-color="#315aa9"/><stop offset=".5" stop-color="#518ac8"/><stop offset="1" stop-color="#315aa9"/></linearGradient><path d="M134 80a45 45 0 00-48-15L24 85 4 120l112 19 20-36c4-7 3-15-2-23z" fill="url(#c)"/><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="75.2" y1="74.5" x2="24.4" y2="260.8"><stop offset="0" stop-color="#4377bb"/><stop offset=".5" stop-color="#1a336b"/><stop offset="1" stop-color="#1a336b"/></linearGradient><path d="M114 115a45 45 0 00-48-15L4 120s53 40 94 30l3-1c17-5 23-21 13-34z" fill="url(#d)"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
39
dev/routes.ts
Normal file
39
dev/routes.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { lazy } from 'solid-js';
|
||||||
|
import type { RouteDefinition } from '@solidjs/router';
|
||||||
|
|
||||||
|
export const routes: RouteDefinition[] = [
|
||||||
|
{
|
||||||
|
path: '/auth/login',
|
||||||
|
component: lazy(() => import('./routes/auth/login')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: lazy(() => import('./routes/(shell)/_layout')),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: lazy(() => import('./routes/(shell)/index')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/forms',
|
||||||
|
component: lazy(() => import('./routes/(shell)/forms')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/data',
|
||||||
|
component: lazy(() => import('./routes/(shell)/data')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/modals',
|
||||||
|
component: lazy(() => import('./routes/(shell)/modals')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/layout',
|
||||||
|
component: lazy(() => import('./routes/(shell)/layout')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
component: lazy(() => import('./routes/(shell)/_404')),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
17
dev/routes/(shell)/_404.tsx
Normal file
17
dev/routes/(shell)/_404.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Component } from 'solid-js';
|
||||||
|
import { SSButton, SSCallout, SSHeader, SSSurface } from 'src';
|
||||||
|
import { IconShield } from '../../demo/content';
|
||||||
|
|
||||||
|
const NotFoundPage: Component = () => (
|
||||||
|
<div class="demo_page">
|
||||||
|
<SSHeader title="Not found" subtitle="This route is not available in the demo." />
|
||||||
|
<SSSurface class="demo_surface">
|
||||||
|
<SSCallout color="amber" icon={<IconShield />}>
|
||||||
|
The page you requested does not exist. Head back to the overview to continue exploring.
|
||||||
|
</SSCallout>
|
||||||
|
<SSButton onclick={() => (window.location.href = '#/')}>Back to overview</SSButton>
|
||||||
|
</SSSurface>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NotFoundPage;
|
||||||
73
dev/routes/(shell)/_layout.tsx
Normal file
73
dev/routes/(shell)/_layout.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Component } from 'solid-js';
|
||||||
|
import type { RouteSectionProps } from '@solidjs/router';
|
||||||
|
import { createSignal } from 'solid-js';
|
||||||
|
import {
|
||||||
|
SSButton,
|
||||||
|
SSCallout,
|
||||||
|
SSDropdown,
|
||||||
|
SSShell,
|
||||||
|
} from 'src';
|
||||||
|
import {
|
||||||
|
IconBell,
|
||||||
|
IconChart,
|
||||||
|
IconDots,
|
||||||
|
IconLink,
|
||||||
|
IconSettings,
|
||||||
|
IconShield,
|
||||||
|
IconSpark,
|
||||||
|
IconBolt,
|
||||||
|
IconCheck,
|
||||||
|
} from '../../demo/content';
|
||||||
|
|
||||||
|
const ShellLayout: Component<RouteSectionProps> = (props) => {
|
||||||
|
const [notice, setNotice] = createSignal(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SSShell
|
||||||
|
title="Sortsys UI"
|
||||||
|
actions={
|
||||||
|
<div class="demo_inline">
|
||||||
|
<SSButton isIconOnly ariaLabel="Notifications">
|
||||||
|
<IconBell />
|
||||||
|
</SSButton>
|
||||||
|
<SSButton class="secondary">Export</SSButton>
|
||||||
|
<SSDropdown
|
||||||
|
icon={<IconDots />}
|
||||||
|
items={[
|
||||||
|
{ label: 'Settings', icon: <IconSettings /> },
|
||||||
|
{ label: 'Share', icon: <IconLink /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
nav={
|
||||||
|
<SSShell.Nav>
|
||||||
|
<SSShell.NavLink href="/" icon={<IconSpark />}>Overview</SSShell.NavLink>
|
||||||
|
<SSShell.NavLink href="/forms" icon={<IconBolt />}>Forms</SSShell.NavLink>
|
||||||
|
<SSShell.NavLink href="/data" icon={<IconChart />}>Data</SSShell.NavLink>
|
||||||
|
<SSShell.NavLink href="/modals" icon={<IconShield />}>Modals</SSShell.NavLink>
|
||||||
|
<SSShell.NavGroup title="Layout" icon={<IconSettings />} initiallyExpanded>
|
||||||
|
<SSShell.NavLink href="/layout" icon={<IconLink />}>Surfaces & tiles</SSShell.NavLink>
|
||||||
|
</SSShell.NavGroup>
|
||||||
|
<SSShell.NavAction onclick={() => setNotice(true)} icon={<IconBell />}>
|
||||||
|
Ping team
|
||||||
|
</SSShell.NavAction>
|
||||||
|
</SSShell.Nav>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{notice() && (
|
||||||
|
<div class="demo_notice">
|
||||||
|
<SSCallout color="green" icon={<IconCheck />}>
|
||||||
|
Team notified. The update will appear in the activity feed.
|
||||||
|
</SSCallout>
|
||||||
|
<SSButton class="tertiary" onclick={() => setNotice(false)}>
|
||||||
|
Clear
|
||||||
|
</SSButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{props.children}
|
||||||
|
</SSShell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShellLayout;
|
||||||
43
dev/routes/(shell)/data.tsx
Normal file
43
dev/routes/(shell)/data.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import type { Component } from 'solid-js';
|
||||||
|
import { createMemo } from 'solid-js';
|
||||||
|
import { SSDataTable, SSHeader, SSSurface } from 'src';
|
||||||
|
import { demoRows, renderStatus } from '../../demo/content';
|
||||||
|
|
||||||
|
const DataPage: Component = () => {
|
||||||
|
const columns = createMemo(() => [
|
||||||
|
{
|
||||||
|
label: 'Name',
|
||||||
|
render: (row: (typeof demoRows)[number]) => row.name,
|
||||||
|
sortKey: (row: (typeof demoRows)[number]) => row.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Role',
|
||||||
|
render: (row: (typeof demoRows)[number]) => row.role,
|
||||||
|
sortKey: (row: (typeof demoRows)[number]) => row.role,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Status',
|
||||||
|
render: (row: (typeof demoRows)[number]) => renderStatus(row.status),
|
||||||
|
sortKey: (row: (typeof demoRows)[number]) => row.status,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Updated',
|
||||||
|
render: (row: (typeof demoRows)[number]) => row.updated,
|
||||||
|
sortKey: (row: (typeof demoRows)[number]) => row.updated,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="demo_page">
|
||||||
|
<SSHeader title="Data table" subtitle="Sorting, paging, and quick actions." />
|
||||||
|
<SSDataTable
|
||||||
|
columns={columns()}
|
||||||
|
rows={demoRows}
|
||||||
|
pageSize={3}
|
||||||
|
onRowClick={async () => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataPage;
|
||||||
118
dev/routes/(shell)/forms.tsx
Normal file
118
dev/routes/(shell)/forms.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import type { Component } from 'solid-js';
|
||||||
|
import { createSignal } from 'solid-js';
|
||||||
|
import {
|
||||||
|
SSChip,
|
||||||
|
SSDivider,
|
||||||
|
SSForm,
|
||||||
|
SSHeader,
|
||||||
|
SSSurface,
|
||||||
|
} from 'src';
|
||||||
|
import { peopleOptions, stackSuggestions, teamOptions } from '../../demo/content';
|
||||||
|
|
||||||
|
const FormsPage: Component = () => {
|
||||||
|
const [result, setResult] = createSignal<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="demo_page">
|
||||||
|
<SSHeader title="Forms" subtitle="Inputs, selections, and validation in one place." />
|
||||||
|
|
||||||
|
<SSSurface class="demo_surface">
|
||||||
|
<SSForm
|
||||||
|
onsubmit={async (context) => {
|
||||||
|
const values = await context.getValues();
|
||||||
|
setResult(JSON.stringify(values, null, 2));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="demo_form_grid">
|
||||||
|
<SSForm.Input label="Project name" name="project" required />
|
||||||
|
<SSForm.Input label="Email" name="email" type="email" required />
|
||||||
|
<SSForm.Input label="Password" name="password" type="password" required />
|
||||||
|
<SSForm.Input label="Contact phone" name="phone" type="tel" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SSForm.Input
|
||||||
|
label="Message"
|
||||||
|
name="message"
|
||||||
|
textArea
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SSForm.Input
|
||||||
|
label="Tech stack"
|
||||||
|
name="stack"
|
||||||
|
suggestions={{
|
||||||
|
prepare: () => stackSuggestions,
|
||||||
|
getItems: ({ query, init }) =>
|
||||||
|
init.filter((item) => item.toLowerCase().includes(query)).slice(0, 5),
|
||||||
|
stringify: ({ item }) => item,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="demo_form_grid">
|
||||||
|
<SSForm.Date label="Start date" name="startDate" editable />
|
||||||
|
<SSForm.Date label="Target date" name="targetDate" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo_form_grid">
|
||||||
|
<SSForm.Select
|
||||||
|
label="Team"
|
||||||
|
name="team"
|
||||||
|
getOptions={() => teamOptions}
|
||||||
|
buildOption={(item) => item.label}
|
||||||
|
/>
|
||||||
|
<SSForm.Checkbox label="Notify stakeholders" name="notify" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SSDivider />
|
||||||
|
|
||||||
|
<SSForm.ACSelect
|
||||||
|
label="Invite members"
|
||||||
|
name="members"
|
||||||
|
minSelectedItems={1}
|
||||||
|
getOptions={({ query }) =>
|
||||||
|
peopleOptions.filter((item) =>
|
||||||
|
item.name.toLowerCase().includes(query.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<div class="demo_ac_item">
|
||||||
|
<strong>{item.name}</strong>
|
||||||
|
<span>{item.role}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SSForm.ACSelect
|
||||||
|
label="Primary owner"
|
||||||
|
name="owner"
|
||||||
|
maxSelectedItems={1}
|
||||||
|
getOptions={({ query }) =>
|
||||||
|
peopleOptions.filter((item) =>
|
||||||
|
item.name.toLowerCase().includes(query.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<div class="demo_ac_item">
|
||||||
|
<strong>{item.name}</strong>
|
||||||
|
<span>{item.role}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
renderSelection={({ item }) => (
|
||||||
|
<SSChip color="indigo">{item.name}</SSChip>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SSForm.SubmitButton>Save form</SSForm.SubmitButton>
|
||||||
|
</SSForm>
|
||||||
|
</SSSurface>
|
||||||
|
|
||||||
|
{result() && (
|
||||||
|
<SSSurface class="demo_surface">
|
||||||
|
<div class="demo_section_title">Submitted values</div>
|
||||||
|
<pre class="demo_code">{result()}</pre>
|
||||||
|
</SSSurface>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormsPage;
|
||||||
202
dev/routes/(shell)/index.tsx
Normal file
202
dev/routes/(shell)/index.tsx
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import type { Component } from 'solid-js';
|
||||||
|
import { For, createSignal } from 'solid-js';
|
||||||
|
import {
|
||||||
|
SSAttrList,
|
||||||
|
SSButton,
|
||||||
|
SSCallout,
|
||||||
|
SSChip,
|
||||||
|
SSDivider,
|
||||||
|
SSDropdown,
|
||||||
|
SSExpandable,
|
||||||
|
SSHeader,
|
||||||
|
SSSurface,
|
||||||
|
SSTile,
|
||||||
|
} from 'src';
|
||||||
|
import {
|
||||||
|
IconBolt,
|
||||||
|
IconChart,
|
||||||
|
IconDots,
|
||||||
|
IconLink,
|
||||||
|
IconSettings,
|
||||||
|
IconShield,
|
||||||
|
IconSpark,
|
||||||
|
IconUser,
|
||||||
|
IconBell,
|
||||||
|
IconCheck,
|
||||||
|
} from '../../demo/content';
|
||||||
|
|
||||||
|
const OverviewPage: Component = () => {
|
||||||
|
const [bannerVisible, setBannerVisible] = createSignal(true);
|
||||||
|
const [chipItems, setChipItems] = createSignal([
|
||||||
|
{ id: 'launch', label: 'Launch ready', color: 'green' },
|
||||||
|
{ id: 'urgent', label: 'Urgent', color: 'red' },
|
||||||
|
{ id: 'info', label: 'Info', color: 'blue' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="demo_page">
|
||||||
|
<SSHeader
|
||||||
|
title="Sortsys UI"
|
||||||
|
subtitle="A crisp SolidJS component suite for operational products."
|
||||||
|
actions={
|
||||||
|
<div class="demo_inline">
|
||||||
|
<SSButton class="secondary">Preview</SSButton>
|
||||||
|
<SSButton>Publish</SSButton>
|
||||||
|
<SSDropdown
|
||||||
|
ariaLabel="Aktionen"
|
||||||
|
icon={<IconDots />}
|
||||||
|
items={[
|
||||||
|
{ label: 'Duplicate', icon: <IconSpark /> },
|
||||||
|
{ label: 'Share link', icon: <IconLink />, checked: true },
|
||||||
|
{ label: 'Archive', icon: <IconShield /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{bannerVisible() && (
|
||||||
|
<div class="demo_notice">
|
||||||
|
<SSCallout color="blue" icon={<IconSpark />}>
|
||||||
|
A fresh demo layout that shows every component in context.
|
||||||
|
</SSCallout>
|
||||||
|
<SSButton class="tertiary" onclick={() => setBannerVisible(false)}>
|
||||||
|
Dismiss
|
||||||
|
</SSButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="demo_grid">
|
||||||
|
<SSSurface class="demo_surface">
|
||||||
|
<div class="demo_section_title">Status tiles</div>
|
||||||
|
<div class="demo_tiles">
|
||||||
|
<SSTile
|
||||||
|
title="Design review"
|
||||||
|
subtitle="Last updated 2 hours ago"
|
||||||
|
icon={<IconSpark />}
|
||||||
|
trailing="92%"
|
||||||
|
/>
|
||||||
|
<SSTile
|
||||||
|
title="Security sweep"
|
||||||
|
subtitle="Next window tomorrow"
|
||||||
|
icon={<IconShield />}
|
||||||
|
trailing="4 issues"
|
||||||
|
/>
|
||||||
|
<SSTile
|
||||||
|
title="Capacity"
|
||||||
|
subtitle="Quarterly planning"
|
||||||
|
icon={<IconChart />}
|
||||||
|
trailing="14 slots"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SSSurface>
|
||||||
|
|
||||||
|
<SSSurface class="demo_surface">
|
||||||
|
<div class="demo_section_title">Quick actions</div>
|
||||||
|
<div class="demo_buttons">
|
||||||
|
<SSButton>Primary action</SSButton>
|
||||||
|
<SSButton class="secondary">Secondary</SSButton>
|
||||||
|
<SSButton class="tertiary">Tertiary</SSButton>
|
||||||
|
<SSButton class="danger">Danger</SSButton>
|
||||||
|
<SSButton isIconOnly ariaLabel="Notifications">
|
||||||
|
<IconBell />
|
||||||
|
</SSButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SSDivider />
|
||||||
|
|
||||||
|
<div class="demo_section_title">Chips</div>
|
||||||
|
<div class="demo_chips">
|
||||||
|
<For each={chipItems()}>
|
||||||
|
{(item) => (
|
||||||
|
<SSChip
|
||||||
|
color={item.color as any}
|
||||||
|
ondismiss={() =>
|
||||||
|
setChipItems((prev) => prev.filter((chip) => chip.id !== item.id))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</SSChip>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<SSChip color="purple" onclick={() => {}}>
|
||||||
|
Clickable
|
||||||
|
</SSChip>
|
||||||
|
<SSChip color="amber">Display only</SSChip>
|
||||||
|
</div>
|
||||||
|
</SSSurface>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SSSurface class="demo_surface">
|
||||||
|
<div class="demo_section_title">Attribute list</div>
|
||||||
|
<SSAttrList>
|
||||||
|
<SSAttrList.Attr
|
||||||
|
name="Lifecycle"
|
||||||
|
value="Production"
|
||||||
|
third={
|
||||||
|
<SSButton isIconOnly class="tertiary" ariaLabel="Edit">
|
||||||
|
<IconSettings />
|
||||||
|
</SSButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SSAttrList.Attr
|
||||||
|
name="Owner"
|
||||||
|
value="Customer Ops"
|
||||||
|
third="2 teams, 6 contributors"
|
||||||
|
/>
|
||||||
|
<SSAttrList.Attr name="Uptime" value="99.98%" third="Rolling 30 days" />
|
||||||
|
<SSAttrList.Attr name="Latency" value="142ms avg" third="p95 down 12%" />
|
||||||
|
<SSAttrList.Attr
|
||||||
|
name="Alerts"
|
||||||
|
value="2 open"
|
||||||
|
third="1 critical"
|
||||||
|
/>
|
||||||
|
<SSAttrList.Attr
|
||||||
|
name="Risk"
|
||||||
|
value="Low"
|
||||||
|
third="Review passed"
|
||||||
|
/>
|
||||||
|
</SSAttrList>
|
||||||
|
</SSSurface>
|
||||||
|
|
||||||
|
<div class="demo_grid">
|
||||||
|
<SSSurface class="demo_surface">
|
||||||
|
<div class="demo_section_title">Callouts</div>
|
||||||
|
<div class="demo_stack">
|
||||||
|
<SSCallout color="green" icon={<IconCheck />}>
|
||||||
|
All services are green. Next audit is scheduled for Friday.
|
||||||
|
</SSCallout>
|
||||||
|
<SSCallout color="amber" icon={<IconBolt />}>
|
||||||
|
2 alerts are pending review in the on-call queue.
|
||||||
|
</SSCallout>
|
||||||
|
<SSCallout color="red" icon={<IconShield />}>
|
||||||
|
Credentials rotation required within 14 days.
|
||||||
|
</SSCallout>
|
||||||
|
</div>
|
||||||
|
</SSSurface>
|
||||||
|
|
||||||
|
<SSSurface class="demo_surface">
|
||||||
|
<div class="demo_section_title">Expandable</div>
|
||||||
|
<SSExpandable title="Deployment checklist" initiallyExpanded>
|
||||||
|
<div class="demo_stack">
|
||||||
|
<div class="demo_inline">
|
||||||
|
<IconCheck />
|
||||||
|
<span>Review release notes and merge outstanding fixes.</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo_inline">
|
||||||
|
<IconCheck />
|
||||||
|
<span>Warm up cache nodes and verify rollout.</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo_inline">
|
||||||
|
<IconCheck />
|
||||||
|
<span>Post update to #ops and notify stakeholders.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SSExpandable>
|
||||||
|
</SSSurface>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OverviewPage;
|
||||||
61
dev/routes/(shell)/layout.tsx
Normal file
61
dev/routes/(shell)/layout.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import type { Component } from 'solid-js';
|
||||||
|
import {
|
||||||
|
SSDivider,
|
||||||
|
SSExpandable,
|
||||||
|
SSHeader,
|
||||||
|
SSSurface,
|
||||||
|
SSTile,
|
||||||
|
} from 'src';
|
||||||
|
import { IconBolt, IconCheck, IconLink, IconShield } from '../../demo/content';
|
||||||
|
|
||||||
|
const LayoutPage: Component = () => (
|
||||||
|
<div class="demo_page">
|
||||||
|
<SSHeader title="Layout" subtitle="Surfaces, tiles, and dividers." />
|
||||||
|
|
||||||
|
<SSSurface class="demo_surface">
|
||||||
|
<div class="demo_section_title">Surface stacks</div>
|
||||||
|
<div class="demo_grid">
|
||||||
|
<SSSurface class="demo_surface demo_surface--nested">
|
||||||
|
<div class="demo_section_title">Sub-surface</div>
|
||||||
|
<p class="demo_text">Perfect for grouped content, metrics, or compact forms.</p>
|
||||||
|
</SSSurface>
|
||||||
|
<SSSurface class="demo_surface demo_surface--nested">
|
||||||
|
<div class="demo_section_title">Another surface</div>
|
||||||
|
<p class="demo_text">Use consistent radii and subtle shadows for hierarchy.</p>
|
||||||
|
</SSSurface>
|
||||||
|
</div>
|
||||||
|
</SSSurface>
|
||||||
|
|
||||||
|
<SSSurface class="demo_surface">
|
||||||
|
<div class="demo_section_title">Tile list</div>
|
||||||
|
<div class="demo_tiles">
|
||||||
|
<SSTile
|
||||||
|
title="Integrations"
|
||||||
|
subtitle="24 connected services"
|
||||||
|
icon={<IconLink />}
|
||||||
|
trailing={<IconCheck />}
|
||||||
|
/>
|
||||||
|
<SSTile
|
||||||
|
title="Automation"
|
||||||
|
subtitle="5 workflows running"
|
||||||
|
icon={<IconBolt />}
|
||||||
|
trailing="Live"
|
||||||
|
/>
|
||||||
|
<SSTile
|
||||||
|
title="Security"
|
||||||
|
subtitle="Next scan Friday"
|
||||||
|
icon={<IconShield />}
|
||||||
|
trailing="Low risk"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SSDivider />
|
||||||
|
<SSExpandable title="Notes">
|
||||||
|
<div class="demo_text">
|
||||||
|
Combine tiles with expandables to progressively reveal details.
|
||||||
|
</div>
|
||||||
|
</SSExpandable>
|
||||||
|
</SSSurface>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default LayoutPage;
|
||||||
103
dev/routes/(shell)/modals.tsx
Normal file
103
dev/routes/(shell)/modals.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import type { Component } from 'solid-js';
|
||||||
|
import { createSignal } from 'solid-js';
|
||||||
|
import {
|
||||||
|
SSAttrList,
|
||||||
|
SSButton,
|
||||||
|
SSCallout,
|
||||||
|
SSForm,
|
||||||
|
SSHeader,
|
||||||
|
SSModal,
|
||||||
|
SSSurface,
|
||||||
|
useSSModals,
|
||||||
|
} from 'src';
|
||||||
|
import { IconSpark } from '../../demo/content';
|
||||||
|
|
||||||
|
const ModalsPage: Component = () => {
|
||||||
|
const modals = useSSModals();
|
||||||
|
const [open, setOpen] = createSignal(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="demo_page">
|
||||||
|
<SSHeader title="Modals" subtitle="Inline and managed modal flows." />
|
||||||
|
|
||||||
|
<SSSurface class="demo_surface">
|
||||||
|
<div class="demo_inline">
|
||||||
|
<SSButton onclick={() => setOpen(true)}>Open modal</SSButton>
|
||||||
|
<SSButton
|
||||||
|
class="secondary"
|
||||||
|
onclick={() =>
|
||||||
|
modals.showDefault({
|
||||||
|
content: () => (
|
||||||
|
<div class="demo_stack">
|
||||||
|
<strong>Delete pipeline?</strong>
|
||||||
|
<span>This action will remove the pipeline and its history.</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
modalProps: () => ({
|
||||||
|
title: 'Delete confirmation',
|
||||||
|
danger: true,
|
||||||
|
primaryButtonText: 'Delete',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Default modal
|
||||||
|
</SSButton>
|
||||||
|
<SSButton
|
||||||
|
class="tertiary"
|
||||||
|
onclick={() =>
|
||||||
|
modals.showForm({
|
||||||
|
content: ({ context }) => (
|
||||||
|
<div class="demo_stack">
|
||||||
|
<SSForm.Input label="Change label" name="label" required />
|
||||||
|
<SSForm.Input label="Summary" name="summary" textArea />
|
||||||
|
<SSForm.Checkbox label="Notify watchers" name="notify" />
|
||||||
|
{context.hasError() && (
|
||||||
|
<span class="demo_hint">Please fix the highlighted fields.</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
modalProps: () => ({
|
||||||
|
title: 'Edit release',
|
||||||
|
primaryButtonText: 'Save',
|
||||||
|
}),
|
||||||
|
onSubmit: async ({ hide }) => {
|
||||||
|
hide();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Form modal
|
||||||
|
</SSButton>
|
||||||
|
</div>
|
||||||
|
</SSSurface>
|
||||||
|
|
||||||
|
<SSModal
|
||||||
|
open={open()}
|
||||||
|
title="Launch overview"
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<SSButton class="secondary" onclick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</SSButton>
|
||||||
|
<SSButton onclick={() => setOpen(false)}>Confirm</SSButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="demo_stack">
|
||||||
|
<SSCallout color="blue" icon={<IconSpark />}>
|
||||||
|
Everything is ready to go. Confirm to publish.
|
||||||
|
</SSCallout>
|
||||||
|
<SSAttrList>
|
||||||
|
<SSAttrList.Attr name="Version" value="v1.18.0" />
|
||||||
|
<SSAttrList.Attr name="Owner" value="Release team" />
|
||||||
|
<SSAttrList.Attr name="Window" value="Tomorrow 09:00 CET" />
|
||||||
|
</SSAttrList>
|
||||||
|
</div>
|
||||||
|
</SSModal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalsPage;
|
||||||
25
dev/routes/auth/login.tsx
Normal file
25
dev/routes/auth/login.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Component } from 'solid-js';
|
||||||
|
import { SSButton, SSCallout, SSForm, SSHeader, SSSurface } from 'src';
|
||||||
|
import { IconSpark } from '../../demo/content';
|
||||||
|
|
||||||
|
const LoginPage: Component = () => (
|
||||||
|
<div class="demo_page">
|
||||||
|
<SSHeader title="Sign in" subtitle="Access the demo workspace." />
|
||||||
|
<SSSurface class="demo_surface">
|
||||||
|
<SSCallout color="blue" icon={<IconSpark />}>
|
||||||
|
Use any email and password to explore the UI kit.
|
||||||
|
</SSCallout>
|
||||||
|
<SSForm onsubmit={() => {}}>
|
||||||
|
<SSForm.Input label="Email" name="email" type="email" required />
|
||||||
|
<SSForm.Input label="Password" name="password" type="password" required />
|
||||||
|
<SSForm.Checkbox label="Remember me" name="remember" />
|
||||||
|
<div class="demo_inline">
|
||||||
|
<SSButton>Sign in</SSButton>
|
||||||
|
<SSButton class="secondary">Request access</SSButton>
|
||||||
|
</div>
|
||||||
|
</SSForm>
|
||||||
|
</SSSurface>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
111
dev/styles.css
Normal file
111
dev/styles.css
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
.demo_page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_section_title {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--fg-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_surface {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_surface--nested {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_tiles {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_inline {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_form_grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_ac_item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_ac_item strong {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--fg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_ac_item span {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--fg-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_text {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--fg-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_code {
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg-surface-subtle);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--fg-primary);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_hint {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--fg-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo_notice {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
7
dev/tsconfig.json
Normal file
7
dev/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
37
dev/vite.config.ts
Normal file
37
dev/vite.config.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import path from 'node:path'
|
||||||
|
import solidPlugin from 'vite-plugin-solid'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
src: path.resolve(__dirname, '../src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
solidPlugin(),
|
||||||
|
{
|
||||||
|
name: 'Reaplace env variables',
|
||||||
|
transform(code, id) {
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
return code
|
||||||
|
.replace(/process\.env\.SSR/g, 'false')
|
||||||
|
.replace(/process\.env\.DEV/g, 'true')
|
||||||
|
.replace(/process\.env\.PROD/g, 'false')
|
||||||
|
.replace(/process\.env\.NODE_ENV/g, '"development"')
|
||||||
|
.replace(/import\.meta\.env\.SSR/g, 'false')
|
||||||
|
.replace(/import\.meta\.env\.DEV/g, 'true')
|
||||||
|
.replace(/import\.meta\.env\.PROD/g, 'false')
|
||||||
|
.replace(/import\.meta\.env\.NODE_ENV/g, '"development"')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
port: 3001,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
})
|
||||||
18
env.d.ts
vendored
Normal file
18
env.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
declare global {
|
||||||
|
interface ImportMeta {
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production' | 'development'
|
||||||
|
PROD: boolean
|
||||||
|
DEV: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
NODE_ENV: 'production' | 'development'
|
||||||
|
PROD: boolean
|
||||||
|
DEV: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
6439
package-lock.json
generated
Normal file
6439
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
92
package.json
Normal file
92
package.json
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"name": "@sortsys/ui",
|
||||||
|
"version": "0.1.9",
|
||||||
|
"description": "Simple and beautiful UI library for sortsys",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "Ludwig Lehnert",
|
||||||
|
"contributors": [],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/stuck-lehnert/sortsys-ui.git"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/stuck-lehnert/sortsys-ui#readme",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/stuck-lehnert/sortsys-ui/issues"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"private": false,
|
||||||
|
"sideEffects": false,
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"browser": {},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"development": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/dev.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"./styles/*": "./dist/styles/*"
|
||||||
|
},
|
||||||
|
"typesVersions": {},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite serve dev",
|
||||||
|
"build": "tsup && esbuild src/styles/default.css --minify --outfile=dist/styles/default.css",
|
||||||
|
"test": "concurrently pnpm:test:*",
|
||||||
|
"test:client": "vitest",
|
||||||
|
"test:ssr": "npm run test:client --mode ssr",
|
||||||
|
"prepublishOnly": "npm run build",
|
||||||
|
"format": "prettier --ignore-path .gitignore -w \"src/**/*.{js,ts,json,css,tsx,jsx}\" \"dev/**/*.{js,ts,json,css,tsx,jsx}\"",
|
||||||
|
"lint": "concurrently pnpm:lint:*",
|
||||||
|
"lint:code": "eslint --ignore-path .gitignore --max-warnings 0 src/**/*.{js,ts,tsx,jsx}",
|
||||||
|
"lint:types": "tsc --noEmit",
|
||||||
|
"update-deps": "pnpm up -Li"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"solid-js": "^1.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.12.12",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
||||||
|
"@typescript-eslint/parser": "^7.9.0",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
|
"esbuild": "^0.21.3",
|
||||||
|
"esbuild-plugin-solid": "^0.6.0",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||||
|
"eslint-plugin-no-only-tests": "^3.1.0",
|
||||||
|
"jsdom": "^24.0.0",
|
||||||
|
"prettier": "3.0.0",
|
||||||
|
"solid-js": "^1.8.17",
|
||||||
|
"tsup": "^8.0.2",
|
||||||
|
"tsup-preset-solid": "^2.2.0",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"vite": "^5.2.11",
|
||||||
|
"vite-plugin-solid": "^2.10.2",
|
||||||
|
"vitest": "^1.6.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"solid"
|
||||||
|
],
|
||||||
|
"packageManager": "pnpm@9.1.1",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18",
|
||||||
|
"pnpm": ">=9.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@solidjs/router": "^0.15.4"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
4199
pnpm-lock.yaml
generated
Normal file
4199
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
src/colors.ts
Normal file
23
src/colors.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export const COLORS = [
|
||||||
|
'red',
|
||||||
|
'pink',
|
||||||
|
'purple',
|
||||||
|
'deepPurple',
|
||||||
|
'indigo',
|
||||||
|
'blue',
|
||||||
|
'lightBlue',
|
||||||
|
'cyan',
|
||||||
|
'teal',
|
||||||
|
'green',
|
||||||
|
'lightGreen',
|
||||||
|
'lime',
|
||||||
|
'yellow',
|
||||||
|
'amber',
|
||||||
|
'orange',
|
||||||
|
'deepOrange',
|
||||||
|
'brown',
|
||||||
|
'grey',
|
||||||
|
'blueGrey',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type SSColor = (typeof COLORS)[number];
|
||||||
38
src/components/SSAttrList.tsx
Normal file
38
src/components/SSAttrList.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { JSX, JSXElement, Show, children } from 'solid-js';
|
||||||
|
|
||||||
|
type SSAttrListProps = {
|
||||||
|
children?: JSXElement;
|
||||||
|
} & Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children'>;
|
||||||
|
|
||||||
|
type SSAttrListAttrProps = {
|
||||||
|
name: JSXElement;
|
||||||
|
value: JSXElement;
|
||||||
|
third?: JSXElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SSAttrListComponent = ((props: SSAttrListProps) => JSXElement | null) & {
|
||||||
|
Attr: (props: SSAttrListAttrProps) => JSXElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SSAttrList: SSAttrListComponent = (props: SSAttrListProps) => {
|
||||||
|
const resolved = children(() => props.children);
|
||||||
|
const { class: className, children: _children, ...rest } = props;
|
||||||
|
|
||||||
|
return <Show when={!!resolved()}>
|
||||||
|
<div {...rest} class={`ss_attr_list ${className ?? ''}`}>
|
||||||
|
{resolved()}
|
||||||
|
</div>
|
||||||
|
</Show>;
|
||||||
|
};
|
||||||
|
|
||||||
|
SSAttrList.Attr = function (props: SSAttrListAttrProps) {
|
||||||
|
return (
|
||||||
|
<div class={`ss_attr_list__row ${props.third ? 'ss_attr_list__row--third' : ''}`}>
|
||||||
|
<div class="ss_attr_list__label">{props.name}</div>
|
||||||
|
<div class="ss_attr_list__value">{props.value}</div>
|
||||||
|
{!!props.third && <div class="ss_attr_list__value">{props.third}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SSAttrList };
|
||||||
35
src/components/SSButton.tsx
Normal file
35
src/components/SSButton.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { JSX } from 'solid-js';
|
||||||
|
|
||||||
|
type SSButtonProps = {
|
||||||
|
children: JSX.Element;
|
||||||
|
class?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
type?: 'button' | 'submit' | 'reset';
|
||||||
|
isIconOnly?: boolean;
|
||||||
|
onclick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
|
||||||
|
ariaLabel?: string;
|
||||||
|
form?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SSButton(props: SSButtonProps) {
|
||||||
|
const classes = () => [
|
||||||
|
'ss_button',
|
||||||
|
props.isIconOnly ? 'ss_button--icon' : '',
|
||||||
|
props.class ?? '',
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={props.type ?? 'button'}
|
||||||
|
class={classes()}
|
||||||
|
disabled={props.disabled}
|
||||||
|
aria-label={props.ariaLabel}
|
||||||
|
form={props.form}
|
||||||
|
onclick={props.onclick}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SSButton };
|
||||||
28
src/components/SSCallout.tsx
Normal file
28
src/components/SSCallout.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { JSX, JSXElement } from 'solid-js';
|
||||||
|
import { COLORS } from 'src/colors';
|
||||||
|
|
||||||
|
type SSCalloutProps = JSX.HTMLAttributes<HTMLDivElement> & {
|
||||||
|
color: (typeof COLORS)[number];
|
||||||
|
icon: JSXElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SSCallout(props: SSCalloutProps) {
|
||||||
|
const { icon, color, class: className, style, children, ...rest } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...rest}
|
||||||
|
class={`ss_callout ss_callout--${color} ${className ?? ''}`}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<span class="ss_callout__icon">
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
<div class="ss_callout__content">
|
||||||
|
<span>{children}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SSCallout };
|
||||||
62
src/components/SSChip.tsx
Normal file
62
src/components/SSChip.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { JSX, JSXElement } from 'solid-js';
|
||||||
|
import { COLORS } from 'src/colors';
|
||||||
|
|
||||||
|
type SSChipBaseProps = {
|
||||||
|
children: JSXElement;
|
||||||
|
color?: (typeof COLORS)[number];
|
||||||
|
class?: string;
|
||||||
|
style?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SSChipClickableProps = {
|
||||||
|
onclick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
|
||||||
|
ondismiss?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SSChipDismissableProps = {
|
||||||
|
ondismiss: () => void;
|
||||||
|
onclick?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SSChipDisplayProps = {
|
||||||
|
onclick?: never;
|
||||||
|
ondismiss?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SSChipProps = SSChipBaseProps &
|
||||||
|
(SSChipClickableProps | SSChipDismissableProps | SSChipDisplayProps);
|
||||||
|
|
||||||
|
function SSChip(props: SSChipProps) {
|
||||||
|
const commonClass = `ss_chip ss_chip--${props.color ?? 'blue'} ${props.class ?? ''}`;
|
||||||
|
|
||||||
|
if ('onclick' in props && props.onclick) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={`${commonClass} ss_chip--clickable`}
|
||||||
|
style={props.style}
|
||||||
|
onclick={props.onclick}
|
||||||
|
>
|
||||||
|
<span class="ss_chip__label">{props.children}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={commonClass} style={props.style}>
|
||||||
|
<span class="ss_chip__label">{props.children}</span>
|
||||||
|
{'ondismiss' in props && props.ondismiss && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ss_chip__dismiss"
|
||||||
|
aria-label="Entfernen"
|
||||||
|
onclick={props.ondismiss}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-x"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18 6l-12 12" /><path d="M6 6l12 12" /></svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SSChip };
|
||||||
245
src/components/SSDataTable.tsx
Normal file
245
src/components/SSDataTable.tsx
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import { JSXElement, createEffect, createMemo, createResource, createSignal, For } from 'solid-js';
|
||||||
|
|
||||||
|
type PromiseOr<T> = Promise<T> | T;
|
||||||
|
|
||||||
|
type SSDataColumn<RowT> = {
|
||||||
|
label: string;
|
||||||
|
render: (row: RowT) => PromiseOr<JSXElement>;
|
||||||
|
sortKey?: ((row: RowT) => string) | ((row: RowT) => number);
|
||||||
|
};
|
||||||
|
|
||||||
|
type SortDir = 'asc' | 'desc';
|
||||||
|
|
||||||
|
type SSDataTableProps<RowT extends object> = {
|
||||||
|
columns: SSDataColumn<RowT>[];
|
||||||
|
rows: RowT[];
|
||||||
|
onRowClick?: (row: RowT) => PromiseOr<void>;
|
||||||
|
pageSize?: number;
|
||||||
|
paginationPosition?: 'top' | 'bottom';
|
||||||
|
class?: string;
|
||||||
|
style?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SSDataCell<RowT extends object>(props: {
|
||||||
|
row: RowT;
|
||||||
|
render: (row: RowT) => PromiseOr<JSXElement>;
|
||||||
|
}) {
|
||||||
|
const rendered = createMemo(() => props.render(props.row));
|
||||||
|
const [asyncContent] = createResource(async () => {
|
||||||
|
const value = rendered();
|
||||||
|
if (value && typeof (value as Promise<JSXElement>).then === 'function') {
|
||||||
|
return await value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{(() => {
|
||||||
|
const value = rendered();
|
||||||
|
if (value && typeof (value as Promise<JSXElement>).then === 'function') {
|
||||||
|
return asyncContent() ?? '';
|
||||||
|
}
|
||||||
|
return value as JSXElement;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SSDataTable<RowT extends object>(props: SSDataTableProps<RowT>) {
|
||||||
|
const [sortIndex, setSortIndex] = createSignal(-1);
|
||||||
|
const [sortDir, setSortDir] = createSignal<SortDir | null>(null);
|
||||||
|
const [page, setPage] = createSignal(1);
|
||||||
|
|
||||||
|
const pageSize = () => Math.max(1, props.pageSize ?? 25);
|
||||||
|
|
||||||
|
const sortedRows = createMemo(() => {
|
||||||
|
const index = sortIndex();
|
||||||
|
const dir = sortDir();
|
||||||
|
if (index < 0 || !dir) return props.rows;
|
||||||
|
|
||||||
|
const column = props.columns[index];
|
||||||
|
if (!column?.sortKey) return props.rows;
|
||||||
|
|
||||||
|
const entries = props.rows.map((row, idx) => ({
|
||||||
|
row,
|
||||||
|
idx,
|
||||||
|
key: column.sortKey!(row),
|
||||||
|
}));
|
||||||
|
|
||||||
|
entries.sort((a, b) => {
|
||||||
|
if (a.key === b.key) return a.idx - b.idx;
|
||||||
|
if (a.key < b.key) return dir === 'asc' ? -1 : 1;
|
||||||
|
return dir === 'asc' ? 1 : -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return entries.map((entry) => entry.row);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = createMemo(() => {
|
||||||
|
return Math.max(1, Math.ceil(sortedRows().length / pageSize()));
|
||||||
|
});
|
||||||
|
|
||||||
|
const pagedRows = createMemo(() => {
|
||||||
|
const current = Math.min(page(), totalPages());
|
||||||
|
const start = (current - 1) * pageSize();
|
||||||
|
return sortedRows().slice(start, start + pageSize());
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (page() > totalPages()) setPage(totalPages());
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleSort = (index: number) => {
|
||||||
|
if (sortIndex() !== index) {
|
||||||
|
setSortIndex(index);
|
||||||
|
setSortDir('asc');
|
||||||
|
setPage(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDir() === 'asc') {
|
||||||
|
setSortDir('desc');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortDir() === 'desc') {
|
||||||
|
setSortIndex(-1);
|
||||||
|
setSortDir(null);
|
||||||
|
setPage(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSortDir('asc');
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToPage = (next: number) => {
|
||||||
|
const safePage = Math.min(Math.max(1, next), totalPages());
|
||||||
|
setPage(safePage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const paginationPosition = () => props.paginationPosition ?? 'bottom';
|
||||||
|
const containerClass = () =>
|
||||||
|
`ss_table ${paginationPosition() === 'top' ? 'ss_table--pagination-top' : ''} ${
|
||||||
|
props.class ?? ''
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const paginationBar = () => (
|
||||||
|
<div class="ss_table__pagination">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ss_table__page_button"
|
||||||
|
disabled={page() === 1}
|
||||||
|
aria-label="Erste Seite"
|
||||||
|
onclick={() => goToPage(1)}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left-pipe"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 6v12" /><path d="M18 6l-6 6l6 6" /></svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ss_table__page_button"
|
||||||
|
disabled={page() === 1}
|
||||||
|
aria-label="Vorherige Seite"
|
||||||
|
onclick={() => goToPage(page() - 1)}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 6l-6 6l6 6" /></svg>
|
||||||
|
</button>
|
||||||
|
<span class="ss_table__page_info">
|
||||||
|
Seite {page()} von {totalPages()}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ss_table__page_button"
|
||||||
|
disabled={page() === totalPages()}
|
||||||
|
aria-label="Nächste Seite"
|
||||||
|
onclick={() => goToPage(page() + 1)}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 6l6 6l-6 6" /></svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ss_table__page_button"
|
||||||
|
disabled={page() === totalPages()}
|
||||||
|
aria-label="Letzte Seite"
|
||||||
|
onclick={() => goToPage(totalPages())}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right-pipe"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6 6l6 6l-6 6" /><path d="M17 5v13" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={containerClass()} style={props.style}>
|
||||||
|
{paginationPosition() === 'top' && paginationBar()}
|
||||||
|
<div class="ss_table__scroll">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<For each={props.columns}>
|
||||||
|
{(column, index) => {
|
||||||
|
const sortable = !!column.sortKey;
|
||||||
|
const isActive = () => sortIndex() === index();
|
||||||
|
const currentDir = () => (isActive() ? sortDir() : null);
|
||||||
|
return (
|
||||||
|
<th>
|
||||||
|
{sortable ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ss_table__sort_button"
|
||||||
|
aria-sort={
|
||||||
|
currentDir() === 'asc'
|
||||||
|
? 'ascending'
|
||||||
|
: currentDir() === 'desc'
|
||||||
|
? 'descending'
|
||||||
|
: 'none'
|
||||||
|
}
|
||||||
|
data-sort={currentDir() ?? 'none'}
|
||||||
|
onclick={() => toggleSort(index())}
|
||||||
|
>
|
||||||
|
{column.label}
|
||||||
|
<span class="ss_table__sort_icon" aria-hidden="true">
|
||||||
|
{currentDir() === 'asc' ? (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-down"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6 9l6 6l6 -6" /></svg>
|
||||||
|
) : currentDir() === 'desc' ? (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-up"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6 15l6 -6l6 6" /></svg>
|
||||||
|
) : (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-selector"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 9l4 -4l4 4" /><path d="M16 15l-4 4l-4 -4" /></svg>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span class="ss_table__header_label">{column.label}</span>
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<For each={pagedRows()}>
|
||||||
|
{(row) => (
|
||||||
|
<tr
|
||||||
|
data-clickable={props.onRowClick ? 'true' : undefined}
|
||||||
|
onclick={() => props.onRowClick?.(row)}
|
||||||
|
>
|
||||||
|
<For each={props.columns}>
|
||||||
|
{(column) => (
|
||||||
|
<td>
|
||||||
|
<SSDataCell row={row} render={column.render} />
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{paginationPosition() === 'bottom' && paginationBar()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SSDataTable };
|
||||||
18
src/components/SSDivider.tsx
Normal file
18
src/components/SSDivider.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
type SSDividerProps = {
|
||||||
|
vertical?: boolean;
|
||||||
|
class?: string;
|
||||||
|
style?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SSDivider(props: SSDividerProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={`ss_divider ${props.vertical ? 'ss_divider--vertical' : ''} ${props.class ?? ''}`}
|
||||||
|
style={props.style}
|
||||||
|
role="separator"
|
||||||
|
aria-orientation={props.vertical ? 'vertical' : 'horizontal'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SSDivider };
|
||||||
109
src/components/SSDropdown.tsx
Normal file
109
src/components/SSDropdown.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { JSXElement, createSignal, For, onCleanup, onMount, Show, createEffect } from 'solid-js';
|
||||||
|
|
||||||
|
type PromiseOr<T> = Promise<T> | T;
|
||||||
|
|
||||||
|
type SSDropdownItem = {
|
||||||
|
label: string;
|
||||||
|
icon: JSXElement;
|
||||||
|
onclick?: () => PromiseOr<void>;
|
||||||
|
checked?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SSDropdownProps = {
|
||||||
|
items: SSDropdownItem[];
|
||||||
|
icon?: JSXElement;
|
||||||
|
ariaLabel?: string;
|
||||||
|
class?: string;
|
||||||
|
style?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SSDropdown(props: SSDropdownProps) {
|
||||||
|
const [open, setOpen] = createSignal(false);
|
||||||
|
const [renderMenu, setRenderMenu] = createSignal(false);
|
||||||
|
const [menuState, setMenuState] = createSignal<'open' | 'closed'>('closed');
|
||||||
|
let rootRef: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
const close = () => setOpen(false);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (open()) {
|
||||||
|
setRenderMenu(true);
|
||||||
|
requestAnimationFrame(() => setMenuState('open'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!renderMenu()) return;
|
||||||
|
setMenuState('closed');
|
||||||
|
const timeout = window.setTimeout(() => setRenderMenu(false), 160);
|
||||||
|
onCleanup(() => window.clearTimeout(timeout));
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const handlePointerDown = (event: MouseEvent) => {
|
||||||
|
const target = event.target as Node | null;
|
||||||
|
if (!rootRef || !target) return;
|
||||||
|
if (!rootRef.contains(target)) close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') close();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handlePointerDown);
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
document.removeEventListener('mousedown', handlePointerDown);
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={`ss_dropdown ${props.class ?? ''}`} style={props.style} ref={el => (rootRef = el)}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ss_dropdown__trigger ss_button ss_button--icon"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={open()}
|
||||||
|
aria-label={props.ariaLabel ?? 'Aktionen öffnen'}
|
||||||
|
onclick={() => setOpen((value) => !value)}
|
||||||
|
>
|
||||||
|
{props.icon ?? (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dots-vertical"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M11 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M11 19a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M11 5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={renderMenu()}>
|
||||||
|
<div
|
||||||
|
class="ss_dropdown__menu"
|
||||||
|
role="menu"
|
||||||
|
data-state={menuState()}
|
||||||
|
>
|
||||||
|
<For each={props.items}>
|
||||||
|
{(item) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ss_dropdown__item"
|
||||||
|
role={item.checked ? 'menuitemcheckbox' : 'menuitem'}
|
||||||
|
aria-checked={item.checked ? 'true' : undefined}
|
||||||
|
onclick={async () => {
|
||||||
|
close();
|
||||||
|
await item.onclick?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="ss_dropdown__item_icon">{item.icon}</span>
|
||||||
|
<span class="ss_dropdown__item_label">{item.label}</span>
|
||||||
|
{item.checked && (
|
||||||
|
<span class="ss_dropdown__item_check" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-check"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10" /></svg>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SSDropdown };
|
||||||
83
src/components/SSExpandable.tsx
Normal file
83
src/components/SSExpandable.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { JSXElement, createMemo, createSignal, onCleanup } from 'solid-js';
|
||||||
|
|
||||||
|
type SSExpandableProps = {
|
||||||
|
title: JSXElement;
|
||||||
|
class?: string;
|
||||||
|
children?: JSXElement;
|
||||||
|
style?: string;
|
||||||
|
initiallyExpanded?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TRANSITION_MS = 200;
|
||||||
|
|
||||||
|
function SSExpandable(props: SSExpandableProps) {
|
||||||
|
const [height, setHeight] = createSignal<string | number>(props.initiallyExpanded ? 'auto' : 0);
|
||||||
|
const isExpanded = createMemo(() => height() !== 0);
|
||||||
|
let contentRef: HTMLDivElement | undefined;
|
||||||
|
let timeoutId: number | undefined;
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
const targetHeight = contentRef?.scrollHeight ?? 0;
|
||||||
|
|
||||||
|
if (isExpanded()) {
|
||||||
|
setHeight(targetHeight);
|
||||||
|
timeoutId = window.setTimeout(() => setHeight(0), 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeight(targetHeight);
|
||||||
|
timeoutId = window.setTimeout(() => setHeight('auto'), TRANSITION_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={`ss_expandable ${props.class ?? ''}`}
|
||||||
|
style={props.style}
|
||||||
|
data-state={isExpanded() ? 'open' : 'closed'}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ss_expandable__header"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-expanded={isExpanded()}
|
||||||
|
onclick={toggle}
|
||||||
|
onkeydown={event => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="ss_expandable__icon" aria-hidden="true">
|
||||||
|
{isExpanded() ? (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-down"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6 9l6 6l6 -6" /></svg>
|
||||||
|
) : (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 6l6 6l-6 6" /></svg>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span class="ss_expandable__title">{props.title}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={el => (contentRef = el)}
|
||||||
|
class="ss_expandable__content"
|
||||||
|
style={{
|
||||||
|
height: (typeof height() === 'number' ? `${height()}px` : height()) as any,
|
||||||
|
'transition-duration': `${TRANSITION_MS}ms`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class='ss_expandable__divider_wrapper'>
|
||||||
|
<div class="ss_expandable__divider" />
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SSExpandable };
|
||||||
1062
src/components/SSForm.tsx
Normal file
1062
src/components/SSForm.tsx
Normal file
File diff suppressed because it is too large
Load Diff
23
src/components/SSHeader.tsx
Normal file
23
src/components/SSHeader.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { JSXElement } from 'solid-js';
|
||||||
|
|
||||||
|
type SSHeaderProps = {
|
||||||
|
title: JSXElement;
|
||||||
|
subtitle?: JSXElement | false;
|
||||||
|
actions?: JSXElement | false;
|
||||||
|
class?: string;
|
||||||
|
style?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SSHeader(props: SSHeaderProps) {
|
||||||
|
return (
|
||||||
|
<div class={`ss_header ${props.class ?? ''}`} style={props.style}>
|
||||||
|
<div class="ss_header__text">
|
||||||
|
<h3 class="ss_header__title">{props.title}</h3>
|
||||||
|
{props.subtitle && <h5 class="ss_header__subtitle">{props.subtitle}</h5>}
|
||||||
|
</div>
|
||||||
|
<div class="ss_header__actions">{props.actions}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SSHeader };
|
||||||
135
src/components/SSModal.tsx
Normal file
135
src/components/SSModal.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { JSXElement, Show, createEffect, createSignal, createUniqueId, onCleanup, onMount } from 'solid-js';
|
||||||
|
import { Portal } from 'solid-js/web';
|
||||||
|
import { SSButton } from './SSButton';
|
||||||
|
|
||||||
|
type SSModalSize = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
|
export type SSModalProps = {
|
||||||
|
open: boolean;
|
||||||
|
title?: string;
|
||||||
|
children: JSXElement;
|
||||||
|
footer?: JSXElement;
|
||||||
|
size?: SSModalSize;
|
||||||
|
fullscreen?: boolean;
|
||||||
|
disableResponsiveFullscreen?: boolean;
|
||||||
|
dismissible?: boolean;
|
||||||
|
lockScroll?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CLOSE_ANIMATION_MS = 180;
|
||||||
|
|
||||||
|
function SSModal(props: SSModalProps) {
|
||||||
|
const [isMounted, setIsMounted] = createSignal(props.open);
|
||||||
|
const [state, setState] = createSignal<'open' | 'closed'>('closed');
|
||||||
|
const titleId = createUniqueId();
|
||||||
|
let closeTimeout: number | undefined;
|
||||||
|
let rafId: number | undefined;
|
||||||
|
let panelRef: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (closeTimeout) clearTimeout(closeTimeout);
|
||||||
|
if (rafId) cancelAnimationFrame(rafId);
|
||||||
|
|
||||||
|
if (props.open) {
|
||||||
|
if (!isMounted()) setIsMounted(true);
|
||||||
|
setState('closed');
|
||||||
|
rafId = requestAnimationFrame(() => setState('open'));
|
||||||
|
} else {
|
||||||
|
setState('closed');
|
||||||
|
if (isMounted()) {
|
||||||
|
closeTimeout = window.setTimeout(() => setIsMounted(false), CLOSE_ANIMATION_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!props.open) return;
|
||||||
|
|
||||||
|
if (props.lockScroll !== false) {
|
||||||
|
const prev = document.body.style.overflowY;
|
||||||
|
document.body.style.overflowY = 'hidden';
|
||||||
|
onCleanup(() => {
|
||||||
|
document.body.style.overflowY = prev;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
rafId = requestAnimationFrame(() => panelRef?.focus());
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape' && props.dismissible !== false) {
|
||||||
|
props.onClose?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
onCleanup(() => window.removeEventListener('keydown', handleKeyDown));
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (closeTimeout) clearTimeout(closeTimeout);
|
||||||
|
if (rafId) cancelAnimationFrame(rafId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBackdropClick = () => {
|
||||||
|
if (props.dismissible === false) return;
|
||||||
|
props.onClose?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={isMounted()}>
|
||||||
|
<Portal>
|
||||||
|
<div class="ss_modal" data-state={state()} aria-hidden={state() === 'closed'}>
|
||||||
|
<div class="ss_modal__backdrop" onclick={handleBackdropClick} />
|
||||||
|
<div
|
||||||
|
class="ss_modal__panel"
|
||||||
|
classList={{
|
||||||
|
'ss_modal__panel--sm': props.size === 'sm',
|
||||||
|
'ss_modal__panel--lg': props.size === 'lg',
|
||||||
|
'ss_modal__panel--fullscreen': props.fullscreen,
|
||||||
|
'ss_modal__panel--no-fullscreen': props.disableResponsiveFullscreen,
|
||||||
|
}}
|
||||||
|
ref={el => (panelRef = el)}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby={props.title ? titleId : undefined}
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
{(props.title || props.onClose) && (
|
||||||
|
<div class="ss_modal__header">
|
||||||
|
{props.title && (
|
||||||
|
<h2 id={titleId} class="ss_modal__title">
|
||||||
|
{props.title}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
<Show when={props.onClose}>
|
||||||
|
<SSButton
|
||||||
|
type="button"
|
||||||
|
class="ss_modal__close"
|
||||||
|
isIconOnly
|
||||||
|
ariaLabel="Dialog schließen"
|
||||||
|
onclick={() => {
|
||||||
|
if (props.dismissible === false) return;
|
||||||
|
props.onClose?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-x"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18 6l-12 12" /><path d="M6 6l12 12" /></svg>
|
||||||
|
</SSButton>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="ss_modal__body">
|
||||||
|
<div class="ss_modal__body_inner">{props.children}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={props.footer}>
|
||||||
|
<div class="ss_modal__footer">{props.footer}</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SSModal };
|
||||||
263
src/components/SSModals.tsx
Normal file
263
src/components/SSModals.tsx
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import { JSXElement, createContext, createSignal, For, onCleanup, useContext } from 'solid-js';
|
||||||
|
import { useLocation, useNavigate } from '@solidjs/router';
|
||||||
|
import { SSModal, type SSModalProps } from './SSModal';
|
||||||
|
import { createLoading } from '../hooks/createLoading';
|
||||||
|
import { SSForm } from './SSForm';
|
||||||
|
import { SSButton } from './SSButton';
|
||||||
|
|
||||||
|
type PromiseOr<T> = Promise<T> | T;
|
||||||
|
|
||||||
|
type SSModalEntry = {
|
||||||
|
id: string;
|
||||||
|
visible: () => boolean;
|
||||||
|
setVisible: (value: boolean) => void;
|
||||||
|
render: (props: { id: string; hide: () => void; visible: () => boolean }) => JSXElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SSFormPublicContext = NonNullable<ReturnType<typeof SSForm.useContext>>;
|
||||||
|
|
||||||
|
type DefaultModalProps = {
|
||||||
|
content: (props: { hide: () => void }) => JSXElement;
|
||||||
|
modalProps?: (props: { hide: () => void }) => Omit<
|
||||||
|
SSModalProps,
|
||||||
|
'open' | 'onClose' | 'children' | 'footer'
|
||||||
|
> & {
|
||||||
|
primaryButtonText?: string;
|
||||||
|
secondaryButtonText?: string;
|
||||||
|
hideSecondaryButton?: boolean;
|
||||||
|
danger?: boolean;
|
||||||
|
};
|
||||||
|
onPrimaryAction?: (props: {
|
||||||
|
hide: () => void;
|
||||||
|
navigate: ReturnType<typeof useNavigate>;
|
||||||
|
pathname: string;
|
||||||
|
}) => PromiseOr<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormModalProps = {
|
||||||
|
content: (props: { hide: () => void; context: SSFormPublicContext }) => JSXElement;
|
||||||
|
modalProps?: (props: {
|
||||||
|
hide: () => void;
|
||||||
|
context: SSFormPublicContext;
|
||||||
|
}) => Omit<
|
||||||
|
SSModalProps,
|
||||||
|
'open' | 'onClose' | 'children' | 'footer'
|
||||||
|
> & {
|
||||||
|
primaryButtonText?: string;
|
||||||
|
secondaryButtonText?: string;
|
||||||
|
hideSecondaryButton?: boolean;
|
||||||
|
danger?: boolean;
|
||||||
|
};
|
||||||
|
onSubmit: (props: {
|
||||||
|
hide: () => void;
|
||||||
|
context: SSFormPublicContext;
|
||||||
|
navigate: ReturnType<typeof useNavigate>;
|
||||||
|
pathname: string;
|
||||||
|
}) => PromiseOr<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SSModalsInterface {
|
||||||
|
show: (render: SSModalEntry['render']) => string;
|
||||||
|
showDefault: (props: DefaultModalProps) => string;
|
||||||
|
showForm: (props: FormModalProps) => string;
|
||||||
|
hide: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SSModalsContext = createContext<SSModalsInterface>();
|
||||||
|
|
||||||
|
let modalCounter = 0;
|
||||||
|
const nextModalId = () => `ss-modal-${modalCounter++}`;
|
||||||
|
|
||||||
|
export function useSSModals() {
|
||||||
|
const context = useContext(SSModalsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useSSModals must be used within SSModalsProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DefaultModal(props: {
|
||||||
|
visible: () => boolean;
|
||||||
|
hide: () => void;
|
||||||
|
config: DefaultModalProps;
|
||||||
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const [loading, process] = createLoading();
|
||||||
|
|
||||||
|
const modalProps = () => props.config.modalProps?.({ hide: props.hide }) ?? {};
|
||||||
|
const {
|
||||||
|
primaryButtonText,
|
||||||
|
secondaryButtonText,
|
||||||
|
hideSecondaryButton,
|
||||||
|
danger,
|
||||||
|
...rest
|
||||||
|
} = modalProps();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SSModal
|
||||||
|
open={props.visible()}
|
||||||
|
onClose={props.hide}
|
||||||
|
{...rest}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
{!hideSecondaryButton && (
|
||||||
|
<SSButton class="secondary" onclick={props.hide}>
|
||||||
|
{secondaryButtonText ?? 'Abbrechen'}
|
||||||
|
</SSButton>
|
||||||
|
)}
|
||||||
|
<SSButton
|
||||||
|
class={danger ? 'danger' : undefined}
|
||||||
|
onclick={() =>
|
||||||
|
process(() =>
|
||||||
|
props.config.onPrimaryAction?.({
|
||||||
|
hide: props.hide,
|
||||||
|
navigate,
|
||||||
|
pathname: location.pathname,
|
||||||
|
}) ?? props.hide(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={loading()}
|
||||||
|
>
|
||||||
|
{primaryButtonText ?? 'Weiter'}
|
||||||
|
</SSButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.config.content({ hide: props.hide })}
|
||||||
|
</SSModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormModal(props: {
|
||||||
|
visible: () => boolean;
|
||||||
|
hide: () => void;
|
||||||
|
config: FormModalProps;
|
||||||
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SSForm
|
||||||
|
onsubmit={(context) =>
|
||||||
|
props.config.onSubmit({
|
||||||
|
hide: props.hide,
|
||||||
|
context,
|
||||||
|
navigate,
|
||||||
|
pathname: location.pathname,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormModalInner visible={props.visible} hide={props.hide} config={props.config} />
|
||||||
|
</SSForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormModalInner(props: {
|
||||||
|
visible: () => boolean;
|
||||||
|
hide: () => void;
|
||||||
|
config: FormModalProps;
|
||||||
|
}) {
|
||||||
|
const context = SSForm.useContext();
|
||||||
|
if (!context) return null;
|
||||||
|
|
||||||
|
const modalProps = () => props.config.modalProps?.({ hide: props.hide, context }) ?? {};
|
||||||
|
const {
|
||||||
|
primaryButtonText,
|
||||||
|
secondaryButtonText,
|
||||||
|
hideSecondaryButton,
|
||||||
|
danger,
|
||||||
|
...rest
|
||||||
|
} = modalProps();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SSModal
|
||||||
|
open={props.visible()}
|
||||||
|
onClose={props.hide}
|
||||||
|
{...rest}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
{!hideSecondaryButton && (
|
||||||
|
<SSButton class="secondary" onclick={props.hide} disabled={context.loading()}>
|
||||||
|
{secondaryButtonText ?? 'Abbrechen'}
|
||||||
|
</SSButton>
|
||||||
|
)}
|
||||||
|
<SSButton
|
||||||
|
class={danger ? 'danger' : undefined}
|
||||||
|
onclick={() => context.submit()}
|
||||||
|
disabled={context.loading()}
|
||||||
|
>
|
||||||
|
{primaryButtonText ?? 'Speichern'}
|
||||||
|
</SSButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.config.content({ hide: props.hide, context })}
|
||||||
|
</SSModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SSModalsProvider(props: { children: JSXElement }) {
|
||||||
|
const [modals, setModals] = createSignal<SSModalEntry[]>([]);
|
||||||
|
const modalsById = new Map<string, SSModalEntry>();
|
||||||
|
const closeTimeouts = new Map<string, number>();
|
||||||
|
const removeDelayMs = 220;
|
||||||
|
|
||||||
|
const hide = (id: string) => {
|
||||||
|
const modal = modalsById.get(id);
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
|
modal.setVisible(false);
|
||||||
|
|
||||||
|
const existing = closeTimeouts.get(id);
|
||||||
|
if (existing) window.clearTimeout(existing);
|
||||||
|
|
||||||
|
const timeout = window.setTimeout(() => {
|
||||||
|
setModals((list) => list.filter((modal) => modal.id !== id));
|
||||||
|
modalsById.delete(id);
|
||||||
|
closeTimeouts.delete(id);
|
||||||
|
}, removeDelayMs);
|
||||||
|
|
||||||
|
closeTimeouts.set(id, timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
const show = (render: SSModalEntry['render']) => {
|
||||||
|
const id = nextModalId();
|
||||||
|
const [visible, setVisible] = createSignal(true);
|
||||||
|
const entry = { id, visible, setVisible, render };
|
||||||
|
modalsById.set(id, entry);
|
||||||
|
setModals((list) => [...list, entry]);
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showDefault = (config: DefaultModalProps) => {
|
||||||
|
return show(({ hide, visible }) => (
|
||||||
|
<DefaultModal visible={visible} hide={hide} config={config} />
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const showForm = (config: FormModalProps) => {
|
||||||
|
return show(({ hide, visible }) => (
|
||||||
|
<FormModal visible={visible} hide={hide} config={config} />
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
closeTimeouts.forEach((timeout) => window.clearTimeout(timeout));
|
||||||
|
closeTimeouts.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SSModalsContext.Provider value={{ show, showDefault, showForm, hide }}>
|
||||||
|
{props.children}
|
||||||
|
|
||||||
|
<For each={modals()}>
|
||||||
|
{(modal) => {
|
||||||
|
const hideModal = () => hide(modal.id);
|
||||||
|
return modal.render({ id: modal.id, hide: hideModal, visible: modal.visible });
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</SSModalsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
159
src/components/SSShell.tsx
Normal file
159
src/components/SSShell.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { JSXElement, createContext, createMemo, createSignal, createUniqueId, onCleanup, onMount, useContext } from 'solid-js';
|
||||||
|
import { useLocation } from '@solidjs/router';
|
||||||
|
|
||||||
|
type SSShellProps = {
|
||||||
|
title: JSXElement;
|
||||||
|
nav: JSXElement;
|
||||||
|
children: JSXElement;
|
||||||
|
actions?: JSXElement;
|
||||||
|
class?: string;
|
||||||
|
style?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SSShellContextValue = {
|
||||||
|
closeDrawer: () => void;
|
||||||
|
activeHref: () => string | null;
|
||||||
|
registerHref: (href: string) => void;
|
||||||
|
unregisterHref: (href: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SSShellContext = createContext<SSShellContextValue>();
|
||||||
|
|
||||||
|
function SSShell(props: SSShellProps) {
|
||||||
|
const drawerId = createUniqueId();
|
||||||
|
const location = useLocation();
|
||||||
|
const [hrefs, setHrefs] = createSignal<string[]>([]);
|
||||||
|
|
||||||
|
const closeDrawer = () => {
|
||||||
|
const input = document.getElementById(drawerId) as HTMLInputElement | null;
|
||||||
|
if (input) input.checked = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerHref = (href: string) => {
|
||||||
|
setHrefs((prev) => (prev.includes(href) ? prev : [...prev, href]));
|
||||||
|
};
|
||||||
|
|
||||||
|
const unregisterHref = (href: string) => {
|
||||||
|
setHrefs((prev) => prev.filter((item) => item !== href));
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeHref = createMemo(() => {
|
||||||
|
const path = location.pathname;
|
||||||
|
let best: string | null = null;
|
||||||
|
for (const href of hrefs()) {
|
||||||
|
if (!path.startsWith(href)) continue;
|
||||||
|
if (!best || href.length > best.length) {
|
||||||
|
best = href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SSShellContext.Provider value={{ closeDrawer, activeHref, registerHref, unregisterHref }}>
|
||||||
|
<div class={`ss_shell ${props.class ?? ''}`} style={props.style}>
|
||||||
|
<input id={drawerId} type="checkbox" class="ss_shell__drawer_toggle_input" />
|
||||||
|
|
||||||
|
<header class="ss_shell__header">
|
||||||
|
<div class="ss_shell__header_left">
|
||||||
|
<label
|
||||||
|
for={drawerId}
|
||||||
|
class="ss_shell__drawer_toggle ss_button ss_button--icon"
|
||||||
|
aria-label="Navigation öffnen"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-menu-2"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 6l16 0" /><path d="M4 12l16 0" /><path d="M4 18l16 0" /></svg>
|
||||||
|
</label>
|
||||||
|
<div class="ss_shell__title">{props.title}</div>
|
||||||
|
</div>
|
||||||
|
<div class="ss_shell__actions">{props.actions}</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="ss_shell__body">
|
||||||
|
<nav class="ss_shell__nav" aria-label="Hauptnavigation">
|
||||||
|
<div class="ss_shell__nav_inner">{props.nav}</div>
|
||||||
|
</nav>
|
||||||
|
<div class="ss_shell__main">{props.children}</div>
|
||||||
|
<label for={drawerId} class="ss_shell__scrim" aria-label="Navigation schließen" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SSShellContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SSShell.Nav = function (props: { children: JSXElement }) {
|
||||||
|
return <div class="ss_shell__nav_list">{props.children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
SSShell.NavLink = function (props: {
|
||||||
|
href: string;
|
||||||
|
children: JSXElement;
|
||||||
|
icon?: JSXElement;
|
||||||
|
onclick?: () => void;
|
||||||
|
}) {
|
||||||
|
const context = useContext(SSShellContext);
|
||||||
|
|
||||||
|
onMount(() => context?.registerHref(props.href));
|
||||||
|
onCleanup(() => context?.unregisterHref(props.href));
|
||||||
|
|
||||||
|
const isActive = () => context?.activeHref() === props.href;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
class="ss_shell__nav_item"
|
||||||
|
classList={{ 'ss_shell__nav_item--active': isActive() }}
|
||||||
|
href={props.href}
|
||||||
|
onclick={() => {
|
||||||
|
props.onclick?.();
|
||||||
|
context?.closeDrawer();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.icon && <span class="ss_shell__nav_icon">{props.icon}</span>}
|
||||||
|
<span class="ss_shell__nav_label">{props.children}</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SSShell.NavAction = function (props: {
|
||||||
|
onclick: () => void;
|
||||||
|
children: JSXElement;
|
||||||
|
icon?: JSXElement;
|
||||||
|
}) {
|
||||||
|
const context = useContext(SSShellContext);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ss_shell__nav_item"
|
||||||
|
onclick={() => {
|
||||||
|
props.onclick();
|
||||||
|
context?.closeDrawer();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.icon && <span class="ss_shell__nav_icon">{props.icon}</span>}
|
||||||
|
<span class="ss_shell__nav_label">{props.children}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SSShell.NavGroup = function (props: {
|
||||||
|
title: JSXElement;
|
||||||
|
children: JSXElement;
|
||||||
|
icon?: JSXElement;
|
||||||
|
initiallyExpanded?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<details class="ss_shell__nav_group" open={props.initiallyExpanded}>
|
||||||
|
<summary class="ss_shell__nav_group_header">
|
||||||
|
{props.icon && <span class="ss_shell__nav_icon">{props.icon}</span>}
|
||||||
|
<span class="ss_shell__nav_label">{props.title}</span>
|
||||||
|
<span class="ss_shell__nav_group_chevron" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="ss_shell__nav_group_chevron_svg"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 6l6 6l-6 6" /></svg>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div class="ss_shell__nav_group_items">{props.children}</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SSShell };
|
||||||
15
src/components/SSSurface.tsx
Normal file
15
src/components/SSSurface.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
type SSSurfaceProps = {
|
||||||
|
children: import('solid-js').JSXElement;
|
||||||
|
class?: string;
|
||||||
|
style?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SSSurface(props: SSSurfaceProps) {
|
||||||
|
return (
|
||||||
|
<div class={`ss_surface ${props.class ?? ''}`} style={props.style}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SSSurface };
|
||||||
59
src/components/SSTile.tsx
Normal file
59
src/components/SSTile.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { JSXElement } from 'solid-js';
|
||||||
|
import { A } from '@solidjs/router';
|
||||||
|
|
||||||
|
type SSTileProps = {
|
||||||
|
icon?: JSXElement;
|
||||||
|
title: JSXElement;
|
||||||
|
href?: string;
|
||||||
|
subtitle?: JSXElement;
|
||||||
|
trailing?: JSXElement;
|
||||||
|
onLinkClick?: () => void;
|
||||||
|
class?: string;
|
||||||
|
style?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SSTile(props: SSTileProps) {
|
||||||
|
return (
|
||||||
|
<div class={`ss_tile ${props.class ?? ''}`} style={props.style}>
|
||||||
|
<div class="ss_tile__row">
|
||||||
|
{props.icon && <span class="ss_tile__icon">{props.icon}</span>}
|
||||||
|
<div class="ss_tile__content">
|
||||||
|
{props.href ? (
|
||||||
|
<h5 class="ss_tile__title">
|
||||||
|
<A class="ss_tile__link" href={props.href} onclick={props.onLinkClick}>
|
||||||
|
{props.title}
|
||||||
|
</A>
|
||||||
|
</h5>
|
||||||
|
) : (
|
||||||
|
<h5 class="ss_tile__title">
|
||||||
|
<span class="ss_tile__text">{props.title}</span>
|
||||||
|
</h5>
|
||||||
|
)}
|
||||||
|
{props.subtitle && <div class="ss_tile__subtitle">{props.subtitle}</div>}
|
||||||
|
</div>
|
||||||
|
{props.trailing && <div class="ss_tile__trailing">{props.trailing}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSSTile<T>(build: (data: T) => SSTileProps) {
|
||||||
|
return function (props: {
|
||||||
|
data: T;
|
||||||
|
noLink?: boolean;
|
||||||
|
noIcon?: boolean;
|
||||||
|
onLinkClick?: () => void;
|
||||||
|
}) {
|
||||||
|
const built = build(props.data);
|
||||||
|
return (
|
||||||
|
<SSTile
|
||||||
|
{...built}
|
||||||
|
onLinkClick={props.onLinkClick ?? built.onLinkClick}
|
||||||
|
href={props.noLink ? undefined : built.href}
|
||||||
|
icon={props.noIcon ? undefined : built.icon}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SSTile, createSSTile };
|
||||||
21
src/hooks/createLoading.ts
Normal file
21
src/hooks/createLoading.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Accessor, createSignal } from "solid-js";
|
||||||
|
|
||||||
|
type PromiseOr<T> = Promise<T> | T;
|
||||||
|
|
||||||
|
export function createLoading(): [
|
||||||
|
Accessor<boolean>,
|
||||||
|
(action: () => PromiseOr<void>) => Promise<void>,
|
||||||
|
] {
|
||||||
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
|
||||||
|
return [loading, async (callback) => {
|
||||||
|
if (loading()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await callback();
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
17
src/index.ts
Normal file
17
src/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export { SSButton } from './components/SSButton';
|
||||||
|
export { SSCallout } from './components/SSCallout';
|
||||||
|
export { SSChip } from './components/SSChip';
|
||||||
|
export { SSAttrList } from './components/SSAttrList';
|
||||||
|
export { SSDataTable } from './components/SSDataTable';
|
||||||
|
export { SSDivider } from './components/SSDivider';
|
||||||
|
export { SSDropdown } from './components/SSDropdown';
|
||||||
|
export { SSExpandable } from './components/SSExpandable';
|
||||||
|
export { SSForm } from './components/SSForm';
|
||||||
|
export { SSHeader } from './components/SSHeader';
|
||||||
|
export { SSModal } from './components/SSModal';
|
||||||
|
export { SSModalsProvider, useSSModals } from './components/SSModals';
|
||||||
|
export { SSShell } from './components/SSShell';
|
||||||
|
export { SSSurface } from './components/SSSurface';
|
||||||
|
export { SSTile, createSSTile } from './components/SSTile';
|
||||||
|
|
||||||
|
export type { SSColor } from './colors';
|
||||||
2441
src/styles/default.css
Normal file
2441
src/styles/default.css
Normal file
File diff suppressed because it is too large
Load Diff
2312
src/styles/proposal.css
Normal file
2312
src/styles/proposal.css
Normal file
File diff suppressed because it is too large
Load Diff
51
test/index.test.tsx
Normal file
51
test/index.test.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { createRoot, createSignal } from 'solid-js'
|
||||||
|
import { isServer } from 'solid-js/web'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { Hello, createHello } from '../src'
|
||||||
|
|
||||||
|
describe('environment', () => {
|
||||||
|
it('runs on client', () => {
|
||||||
|
expect(typeof window).toBe('object')
|
||||||
|
expect(isServer).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('createHello', () => {
|
||||||
|
it('Returns a Hello World signal', () =>
|
||||||
|
createRoot(dispose => {
|
||||||
|
const [hello] = createHello()
|
||||||
|
expect(hello()).toBe('Hello World!')
|
||||||
|
dispose()
|
||||||
|
}))
|
||||||
|
|
||||||
|
it('Changes the hello target', () =>
|
||||||
|
createRoot(dispose => {
|
||||||
|
const [hello, setHello] = createHello()
|
||||||
|
setHello('Solid')
|
||||||
|
expect(hello()).toBe('Hello Solid!')
|
||||||
|
dispose()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Hello', () => {
|
||||||
|
it('renders a hello component', () => {
|
||||||
|
createRoot(() => {
|
||||||
|
const container = (<Hello />) as HTMLDivElement
|
||||||
|
expect(container.outerHTML).toBe('<div>Hello World!</div>')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('changes the hello target', () =>
|
||||||
|
createRoot(dispose => {
|
||||||
|
const [to, setTo] = createSignal('Solid')
|
||||||
|
const container = (<Hello to={to()} />) as HTMLDivElement
|
||||||
|
expect(container.outerHTML).toBe('<div>Hello Solid!</div>')
|
||||||
|
setTo('Tests')
|
||||||
|
|
||||||
|
// rendering is async
|
||||||
|
queueMicrotask(() => {
|
||||||
|
expect(container.outerHTML).toBe('<div>Hello Tests!</div>')
|
||||||
|
dispose()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
})
|
||||||
30
test/server.test.tsx
Normal file
30
test/server.test.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { isServer, renderToString } from 'solid-js/web'
|
||||||
|
import { Hello, createHello } from '../src'
|
||||||
|
|
||||||
|
describe('environment', () => {
|
||||||
|
it('runs on server', () => {
|
||||||
|
expect(typeof window).toBe('undefined')
|
||||||
|
expect(isServer).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('createHello', () => {
|
||||||
|
it('Returns a Hello World signal', () => {
|
||||||
|
const [hello] = createHello()
|
||||||
|
expect(hello()).toBe('Hello World!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Changes the hello target', () => {
|
||||||
|
const [hello, setHello] = createHello()
|
||||||
|
setHello('Solid')
|
||||||
|
expect(hello()).toBe('Hello Solid!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Hello', () => {
|
||||||
|
it('renders a hello component', () => {
|
||||||
|
const string = renderToString(() => <Hello />)
|
||||||
|
expect(string).toBe('<div>Hello World!</div>')
|
||||||
|
})
|
||||||
|
})
|
||||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
"types": [],
|
||||||
|
"baseUrl": "."
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist", "./dev"]
|
||||||
|
}
|
||||||
58
tsup.config.ts
Normal file
58
tsup.config.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { defineConfig } from 'tsup'
|
||||||
|
import * as preset from 'tsup-preset-solid'
|
||||||
|
|
||||||
|
const preset_options: preset.PresetOptions = {
|
||||||
|
// array or single object
|
||||||
|
entries: [
|
||||||
|
// default entry (index)
|
||||||
|
{
|
||||||
|
// entries with '.tsx' extension will have `solid` export condition generated
|
||||||
|
entry: 'src/index.ts',
|
||||||
|
// will generate a separate development entry
|
||||||
|
dev_entry: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Set to `true` to remove all `console.*` calls and `debugger` statements in prod builds
|
||||||
|
drop_console: true,
|
||||||
|
// Set to `true` to generate a CommonJS build alongside ESM
|
||||||
|
// cjs: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CI =
|
||||||
|
process.env['CI'] === 'true' ||
|
||||||
|
process.env['GITHUB_ACTIONS'] === 'true' ||
|
||||||
|
process.env['CI'] === '"1"' ||
|
||||||
|
process.env['GITHUB_ACTIONS'] === '"1"'
|
||||||
|
|
||||||
|
export default defineConfig(config => {
|
||||||
|
const watching = !!config.watch
|
||||||
|
|
||||||
|
const parsed_options = preset.parsePresetOptions(preset_options, watching)
|
||||||
|
|
||||||
|
if (!watching && !CI) {
|
||||||
|
const package_fields = preset.generatePackageExports(parsed_options)
|
||||||
|
const export_keys = Object.keys(package_fields.exports || {})
|
||||||
|
const is_condition_exports = export_keys.length > 0 && export_keys.every(key => !key.startsWith('.'))
|
||||||
|
if (is_condition_exports) {
|
||||||
|
package_fields.exports = {
|
||||||
|
'.': package_fields.exports,
|
||||||
|
'./styles/*': './dist/styles/*',
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
package_fields.exports = {
|
||||||
|
...package_fields.exports,
|
||||||
|
'./styles/*': './dist/styles/*',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`package.json: \n\n${JSON.stringify(package_fields, null, 2)}\n\n`)
|
||||||
|
|
||||||
|
// will update ./package.json with the correct export fields
|
||||||
|
preset.writePackageJson(package_fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
return preset.generateTsupOptions(parsed_options).map(opts => ({
|
||||||
|
...opts,
|
||||||
|
// minify: !watching,
|
||||||
|
}));
|
||||||
|
})
|
||||||
42
vitest.config.ts
Normal file
42
vitest.config.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import solidPlugin from 'vite-plugin-solid'
|
||||||
|
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
// to test in server environment, run with "--mode ssr" or "--mode test:ssr" flag
|
||||||
|
// loads only server.test.ts file
|
||||||
|
const testSSR = mode === 'test:ssr' || mode === 'ssr'
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins: [
|
||||||
|
solidPlugin({
|
||||||
|
// https://github.com/solidjs/solid-refresh/issues/29
|
||||||
|
hot: false,
|
||||||
|
// For testing SSR we need to do a SSR JSX transform
|
||||||
|
solid: { generate: testSSR ? 'ssr' : 'dom' },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
test: {
|
||||||
|
watch: false,
|
||||||
|
isolate: !testSSR,
|
||||||
|
env: {
|
||||||
|
NODE_ENV: testSSR ? 'production' : 'development',
|
||||||
|
DEV: testSSR ? '' : '1',
|
||||||
|
SSR: testSSR ? '1' : '',
|
||||||
|
PROD: testSSR ? '1' : '',
|
||||||
|
},
|
||||||
|
environment: testSSR ? 'node' : 'jsdom',
|
||||||
|
transformMode: { web: [/\.[jt]sx$/] },
|
||||||
|
...(testSSR
|
||||||
|
? {
|
||||||
|
include: ['test/server.test.{ts,tsx}'],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
include: ['test/*.test.{ts,tsx}'],
|
||||||
|
exclude: ['test/server.test.{ts,tsx}'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
conditions: testSSR ? ['node'] : ['browser', 'development'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user