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